Sidebar code mirror interactions (#2433)

* Lifting path clicked up to sidebar root

* Abstracting out path click to story (and app)

* Adding click to scroll, updating sidebar components

* Scroll fix for sidebar

* Cleaning up log

* PR feedback

* PR Feedback, fix schemas & request body rendering

* PR feedback & cleanup

* Prop value checking, selection positioning

* Pulling remote import

* Styled components into insomnia-app, obj destructuring, typing

* Refactoring item path mapping

* Abstracting the mapping of specs for position, bumping down styled components version.

* Directly passing ... rest args initial work

* Pulling logs, removing array concatenation

* Pulling click handler out of render method

* Rolling position mapping into scroll positining method

* Opening up type, could be number/string/array

* Update package-lock.json

* Grabbing height from window, typing API vars, checking path

* Creating scroll method dedicated to design mode

* Moving typing of spec related content to Props def.

* Type checking on sections, invalid section component
This commit is contained in:
Mike Ellan authored and GitHub committed 2020-08-03 13:57:12 -04:00
1 parent e9fb34490a
commit 784357f089
21 files changed
+2981 -8445

No files matched your search

+2446 -8158
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -69,6 +69,5 @@
"lint-staged": "^10.2.2",
"prettier": "^1.16.4",
"rimraf": "^2.6.3"
},
"dependencies": {}
}
}
@@ -160,6 +160,19 @@ class CodeEditor extends React.Component {
setSelection(chStart, chEnd, lineStart, lineEnd) {
if (this.codeMirror) {
this.codeMirror.setSelection({ line: lineStart, ch: chStart }, { line: lineEnd, ch: chEnd });
this.codeMirror.scrollIntoView({ line: lineStart, char: chStart });
}
}
scrollToSelection(chStart, chEnd, lineStart, lineEnd) {
const selectionFocusPos = window.innerHeight / 2 - 100;
if (this.codeMirror) {
this.codeMirror.setSelection({ line: lineStart, ch: chStart }, { line: lineEnd, ch: chEnd });
this.codeMirror.scrollIntoView(
{ line: lineStart, char: chStart },
// If sizing permits, position selection just above center
selectionFocusPos,
);
}
}
@@ -1,48 +0,0 @@
// @flow
import * as React from 'react';
import classnames from 'classnames';
import autobind from 'autobind-decorator';
type Props = {|
name: string,
onClick: (SyntheticEvent<HTMLButtonElement>) => void,
children?: React.Node,
|};
type State = {|
open: boolean,
|};
@autobind
class SpecEditorSidebarItem extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
open: false,
};
}
_handleExpand(e: SyntheticEvent<HTMLButtonElement>) {
this.setState(state => ({ open: !state.open }));
}
render() {
const { name, onClick, children } = this.props;
const { open } = this.state;
return (
<li>
<button onClick={this._handleExpand}>
{children ? (
<i className={classnames('fa', open ? 'fa-caret-down' : 'fa-caret-right')} />
) : (
<i className="fa fa-empty" />
)}
</button>
<button onClick={onClick}>{name}</button>
{open ? children : null}
</li>
);
}
}
export default SpecEditorSidebarItem;
@@ -1,8 +1,9 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import autobind from 'autobind-decorator';
import YAML from 'yaml';
import SpecEditorSidebarItem from './spec-editor-sidebar-item';
import YAMLSourceMap from 'yaml-source-map';
import { Sidebar } from 'insomnia-components';
import type { ApiSpec } from '../../../models/api-spec';
import { trackEvent } from '../../../common/analytics';
@@ -16,8 +17,13 @@ type State = {|
error: string,
|};
const StyledSpecEditorSidebar: React.ComponentType<{}> = styled.div`
overflow: hidden;
overflow-y: auto;
`;
@autobind
class SpecEditorSidebar extends React.PureComponent<Props, State> {
class SpecEditorSidebar extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@@ -37,120 +43,46 @@ class SpecEditorSidebar extends React.PureComponent<Props, State> {
handleSetSelection(pos.start.col - 1, pos.end.col - 1, pos.start.line - 1, pos.end.line - 1);
}
componentDidUpdate() {
const { apiSpec } = this.props;
const { error } = this.state;
if (apiSpec.type === 'json') {
this.setState({
error:
'Tree navigation is not yet supported for JSON-formatted spec ' +
'files. Please use YAML instead.',
});
} else if (error) {
this.setState({ error: '' });
}
_mapPosition(itemPath: Array<any>) {
const sourceMap = new YAMLSourceMap();
const specMap = sourceMap.index(
YAML.parseDocument(this.props.apiSpec.contents, { keepCstNodes: true }),
);
const itemMappedPosition = sourceMap.lookup(itemPath, specMap);
const isServersSection = itemPath[0] === 'servers';
const scrollPosition = {
start: {
line: 0,
col: 0,
},
end: {
line: 0,
col: 200,
},
};
isServersSection
? (scrollPosition.start.line = itemMappedPosition.start.line)
: (scrollPosition.start.line = itemMappedPosition.start.line - 1);
scrollPosition.end.line = scrollPosition.start.line;
this._handleScrollEditor(scrollPosition);
}
renderMap(collection: Object, path: string) {
const children = [];
for (let i = 0; i < collection.items.length; i++) {
const curr: Object = collection.items[i];
const next: ?Object = collection.items[i + 1];
// If the next one is a map value, we know we have a key in current
if (!next || next.type !== 'MAP_VALUE') {
continue;
}
const newPath = `${path}.${curr.strValue}`;
children.push(
<SpecEditorSidebarItem
key={newPath}
name={curr.strValue}
onClick={() => this._handleScrollEditor(curr.rangeAsLinePos)}>
{this.renderNext(next, newPath)}
</SpecEditorSidebarItem>,
);
// Bump one more since we used both curr and next
i++;
}
return <ul key={path}>{children}</ul>;
}
renderSequence(collection: Object, path: string) {
const children = [];
for (let i = 0; i < collection.items.length; i++) {
const item = collection.items[i];
if (item.type !== 'SEQ_ITEM') {
continue;
}
const newPath = `${path}[${i}]`;
children.push(
<SpecEditorSidebarItem
key={newPath}
name={String(i)}
onClick={() => this._handleScrollEditor(item.rangeAsLinePos)}>
{this.renderNext(item, newPath)}
</SpecEditorSidebarItem>,
);
}
return <ul key={path}>{children}</ul>;
}
renderNext(thing: Object, path: string) {
if (thing.type === 'MAP') {
return this.renderMap(thing, path);
} else if (thing.type === 'SEQ') {
return this.renderSequence(thing, path);
} else if (thing.node) {
// Try rendering child node if it has one
return this.renderNext(thing.node, path);
}
// Everything else terminates recursion (eg. FLOW_SEQ, MAP_SEQ)
return null;
}
_handleItemClick = (...itemPath): void => {
this._mapPosition(itemPath);
};
render() {
const { error } = this.state;
if (error) {
return <p className="notice error margin-sm">{error}</p>;
}
const { apiSpec } = this.props;
const specCst = YAML.parseCST(apiSpec.contents);
let specJSON = {};
if (apiSpec.contentType === 'yaml') {
specJSON = YAML.parse(apiSpec.contents);
}
if (!specCst) {
return null;
}
const [document] = specCst;
if (!document.contents) {
return null;
}
// const navigationEl = document.contents.map(v => this.renderNext(v, '$'));
const specJSON = YAML.parse(this.props.apiSpec.contents);
return (
<div className="spec-editor-sidebar">
<Sidebar jsonData={specJSON} />
{/* navigationEl */}
</div>
<StyledSpecEditorSidebar>
<Sidebar jsonData={specJSON} onClick={this._handleItemClick} />
</StyledSpecEditorSidebar>
);
}
}
@@ -103,7 +103,7 @@ class WrapperDesign extends React.PureComponent<Props, State> {
return;
}
editor.setSelection(chStart, chEnd, lineStart, lineEnd);
editor.scrollToSelection(chStart, chEnd, lineStart, lineEnd);
}
_handleLintClick(notice: {}) {
@@ -49,6 +49,11 @@
}
}
.spec-editor-sidebar {
overflow: hidden;
overflow-y: auto;
}
#swagger-ui-wrapper {
border-left: 1px solid var(--hl-sm);
overflow-y: scroll;
+97 -21
View File
@@ -79,7 +79,6 @@
"version": "7.10.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.5.tgz",
"integrity": "sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig==",
"dev": true,
"requires": {
"@babel/types": "^7.10.5",
"jsesc": "^2.5.1",
@@ -89,8 +88,7 @@
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
}
}
},
@@ -98,7 +96,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz",
"integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==",
"dev": true,
"requires": {
"@babel/types": "^7.10.4"
}
@@ -205,7 +202,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz",
"integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==",
"dev": true,
"requires": {
"@babel/helper-get-function-arity": "^7.10.4",
"@babel/template": "^7.10.4",
@@ -216,7 +212,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz",
"integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==",
"dev": true,
"requires": {
"@babel/types": "^7.10.4"
}
@@ -243,7 +238,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz",
"integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==",
"dev": true,
"requires": {
"@babel/types": "^7.10.4"
}
@@ -326,7 +320,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz",
"integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==",
"dev": true,
"requires": {
"@babel/types": "^7.10.4"
}
@@ -1141,7 +1134,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
"integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/parser": "^7.10.4",
@@ -1152,7 +1144,6 @@
"version": "7.10.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.5.tgz",
"integrity": "sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.10.5",
@@ -1169,7 +1160,6 @@
"version": "7.10.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.5.tgz",
"integrity": "sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
@@ -1348,8 +1338,7 @@
"@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
"dev": true
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@emotion/utils": {
"version": "0.11.3",
@@ -4040,11 +4029,21 @@
"recast": "^0.14.7"
}
},
"babel-plugin-styled-components": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.11.1.tgz",
"integrity": "sha512-YwrInHyKUk1PU3avIRdiLyCpM++18Rs1NgyMXEAQC33rIXs/vro0A+stf4sT0Gf22Got+xRWB8Cm0tw+qkRzBA==",
"requires": {
"@babel/helper-annotate-as-pure": "^7.0.0",
"@babel/helper-module-imports": "^7.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11"
}
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=",
"dev": true
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-plugin-transform-inline-consecutive-adds": {
"version": "0.4.3",
@@ -5092,6 +5091,11 @@
}
}
},
"camelize": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
},
"can-use-dom": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/can-use-dom/-/can-use-dom-0.1.0.tgz",
@@ -6140,6 +6144,11 @@
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
"dev": true
},
"css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
},
"css-loader": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.1.tgz",
@@ -6206,6 +6215,23 @@
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
"dev": true
},
"css-to-react-native": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-2.3.2.tgz",
"integrity": "sha512-VOFaeZA053BqvvvqIA8c9n0+9vFppVBAHCp6JgFTtTMU3Mzi+XnelJ9XC9ul3BqFzZyQ5N+H0SnwsWT2Ebchxw==",
"requires": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^3.3.0"
},
"dependencies": {
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
}
}
},
"css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -9674,8 +9700,7 @@
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"globalthis": {
"version": "1.0.1",
@@ -11307,6 +11332,11 @@
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"is-what": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.10.0.tgz",
"integrity": "sha512-U4RYCXNOmATQHlOPlOCHCfXyKEFIPqvyaKDqYRuLbD6EYKcTTfc3YXkAYjzOVxO3zt34L+Wh2feIyWrYiZ7kng=="
},
"is-window": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz",
@@ -11604,8 +11634,7 @@
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
},
"jshint": {
"version": "2.11.1",
@@ -12511,6 +12540,11 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true
},
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"memoizee": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz",
@@ -12579,6 +12613,14 @@
}
}
},
"merge-anything": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-2.4.4.tgz",
"integrity": "sha512-l5XlriUDJKQT12bH+rVhAHjwIuXWdAIecGwsYjv2LJo+dA1AeRTmeQS+3QBpO6lEthBMDi2IUMpLC1yyRvGlwQ==",
"requires": {
"is-what": "^3.3.1"
}
},
"merge-deep": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz",
@@ -17607,6 +17649,26 @@
"tslib": "^1.10.0"
}
},
"styled-components": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz",
"integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.0.0",
"@emotion/is-prop-valid": "^0.8.1",
"@emotion/unitless": "^0.7.0",
"babel-plugin-styled-components": ">= 1",
"css-to-react-native": "^2.2.2",
"memoize-one": "^5.0.0",
"merge-anything": "^2.2.4",
"prop-types": "^15.5.4",
"react-is": "^16.6.0",
"stylis": "^3.5.0",
"stylis-rule-sheet": "^0.0.10",
"supports-color": "^5.5.0"
}
},
"stylefire": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/stylefire/-/stylefire-7.0.3.tgz",
@@ -17619,6 +17681,16 @@
"tslib": "^1.10.0"
}
},
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
},
"stylis-rule-sheet": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
},
"sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
@@ -18104,8 +18176,7 @@
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
},
"to-object-path": {
"version": "0.3.0",
@@ -20902,6 +20973,11 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg=="
},
"yaml-source-map": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/yaml-source-map/-/yaml-source-map-2.1.1.tgz",
"integrity": "sha512-eILAwrW4K1WA83mJXF0PLyeBhL4t6v9xyesRYhncG1zh9UPc43mMRgFTCi2FRYpOgUvGECNlNjoiksiX2xBKVg=="
},
"yargs": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
+2
View File
@@ -164,6 +164,7 @@
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"srp-js": "^0.2.1",
"styled-components": "^4.4.1",
"swagger-ui-react": "^3.24.3",
"tough-cookie": "^2.3.1",
"url-join": "^4.0.1",
@@ -171,6 +172,7 @@
"vkbeautify": "^0.99.1",
"whatwg-fetch": "^2.0.1",
"yaml": "^1.5.0",
"yaml-source-map": "^2.1.1",
"zprint-clj": "0.2.0"
},
"devDependencies": {
@@ -21,6 +21,21 @@ import { useToggle } from 'react-use';
type Props = {|
className?: string,
jsonData: Object,
onClick: (section: string, path: any) => void,
jsonData: {
servers?: Object,
info?: Object,
paths?: Object,
components?: {
requestBodies?: Object,
responses?: Object,
parameters?: Object,
headers?: Object,
schemas?: Object,
securitySchemes?: Object,
},
},
pathItems: Array<Object>,
|};
const StyledSidebar: React.ComponentType<{}> = styled.div`
@@ -67,16 +82,13 @@ function Sidebar(props: Props) {
if (props.jsonData === null) {
return null;
}
const { servers, info } = props.jsonData;
const {
requestBodies,
responses,
parameters,
headers,
schemas,
securitySchemes,
} = props.jsonData.components;
const paths = Object.entries(props.jsonData.paths);
const { servers, info, paths } = props.jsonData || {};
const { requestBodies, responses, parameters, headers, schemas, securitySchemes } =
props.jsonData.components || {};
let pathItems = {};
if (typeof paths !== 'string') {
pathItems = Object.entries(paths || {});
}
return (
<StyledSidebar className="theme--sidebar">
@@ -150,14 +162,24 @@ function Sidebar(props: Props) {
<SidebarInfo childrenVisible={infoSec} info={info} />
</StyledSection>
)}
{serversVisible && servers && <SidebarServers servers={servers} />}
{pathsVisible && paths && <SidebarPaths paths={paths} />}
{requestsVisible && requestBodies && <SidebarRequests requests={requestBodies} />}
{responsesVisible && responses && <SidebarResponses responses={responses} />}
{parametersVisible && parameters && <SidebarParameters parameters={parameters} />}
{headersVisible && headers && <SidebarHeaders headers={headers} />}
{schemasVisible && schemas && <SidebarSchemas schemas={schemas} />}
{securityVisible && schemas && <SidebarSecurity security={securitySchemes} />}
{serversVisible && servers && <SidebarServers servers={servers} onClick={props.onClick} />}
{pathsVisible && pathItems && pathItems.length && (
<SidebarPaths paths={pathItems} onClick={props.onClick} />
)}
{requestsVisible && requestBodies && (
<SidebarRequests requests={requestBodies} onClick={props.onClick} />
)}
{responsesVisible && responses && (
<SidebarResponses responses={responses} onClick={props.onClick} />
)}
{parametersVisible && parameters && (
<SidebarParameters parameters={parameters} onClick={props.onClick} />
)}
{headersVisible && headers && <SidebarHeaders headers={headers} onClick={props.onClick} />}
{schemasVisible && schemas && <SidebarSchemas schemas={schemas} onClick={props.onClick} />}
{securityVisible && securitySchemes && (
<SidebarSecurity security={securitySchemes} onClick={props.onClick} />
)}
</StyledSidebar>
);
}
@@ -4,22 +4,24 @@ import Tooltip from '../tooltip';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
headers: Object,
onClick: (section: string, ...args: any) => void,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('header');
itemPath.push.apply(itemPath, items);
itemPath = [];
};
// Implemented as a class component because of a caveat with render props
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarHeaders extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = Object.keys(this.props.headers).filter(header =>
const { headers, onClick } = this.props;
if (Object.prototype.toString.call(headers) !== '[object Object]') {
return <StyledInvalidSection name={'header'} />;
}
const filteredValues = Object.keys(headers).filter(header =>
header.toLowerCase().includes(filter.toLocaleLowerCase()),
);
@@ -35,8 +37,8 @@ export default class SidebarHeaders extends React.Component<Props> {
<div>
<SvgIcon icon={IconEnum.indentation} />
</div>
<span onClick={() => handleClick([header])}>
<Tooltip message={this.props.headers[header].description} position="right">
<span onClick={() => onClick('components', 'headers', header)}>
<Tooltip message={headers[header].description} position="right">
{header}
</Tooltip>
</span>
@@ -28,7 +28,7 @@ function SidebarInfo(props: Props) {
<SidebarTextItem label={'Version:'} headline={version} />
</SidebarItem>
)}
{license.name && (
{license && license.name && (
<SidebarItem>
<SidebarTextItem label={'License:'} headline={license.name} />
</SidebarItem>
@@ -0,0 +1,18 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
type Props = {
name: string,
};
const StyledInvalidSection: React.ComponentType<{}> = styled.div`
padding: var(--padding-xs) var(--padding-xs) var(--padding-md) var(--padding-md);
color: var(--color-warning);
`;
const SidebarInvalidSection = ({ name }: Props) => (
<StyledInvalidSection>Error: Invalid {name} specification.</StyledInvalidSection>
);
export default SidebarInvalidSection;
@@ -4,23 +4,24 @@ import Tooltip from '../tooltip';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
parameters: Object,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('parameter');
itemPath.push.apply(itemPath, items);
itemPath = [];
onClick: (section: string, ...args: any) => void,
};
// Implemented as a class component because of a caveat with render props
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarParameters extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = Object.keys(this.props.parameters).filter(parameter =>
const { parameters, onClick } = this.props;
if (Object.prototype.toString.call(parameters) !== '[object Object]') {
return <StyledInvalidSection name={'parameter'} />;
}
const filteredValues = Object.keys(parameters).filter(parameter =>
parameter.toLowerCase().includes(filter.toLocaleLowerCase()),
);
@@ -36,8 +37,8 @@ export default class SidebarParameters extends React.Component<Props> {
<div>
<SvgIcon icon={IconEnum.indentation} />
</div>
<span onClick={() => handleClick([parameter])}>
<Tooltip message={this.props.parameters[parameter].description} position="right">
<span onClick={() => onClick('components', 'parameters', parameter)}>
<Tooltip message={parameters[parameter].description} position="right">
{parameter}
</Tooltip>
</span>
@@ -4,16 +4,11 @@ import styled from 'styled-components';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
paths: Array<any>,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('path');
itemPath.push.apply(itemPath, items);
itemPath = [];
paths: Object,
onClick: (section: string, ...args: any) => void,
};
const StyledMethods: React.ComponentType<{}> = styled.span`
@@ -24,7 +19,11 @@ const StyledMethods: React.ComponentType<{}> = styled.span`
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarPaths extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = this.props.paths.filter(pathDetail =>
const { paths, onClick } = this.props;
if (Object.prototype.toString.call(paths) !== '[object Array]') {
return <StyledInvalidSection name={'path'} />;
}
const filteredValues = paths.filter(pathDetail =>
pathDetail[0].toLowerCase().includes(filter.toLocaleLowerCase()),
);
@@ -40,7 +39,7 @@ export default class SidebarPaths extends React.Component<Props> {
<div>
<SvgIcon icon={IconEnum.indentation} />
</div>
<span onClick={() => handleClick([route])}>{route}</span>
<span onClick={() => onClick('paths', route)}>{route}</span>
</SidebarItem>
<SidebarItem>
<StyledMethods>
@@ -48,7 +47,7 @@ export default class SidebarPaths extends React.Component<Props> {
<span
key={method}
className={`method-${method}`}
onClick={() => handleClick([route, method])}>
onClick={() => onClick('paths', route, method)}>
{method}
</span>
))}
@@ -5,16 +5,11 @@ import Tooltip from '../tooltip';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
requests: Object,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('request');
itemPath.push.apply(itemPath, items);
itemPath = [];
onClick: (section: string, ...args: any) => void,
};
const StyledRequestExample: React.ComponentType<{}> = styled.span`
@@ -32,7 +27,13 @@ const StyledRequestFormat: React.ComponentType<{}> = styled.span`
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarRequests extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = Object.keys(this.props.requests).filter(requestName =>
const { requests, onClick } = this.props;
if (Object.prototype.toString.call(requests) !== '[object Object]') {
return <StyledInvalidSection name={'request'} />;
}
const filteredValues = Object.keys(requests).filter(requestName =>
requestName.toLowerCase().includes(filter.toLocaleLowerCase()),
);
@@ -43,14 +44,14 @@ export default class SidebarRequests extends React.Component<Props> {
return (
<div>
{filteredValues.map(requestName => {
const { description, content } = this.props.requests[requestName];
const { description, content } = requests[requestName];
return (
<React.Fragment key={requestName}>
<SidebarItem gridLayout>
<div>
<SvgIcon icon={IconEnum.folderOpen} />
</div>
<span onClick={() => handleClick([requestName])}>
<span onClick={() => onClick('components', 'requestBodies', requestName)}>
<Tooltip message={description} position="right">
{requestName}
</Tooltip>
@@ -61,18 +62,41 @@ export default class SidebarRequests extends React.Component<Props> {
<SidebarItem>
<StyledRequestFormat>
<SvgIcon icon={IconEnum.indentation} />
<span onClick={() => handleClick([requestFormat])}>{requestFormat}</span>
<span
onClick={() =>
onClick(
'components',
'requestBodies',
requestName,
'content',
requestFormat,
)
}>
{requestFormat}
</span>
</StyledRequestFormat>
</SidebarItem>
<SidebarItem>
{Object.keys(content[requestFormat].examples).map(requestExample => (
<StyledRequestExample
onClick={() => handleClick([requestExample])}
key={requestExample}>
{requestExample}
</StyledRequestExample>
))}
</SidebarItem>
{content[requestFormat].examples && (
<SidebarItem>
{Object.keys(content[requestFormat].examples).map(requestExample => (
<StyledRequestExample
onClick={() =>
onClick(
'components',
'requestBodies',
requestName,
'content',
requestFormat,
'examples',
requestExample,
)
}
key={requestExample}>
{requestExample}
</StyledRequestExample>
))}
</SidebarItem>
)}
</React.Fragment>
))}
</React.Fragment>
@@ -4,23 +4,24 @@ import Tooltip from '../tooltip';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
responses: Object,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('response');
itemPath.push.apply(itemPath, items);
itemPath = [];
onClick: (section: string, ...args: any) => void,
};
// Implemented as a class component because of a caveat with render props
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarResponses extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = Object.keys(this.props.responses).filter(response =>
const { responses, onClick } = this.props;
if (Object.prototype.toString.call(responses) !== '[object Object]') {
return <StyledInvalidSection name={'response'} />;
}
const filteredValues = Object.keys(responses).filter(response =>
response.toLowerCase().includes(filter.toLocaleLowerCase()),
);
@@ -35,8 +36,8 @@ export default class SidebarResponses extends React.Component<Props> {
<div>
<SvgIcon icon={IconEnum.indentation} />
</div>
<span onClick={() => handleClick([response])}>
<Tooltip message={this.props.responses[response].description} position="right">
<span onClick={() => onClick('components', 'responses', response)}>
<Tooltip message={responses[response].description} position="right">
{response}
</Tooltip>
</span>
@@ -3,23 +3,24 @@ import * as React from 'react';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
schemas: Object,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('schema');
itemPath.push.apply(itemPath, items);
itemPath = [];
onClick: (section: string, ...args: any) => void,
};
// Implemented as a class component because of a caveat with render props
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarSchemas extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = Object.keys(this.props.schemas).filter(schema =>
const { schemas, onClick } = this.props;
if (Object.prototype.toString.call(schemas) !== '[object Object]') {
return <StyledInvalidSection name={'schema'} />;
}
const filteredValues = Object.keys(schemas).filter(schema =>
schema.toLowerCase().includes(filter.toLocaleLowerCase()),
);
@@ -34,7 +35,7 @@ export default class SidebarSchemas extends React.Component<Props> {
<div>
<SvgIcon icon={IconEnum.brackets} />
</div>
<span onClick={() => handleClick([schema])}>{schema}</span>
<span onClick={() => onClick('components', 'schemas', schema)}>{schema}</span>
</SidebarItem>
))}
</div>
@@ -3,22 +3,24 @@ import * as React from 'react';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
security: Object,
onClick: (section: string, ...args: any) => void,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('security');
itemPath.push.apply(itemPath, items);
itemPath = [];
};
// Implemented as a class component because of a caveat with render props
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarSecurity extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = Object.keys(this.props.security).filter(scheme =>
const { security, onClick } = this.props;
if (Object.prototype.toString.call(security) !== '[object Object]') {
return <StyledInvalidSection name={'security'} />;
}
const filteredValues = Object.keys(security).filter(scheme =>
scheme.toLowerCase().includes(filter.toLocaleLowerCase()),
);
@@ -34,7 +36,7 @@ export default class SidebarSecurity extends React.Component<Props> {
<div>
<SvgIcon icon={IconEnum.key} />
</div>
<span onClick={() => handleClick([scheme])}>{scheme}</span>
<span onClick={() => onClick('components', 'securitySchemes', scheme)}>{scheme}</span>
</SidebarItem>
</React.Fragment>
))}
@@ -3,22 +3,22 @@ import * as React from 'react';
import SidebarItem from './sidebar-item';
import SvgIcon, { IconEnum } from '../svg-icon';
import SidebarSection from './sidebar-section';
import StyledInvalidSection from './sidebar-invalid-section';
type Props = {
servers: Array<any>,
onClick: (section: string, path: string | number) => void,
};
let itemPath = [];
const handleClick = items => {
itemPath.push('server');
itemPath.push.apply(itemPath, items);
itemPath = [];
};
// Implemented as a class component because of a caveat with render props
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
export default class SidebarServers extends React.Component<Props> {
renderBody = (filter: string): null | React.Node => {
const filteredValues = this.props.servers.filter(server =>
const { servers, onClick } = this.props;
if (!Array.isArray(servers)) {
return <StyledInvalidSection name={'server'} />;
}
const filteredValues = servers.filter(server =>
server.url.includes(filter.toLocaleLowerCase()),
);
@@ -28,13 +28,13 @@ export default class SidebarServers extends React.Component<Props> {
return (
<div>
{filteredValues.map(server => (
{filteredValues.map((server, index) => (
<React.Fragment key={server.url}>
<SidebarItem>
<div>
<SvgIcon icon={IconEnum.indentation} />
</div>
<span onClick={() => handleClick([server.url])}>{server.url}</span>
<span onClick={() => onClick('servers', index)}>{server.url}</span>
</SidebarItem>
</React.Fragment>
))}
@@ -7,6 +7,7 @@ export default {
title: 'Navigation | Sidebar',
decorators: [withKnobs, withDesign],
};
const apiSpec = {
openapi: '3.0.0',
info: {
@@ -2944,11 +2945,209 @@ const apiSpec = {
security: [],
};
export const _default = () => (
<div style={{ width: '350px' }}>
<Sidebar jsonData={apiSpec} />
</div>
);
const apiSpec2 = {
openapi: '3.0.0',
info: {
title: 'Simple API overview',
version: '2.0.0',
},
paths: {
'/': {
get: {
operationId: 'listVersionsv2',
summary: 'List API versions',
responses: {
'200': {
description: '200 response',
content: {
'application/json': {
examples: {
foo: {
value: {
versions: [
{
status: 'CURRENT',
updated: '2011-01-21T11:33:21Z',
id: 'v2.0',
links: [
{
href: 'http://127.0.0.1:8774/v2/',
rel: 'self',
},
],
},
{
status: 'EXPERIMENTAL',
updated: '2013-07-23T11:33:21Z',
id: 'v3.0',
links: [
{
href: 'http://127.0.0.1:8774/v3/',
rel: 'self',
},
],
},
],
},
},
},
},
},
},
'300': {
description: '300 response',
content: {
'application/json': {
examples: {
foo: {
value:
'{\n "versions": [\n {\n "status": "CURRENT",\n "updated": "2011-01-21T11:33:21Z",\n "id": "v2.0",\n "links": [\n {\n "href": "http://127.0.0.1:8774/v2/",\n "rel": "self"\n }\n ]\n },\n {\n "status": "EXPERIMENTAL",\n "updated": "2013-07-23T11:33:21Z",\n "id": "v3.0",\n "links": [\n {\n "href": "http://127.0.0.1:8774/v3/",\n "rel": "self"\n }\n ]\n }\n ]\n}\n',
},
},
},
},
},
},
},
},
'/v2': {
get: {
operationId: 'getVersionDetailsv2',
summary: 'Show API version details',
responses: {
'200': {
description: '200 response',
content: {
'application/json': {
examples: {
foo: {
value: {
version: {
status: 'CURRENT',
updated: '2011-01-21T11:33:21Z',
'media-types': [
{
base: 'application/xml',
type: 'application/vnd.openstack.compute+xml;version=2',
},
{
base: 'application/json',
type: 'application/vnd.openstack.compute+json;version=2',
},
],
id: 'v2.0',
links: [
{
href: 'http://127.0.0.1:8774/v2/',
rel: 'self',
},
{
href:
'http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf',
type: 'application/pdf',
rel: 'describedby',
},
{
href:
'http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl',
type: 'application/vnd.sun.wadl+xml',
rel: 'describedby',
},
{
href:
'http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl',
type: 'application/vnd.sun.wadl+xml',
rel: 'describedby',
},
],
},
},
},
},
},
},
},
'203': {
description: '203 response',
content: {
'application/json': {
examples: {
foo: {
value: {
version: {
status: 'CURRENT',
updated: '2011-01-21T11:33:21Z',
'media-types': [
{
base: 'application/xml',
type: 'application/vnd.openstack.compute+xml;version=2',
},
{
base: 'application/json',
type: 'application/vnd.openstack.compute+json;version=2',
},
],
id: 'v2.0',
links: [
{
href: 'http://23.253.228.211:8774/v2/',
rel: 'self',
},
{
href:
'http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf',
type: 'application/pdf',
rel: 'describedby',
},
{
href:
'http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl',
type: 'application/vnd.sun.wadl+xml',
rel: 'describedby',
},
],
},
},
},
},
},
},
},
},
},
},
},
};
export const _default = () => {
const [targetPath, setTargetPath] = React.useState('Awaiting click...');
const _handleItemClick = (...items) => {
setTargetPath(items.join(' > '));
};
return (
<div style={{ width: '350px' }}>
<div style={{ display: 'block', margin: '0px 0px 20px 0px' }}>Target: {targetPath}</div>
<Sidebar jsonData={apiSpec} onClick={_handleItemClick} />
</div>
);
};
export const SimpleAPI = () => {
const [targetPath, setTargetPath] = React.useState('Awaiting click...');
const _handleItemClick = (...items) => {
setTargetPath(items.join(' > '));
};
return (
<div style={{ width: '350px' }}>
<div style={{ display: 'block', margin: '0px 0px 20px 0px' }}>Target: {targetPath}</div>
<Sidebar jsonData={apiSpec2} onClick={_handleItemClick} />
</div>
);
};
_default.story = {
parameters: {