Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@
"@stoplight/json-schema-tree": "^1.0.0",
"@stoplight/react-error-boundary": "^1.0.0",
"@stoplight/tree-list": "^5.0.3",
"classnames": "^2.2.6"
"classnames": "^2.2.6",
"lodash": "^4.17.19"
},
"devDependencies": {
"@rollup/plugin-typescript": "^3.1.1",
"@sambego/storybook-state": "^1.3.6",
"@stoplight/eslint-config": "^1.2.0",
"@stoplight/markdown-viewer": "^4.3.2",
"@stoplight/mosaic": "1.0.0-beta.22",
"@stoplight/scripts": "^8.2.0",
"@stoplight/storybook-config": "^2.0.6",
"@stoplight/types": "^11.9.0",
Expand All @@ -61,6 +63,7 @@
"@types/enzyme": "^3.10.8",
"@types/jest": "^26.0.18",
"@types/json-schema": "^7.0.6",
"@types/lodash": "^4.14.149",
"@types/node": "^12.7.2",
"@types/react": "16.9.2",
"@types/react-dom": "16.9.0",
Expand Down
5 changes: 3 additions & 2 deletions src/__stories__/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Button, Checkbox, Icon } from '@stoplight/ui-kit';
import { Checkbox } from '@stoplight/ui-kit';
import { Button } from '@stoplight/mosaic';
import { action } from '@storybook/addon-actions';
import { boolean, number, object, select, withKnobs } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
Expand Down Expand Up @@ -48,7 +49,7 @@ storiesOf('JsonSchemaViewer', module)
<>
<SchemaRow treeListNode={node} rowOptions={rowOptions} />
<div className="flex h-full items-center">
<Button className="pl-1 mr-1" small minimal icon={<Icon color="grey" iconSize={12} icon="issue" />} />
<Button className="pl-1 mr-1" size="sm" appearance="minimal" icon="issue" />
<Checkbox className="mb-0" />
</div>
</>
Expand Down
2 changes: 2 additions & 0 deletions src/__stories__/_styles.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
@import "~@stoplight/tree-list/styles/_tree-list.scss";
@import "~@stoplight/ui-kit/styles/_ui-kit.scss";
@import "~@stoplight/mosaic/styles.css";
@import "~@stoplight/mosaic/themes/default.css";

.JsonSchemaViewer {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', system-ui, sans-serif,
Expand Down
9 changes: 5 additions & 4 deletions src/components/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SchemaTreeRefDereferenceFn } from '@stoplight/json-schema-tree';
import { isRegularNode } from '@stoplight/json-schema-tree';
import { Box, Flex } from '@stoplight/mosaic';
import { ErrorBoundaryForwardedProps, FallbackComponent, withErrorBoundary } from '@stoplight/react-error-boundary';
import { Tree, TreeState, TreeStore } from '@stoplight/tree-list';
import cn from 'classnames';
Expand Down Expand Up @@ -118,23 +119,23 @@ export class JsonSchemaViewerComponent extends React.PureComponent<
}

return (
<div className={cn(className, 'JsonSchemaViewer flex flex-col relative')}>
<Flex className={cn(className, 'JsonSchemaViewer relative h-full')}>
<SchemaTreeContext.Provider value={this.tree}>
<ViewModeContext.Provider value={this.props.viewMode ?? 'standalone'}>
<SchemaTreeComponent expanded={expanded} schema={schema} treeStore={this.treeStore} {...props} />
</ViewModeContext.Provider>
</SchemaTreeContext.Provider>
</div>
</Flex>
);
}
}

const JsonSchemaFallbackComponent: typeof FallbackComponent = ({ error }) => {
return (
<div className="p-4">
<Box p={4}>
<b className="text-danger">Error</b>
{error !== null ? `: ${error.message}` : null}
</div>
</Box>
);
};

Expand Down
114 changes: 64 additions & 50 deletions src/components/SchemaRow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isReferenceNode, isRegularNode, ReferenceNode, SchemaNode, SchemaNodeKind } from '@stoplight/json-schema-tree';
import { IRowRendererOptions, isParentNode, Tree } from '@stoplight/tree-list';
import { Optional } from '@stoplight/types';
import { Icon, Tooltip } from '@stoplight/ui-kit';
import { Tooltip } from '@stoplight/ui-kit';
import { Box, Flex, Icon } from '@stoplight/mosaic'
import cn from 'classnames';
import * as React from 'react';

Expand All @@ -11,8 +12,9 @@ import { isCombiner } from '../guards/isCombiner';
import { useSchemaNode, useSchemaTree, useTreeListNode } from '../hooks';
import { GoToRefHandler, SchemaTreeListNode } from '../types';
import { isPropertyRequired } from '../utils/isPropertyRequired';
import { Caret, Description, Divider, Property, Validations } from './shared';
import { Caret, Description, Divider, getValidationsFromSchema, Property, Validations } from './shared';
import { Format } from './shared/Format';
import { Properties } from './shared/Properties';

export interface ISchemaRow {
className?: string;
Expand Down Expand Up @@ -50,49 +52,55 @@ export const SchemaPropertyRow: React.FunctionComponent<Pick<ISchemaRow, 'rowOpt

return (
<>
{!isBrokenRef && isParentNode(treeListNode) && Tree.getLevel(treeListNode) > 0 ? (
<Caret
isExpanded={!!rowOptions.isExpanded}
style={{
width: CARET_ICON_BOX_DIMENSION,
height: CARET_ICON_BOX_DIMENSION,
...(!isBrokenRef && Tree.getLevel(treeListNode) === 0
? {
position: 'relative',
}
: {
left: CARET_ICON_BOX_DIMENSION * -1 + SCHEMA_ROW_OFFSET / -2,
}),
}}
size={CARET_ICON_SIZE}
<Flex my={2}>
{!isBrokenRef && isParentNode(treeListNode) && Tree.getLevel(treeListNode) > 0 ? (
<Caret
isExpanded={!!rowOptions.isExpanded}
style={{
width: CARET_ICON_BOX_DIMENSION,
height: CARET_ICON_BOX_DIMENSION,
...(!isBrokenRef && Tree.getLevel(treeListNode) === 0
? {
position: 'relative',
}
: {
left: CARET_ICON_BOX_DIMENSION * -1 + SCHEMA_ROW_OFFSET / -2,
}),
}}
size={CARET_ICON_SIZE}
/>
) : null}

{schemaNode.subpath.length > 0 &&
isCombiner(schemaNode.subpath[0]) &&
schemaNode.parent?.children?.indexOf(schemaNode as any) !== 0 && <Divider kind={schemaNode.subpath[0]} />}

<Flex flex={1} className="truncate">
<Property onGoToRef={onGoToRef} />
<Format />
</Flex>
<Properties
required={isPropertyRequired(schemaNode)}
deprecated={isRegularNode(schemaNode) && schemaNode.deprecated}
validations={
isRegularNode(schemaNode)
? schemaNode.validations
: {}
}
/>
) : null}

{schemaNode.subpath.length > 0 &&
isCombiner(schemaNode.subpath[0]) &&
schemaNode.parent?.children?.indexOf(schemaNode as any) !== 0 && <Divider kind={schemaNode.subpath[0]} />}

<div className="flex-1 flex truncate">
<Property onGoToRef={onGoToRef} />
<Format />
{typeof description === 'string' && description.length > 0 && <Description value={description} />}
</div>

<Validations
required={isPropertyRequired(schemaNode)}
deprecated={isRegularNode(schemaNode) && schemaNode.deprecated}
validations={
</Flex>

{typeof description === 'string' && description.length > 0 && (
<Flex flex={1} my={2} className="truncate">
<Description value={description} />
</Flex>
)}

<Validations validations={
isRegularNode(schemaNode)
? {
...(schemaNode.enum !== null ? { enum: schemaNode.enum } : null),
...('annotations' in schemaNode && schemaNode.annotations.default
? { default: schemaNode.annotations.default }
: null),
...schemaNode.validations,
}
? getValidationsFromSchema(schemaNode)
: {}
}
/>
} />

{isBrokenRef && (
<Tooltip content={refNode!.error!}>
Expand All @@ -107,21 +115,27 @@ SchemaPropertyRow.displayName = 'JsonSchemaViewer.SchemaPropertyRow';
export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, treeListNode, rowOptions, onGoToRef }) => {
const schemaNode = treeListNode.metadata as SchemaNode;

const offsetStyle = {
marginLeft: CARET_ICON_BOX_DIMENSION * Tree.getLevel(treeListNode), // offset for spacing
};

return (
<SchemaNodeContext.Provider value={schemaNode}>
<TreeListNodeContext.Provider value={treeListNode}>
<div className={cn('px-2 flex-1 w-full max-w-full', className)}>
<div
className="flex items-center text-sm relative"
style={{
marginLeft: CARET_ICON_BOX_DIMENSION * Tree.getLevel(treeListNode), // offset for spacing
}}
<Box flex={1} px={2} className={cn('w-full max-w-full', className)}>
<Box
className="items-center text-sm relative"
style={offsetStyle}
>
<SchemaPropertyRow onGoToRef={onGoToRef} rowOptions={rowOptions} />
</div>
</div>
</Box>
{!rowOptions.isExpanded && <Divider
style={offsetStyle}
/>}
</Box>
</TreeListNodeContext.Provider>
</SchemaNodeContext.Provider>
);
};
SchemaRow.displayName = 'JsonSchemaViewer.SchemaRow';

20 changes: 18 additions & 2 deletions src/components/SchemaTree.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { isParentNode, TreeList, TreeListEvents, TreeStore } from '@stoplight/tree-list';
import { isRegularNode, SchemaNode } from '@stoplight/json-schema-tree';
import { isParentNode, TreeList, TreeListEvents, TreeListNode, TreeStore } from '@stoplight/tree-list';
import { JSONSchema4 } from 'json-schema';
import * as React from 'react';

import { GoToRefHandler, RowRenderer } from '../types';
import { SchemaRow } from './SchemaRow';
import { validationCount } from './shared/Validations';

export interface ISchemaTree {
treeStore: TreeStore;
Expand Down Expand Up @@ -42,10 +44,24 @@ export const SchemaTree: React.FC<ISchemaTree> = props => {

return (
<TreeList
className="flex-1"
draggable={false}
striped
interactive={false}
maxRows={maxRows !== void 0 ? maxRows + 0.5 : maxRows}
store={treeStore}
rowHeight={(node: TreeListNode<SchemaNode>) => {
const padding = 8;
const lineHeight = 16;
let numberOfLines = 1;
const schemaNode = node.metadata;

if (schemaNode && isRegularNode(schemaNode)) {
const hasDescription = schemaNode.annotations.description !== undefined;
const validations = validationCount(schemaNode);
numberOfLines += (validations + (hasDescription ? 1 : 0));
}
return (numberOfLines + 1) * padding + numberOfLines * lineHeight;
}}
rowRenderer={rowRenderer}
/>
);
Expand Down
8 changes: 4 additions & 4 deletions src/components/shared/Caret.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Icon, IIconProps } from '@stoplight/ui-kit';
import { Icon, IIconProps } from '@stoplight/mosaic';
import * as React from 'react';

export interface ICaret {
isExpanded: boolean;
style?: React.CSSProperties;
size?: IIconProps['iconSize'];
size?: IIconProps['size'];
}

export const Caret: React.FunctionComponent<ICaret> = ({ style, size, isExpanded }) => (
<span
className="absolute flex justify-center cursor-pointer p-1 rounded hover:bg-darken-3"
className="absolute flex justify-center cursor-pointer p-1"
role="button"
style={style}
>
<Icon
iconSize={size}
icon={isExpanded ? 'caret-down' : 'caret-right'}
icon={['fas', isExpanded ? 'chevron-down' : 'chevron-right']}
className="text-darken-9 dark:text-lighten-7"
/>
</span>
Expand Down
9 changes: 5 additions & 4 deletions src/components/shared/Description.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { MarkdownViewer } from '@stoplight/markdown-viewer';
import { Popover } from '@stoplight/ui-kit';
import { Box } from '@stoplight/mosaic';
import * as React from 'react';

export const Description: React.FunctionComponent<{ value: string }> = ({ value }) => (
<Popover
boundary="window"
interactionKind="hover"
className="ml-2 flex-1 truncate flex items-baseline"
target={<div className="text-darken-7 dark:text-lighten-7 w-full truncate">{value}</div>}
className="flex-1 truncate flex items-baseline"
target={<Box className="text-darken-7 dark:text-lighten-7 w-full truncate">{value}</Box>}
targetClassName="text-darken-7 dark:text-lighten-6 w-full truncate"
content={
<div className="p-5" style={{ maxHeight: 500, maxWidth: 400 }}>
<Box p={5} style={{ maxHeight: 500, maxWidth: 400 }}>
<MarkdownViewer markdown={value} />
</div>
</Box>
}
/>
);
11 changes: 6 additions & 5 deletions src/components/shared/Divider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { SchemaCombinerName } from '@stoplight/json-schema-tree';
import { Box, Flex } from '@stoplight/mosaic';
import * as React from 'react';

import { COMBINER_PRETTY_NAMES } from '../../consts';

export const Divider: React.FunctionComponent<{ kind: SchemaCombinerName }> = ({ kind }) => (
<div className="flex items-center w-full absolute" style={{ top: -9, height: 1 }}>
<div className="text-darken-7 dark:text-lighten-8 uppercase text-xs pr-2 -ml-4">{COMBINER_PRETTY_NAMES[kind]}</div>
<div className="flex-1 bg-darken-5 dark:bg-lighten-5" style={{ height: 1 }} />
</div>
export const Divider: React.FunctionComponent<{ kind?: SchemaCombinerName, style?: React.CSSProperties }> = ({ kind, style }) => (
<Flex align="center" className="w-full absolute" style={{ ...style, ...(kind && { top: -9 }), height: 1 }}>
{kind && <Box pr={2} ml={-4} className="text-darken-7 dark:text-lighten-8 uppercase text-xs">{COMBINER_PRETTY_NAMES[kind]}</Box>}
{!kind && <Box flex={1} className="bg-darken-5 dark:bg-lighten-5" style={{ height: 1 }} />}
</Flex>
);
12 changes: 2 additions & 10 deletions src/components/shared/Format.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import { isRegularNode, RegularNode } from '@stoplight/json-schema-tree';
import { isRegularNode } from '@stoplight/json-schema-tree';
import cn from 'classnames';
import * as React from 'react';

import { PROPERTY_TYPE_COLORS } from '../../consts';
import { useSchemaNode } from '../../hooks/useSchemaNode';

function matchPropertyColor(node: RegularNode): string | null {
if (node.types === null || node.types.length !== 1) return null;

return PROPERTY_TYPE_COLORS[node.types[0]];
}

export const Format: React.FunctionComponent = () => {
const schemaNode = useSchemaNode();

if (!isRegularNode(schemaNode) || schemaNode.format === null) {
return null;
}

return <span className={cn('ml-2', matchPropertyColor(schemaNode))}>{`<${schemaNode.format}>`}</span>;
return <span className={cn('ml-2', 'text-gray-5 dark:text-gray-3')}>{`<${schemaNode.format}>`}</span>;
};
Loading