Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
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
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@
"react-dom": ">=16.8"
},
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@stoplight/json": "^3.10.0",
"@stoplight/json-schema-tree": "^1.0.0",
"@stoplight/mosaic": "^1.0.0-beta.22",
"@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",
Expand All @@ -61,6 +64,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
38 changes: 22 additions & 16 deletions src/__stories__/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Checkbox, Icon } from '@stoplight/ui-kit';
import { Button, Flex, InvertTheme, subscribeTheme } 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 All @@ -12,6 +12,8 @@ const allOfSchema = require('../__fixtures__/combiners/allOfs/base.json');
const schema = require('../__fixtures__/default-schema.json');
const stressSchema = require('../__fixtures__/stress-schema.json');

subscribeTheme({ mode: 'light' });

storiesOf('JsonSchemaViewer', module)
.addDecorator(withKnobs)
.addDecorator(storyFn => <Wrapper>{storyFn()}</Wrapper>)
Expand Down Expand Up @@ -47,10 +49,10 @@ storiesOf('JsonSchemaViewer', module)
return (
<>
<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" />} />
<Checkbox className="mb-0" />
</div>
<Flex h="full" alignItems="center">
<Button pl={1} mr={1} size="sm" appearance="minimal" icon="issue" />
<input type="checkbox" />
</Flex>
</>
);
};
Expand Down Expand Up @@ -149,14 +151,18 @@ storiesOf('JsonSchemaViewer', module)
mergeAllOf={boolean('mergeAllOf', true)}
/>
))
.add('dark', () => (
<div style={{ height: '100vh' }} className="bp3-dark bg-gray-8">
<JsonSchemaViewer
schema={schema as JSONSchema4}
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
expanded={boolean('expanded', false)}
onGoToRef={action('onGoToRef')}
mergeAllOf={boolean('mergeAllOf', true)}
/>
</div>
));
.add('dark', () => {
return (
<InvertTheme>
<div style={{ height: '100vh' }}>
<JsonSchemaViewer
schema={schema as JSONSchema4}
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
expanded={boolean('expanded', false)}
onGoToRef={action('onGoToRef')}
mergeAllOf={boolean('mergeAllOf', true)}
/>
</div>
</InvertTheme>
);
});
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 pos="relative" h="full" className={cn(className, 'JsonSchemaViewer')}>
<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
103 changes: 52 additions & 51 deletions src/components/SchemaRow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { isReferenceNode, isRegularNode, ReferenceNode, SchemaNode, SchemaNodeKind } from '@stoplight/json-schema-tree';
import { Box, Flex, Icon } from '@stoplight/mosaic';
import { IRowRendererOptions, isParentNode, Tree } from '@stoplight/tree-list';
import { Optional } from '@stoplight/types';
import { Icon, Tooltip } from '@stoplight/ui-kit';
import cn from 'classnames';
import * as React from 'react';

import { CARET_ICON_BOX_DIMENSION, CARET_ICON_SIZE, SCHEMA_ROW_OFFSET } from '../consts';
Expand All @@ -11,8 +11,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,55 +51,53 @@ 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}
/>
) : null}
<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]} />}
{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>
<Flex flex={1} textOverflow="truncate" fontSize="base">
<Property onGoToRef={onGoToRef} />
<Format />
</Flex>
<Properties
required={isPropertyRequired(schemaNode)}
deprecated={isRegularNode(schemaNode) && schemaNode.deprecated}
validations={isRegularNode(schemaNode) ? schemaNode.validations : {}}
/>
</Flex>

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

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

{isBrokenRef && (
<Tooltip content={refNode!.error!}>
<Icon className="text-red-5 dark:text-red-4" icon="warning-sign" iconSize={12} />
</Tooltip>
// TODO (JJ): Add mosaic tooltip showing ref error
<Icon title={refNode!.error!} color="danger" icon={faExclamationTriangle} size="sm" />
)}
{!rowOptions.isExpanded && <Divider />}
</>
);
};
Expand All @@ -110,16 +109,18 @@ export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, tree
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"
<Box flex={1} px={2} w="full" maxW="full" className={className}>
<Box
alignItems="center"
pos="relative"
fontSize="sm"
style={{
marginLeft: CARET_ICON_BOX_DIMENSION * Tree.getLevel(treeListNode), // offset for spacing
}}
>
<SchemaPropertyRow onGoToRef={onGoToRef} rowOptions={rowOptions} />
</div>
</div>
</Box>
</Box>
</TreeListNodeContext.Provider>
</SchemaNodeContext.Provider>
);
Expand Down
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="sl-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 = 18;
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
40 changes: 7 additions & 33 deletions src/components/__tests__/SchemaRow.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,16 @@
import 'jest-enzyme';

import { Icon } from '@stoplight/mosaic';
import { TreeState } from '@stoplight/tree-list';
import { Popover, Tooltip } from '@stoplight/ui-kit';
import { mount, shallow } from 'enzyme';
import { mount } from 'enzyme';
import { JSONSchema4 } from 'json-schema';
import * as React from 'react';

import { SchemaTreeListTree } from '../../tree';
import { SchemaPropertyRow, SchemaRow } from '../SchemaRow';
import { Validations } from '../shared/Validations';
import { SchemaRow } from '../SchemaRow';
import { Properties } from '../shared/Properties';

describe('SchemaRow component', () => {
it('should render falsy validations', () => {
const tree = new SchemaTreeListTree(
{
enum: [null, 0, false, ''],
},
new TreeState(),
{
expandedDepth: Infinity,
mergeAllOf: false,
resolveRef: void 0,
},
);

tree.populate();

const wrapper = shallow(
mount(<SchemaRow treeListNode={tree.itemAt(0)!} rowOptions={{}} />)
.find(SchemaPropertyRow)
.find(Validations)
.find(Popover)
.prop('content') as React.ReactElement,
);

expect(wrapper).toHaveText('enum:null,0,false,');
});

describe('resolving error', () => {
let tree: SchemaTreeListTree;
let schema: JSONSchema4;
Expand All @@ -62,7 +36,7 @@ describe('SchemaRow component', () => {

it('given no custom resolver, should render a generic error message', () => {
const wrapper = mount(<SchemaRow treeListNode={tree.itemAt(1)!} rowOptions={{}} />);
expect(wrapper.find(Tooltip)).toHaveProp('content', `Could not resolve '#/properties/foo'`);
expect(wrapper.find(Icon)).toHaveProp('title', `Could not resolve '#/properties/foo'`);
wrapper.unmount();
});

Expand All @@ -80,7 +54,7 @@ describe('SchemaRow component', () => {
tree.populate();

const wrapper = mount(<SchemaRow treeListNode={tree.itemAt(1)!} rowOptions={{}} />);
expect(wrapper.find(Tooltip)).toHaveProp('content', message);
expect(wrapper.find(Icon)).toHaveProp('title', message);
wrapper.unmount();
});
});
Expand All @@ -98,7 +72,7 @@ describe('SchemaRow component', () => {
tree.populate();

const wrapper = mount(<SchemaRow treeListNode={tree.itemAt(pos)!} rowOptions={{}} />);
expect(wrapper.find(Validations)).toHaveProp('required', value);
expect(wrapper.find(Properties)).toHaveProp('required', value);
wrapper.unmount();
}

Expand Down
Loading