Initial addition of JSON Tree Inspector

This commit is contained in:
dzannotti 2015-08-07 02:49:47 +01:00
parent 9e9d23daf3
commit 45086e246c
15 changed files with 492 additions and 1 deletions

View File

@ -47,6 +47,8 @@
"redux": "^1.0.0 || 1.0.0-rc"
},
"dependencies": {
"lodash.assign": "^3.2.0",
"react-mixin": "^1.7.0",
"react-redux": "^0.2.2",
"redux": "^1.0.0-rc"
}

View File

@ -0,0 +1,109 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { ExpandedStateHandlerMixin } from './mixins';
import JSONArrow from './JSONArrow';
import grabNode from './grab-node';
import assign from 'lodash.assign';
const styles = {
base: {
paddingTop: 2,
paddingBottom: 2,
paddingRight: 4,
letterSpacing: 1,
marginLeft: 10
},
label: {
margin: 0,
padding: 0,
display: 'inline-block',
color: '#8fa1b2'
},
span: {
color: '#C042DF',
fontSize: 12,
cursor: 'default'
},
spanExpanded: {
color: '#D7D5D8'
},
spanType: {
marginLeft: 5,
marginRight: 5,
fontSize: 16
}
};
@reactMixin.decorate(ExpandedStateHandlerMixin)
export default class JSONArrayNode extends React.Component {
defaultProps = {
data: [],
initialExpanded: false
};
// flag to see if we still need to render our child nodes
needsChildNodes = true;
// cache store for our child nodes
renderedChildren = [];
// cache store for the number of items string we display
itemString = false;
constructor(props) {
super(props);
this.state = {
expanded: this.props.initialExpanded,
createdChildNodes: false
};
}
// Returns the child nodes for each element in the array. If we have
// generated them previously, we return from cache, otherwise we create
// them.
getChildNodes() {
let childNodes = [];
if (this.state.expanded && this.needsChildNodes) {
this.props.data.forEach((element, idx) => {
childNodes.push(grabNode(idx, element));
});
this.needsChildNodes = false;
this.renderedChildren = childNodes;
}
return this.renderedChildren;
}
// Returns the "n Items" string for this node, generating and
// caching it if it hasn't been created yet.
getItemString() {
if (!this.itemString) {
this.itemString = this.props.data.length + ' Item' + (this.props.data.length > 1 ? 's' : '');
}
return this.itemString;
}
render() {
const childNodes = this.getChildNodes();
const childListStyle = {
padding: 0,
margin: 0,
listStyle: 'none',
display: (this.state.expanded) ? 'block' : 'none'
};
let containerStyle = assign({}, styles.base, styles.parentNode);
let spanStyle = assign({}, styles.span, this.state.expanded ? styles.spanExpanded : {});
return (
<li style={containerStyle} onClick={::this.handleClick}>
<JSONArrow open={this.state.expanded}/>
<label style={styles.label}>{this.props.keyName}:</label>
<span style={spanStyle}>
<span style={styles.spanType}>[]</span>
{this.getItemString()}
</span>
<ol style={childListStyle}>
{childNodes}
</ol>
</li>
);
}
}

View File

@ -0,0 +1,32 @@
import React from 'react';
import assign from 'lodash.assign';
const styles = {
base: {
display: 'inline-block',
marginLeft: -15,
'float': 'left',
transition: '150ms',
marginTop: 7,
WebkitTransition: '150ms',
MozTransition: '150ms',
borderLeft: '5px solid transparent',
borderRight: '5px solid transparent',
borderTop: '5px solid #39ace6',
WebkitTransform: 'rotateZ(-90deg)',
MozTransform: 'rotateZ(-90deg)',
transform: 'rotateZ(-90deg)'
},
open: {
WebkitTransform: 'rotateZ(0deg)',
MozTransform: 'rotateZ(0deg)',
transform: 'rotateZ(0deg)'
}
};
export default class JSONArrow extends React.Component {
render() {
const style = assign({}, styles.base, this.props.open ? styles.open : {});
return <div style={style}/>;
}
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
const styles = {
base: {
paddingTop: 2,
paddingBottom: 2,
paddingRight: 4,
letterSpacing: 1,
marginLeft: 10
},
label: {
display: 'inline-block',
marginRight: 5,
color: '#8fa1b2'
},
value: {
color: '#08c6eE'
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONBooleanNode extends React.Component {
render() {
const truthString = (this.props.value) ? 'true' : 'false';
return (
<li style={styles.base} onClick={::this.handleClick}>
<label style={styles.label}>{this.props.keyName}:</label>
<span style={styles.value}>{truthString}</span>
</li>
);
}
}

View File

@ -0,0 +1,33 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
const styles = {
base: {
paddingTop: 2,
paddingBottom: 2,
paddingRight: 4,
letterSpacing: 1,
marginLeft: 10
},
label: {
display: 'inline-block',
marginRight: 5,
color: '#8fa1b2'
},
value: {
color: '#DF113A'
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONNullNode extends React.Component {
render() {
return (
<li style={styles.base} onClick={::this.handleClick}>
<label style={styles.label}>{this.props.keyName}:</label>
<span style={styles.value}>null</span>
</li>
);
}
}

View File

@ -0,0 +1,33 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
const styles = {
base: {
paddingTop: 2,
paddingBottom: 2,
paddingRight: 4,
letterSpacing: 1,
marginLeft: 10
},
label: {
display: 'inline-block',
marginRight: 5,
color: '#8fa1b2'
},
value: {
color: '#0B75F5'
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONNumberNode extends React.Component {
render() {
return (
<li style={styles.base} onClick={::this.handleClick}>
<label style={styles.label}>{this.props.keyName}:</label>
<span style={styles.value}>{this.props.value}</span>
</li>
);
}
}

View File

@ -0,0 +1,111 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { ExpandedStateHandlerMixin } from './mixins';
import JSONArrow from './JSONArrow';
import assign from 'lodash.assign';
import grabNode from './grab-node';
const styles = {
base: {
paddingTop: 2,
paddingBottom: 2,
paddingRight: 4,
letterSpacing: 1,
marginLeft: 10
},
label: {
margin: 0,
padding: 0,
display: 'inline-block',
color: '#8fa1b2'
},
span: {
color: '#049977',
fontSize: 12,
cursor: 'default'
},
spanExpanded: {
color: '#D7D5D8'
},
spanType: {
marginLeft: 5,
marginRight: 5,
fontSize: 16
}
};
@reactMixin.decorate(ExpandedStateHandlerMixin)
export default class JSONObjectNode extends React.Component {
defaultProps = {
data: [],
initialExpanded: false
};
// cache store for the number of items string we display
itemString = false;
// flag to see if we still need to render our child nodes
needsChildNodes = true;
// cache store for our child nodes
renderedChildren = [];
constructor(props) {
super(props);
this.state = {
expanded: this.props.initialExpanded,
createdChildNodes: false
};
}
// Returns the child nodes for each element in the object. If we have
// generated them previously, we return from cache, otherwise we create
// them.
getChildNodes() {
if (this.state.expanded && this.needsChildNodes) {
const obj = this.props.data;
let childNodes = [];
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
childNodes.push(grabNode(k, obj[k]));
}
}
this.needsChildNodes = false;
this.renderedChildren = childNodes;
}
return this.renderedChildren;
}
// Returns the "n Items" string for this node, generating and
// caching it if it hasn't been created yet.
getItemString() {
if (!this.itemString) {
const len = Object.keys(this.props.data).length;
this.itemString = len + ' Item' + (len > 1 ? 's' : '');
}
return this.itemString;
}
render() {
let childListStyle = {
padding: 0,
margin: 0,
listStyle: 'none',
display: (this.state.expanded) ? 'block' : 'none'
};
let containerStyle = assign({}, styles.base, styles.parentNode);
let spanStyle = assign({}, styles.span, this.state.expanded ? styles.spanExpanded : {});
return (
<li style={containerStyle} onClick={::this.handleClick}>
<JSONArrow open={this.state.expanded}/>
<label style={styles.label}>{this.props.keyName}:</label>
<span style={spanStyle}>
<span style={styles.spanType}>&#123;&#125;</span>
{this.getItemString()}
</span>
<ul style={childListStyle}>
{this.getChildNodes()}
</ul>
</li>
);
}
}

View File

@ -0,0 +1,33 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
const styles = {
base: {
paddingTop: 2,
paddingBottom: 2,
paddingRight: 4,
letterSpacing: 1,
marginLeft: 10
},
label: {
display: 'inline-block',
marginRight: 5,
color: '#8fa1b2'
},
value: {
color: '#717c93'
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONStringNode extends React.Component {
render() {
return (
<li style={styles.base} onClick={::this.handleClick}>
<label style={styles.label}>{this.props.keyName}:</label>
<span style={styles.value}>{this.props.value}</span>
</li>
);
}
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import objType from './obj-type';
import JSONObjectNode from './JSONObjectNode';
import JSONArrayNode from './JSONArrayNode';
import JSONStringNode from './JSONStringNode';
import JSONNumberNode from './JSONNumberNode';
import JSONBooleanNode from './JSONBooleanNode';
import JSONNullNode from './JSONNullNode';
export default function(key, value) {
const nodeType = objType(value);
const aKey = key + Date.now();
if (nodeType === 'Object') {
return <JSONObjectNode data={value} keyName={key} key={aKey} />;
} else if (nodeType === 'Array') {
return <JSONArrayNode data={value} keyName={key} key={aKey} />;
} else if (nodeType === 'String') {
return <JSONStringNode keyName={key} value={value} key={aKey} />;
} else if (nodeType === 'Number') {
return <JSONNumberNode keyName={key} value={value} key={aKey} />;
} else if (nodeType === 'Boolean') {
return <JSONBooleanNode keyName={key} value={value} key={aKey} />;
} else if (nodeType === 'Null') {
return <JSONNullNode keyName={key} value={value} key={aKey} />;
}
console.error('Unknown node type:', nodeType);
return false;
}

View File

@ -0,0 +1,50 @@
// ES6 + inline style port of JSONViewer https://bitbucket.org/davevedder/react-json-viewer/
// all credits and original code to the author
// Dave Vedder <veddermatic@gmail.com> http://www.eskimospy.com/
// port by Daniele Zannotti http://www.github.com/dzannotti <dzannotti@me.com>
import React from 'react';
import objectType from './obj-type';
import JSONObjectNode from './JSONObjectNode';
import JSONArrayNode from './JSONArrayNode';
const styles = {
tree: {
border: 0,
padding: 0,
margin: 0,
fontSize: 14,
listStyle: 'none',
fontFamily: '"Helvetica Neue", Helvetica, Arial, freesans, sans-serif',
MozUserSelect: 'none',
WebkitUserSelect: 'none'
}
};
export default class JSONTree extends React.Component {
static propTypes = {
data: React.PropTypes.oneOfType([
React.PropTypes.array,
React.PropTypes.object
]).isRequired
};
constructor(props) {
super(props);
}
render() {
const nodeType = objectType(this.props.data);
let rootNode = false;
if (nodeType === 'Object') {
rootNode = <JSONObjectNode data={this.props.data} keyName="(root)" initialExpanded={true} />;
} else if (nodeType === 'Array') {
rootNode = <JSONArrayNode data={this.props.data} initialExpanded={true} keyName="(root)" />;
}
return (
<ul style={styles.tree}>
{rootNode}
</ul>
);
}
}

View File

@ -0,0 +1,15 @@
export default {
handleClick(e) {
e.stopPropagation();
this.setState({
expanded: !this.state.expanded
});
},
componentWillReceiveProps() {
// resets our caches and flags we need to build child nodes again
this.renderedChildren = [];
this.itemString = false;
this.needsChildNodes = true;
}
};

View File

@ -0,0 +1,2 @@
export { default as SquashClickEventMixin } from './squash-click-event';
export { default as ExpandedStateHandlerMixin } from './expanded-state-handler';

View File

@ -0,0 +1,5 @@
export default {
handleClick(e) {
e.stopPropagation();
}
};

View File

@ -0,0 +1,3 @@
export default function(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
}

View File

@ -1,4 +1,5 @@
import React, { PropTypes } from 'react';
import JSONTree from './JSONTree';
function hsvToRgb(h, s, v) {
const i = Math.floor(h);
@ -49,7 +50,7 @@ export default class LogMonitorEntry {
let errorText = error;
if (!errorText) {
try {
return JSON.stringify(this.props.select(state));
return <JSONTree data={this.props.select(state)} />
} catch (err) {
errorText = 'Error selecting state.';
}