Merge pull request #86 from chibicode/master

Extract JSONTree
This commit is contained in:
Dan Abramov 2015-08-26 20:01:59 +03:00
commit 6c81eebb0b
19 changed files with 3 additions and 746 deletions

View File

@ -47,6 +47,7 @@
"redux": "^1.0.0 || 1.0.0-rc" "redux": "^1.0.0 || 1.0.0-rc"
}, },
"dependencies": { "dependencies": {
"react-json-tree": "^0.1.3",
"react-mixin": "^1.7.0", "react-mixin": "^1.7.0",
"react-redux": "^0.9.0", "react-redux": "^0.9.0",
"redux": "^1.0.1" "redux": "^1.0.1"

View File

@ -1,125 +0,0 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { ExpandedStateHandlerMixin } from './mixins';
import JSONArrow from './JSONArrow';
import grabNode from './grab-node';
const styles = {
base: {
position: 'relative',
paddingTop: 3,
paddingBottom: 3,
paddingRight: 0,
marginLeft: 14
},
label: {
margin: 0,
padding: 0,
display: 'inline-block'
},
span: {
cursor: 'default'
},
spanType: {
marginLeft: 5,
marginRight: 5
}
};
@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() {
if (this.state.expanded && this.needsChildNodes) {
let childNodes = [];
this.props.data.forEach((element, idx) => {
let prevData;
if (typeof this.props.previousData !== 'undefined' && this.props.previousData !== null) {
prevData = this.props.previousData[idx];
}
const node = grabNode(idx, element, prevData, this.props.theme);
if (node !== false) {
childNodes.push(node);
}
});
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;
let spanStyle = {
...styles.span,
color: this.props.theme.base0E
};
containerStyle = {
...styles.base
};
if (this.state.expanded) {
spanStyle = {
...spanStyle,
color: this.props.theme.base03
};
}
return (
<li style={containerStyle}>
<JSONArrow theme={this.props.theme} open={this.state.expanded} onClick={::this.handleClick}/>
<label style={{
...styles.label,
color: this.props.theme.base0D
}} onClick={::this.handleClick}>
{this.props.keyName}:
</label>
<span style={spanStyle} onClick={::this.handleClick}>
<span style={styles.spanType}>[]</span>
{this.getItemString()}
</span>
<ol style={childListStyle}>
{childNodes}
</ol>
</li>
);
}
}

View File

@ -1,42 +0,0 @@
import React from 'react';
const styles = {
base: {
display: 'inline-block',
marginLeft: 0,
marginTop: 8,
marginRight: 5,
'float': 'left',
transition: '150ms',
WebkitTransition: '150ms',
MozTransition: '150ms',
borderLeft: '5px solid transparent',
borderRight: '5px solid transparent',
borderTopWidth: 5,
borderTopStyle: 'solid',
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() {
let style = {
...styles.base,
borderTopColor: this.props.theme.base0D
};
if (this.props.open) {
style = {
...style,
...styles.open
};
}
return <div style={style} onClick={this.props.onClick}/>;
}
}

View File

@ -1,40 +0,0 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
import hexToRgb from '../../utils/hexToRgb';
const styles = {
base: {
paddingTop: 3,
paddingBottom: 3,
paddingRight: 0,
marginLeft: 14
},
label: {
display: 'inline-block',
marginRight: 5
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONBooleanNode extends React.Component {
render() {
const truthString = (this.props.value) ? 'true' : 'false';
let backgroundColor = 'transparent';
if (this.props.previousValue !== this.props.value) {
const bgColor = hexToRgb(this.props.theme.base06);
backgroundColor = `rgba(${bgColor.r}, ${bgColor.g}, ${bgColor.b}, 0.1)`;
}
return (
<li style={{ ...styles.base, backgroundColor }} onClick={::this.handleClick}>
<label style={{
...styles.label,
color: this.props.theme.base0D
}}>
{this.props.keyName}:
</label>
<span style={{ color: this.props.theme.base09 }}>{truthString}</span>
</li>
);
}
}

View File

@ -1,143 +0,0 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { ExpandedStateHandlerMixin } from './mixins';
import JSONArrow from './JSONArrow';
import grabNode from './grab-node';
const styles = {
base: {
position: 'relative',
paddingTop: 3,
paddingBottom: 3,
paddingRight: 0,
marginLeft: 14
},
label: {
margin: 0,
padding: 0,
display: 'inline-block'
},
span: {
cursor: 'default'
},
spanType: {
marginLeft: 5,
marginRight: 5
}
};
@reactMixin.decorate(ExpandedStateHandlerMixin)
export default class JSONIterableNode 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 entry in iterable. If we have
// generated them previously, we return from cache, otherwise we create
// them.
getChildNodes() {
if (this.state.expanded && this.needsChildNodes) {
let childNodes = [];
for (const entry of this.props.data) {
let key = null;
let value = null;
if (Array.isArray(entry)) {
[key, value] = entry;
} else {
key = childNodes.length;
value = entry;
}
let prevData;
if (typeof this.props.previousData !== 'undefined' && this.props.previousData !== null) {
prevData = this.props.previousData[key];
}
const node = grabNode(key, value, prevData, this.props.theme);
if (node !== false) {
childNodes.push(node);
}
}
this.needsChildNodes = false;
this.renderedChildren = childNodes;
}
return this.renderedChildren;
}
// Returns the "n entries" string for this node, generating and
// caching it if it hasn't been created yet.
getItemString() {
if (!this.itemString) {
const { data } = this.props;
let count = 0;
if (Number.isSafeInteger(data.size)) {
count = data.size;
} else {
for (const entry of data) { // eslint-disable-line no-unused-vars
count += 1;
}
}
this.itemString = count + ' entr' + (count !== 1 ? 'ies' : 'y');
}
return this.itemString;
}
render() {
const childNodes = this.getChildNodes();
const childListStyle = {
padding: 0,
margin: 0,
listStyle: 'none',
display: (this.state.expanded) ? 'block' : 'none'
};
let containerStyle;
let spanStyle = {
...styles.span,
color: this.props.theme.base0E
};
containerStyle = {
...styles.base
};
if (this.state.expanded) {
spanStyle = {
...spanStyle,
color: this.props.theme.base03
};
}
return (
<li style={containerStyle}>
<JSONArrow theme={this.props.theme} open={this.state.expanded} onClick={::this.handleClick}/>
<label style={{
...styles.label,
color: this.props.theme.base0D
}} onClick={::this.handleClick}>
{this.props.keyName}:
</label>
<span style={spanStyle} onClick={::this.handleClick}>
<span style={styles.spanType}>()</span>
{this.getItemString()}
</span>
<ol style={childListStyle}>
{childNodes}
</ol>
</li>
);
}
}

View File

@ -1,39 +0,0 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
import hexToRgb from '../../utils/hexToRgb';
const styles = {
base: {
paddingTop: 3,
paddingBottom: 3,
paddingRight: 0,
marginLeft: 14
},
label: {
display: 'inline-block',
marginRight: 5
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONNullNode extends React.Component {
render() {
let backgroundColor = 'transparent';
if (this.props.previousValue !== this.props.value) {
const bgColor = hexToRgb(this.props.theme.base06);
backgroundColor = `rgba(${bgColor.r}, ${bgColor.g}, ${bgColor.b}, 0.1)`;
}
return (
<li style={{ ...styles.base, backgroundColor }} onClick={::this.handleClick}>
<label style={{
...styles.label,
color: this.props.theme.base0D
}}>
{this.props.keyName}:
</label>
<span style={{ color: this.props.theme.base08 }}>null</span>
</li>
);
}
}

View File

@ -1,39 +0,0 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
import hexToRgb from '../../utils/hexToRgb';
const styles = {
base: {
paddingTop: 3,
paddingBottom: 3,
paddingRight: 0,
marginLeft: 14
},
label: {
display: 'inline-block',
marginRight: 5
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONNumberNode extends React.Component {
render() {
let backgroundColor = 'transparent';
if (this.props.previousValue !== this.props.value) {
const bgColor = hexToRgb(this.props.theme.base06);
backgroundColor = `rgba(${bgColor.r}, ${bgColor.g}, ${bgColor.b}, 0.1)`;
}
return (
<li style={{ ...styles.base, backgroundColor }} onClick={::this.handleClick}>
<label style={{
...styles.label,
color: this.props.theme.base0D
}}>
{this.props.keyName}:
</label>
<span style={{ color: this.props.theme.base09 }}>{this.props.value}</span>
</li>
);
}
}

View File

@ -1,126 +0,0 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { ExpandedStateHandlerMixin } from './mixins';
import JSONArrow from './JSONArrow';
import grabNode from './grab-node';
const styles = {
base: {
position: 'relative',
paddingTop: 3,
paddingBottom: 3,
marginLeft: 14
},
label: {
margin: 0,
padding: 0,
display: 'inline-block'
},
span: {
cursor: 'default'
},
spanType: {
marginLeft: 5,
marginRight: 5
}
};
@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)) {
let prevData;
if (typeof this.props.previousData !== 'undefined' && this.props.previousData !== null) {
prevData = this.props.previousData[k];
}
const node = grabNode(k, obj[k], prevData, this.props.theme);
if (node !== false) {
childNodes.push(node);
}
}
}
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 + ' key' + (len !== 1 ? 's' : '');
}
return this.itemString;
}
render() {
let childListStyle = {
padding: 0,
margin: 0,
listStyle: 'none',
display: (this.state.expanded) ? 'block' : 'none'
};
let containerStyle;
let spanStyle = {
...styles.span,
color: this.props.theme.base0B
};
containerStyle = {
...styles.base
};
if (this.state.expanded) {
spanStyle = {
...spanStyle,
color: this.props.theme.base03
};
}
return (
<li style={containerStyle}>
<JSONArrow theme={this.props.theme} open={this.state.expanded} onClick={::this.handleClick}/>
<label style={{
...styles.label,
color: this.props.theme.base0D
}} onClick={::this.handleClick}>
{this.props.keyName}:
</label>
<span style={spanStyle} onClick={::this.handleClick}>
<span style={styles.spanType}>&#123;&#125;</span>
{this.getItemString()}
</span>
<ul style={childListStyle}>
{this.getChildNodes()}
</ul>
</li>
);
}
}

View File

@ -1,39 +0,0 @@
import React from 'react';
import reactMixin from 'react-mixin';
import { SquashClickEventMixin } from './mixins';
import hexToRgb from '../../utils/hexToRgb';
const styles = {
base: {
paddingTop: 3,
paddingBottom: 3,
paddingRight: 0,
marginLeft: 14
},
label: {
display: 'inline-block',
marginRight: 5
}
};
@reactMixin.decorate(SquashClickEventMixin)
export default class JSONStringNode extends React.Component {
render() {
let backgroundColor = 'transparent';
if (this.props.previousValue !== this.props.value) {
const bgColor = hexToRgb(this.props.theme.base06);
backgroundColor = `rgba(${bgColor.r}, ${bgColor.g}, ${bgColor.b}, 0.1)`;
}
return (
<li style={{ ...styles.base, backgroundColor }} onClick={::this.handleClick}>
<label style={{
...styles.label,
color: this.props.theme.base0D
}}>
{this.props.keyName}:
</label>
<span style={{ color: this.props.theme.base0B }}>"{this.props.value}"</span>
</li>
);
}
}

View File

@ -1,29 +0,0 @@
import React from 'react';
import objType from './obj-type';
import JSONObjectNode from './JSONObjectNode';
import JSONArrayNode from './JSONArrayNode';
import JSONIterableNode from './JSONIterableNode';
import JSONStringNode from './JSONStringNode';
import JSONNumberNode from './JSONNumberNode';
import JSONBooleanNode from './JSONBooleanNode';
import JSONNullNode from './JSONNullNode';
export default function(key, value, prevValue, theme) {
const nodeType = objType(value);
if (nodeType === 'Object') {
return <JSONObjectNode data={value} previousData={prevValue} theme={theme} keyName={key} key={key} />;
} else if (nodeType === 'Array') {
return <JSONArrayNode data={value} previousData={prevValue} theme={theme} keyName={key} key={key} />;
} else if (nodeType === 'Iterable') {
return <JSONIterableNode data={value} previousData={prevValue} theme={theme} keyName={key} key={key} />;
} else if (nodeType === 'String') {
return <JSONStringNode keyName={key} previousValue={prevValue} theme={theme} value={value} key={key} />;
} else if (nodeType === 'Number') {
return <JSONNumberNode keyName={key} previousValue={prevValue} theme={theme} value={value} key={key} />;
} else if (nodeType === 'Boolean') {
return <JSONBooleanNode keyName={key} previousValue={prevValue} theme={theme} value={value} key={key} />;
} else if (nodeType === 'Null') {
return <JSONNullNode keyName={key} previousValue={prevValue} theme={theme} value={value} key={key} />;
}
return false;
}

View File

@ -1,56 +0,0 @@
// 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,
marginTop: 8,
marginBottom: 8,
marginLeft: 2,
marginRight: 0,
fontSize: '0.90em',
listStyle: 'none',
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;
const keyName = this.props.keyName || 'root';
if (nodeType === 'Object') {
rootNode = <JSONObjectNode theme={this.props.theme} data={this.props.data} previousData={this.props.previousData} keyName={keyName} initialExpanded={true} />;
} else if (nodeType === 'Array') {
rootNode = <JSONArrayNode theme={this.props.theme} data={this.props.data} previousData={this.props.previousData} initialExpanded={true} keyName={keyName} />;
}
return (
<ul style={{
...styles.tree,
...this.props.style
}}>
{rootNode}
</ul>
);
}
}

View File

@ -1,21 +0,0 @@
import deepEqual from '../../../utils/deepEqual';
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;
},
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(this.state, nextState) || !deepEqual(this.props, nextProps);
}
};

View File

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

View File

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

View File

@ -1,8 +0,0 @@
export default function(obj) {
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj) &&
typeof obj[Symbol.iterator] === 'function'
) {
return 'Iterable';
}
return Object.prototype.toString.call(obj).slice(8, -1);
}

View File

@ -1,5 +1,5 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import JSONTree from './JSONTree'; import JSONTree from 'react-json-tree';
import LogMonitorEntryAction from './LogMonitorEntryAction'; import LogMonitorEntryAction from './LogMonitorEntryAction';
const styles = { const styles = {

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import JSONTree from './JSONTree'; import JSONTree from 'react-json-tree';
const styles = { const styles = {
actionBar: { actionBar: {

View File

@ -1,22 +0,0 @@
function deepEqual(x, y) {
if ((typeof x === 'object' && x !== null) && (typeof y === 'object' && y !== null)) {
if (Object.keys(x).length !== Object.keys(y).length) {
return false;
}
for (let prop in x) {
if (y.hasOwnProperty(prop)) {
if (!deepEqual(x[prop], y[prop])) {
return false;
}
} else {
return false;
}
}
return true;
} else if (x !== y) {
return false;
}
return true;
}
export default deepEqual;

View File

@ -1,8 +0,0 @@
export default function(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}