config-schema: initial propper schema viewer
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -14,13 +14,270 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { CodeSnippet } from '@backstage/core';
|
||||
import { Schema } from 'jsonschema';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
Divider,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
interface SchemaViewProps {
|
||||
path: string;
|
||||
depth: number;
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
export interface MetadataViewRowProps {
|
||||
label: string;
|
||||
text?: string;
|
||||
data?: JsonValue;
|
||||
}
|
||||
|
||||
export function MetadataViewRow({ label, text, data }: MetadataViewRowProps) {
|
||||
if (text === undefined && data === undefined) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell variant="head" style={{ width: 160 }}>
|
||||
<Typography variant="body1" noWrap>
|
||||
{label}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body1">
|
||||
{data ? JSON.stringify(data) : text}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetadataView({ schema }: { schema: Schema }) {
|
||||
return (
|
||||
<Paper variant="outlined" square style={{ width: '100%' }}>
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
<MetadataViewRow label="Type" data={schema.type} />
|
||||
<MetadataViewRow label="Allowed values" data={schema.enum} />
|
||||
<MetadataViewRow label="Format" text={schema.format} />
|
||||
<MetadataViewRow
|
||||
label="Pattern"
|
||||
text={schema.pattern && String(schema.pattern)}
|
||||
/>
|
||||
<MetadataViewRow label="Minimum" data={schema.minimum} />
|
||||
<MetadataViewRow label="Maximum" data={schema.maximum} />
|
||||
<MetadataViewRow
|
||||
label="Exclusive minimum"
|
||||
data={schema.exclusiveMinimum}
|
||||
/>
|
||||
<MetadataViewRow
|
||||
label="Exclusive maximum"
|
||||
data={schema.exclusiveMaximum}
|
||||
/>
|
||||
<MetadataViewRow label="Multiple of" data={schema.multipleOf} />
|
||||
<MetadataViewRow
|
||||
label="Maximum number of items"
|
||||
data={schema.maxItems}
|
||||
/>
|
||||
<MetadataViewRow
|
||||
label="Minimum number of items"
|
||||
data={schema.minItems}
|
||||
/>
|
||||
<MetadataViewRow
|
||||
label="Maximum number of properties"
|
||||
data={schema.maxProperties}
|
||||
/>
|
||||
<MetadataViewRow
|
||||
label="Minimum number of properties"
|
||||
data={schema.minProperties}
|
||||
/>
|
||||
<MetadataViewRow label="Maximum Length" data={schema.maxLength} />
|
||||
<MetadataViewRow label="Minimum Length" data={schema.minLength} />
|
||||
<MetadataViewRow
|
||||
label="Items must be unique"
|
||||
data={schema.uniqueItems}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScalarView({ schema }: SchemaViewProps) {
|
||||
return (
|
||||
<>
|
||||
{schema.description && (
|
||||
<Box marginBottom={4}>
|
||||
<Typography variant="body1">{schema.description}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<MetadataView schema={schema} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function isRequired(name: string, required?: boolean | string[]) {
|
||||
if (required === true) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(required)) {
|
||||
return required.includes(name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function titleVariant(depth: number) {
|
||||
if (depth <= 1) {
|
||||
return 'h2';
|
||||
} else if (depth === 2) {
|
||||
return 'h3';
|
||||
} else if (depth === 3) {
|
||||
return 'h4';
|
||||
} else if (depth === 4) {
|
||||
return 'h5';
|
||||
}
|
||||
return 'h6';
|
||||
}
|
||||
|
||||
export function VisibilityView({ schema }: { schema: Schema }) {
|
||||
const { visibility } = schema as { visibility?: string };
|
||||
if (visibility === 'frontend') {
|
||||
return (
|
||||
<Box
|
||||
marginLeft={1}
|
||||
children={<Chip label="frontend" color="primary" size="small" />}
|
||||
/>
|
||||
);
|
||||
} else if (visibility === 'secret') {
|
||||
return (
|
||||
<Box
|
||||
marginLeft={1}
|
||||
children={<Chip label="secret" color="secondary" size="small" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ArrayView({ path, depth, schema }: SchemaViewProps) {
|
||||
const itemDepth = depth + 1;
|
||||
const itemPath = path ? `${path}[]` : '[]';
|
||||
const itemSchema = schema.items;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box marginBottom={4}>
|
||||
{schema.description && (
|
||||
<Box marginBottom={4}>
|
||||
<Typography variant="body1">{schema.description}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<MetadataView schema={schema} />
|
||||
</Box>
|
||||
<Typography variant="overline">Items</Typography>
|
||||
<Box paddingBottom={2} display="flex" flexDirection="row">
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box paddingLeft={2} flex={1}>
|
||||
<Box marginBottom={2}>
|
||||
<Typography variant={titleVariant(itemDepth)}>
|
||||
{itemPath}
|
||||
</Typography>
|
||||
</Box>
|
||||
{itemSchema && (
|
||||
<SchemaView
|
||||
path={itemPath}
|
||||
depth={itemDepth}
|
||||
schema={itemSchema as Schema}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectView({ path, depth, schema }: SchemaViewProps) {
|
||||
const properties = Object.entries(schema.properties ?? {});
|
||||
return (
|
||||
<>
|
||||
{depth > 0 && (
|
||||
<Box marginBottom={4}>
|
||||
{schema.description && (
|
||||
<Box marginBottom={4}>
|
||||
<Typography variant="body1">{schema.description}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<MetadataView schema={schema} />
|
||||
</Box>
|
||||
)}
|
||||
{properties.length > 0 && (
|
||||
<>
|
||||
{depth > 0 && <Typography variant="overline">Properties</Typography>}
|
||||
{properties.map(([name, propSchema], index) => {
|
||||
const propDepth = depth + 1;
|
||||
const propPath = path ? `${path}.${name}` : name;
|
||||
|
||||
return (
|
||||
<Box
|
||||
paddingBottom={index < properties.length - 1 ? 6 : 2}
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box paddingLeft={2} flex={1}>
|
||||
<Box display="flex" flexDirection="row" marginBottom={2}>
|
||||
<Typography variant={titleVariant(propDepth)}>
|
||||
{propPath}
|
||||
</Typography>
|
||||
{isRequired(name, schema.required) && (
|
||||
<Box marginLeft={1}>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
required
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<VisibilityView schema={propSchema} />
|
||||
</Box>
|
||||
<SchemaView
|
||||
path={propPath}
|
||||
depth={propDepth}
|
||||
schema={propSchema}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SchemaView(props: SchemaViewProps) {
|
||||
// TODO(Rugvip): allOf, anyOf, oneOf
|
||||
switch (props.schema.type) {
|
||||
case 'array':
|
||||
return <ArrayView {...props} />;
|
||||
case 'object':
|
||||
case undefined:
|
||||
return <ObjectView {...props} />;
|
||||
default:
|
||||
return <ScalarView {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SchemaViewerProps {
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
export const SchemaViewer = ({ schema }: SchemaViewerProps) => {
|
||||
return <CodeSnippet language="json" text={JSON.stringify(schema, null, 2)} />;
|
||||
return <SchemaView schema={schema} path="" depth={0} />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user