From 52c18c1f9a414fc00da519cd87938108386c373a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Mar 2026 16:43:22 +0100 Subject: [PATCH] scaffolder: migrate RenderSchema and ActionsPage to @backstage/ui components Replace all @material-ui imports in RenderSchema with @backstage/ui equivalents (TableRoot, TableHeader, TableBody, Row, Column, Cell, CellText, Button, Text, Flex, Tooltip). Simplify SchemaRenderContext by removing the classes and headings fields that were only needed for @material-ui styling. Redesign the ActionsPage to use a List with selectable rows instead of a Table with a separate detail Card. When an action is selected, all descriptions are hidden from the list and the action detail is shown below with input/output schemas and examples in accordions. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../ActionsPage/ActionsPage.test.tsx | 44 +- .../components/ActionsPage/ActionsPage.tsx | 289 ++++----- .../RenderSchema/RenderSchema.test.tsx | 122 +--- .../components/RenderSchema/RenderSchema.tsx | 558 +++++++++--------- .../src/components/RenderSchema/types.ts | 3 - .../TemplateFilters.tsx | 3 - .../TemplateGlobals.tsx | 3 - 7 files changed, 455 insertions(+), 567 deletions(-) diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index d11b47498d..b56d652a83 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -95,7 +95,7 @@ describe('ActionsPage', () => { await selectAction('test'); expect(await screen.findByText('Test title')).toBeInTheDocument(); - expect(screen.getByText('foobar')).toBeInTheDocument(); + expect(screen.getByText('foobar *')).toBeInTheDocument(); }); it('renders action with input and output on row click', async () => { @@ -140,7 +140,7 @@ describe('ActionsPage', () => { await selectAction('test'); expect(await screen.findByText('Test title')).toBeInTheDocument(); - expect(screen.getByText('foobar')).toBeInTheDocument(); + expect(screen.getByText('foobar *')).toBeInTheDocument(); expect(screen.getByText('Test output')).toBeInTheDocument(); }); @@ -347,20 +347,20 @@ describe('ActionsPage', () => { await selectAction('test'); expect(await screen.findByText('Test object')).toBeInTheDocument(); - const objectChip = screen.getByText('object'); - expect(objectChip).toBeInTheDocument(); + const objectButton = screen.getByRole('button', { name: /object/ }); + expect(objectButton).toBeInTheDocument(); expect(screen.queryByText('nested prop a')).not.toBeInTheDocument(); - expect(screen.queryByText('string')).not.toBeInTheDocument(); + expect(screen.queryByText(/^string$/)).not.toBeInTheDocument(); expect(screen.queryByText('nested prop b')).not.toBeInTheDocument(); - expect(screen.queryByText('number')).not.toBeInTheDocument(); + expect(screen.queryByText(/^number$/)).not.toBeInTheDocument(); - await userEvent.click(objectChip); + await userEvent.click(objectButton); expect(screen.queryByText('nested prop a')).toBeInTheDocument(); - expect(screen.queryByText('string')).toBeInTheDocument(); + expect(screen.queryByText(/^string$/)).toBeInTheDocument(); expect(screen.queryByText('nested prop b')).toBeInTheDocument(); - expect(screen.queryByText('number')).toBeInTheDocument(); + expect(screen.queryByText(/^number$/)).toBeInTheDocument(); }); it('renders action with nested object input type', async () => { @@ -412,22 +412,22 @@ describe('ActionsPage', () => { await selectAction('test'); expect(await screen.findByText('Test object')).toBeInTheDocument(); - const objectChip = screen.getByText('object'); - expect(objectChip).toBeInTheDocument(); + const objectButton = screen.getByRole('button', { name: /object/ }); + expect(objectButton).toBeInTheDocument(); expect(screen.queryByText('nested object a')).not.toBeInTheDocument(); expect(screen.queryByText('nested prop b')).not.toBeInTheDocument(); expect(screen.queryByText('nested object c')).not.toBeInTheDocument(); - await userEvent.click(objectChip); + await userEvent.click(objectButton); expect(screen.queryByText('nested object a')).toBeInTheDocument(); expect(screen.queryByText('nested prop b')).toBeInTheDocument(); expect(screen.queryByText('nested object c')).not.toBeInTheDocument(); - const allObjectChips = screen.getAllByText('object'); - expect(allObjectChips.length).toBe(2); - await userEvent.click(allObjectChips[1]); + const allObjectButtons = screen.getAllByRole('button', { name: /object/ }); + expect(allObjectButtons.length).toBe(2); + await userEvent.click(allObjectButtons[1]); expect(screen.queryByText('nested object a')).toBeInTheDocument(); expect(screen.queryByText('nested prop b')).toBeInTheDocument(); @@ -467,12 +467,12 @@ describe('ActionsPage', () => { await selectAction('test'); expect(await screen.findByText('Test object')).toBeInTheDocument(); - const objectChip = screen.getByText('object'); - expect(objectChip).toBeInTheDocument(); + const objectButton = screen.getByRole('button', { name: /object/ }); + expect(objectButton).toBeInTheDocument(); expect(screen.queryByText('No schema defined')).not.toBeInTheDocument(); - await userEvent.click(objectChip); + await userEvent.click(objectButton); expect(screen.queryByText('No schema defined')).toBeInTheDocument(); }); @@ -567,13 +567,15 @@ describe('ActionsPage', () => { await selectAction('test'); expect(await screen.findByText('Test array')).toBeInTheDocument(); - const objectChip = screen.getByText('array(object)'); - expect(objectChip).toBeInTheDocument(); + const arrayButton = screen.getByRole('button', { + name: /array\(object\)/, + }); + expect(arrayButton).toBeInTheDocument(); expect(screen.queryByText('nested object a')).not.toBeInTheDocument(); expect(screen.queryByText('nested prop b')).not.toBeInTheDocument(); - await userEvent.click(objectChip); + await userEvent.click(arrayButton); expect(screen.queryByText('nested object a')).toBeInTheDocument(); expect(screen.queryByText('nested prop b')).toBeInTheDocument(); diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 1088a44708..98beeaa997 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -16,8 +16,6 @@ import { useMemo, useState } from 'react'; import useAsync from 'react-use/esm/useAsync'; import { Action, scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { @@ -32,17 +30,11 @@ import { AccordionGroup, AccordionPanel, AccordionTrigger, - Card, - CardBody, - CardHeader, - CellText, Flex, + List, + ListRow, SearchField, - Table, Text, - useTable, - type ColumnConfig, - type TableItem, } from '@backstage/ui'; import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/alpha'; import { useNavigate } from 'react-router-dom'; @@ -57,128 +49,66 @@ import { scaffolderTranslationRef } from '../../translation'; import { Expanded, RenderSchema, SchemaRenderContext } from '../RenderSchema'; import { ScaffolderUsageExamplesTable } from '../ScaffolderUsageExamplesTable'; -const useStyles = makeStyles(theme => ({ - code: { - fontFamily: 'Menlo, monospace', - padding: theme.spacing(1), - backgroundColor: - theme.palette.type === 'dark' - ? theme.palette.grey[700] - : theme.palette.grey[300], - display: 'inline-block', - borderRadius: 5, - border: `1px solid ${theme.palette.grey[500]}`, - position: 'relative', - }, - - codeRequired: { - '&::after': { - position: 'absolute', - content: '"*"', - top: 0, - right: theme.spacing(0.5), - fontWeight: 'bolder', - color: theme.palette.error.light, - }, - }, -})); - -interface ActionTableItem extends TableItem { - action: Action; -} - function ActionDetail({ action }: { action: Action }) { - const classes = useStyles(); const { t } = useTranslationRef(scaffolderTranslationRef); const expanded = useState({}); const partialSchemaRenderContext: Omit = { - classes, expanded, - headings: [], }; const hasInput = !!action.schema?.input; const hasOutput = !!action.schema?.output; const hasExamples = !!action.examples; + if (!hasInput && !hasOutput && !hasExamples) { + return null; + } + return ( - - - - - {action.id} - - {action.description && ( - - {action.description} - - )} - - - {(hasInput || hasOutput || hasExamples) && ( - - - {hasInput && ( - - - - - - - )} - {hasOutput && ( - - - - - - - )} - {hasExamples && ( - - - - - - - )} - - + + {hasInput && ( + + + + + + )} - + {hasOutput && ( + + + + + + + )} + {hasExamples && ( + + + + + + + )} + ); } -const columnConfig: ColumnConfig[] = [ - { - id: 'name', - label: 'Name', - isRowHeader: true, - defaultWidth: '1fr', - cell: item => ( - - ), - }, -]; - export const ActionPageContent = () => { const api = useApi(scaffolderApiRef); const { t } = useTranslationRef(scaffolderTranslationRef); @@ -191,35 +121,30 @@ export const ActionPageContent = () => { return api.listActions(); }, [api]); - const [selectedAction, setSelectedAction] = useState(); + const [selectedActionId, setSelectedActionId] = useState< + string | undefined + >(); + const [searchQuery, setSearchQuery] = useState(''); - const tableData = useMemo( - () => - actions - ?.filter(action => !action.id.startsWith('legacy:')) - .map( - (action): ActionTableItem => ({ - id: action.id, - action, - }), - ), - [actions], + const filteredActions = useMemo(() => { + const nonLegacy = + actions?.filter(action => !action.id.startsWith('legacy:')) ?? []; + if (!searchQuery) { + return nonLegacy; + } + const lowerQuery = searchQuery.toLowerCase(); + return nonLegacy.filter( + action => + action.id.toLowerCase().includes(lowerQuery) || + action.description?.toLowerCase().includes(lowerQuery), + ); + }, [actions, searchQuery]); + + const selectedAction = useMemo( + () => filteredActions.find(a => a.id === selectedActionId), + [filteredActions, selectedActionId], ); - const { tableProps, search } = useTable({ - mode: 'complete', - data: tableData, - paginationOptions: { pageSize: 20 }, - searchFn: (items, query) => { - const lowerQuery = query.toLowerCase(); - return items.filter( - item => - item.action.id.toLowerCase().includes(lowerQuery) || - item.action.description?.toLowerCase().includes(lowerQuery), - ); - }, - }); - if (error) { return ( <> @@ -233,33 +158,75 @@ export const ActionPageContent = () => { ); } + if (!loading && !filteredActions.length) { + return ( + + + + + ); + } + return ( - - } - rowConfig={{ - onClick: item => { - setSelectedAction(prev => - prev?.id === item.action.id ? undefined : item.action, - ); - }, + { + if (selection === 'all') { + return; + } + const selected = [...selection][0] as string | undefined; + setSelectedActionId(prev => + prev === selected ? undefined : selected, + ); }} - /> - {selectedAction && } + > + {filteredActions.map(action => ( + + {action.id} + + ))} + + {selectedAction && ( + + + + {selectedAction.id} + + {selectedAction.description && ( + + {selectedAction.description} + + )} + + + + )} ); }; diff --git a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx index ec49f414f9..a5d7915482 100644 --- a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx +++ b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ import { renderInTestApp } from '@backstage/test-utils'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; import { BoundFunction, queries, @@ -35,35 +33,8 @@ import { RenderEnum, RenderSchema } from './RenderSchema'; /* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "assert*"] }] */ -const useStyles = makeStyles(theme => ({ - code: { - fontFamily: 'Menlo, monospace', - padding: theme.spacing(1), - backgroundColor: - theme.palette.type === 'dark' - ? theme.palette.grey[700] - : theme.palette.grey[300], - display: 'inline-block', - borderRadius: 5, - border: `1px solid ${theme.palette.grey[500]}`, - position: 'relative', - }, - - codeRequired: { - '&::after': { - position: 'absolute', - content: '"*"', - top: 0, - right: theme.spacing(0.5), - fontWeight: 'bolder', - color: theme.palette.error.light, - }, - }, -})); - const LocalRenderEnum = ({ e }: { e: JSONSchema7Type[] }) => { - const classes = useStyles(); - return ; + return ; }; const LocalRenderSchema = ({ @@ -73,7 +44,6 @@ const LocalRenderSchema = ({ strategy: SchemaRenderStrategy; schema?: JSONSchema7Definition; }) => { - const classes = useStyles(); const expanded = useState({}); return ( ], }, }} /> @@ -119,19 +87,18 @@ it('enum rendering', async () => { expect(el).toBeInTheDocument(); expect(el).toHaveTextContent(JSON.stringify(each.value)); - const wrapTextIcon = getByTestId(`wrap-text_${each.index}`); - expect(wrapTextIcon).toBeInTheDocument(); + const wrapTextButton = getByTestId(`wrap-text_${each.index}`); + expect(wrapTextButton).toBeInTheDocument(); expect(queryByTestId(`pretty_${each.index}`)).toBeNull(); - await userEvent.hover(wrapTextIcon); + wrapTextButton.focus(); const pretty = await findByTestId(`pretty_${each.index}`); expect(pretty).toBeInTheDocument(); - // test textContent directly to avoid ws normalization: expect(pretty.textContent).toBe(JSON.stringify(each.value, null, 2)); - await userEvent.unhover(wrapTextIcon); + wrapTextButton.blur(); await waitFor(() => expect(queryByTestId(`pretty_${each.index}`)).toBeNull(), @@ -171,27 +138,29 @@ describe('JSON schema UI rendering', () => { const { getByTestId, queryByTestId } = q; const t = getByTestId(`properties_${id}`); expect(t).toBeInTheDocument(); - expect(t.tagName).toBe('TABLE'); - - expect( - Array.from(t.querySelectorAll('thead > tr > th'), e => e.textContent), - ).toEqual(['Name', 'Title', 'Description', 'Type']); for (const p of Object.keys(basic.properties!)) { const tr = getByTestId(`properties-row_${id}.${p}`); expect(tr).toBeInTheDocument(); - expect(tr.tagName).toBe('TR'); const pt = (basic.properties![p] as JSONSchema7).type; + const headerCells = within(tr).queryAllByRole('rowheader'); + const dataCells = within(tr).getAllByRole('gridcell'); + const allCells = [...headerCells, ...dataCells]; + const cellTexts = allCells.map(c => c.textContent); - expect(Array.from(tr.querySelectorAll('td'), n => n.textContent)).toEqual( - [p, capitalize(p), `the ${pt}`, pt], + expect(cellTexts).toEqual( + expect.arrayContaining([ + expect.stringContaining(p), + expect.stringContaining(capitalize(p)), + expect.stringContaining(`the ${pt}`), + expect.stringContaining(String(pt)), + ]), ); - expect( - Array.from(within(tr).getByText(p).classList).some(c => - c.includes('codeRequired'), - ), - ).toBe(basic.required?.includes(p)); + + if (basic.required?.includes(p)) { + expect(cellTexts[0]).toContain('*'); + } expect(queryByTestId(`expand_${id}.${p}`)).not.toBeInTheDocument(); } @@ -241,11 +210,6 @@ describe('JSON schema UI rendering', () => { const { findByTestId, getByTestId, queryByTestId } = q; const t = getByTestId(`properties_${id}`); expect(t).toBeInTheDocument(); - expect(t.tagName).toBe('TABLE'); - - expect( - Array.from(t.querySelectorAll('thead > tr > th'), n => n.textContent), - ).toEqual(['Name', 'Title', 'Description', 'Type']); const xp: (k: string) => Promise<{ expander: HTMLElement; @@ -254,7 +218,6 @@ describe('JSON schema UI rendering', () => { }> = async (k: string) => { const r = getByTestId(`properties-row_${id}.${k}`); expect(r).toBeInTheDocument(); - expect(r.tagName).toBe('TR'); const expansionId = `expansion_${id}.${k}`; @@ -311,19 +274,6 @@ describe('JSON schema UI rendering', () => { const t = getByTestId('root_test'); expect(t).toBeInTheDocument(); - expect(t.tagName).toBe('TABLE'); - - expect( - Array.from(t.querySelectorAll('thead > tr > th'), n => n.textContent), - ).toEqual(['Type']); - - const tr = getByTestId('root-row_test'); - expect(tr).toBeInTheDocument(); - expect(tr.tagName).toBe('TR'); - - expect(Array.from(tr.querySelectorAll('td'), n => n.textContent)).toEqual( - ['object'], - ); expect(queryByTestId('expansion_test')).not.toBeInTheDocument(); @@ -349,18 +299,6 @@ describe('JSON schema UI rendering', () => { const t = getByTestId('root_test'); expect(t).toBeInTheDocument(); - expect(t.tagName).toBe('TABLE'); - - expect( - Array.from(t.querySelectorAll('thead > tr > th'), n => n.textContent), - ).toEqual(['Title', 'Description', 'Type']); - const tr = getByTestId('root-row_test'); - expect(tr).toBeInTheDocument(); - expect(tr.tagName).toBe('TR'); - - expect(Array.from(tr.querySelectorAll('td'), n => n.textContent)).toEqual( - [msv.title, msv.description, 'string'], - ); expect(queryByTestId('expansion_test')).not.toBeInTheDocument(); @@ -385,18 +323,7 @@ describe('JSON schema UI rendering', () => { const t = getByTestId('root_test'); expect(t).toBeInTheDocument(); - expect(t.tagName).toBe('TABLE'); - expect( - Array.from(t.querySelectorAll('thead > tr > th'), n => n.textContent), - ).toEqual(['Title', 'Description', 'Type']); - const tr = getByTestId('root-row_test'); - expect(tr).toBeInTheDocument(); - expect(tr.tagName).toBe('TR'); - - expect(Array.from(tr.querySelectorAll('td'), n => n.textContent)).toEqual( - [deep.title, deep.description, 'object'], - ); expect(queryByTestId('expansion_test')).not.toBeInTheDocument(); const expand = getByTestId('expand_test'); @@ -478,11 +405,10 @@ describe('JSON schema UI rendering', () => { const sub = getByTestId(`properties-row_test_oneOf${i}.${k}`); expect(sub).toBeInTheDocument(); - expect( - Array.from(within(sub).getByText(k).classList).some(c => - c.includes('codeRequired'), - ), - ).toBe(true); + const nameCell = + within(sub).queryAllByRole('rowheader')[0] ?? + within(sub).getAllByRole('gridcell')[0]; + expect(nameCell.textContent).toContain('*'); expect( getByTestId(`properties-row_test_oneOf${i}.${k}Flag`), diff --git a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx index 2d4ebd6fca..a195da7b60 100644 --- a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx +++ b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx @@ -15,36 +15,28 @@ */ import { MarkdownContent } from '@backstage/core-components'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import Box from '@material-ui/core/Box'; -import Chip from '@material-ui/core/Chip'; -import Collapse from '@material-ui/core/Collapse'; -import IconButton from '@material-ui/core/IconButton'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import Paper from '@material-ui/core/Paper'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import Tooltip from '@material-ui/core/Tooltip'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; -import { ClassNameMap } from '@material-ui/core/styles/withStyles'; -import ExpandLessIcon from '@material-ui/icons/ExpandLess'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import WrapText from '@material-ui/icons/WrapText'; -import classNames from 'classnames'; +import { + Button, + Cell, + CellText, + Column, + Flex, + Row, + TableBody, + TableHeader, + TableRoot, + Text, + Tooltip, + TooltipTrigger, +} from '@backstage/ui'; import { JSONSchema7, JSONSchema7Definition, JSONSchema7Type, } from 'json-schema'; -import { FC, JSX, cloneElement, Fragment, ReactElement } from 'react'; +import { FC, Fragment, JSX } from 'react'; import { scaffolderTranslationRef } from '../../translation'; import { SchemaRenderContext, SchemaRenderStrategy } from './types'; -import { TranslationMessages } from '../TemplatingExtensionsPage/types'; const compositeSchemaProperties = ['allOf', 'anyOf', 'not', 'oneOf'] as const; @@ -131,20 +123,6 @@ const getSubschemas = (schema: JSONSchema7Definition): subSchemasType => { ); }; -const useColumnStyles = makeStyles({ - description: { - width: '40%', - whiteSpace: 'normal', - wordWrap: 'break-word', - '&.MuiTableCell-root': { - whiteSpace: 'normal', - }, - }, - standard: { - whiteSpace: 'normal', - }, -}); - type SchemaRenderElement = { schema: JSONSchema7Definition; key?: string; @@ -156,11 +134,11 @@ type RenderColumn = ( context: SchemaRenderContext, ) => JSX.Element; -type Column = { +type ColumnDef = { key: string; - title: (t: TranslationMessages) => string; + title: string; render: RenderColumn; - className?: keyof ReturnType; + width?: `${number}fr`; }; const generateId = ( @@ -170,41 +148,6 @@ const generateId = ( return element.key ? `${context.parentId}.${element.key}` : context.parentId; }; -const nameColumn = { - key: 'name', - title: t => t('renderSchema.tableCell.name'), - render: (element: SchemaRenderElement, context: SchemaRenderContext) => { - return ( -
- {element.key} -
- ); - }, -} as Column; - -const titleColumn = { - key: 'title', - title: t => t('renderSchema.tableCell.title'), - render: (element: SchemaRenderElement) => ( - - ), -} as Column; - -const descriptionColumn = { - key: 'description', - title: t => t('renderSchema.tableCell.description'), - render: (element: SchemaRenderElement) => ( - - ), - className: 'description', -} as Column; - const enumFrom = (schema: JSONSchema7) => { if (schema.type === 'array') { if (schema.items && typeof schema.items !== 'boolean') { @@ -242,107 +185,57 @@ const inspectSchema = ( }; }; -const typeColumn = { - key: 'type', - title: t => t('renderSchema.tableCell.type'), - render: (element: SchemaRenderElement, context: SchemaRenderContext) => { - if (typeof element.schema === 'boolean') { - return {element.schema ? 'any' : 'none'}; - } - const types = getTypes(element.schema); - const [isExpanded, setIsExpanded] = context.expanded; - const id = generateId(element, context); - const info = inspectSchema(element.schema); - return ( - <> - {types?.map((type, index) => - info.canSubschema || (info.hasEnum && index === 0) ? ( - : } - variant="outlined" - onClick={() => - setIsExpanded(prevState => { - return { - ...prevState, - [id]: !!!prevState[id], - }; - }) - } - /> - ) : ( - - ), - )} - - ); - }, -} as Column; - export const RenderEnum: FC<{ e: JSONSchema7Type[]; - classes: ClassNameMap; [key: string]: any; -}> = ({ - e, - classes, - ...props -}: { - e: JSONSchema7Type[]; - classes: ClassNameMap; -}) => { +}> = ({ e, ...props }: { e: JSONSchema7Type[] }) => { return ( - +
    {e.map((v, i) => { - let inner: JSX.Element = ( - - {JSON.stringify(v)} - - ); - if (v !== null && ['object', 'array'].includes(typeof v)) { - inner = ( - <> - {inner} - - {JSON.stringify(v, undefined, 2)} - - } + const text = JSON.stringify(v); + const isComplex = v !== null && ['object', 'array'].includes(typeof v); + return ( +
  • + + - - - - - - ); - } - return {inner}; + {text} + + {isComplex && ( + + + + + {JSON.stringify(v, undefined, 2)} + + + + )} + +
  • + ); })} - +
); }; -const useTableStyles = makeStyles({ - schema: { - width: '100%', - overflowX: 'hidden', - '& table': { - width: '100%', - tableLayout: 'fixed', - }, - }, -}); - export const RenderSchema = ({ strategy, context, @@ -353,26 +246,62 @@ export const RenderSchema = ({ schema?: JSONSchema7Definition; }) => { const { t } = useTranslationRef(scaffolderTranslationRef); - const tableStyles = useTableStyles(); - const columnStyles = useColumnStyles(); + const result = (() => { if (typeof schema === 'object') { const subschemas = getSubschemas(schema); - let columns: Column[] | undefined; + let columns: ColumnDef[] | undefined; let elements: SchemaRenderElement[] | undefined; if (strategy === 'root') { if ('type' in schema || !Object.keys(subschemas).length) { elements = [{ schema }]; - columns = [typeColumn]; + columns = [ + { + key: 'type', + title: t('renderSchema.tableCell.type'), + render: renderTypeCell, + }, + ]; if (schema.description) { - columns.unshift(descriptionColumn); + columns.unshift({ + key: 'description', + title: t('renderSchema.tableCell.description'), + render: renderDescriptionCell, + width: '1fr', + }); } if (schema.title) { - columns.unshift(titleColumn); + columns.unshift({ + key: 'title', + title: t('renderSchema.tableCell.title'), + render: renderTitleCell, + }); } } } else if (schema.properties) { - columns = [nameColumn, titleColumn, descriptionColumn, typeColumn]; + columns = [ + { + key: 'name', + title: t('renderSchema.tableCell.name'), + render: renderNameCell, + }, + { + key: 'title', + title: t('renderSchema.tableCell.title'), + render: renderTitleCell, + }, + { + key: 'description', + title: t('renderSchema.tableCell.description'), + render: renderDescriptionCell, + width: '1fr', + }, + { + key: 'type', + title: t('renderSchema.tableCell.type'), + render: renderTypeCell, + }, + ]; elements = Object.entries(schema.properties!).map(([key, v]) => ({ schema: v, key, @@ -386,109 +315,112 @@ export const RenderSchema = ({ return ( <> {columns && elements && ( - -
- - - {columns.map((col, index) => ( - + + {columns.map(col => ( + + {col.title} + + ))} + + + {elements.map(el => { + const id = generateId(el, context); + const info = inspectSchema(el.schema); + const hasDetails = + typeof el.schema !== 'boolean' && + (info.canSubschema || info.hasEnum); + + return ( + + - {col.title(t)} - - ))} - - - - {elements.map(el => { - const id = generateId(el, context); - const info = inspectSchema(el.schema); - const rows = [ - - {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)} + + {hasDetails && + typeof el.schema !== 'boolean' && + (() => { + const s = el.schema as JSONSchema7; + const isOpen = !getTypes(s) || isExpanded[id]; + if (!isOpen) { + return null; + } + return ( + + {(col: ColumnDef) => + col.key === columns![0].key ? ( + + {info.canSubschema && ( + + )} + {info.hasEnum && ( + <> + + Valid values: + + + + )} + + ) : ( + + ) } - /> - )} - {info.hasEnum && ( - <> - {cloneElement( - context.headings[0], - {}, - 'Valid values:', - )} - - - )} - - ); - if (getTypes(el.schema)) { - details = ( - - {details} - - ); - } - rows.push( - - - {details} - - , - ); - } - return {rows}; - })} - -
- + + ); + })()} + + ); + })} + + )} {(Object.keys(subschemas) as Array).map(sk => ( - {cloneElement(context.headings[0], {}, sk)} + + {sk} + {subschemas[sk]!.map((sub, index) => ( ))} @@ -513,5 +443,77 @@ export const RenderSchema = ({ } return undefined; })(); - return result ?? No schema defined; + return result ?? No schema defined; }; + +function renderNameCell( + element: SchemaRenderElement, + _context: SchemaRenderContext, +) { + const name = element.key ?? ''; + return ; +} + +function renderTitleCell(element: SchemaRenderElement) { + return ( + + + + ); +} + +function renderDescriptionCell(element: SchemaRenderElement) { + return ( + + + + ); +} + +function renderTypeCell( + element: SchemaRenderElement, + context: SchemaRenderContext, +) { + if (typeof element.schema === 'boolean') { + return ; + } + const types = getTypes(element.schema); + const [isExpanded, setIsExpanded] = context.expanded; + const id = generateId(element, context); + const info = inspectSchema(element.schema); + return ( + + + {types?.map((type, index) => + (info.canSubschema || info.hasEnum) && index === 0 ? ( + + ) : ( + + {type} + + ), + )} + + + ); +} diff --git a/plugins/scaffolder/src/components/RenderSchema/types.ts b/plugins/scaffolder/src/components/RenderSchema/types.ts index 70f3c27dfd..f12f0765ea 100644 --- a/plugins/scaffolder/src/components/RenderSchema/types.ts +++ b/plugins/scaffolder/src/components/RenderSchema/types.ts @@ -13,16 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ClassNameMap } from '@material-ui/core/styles/withStyles'; export type Expanded = { [key: string]: boolean }; export type SchemaRenderContext = { parent?: SchemaRenderContext; parentId: string; - classes: ClassNameMap; expanded: [Expanded, React.Dispatch>]; - headings: [React.JSX.Element, ...React.JSX.Element[]]; }; export type SchemaRenderStrategy = 'root' | 'properties'; diff --git a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateFilters.tsx b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateFilters.tsx index 05f3043dd1..c3d425ae44 100644 --- a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateFilters.tsx +++ b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateFilters.tsx @@ -55,9 +55,7 @@ const FilterDetailContent = ({ } const schema = filter.schema; const partialSchemaRenderContext: Omit = { - classes, expanded, - headings: [], }; return ( @@ -97,7 +95,6 @@ const FilterDetailContent = ({ context={{ parentId: `${name}.arg${i}`, ...partialSchemaRenderContext, - headings: [], }} schema={argSchema} /> diff --git a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateGlobals.tsx b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateGlobals.tsx index 249e2a581d..e274b11944 100644 --- a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateGlobals.tsx +++ b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplateGlobals.tsx @@ -58,9 +58,7 @@ const FunctionDetailContent = ({ } const schema = fn.schema; const partialSchemaRenderContext: Omit = { - classes, expanded, - headings: [], }; return ( @@ -88,7 +86,6 @@ const FunctionDetailContent = ({ context={{ parentId: `${name}.arg${i}`, ...partialSchemaRenderContext, - headings: [], }} schema={argSchema} />