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 <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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<Expanded>({});
|
||||
|
||||
const partialSchemaRenderContext: Omit<SchemaRenderContext, 'parentId'> = {
|
||||
classes,
|
||||
expanded,
|
||||
headings: [<Typography variant="h6" component="h4" />],
|
||||
};
|
||||
|
||||
const hasInput = !!action.schema?.input;
|
||||
const hasOutput = !!action.schema?.output;
|
||||
const hasExamples = !!action.examples;
|
||||
|
||||
if (!hasInput && !hasOutput && !hasExamples) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Flex direction="column" gap="1">
|
||||
<Text as="h2" variant="title-medium" weight="bold">
|
||||
{action.id}
|
||||
</Text>
|
||||
{action.description && (
|
||||
<Text as="p" variant="body-medium" color="secondary">
|
||||
{action.description}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
{(hasInput || hasOutput || hasExamples) && (
|
||||
<CardBody>
|
||||
<AccordionGroup allowsMultiple>
|
||||
{hasInput && (
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionTrigger title={t('actionsPage.action.input')} />
|
||||
<AccordionPanel>
|
||||
<RenderSchema
|
||||
strategy="properties"
|
||||
context={{
|
||||
parentId: `${action.id}.input`,
|
||||
...partialSchemaRenderContext,
|
||||
}}
|
||||
schema={action?.schema?.input}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
)}
|
||||
{hasOutput && (
|
||||
<Accordion>
|
||||
<AccordionTrigger title={t('actionsPage.action.output')} />
|
||||
<AccordionPanel>
|
||||
<RenderSchema
|
||||
strategy="properties"
|
||||
context={{
|
||||
parentId: `${action.id}.output`,
|
||||
...partialSchemaRenderContext,
|
||||
}}
|
||||
schema={action?.schema?.output}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
)}
|
||||
{hasExamples && (
|
||||
<Accordion>
|
||||
<AccordionTrigger title={t('actionsPage.action.examples')} />
|
||||
<AccordionPanel>
|
||||
<ScaffolderUsageExamplesTable examples={action.examples!} />
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
)}
|
||||
</AccordionGroup>
|
||||
</CardBody>
|
||||
<AccordionGroup allowsMultiple defaultExpandedKeys={['input']}>
|
||||
{hasInput && (
|
||||
<Accordion id="input">
|
||||
<AccordionTrigger title={t('actionsPage.action.input')} />
|
||||
<AccordionPanel>
|
||||
<RenderSchema
|
||||
strategy="properties"
|
||||
context={{
|
||||
parentId: `${action.id}.input`,
|
||||
...partialSchemaRenderContext,
|
||||
}}
|
||||
schema={action?.schema?.input}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
)}
|
||||
</Card>
|
||||
{hasOutput && (
|
||||
<Accordion id="output">
|
||||
<AccordionTrigger title={t('actionsPage.action.output')} />
|
||||
<AccordionPanel>
|
||||
<RenderSchema
|
||||
strategy="properties"
|
||||
context={{
|
||||
parentId: `${action.id}.output`,
|
||||
...partialSchemaRenderContext,
|
||||
}}
|
||||
schema={action?.schema?.output}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
)}
|
||||
{hasExamples && (
|
||||
<Accordion id="examples">
|
||||
<AccordionTrigger title={t('actionsPage.action.examples')} />
|
||||
<AccordionPanel>
|
||||
<ScaffolderUsageExamplesTable examples={action.examples!} />
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
)}
|
||||
</AccordionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const columnConfig: ColumnConfig<ActionTableItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
defaultWidth: '1fr',
|
||||
cell: item => (
|
||||
<CellText
|
||||
title={item.action.id}
|
||||
description={item.action.description ?? undefined}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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<Action | undefined>();
|
||||
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 (
|
||||
<Flex direction="column" gap="4">
|
||||
<SearchField
|
||||
aria-label={t('actionsPage.content.searchFieldPlaceholder')}
|
||||
placeholder={t('actionsPage.content.searchFieldPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
/>
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title={t('actionsPage.content.emptyState.title')}
|
||||
description={t('actionsPage.content.emptyState.description')}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="4">
|
||||
<SearchField
|
||||
aria-label={t('actionsPage.content.searchFieldPlaceholder')}
|
||||
placeholder={t('actionsPage.content.searchFieldPlaceholder')}
|
||||
{...search}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
/>
|
||||
<Table
|
||||
columnConfig={columnConfig}
|
||||
{...tableProps}
|
||||
loading={loading}
|
||||
emptyState={
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title={t('actionsPage.content.emptyState.title')}
|
||||
description={t('actionsPage.content.emptyState.description')}
|
||||
/>
|
||||
}
|
||||
rowConfig={{
|
||||
onClick: item => {
|
||||
setSelectedAction(prev =>
|
||||
prev?.id === item.action.id ? undefined : item.action,
|
||||
);
|
||||
},
|
||||
<List
|
||||
aria-label={t('actionsPage.title')}
|
||||
selectionMode="single"
|
||||
selectionBehavior="toggle"
|
||||
selectedKeys={selectedActionId ? [selectedActionId] : []}
|
||||
onSelectionChange={selection => {
|
||||
if (selection === 'all') {
|
||||
return;
|
||||
}
|
||||
const selected = [...selection][0] as string | undefined;
|
||||
setSelectedActionId(prev =>
|
||||
prev === selected ? undefined : selected,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{selectedAction && <ActionDetail action={selectedAction} />}
|
||||
>
|
||||
{filteredActions.map(action => (
|
||||
<ListRow
|
||||
key={action.id}
|
||||
id={action.id}
|
||||
textValue={action.id}
|
||||
description={
|
||||
selectedAction ? undefined : action.description ?? undefined
|
||||
}
|
||||
>
|
||||
{action.id}
|
||||
</ListRow>
|
||||
))}
|
||||
</List>
|
||||
{selectedAction && (
|
||||
<Flex direction="column" gap="3">
|
||||
<Flex direction="column" gap="1">
|
||||
<Text as="h2" variant="title-medium" weight="bold">
|
||||
{selectedAction.id}
|
||||
</Text>
|
||||
{selectedAction.description && (
|
||||
<Text as="p" variant="body-medium" color="secondary">
|
||||
{selectedAction.description}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<ActionDetail action={selectedAction} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 <RenderEnum {...{ classes, e }} />;
|
||||
return <RenderEnum {...{ e }} />;
|
||||
};
|
||||
|
||||
const LocalRenderSchema = ({
|
||||
@@ -73,7 +44,6 @@ const LocalRenderSchema = ({
|
||||
strategy: SchemaRenderStrategy;
|
||||
schema?: JSONSchema7Definition;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const expanded = useState<Expanded>({});
|
||||
return (
|
||||
<RenderSchema
|
||||
@@ -82,9 +52,7 @@ const LocalRenderSchema = ({
|
||||
schema,
|
||||
context: {
|
||||
parentId: 'test',
|
||||
classes,
|
||||
expanded,
|
||||
headings: [<Typography component="h1" />],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -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`),
|
||||
|
||||
@@ -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<typeof scaffolderTranslationRef>) => string;
|
||||
title: string;
|
||||
render: RenderColumn;
|
||||
className?: keyof ReturnType<typeof useColumnStyles>;
|
||||
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 (
|
||||
<div
|
||||
className={classNames(context.classes.code, {
|
||||
[context.classes.codeRequired]: element.required,
|
||||
})}
|
||||
>
|
||||
{element.key}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
} as Column;
|
||||
|
||||
const titleColumn = {
|
||||
key: 'title',
|
||||
title: t => t('renderSchema.tableCell.title'),
|
||||
render: (element: SchemaRenderElement) => (
|
||||
<MarkdownContent content={(element.schema as JSONSchema7).title ?? ''} />
|
||||
),
|
||||
} as Column;
|
||||
|
||||
const descriptionColumn = {
|
||||
key: 'description',
|
||||
title: t => t('renderSchema.tableCell.description'),
|
||||
render: (element: SchemaRenderElement) => (
|
||||
<MarkdownContent
|
||||
content={(element.schema as JSONSchema7).description ?? ''}
|
||||
/>
|
||||
),
|
||||
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 <Typography>{element.schema ? 'any' : 'none'}</Typography>;
|
||||
}
|
||||
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) ? (
|
||||
<Chip
|
||||
data-testid={`expand_${id}`}
|
||||
label={type}
|
||||
key={type}
|
||||
icon={isExpanded[id] ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={() =>
|
||||
setIsExpanded(prevState => {
|
||||
return {
|
||||
...prevState,
|
||||
[id]: !!!prevState[id],
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Chip label={type} key={type} variant="outlined" />
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
} 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 (
|
||||
<List {...props}>
|
||||
<ul {...props} style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||
{e.map((v, i) => {
|
||||
let inner: JSX.Element = (
|
||||
<Typography
|
||||
data-testid={`enum_el${i}`}
|
||||
className={classNames(classes.code)}
|
||||
>
|
||||
{JSON.stringify(v)}
|
||||
</Typography>
|
||||
);
|
||||
if (v !== null && ['object', 'array'].includes(typeof v)) {
|
||||
inner = (
|
||||
<>
|
||||
{inner}
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography
|
||||
data-testid={`pretty_${i}`}
|
||||
className={classNames(classes.code)}
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
>
|
||||
{JSON.stringify(v, undefined, 2)}
|
||||
</Typography>
|
||||
}
|
||||
const text = JSON.stringify(v);
|
||||
const isComplex = v !== null && ['object', 'array'].includes(typeof v);
|
||||
return (
|
||||
<li key={i} style={{ padding: '2px 0' }}>
|
||||
<Flex gap="2" align="center">
|
||||
<Text
|
||||
as="span"
|
||||
variant="body-small"
|
||||
data-testid={`enum_el${i}`}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
<IconButton data-testid={`wrap-text_${i}`}>
|
||||
<WrapText />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <ListItem key={i}>{inner}</ListItem>;
|
||||
{text}
|
||||
</Text>
|
||||
{isComplex && (
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
data-testid={`wrap-text_${i}`}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
>
|
||||
↵
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Text
|
||||
as="span"
|
||||
data-testid={`pretty_${i}`}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(v, undefined, 2)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
</Flex>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
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 && (
|
||||
<TableContainer component={Paper} className={tableStyles.schema}>
|
||||
<Table
|
||||
data-testid={`${strategy}_${context.parentId}`}
|
||||
size="small"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((col, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
className={columnStyles[col.className ?? 'standard']}
|
||||
<TableRoot
|
||||
data-testid={`${strategy}_${context.parentId}`}
|
||||
aria-label={`${strategy} schema`}
|
||||
>
|
||||
<TableHeader>
|
||||
{columns.map(col => (
|
||||
<Column
|
||||
key={col.key}
|
||||
id={col.key}
|
||||
isRowHeader={col.key === 'name'}
|
||||
defaultWidth={col.width ?? undefined}
|
||||
>
|
||||
{col.title}
|
||||
</Column>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{elements.map(el => {
|
||||
const id = generateId(el, context);
|
||||
const info = inspectSchema(el.schema);
|
||||
const hasDetails =
|
||||
typeof el.schema !== 'boolean' &&
|
||||
(info.canSubschema || info.hasEnum);
|
||||
|
||||
return (
|
||||
<Fragment key={id}>
|
||||
<Row
|
||||
id={`${strategy}-row_${id}`}
|
||||
data-testid={`${strategy}-row_${id}`}
|
||||
columns={columns}
|
||||
>
|
||||
{col.title(t)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{elements.map(el => {
|
||||
const id = generateId(el, context);
|
||||
const info = inspectSchema(el.schema);
|
||||
const rows = [
|
||||
<TableRow data-testid={`${strategy}-row_${id}`}>
|
||||
{columns!.map(col => (
|
||||
<TableCell
|
||||
key={col.key}
|
||||
className={
|
||||
columnStyles[col.className ?? 'standard']
|
||||
}
|
||||
>
|
||||
{col.render(el, context)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>,
|
||||
];
|
||||
if (
|
||||
typeof el.schema !== 'boolean' &&
|
||||
(info.canSubschema || info.hasEnum)
|
||||
) {
|
||||
let details: ReactElement = (
|
||||
<Box data-testid={`expansion_${id}`} sx={{ margin: 1 }}>
|
||||
{info.canSubschema && (
|
||||
<RenderSchema
|
||||
strategy="properties"
|
||||
context={{
|
||||
...context,
|
||||
parentId: id,
|
||||
parent: context,
|
||||
}}
|
||||
schema={
|
||||
el.schema.type === 'array'
|
||||
? (el.schema.items as JSONSchema7 | undefined)
|
||||
: el.schema
|
||||
{col => col.render(el, context)}
|
||||
</Row>
|
||||
{hasDetails &&
|
||||
typeof el.schema !== 'boolean' &&
|
||||
(() => {
|
||||
const s = el.schema as JSONSchema7;
|
||||
const isOpen = !getTypes(s) || isExpanded[id];
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Row
|
||||
id={`${strategy}-detail_${id}`}
|
||||
columns={columns}
|
||||
>
|
||||
{(col: ColumnDef) =>
|
||||
col.key === columns![0].key ? (
|
||||
<Cell
|
||||
data-testid={`expansion_${id}`}
|
||||
style={{
|
||||
gridColumn: `1 / -1`,
|
||||
padding: '8px 16px',
|
||||
}}
|
||||
>
|
||||
{info.canSubschema && (
|
||||
<RenderSchema
|
||||
strategy="properties"
|
||||
context={{
|
||||
...context,
|
||||
parentId: id,
|
||||
parent: context,
|
||||
}}
|
||||
schema={
|
||||
s.type === 'array'
|
||||
? (s.items as
|
||||
| JSONSchema7
|
||||
| undefined)
|
||||
: s
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{info.hasEnum && (
|
||||
<>
|
||||
<Text
|
||||
as="h4"
|
||||
variant="title-small"
|
||||
weight="bold"
|
||||
>
|
||||
Valid values:
|
||||
</Text>
|
||||
<RenderEnum
|
||||
data-testid={`enum_${id}`}
|
||||
e={enumFrom(s)!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Cell>
|
||||
) : (
|
||||
<Cell style={{ display: 'none' }} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{info.hasEnum && (
|
||||
<>
|
||||
{cloneElement(
|
||||
context.headings[0],
|
||||
{},
|
||||
'Valid values:',
|
||||
)}
|
||||
<RenderEnum
|
||||
data-testid={`enum_${id}`}
|
||||
e={enumFrom(el.schema)!}
|
||||
classes={context.classes}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
if (getTypes(el.schema)) {
|
||||
details = (
|
||||
<Collapse
|
||||
in={isExpanded[id]}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
>
|
||||
{details}
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
rows.push(
|
||||
<TableRow>
|
||||
<TableCell
|
||||
style={{ paddingBottom: 0, paddingTop: 0 }}
|
||||
colSpan={columns!.length}
|
||||
>
|
||||
{details}
|
||||
</TableCell>
|
||||
</TableRow>,
|
||||
);
|
||||
}
|
||||
return <Fragment key={id}>{rows}</Fragment>;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Row>
|
||||
);
|
||||
})()}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
)}
|
||||
{(Object.keys(subschemas) as Array<keyof subSchemasType>).map(sk => (
|
||||
<Fragment key={sk}>
|
||||
{cloneElement(context.headings[0], {}, sk)}
|
||||
<Text as="h4" variant="title-small" weight="bold">
|
||||
{sk}
|
||||
</Text>
|
||||
{subschemas[sk]!.map((sub, index) => (
|
||||
<RenderSchema
|
||||
key={index}
|
||||
@@ -497,13 +429,11 @@ export const RenderSchema = ({
|
||||
? strategy
|
||||
: 'root'
|
||||
}
|
||||
{...{
|
||||
context: {
|
||||
...context,
|
||||
parentId: `${context.parentId}_${sk}${index}`,
|
||||
},
|
||||
schema: sub,
|
||||
context={{
|
||||
...context,
|
||||
parentId: `${context.parentId}_${sk}${index}`,
|
||||
}}
|
||||
schema={sub}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
@@ -513,5 +443,77 @@ export const RenderSchema = ({
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
return result ?? <Typography>No schema defined</Typography>;
|
||||
return result ?? <Text as="p">No schema defined</Text>;
|
||||
};
|
||||
|
||||
function renderNameCell(
|
||||
element: SchemaRenderElement,
|
||||
_context: SchemaRenderContext,
|
||||
) {
|
||||
const name = element.key ?? '';
|
||||
return <CellText title={element.required ? `${name} *` : name} />;
|
||||
}
|
||||
|
||||
function renderTitleCell(element: SchemaRenderElement) {
|
||||
return (
|
||||
<Cell>
|
||||
<MarkdownContent content={(element.schema as JSONSchema7).title ?? ''} />
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDescriptionCell(element: SchemaRenderElement) {
|
||||
return (
|
||||
<Cell>
|
||||
<MarkdownContent
|
||||
content={(element.schema as JSONSchema7).description ?? ''}
|
||||
/>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTypeCell(
|
||||
element: SchemaRenderElement,
|
||||
context: SchemaRenderContext,
|
||||
) {
|
||||
if (typeof element.schema === 'boolean') {
|
||||
return <CellText title={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 (
|
||||
<Cell>
|
||||
<Flex gap="1" align="center">
|
||||
{types?.map((type, index) =>
|
||||
(info.canSubschema || info.hasEnum) && index === 0 ? (
|
||||
<Button
|
||||
key={type}
|
||||
data-testid={`expand_${id}`}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onPress={() =>
|
||||
setIsExpanded(prevState => ({
|
||||
...prevState,
|
||||
[id]: !prevState[id],
|
||||
}))
|
||||
}
|
||||
>
|
||||
{isExpanded[id] ? '▴' : '▾'} {type}
|
||||
</Button>
|
||||
) : (
|
||||
<Text
|
||||
key={type}
|
||||
as="span"
|
||||
variant="body-small"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
{type}
|
||||
</Text>
|
||||
),
|
||||
)}
|
||||
</Flex>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<React.SetStateAction<Expanded>>];
|
||||
headings: [React.JSX.Element, ...React.JSX.Element[]];
|
||||
};
|
||||
|
||||
export type SchemaRenderStrategy = 'root' | 'properties';
|
||||
|
||||
@@ -55,9 +55,7 @@ const FilterDetailContent = ({
|
||||
}
|
||||
const schema = filter.schema;
|
||||
const partialSchemaRenderContext: Omit<SchemaRenderContext, 'parentId'> = {
|
||||
classes,
|
||||
expanded,
|
||||
headings: [<Typography variant="h6" component="h4" />],
|
||||
};
|
||||
return (
|
||||
<Fragment key={`${name}.detail`}>
|
||||
@@ -97,7 +95,6 @@ const FilterDetailContent = ({
|
||||
context={{
|
||||
parentId: `${name}.arg${i}`,
|
||||
...partialSchemaRenderContext,
|
||||
headings: [<Typography variant="h6" component="h5" />],
|
||||
}}
|
||||
schema={argSchema}
|
||||
/>
|
||||
|
||||
@@ -58,9 +58,7 @@ const FunctionDetailContent = ({
|
||||
}
|
||||
const schema = fn.schema;
|
||||
const partialSchemaRenderContext: Omit<SchemaRenderContext, 'parentId'> = {
|
||||
classes,
|
||||
expanded,
|
||||
headings: [<Typography variant="h6" component="h4" />],
|
||||
};
|
||||
return (
|
||||
<Fragment key={`${name}.detail`}>
|
||||
@@ -88,7 +86,6 @@ const FunctionDetailContent = ({
|
||||
context={{
|
||||
parentId: `${name}.arg${i}`,
|
||||
...partialSchemaRenderContext,
|
||||
headings: [<Typography variant="h6" component="h5" />],
|
||||
}}
|
||||
schema={argSchema}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user