Work in progress
This commit is contained in:
74
app/src/components/Sidebar.tsx
Normal file
74
app/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as React from "react";
|
||||
import * as q from '../../../src/Model'
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { ValueRenderer } from './ValueRenderer'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode | undefined
|
||||
}
|
||||
|
||||
interface State {
|
||||
node?: q.TreeNode | undefined
|
||||
}
|
||||
|
||||
export class Sidebar extends React.Component<Props, State> {
|
||||
private updateNode: (node?: q.TreeNode | undefined) => void
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {}
|
||||
this.updateNode = (node) => {
|
||||
if (!node) {
|
||||
this.setState(this.state)
|
||||
} else {
|
||||
this.setState({node: node})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getStyle(): {[s: string]: any} {
|
||||
return {
|
||||
marginTop: '64px'
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
this.props.node && this.props.node.removeListener('update', this.updateNode)
|
||||
nextProps.node && nextProps.node.on('update', this.updateNode)
|
||||
nextProps.node && this.updateNode(nextProps.node)
|
||||
}
|
||||
|
||||
private open() {
|
||||
return this.props.node !== undefined
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <Drawer open={this.open()} variant="permanent" anchor="right">
|
||||
{this.renderNode()}
|
||||
</Drawer>
|
||||
}
|
||||
|
||||
private renderNode() {
|
||||
let style: React.CSSProperties = {display: 'block', width: '40vw'}
|
||||
let topicStyle: React.CSSProperties = {width: '100%'}
|
||||
|
||||
if (!this.state.node) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div style={style}>
|
||||
<TextField style={topicStyle}
|
||||
label="Topic"
|
||||
value={this.state.node.path()}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Paper>
|
||||
<ValueRenderer node={this.state.node} />
|
||||
</Paper>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
73
app/src/components/Tree.tsx
Normal file
73
app/src/components/Tree.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as React from "react";
|
||||
import * as io from 'socket.io-client';
|
||||
import * as q from '../../../src/Model'
|
||||
import { TreeNode } from './TreeNode'
|
||||
import List from '@material-ui/core/List';
|
||||
|
||||
var throttle = require('lodash.throttle');
|
||||
|
||||
class TreeState {
|
||||
public tree: q.Tree
|
||||
public msg: any
|
||||
constructor(tree: q.Tree, msg: any) {
|
||||
this.tree = tree
|
||||
this.msg = msg
|
||||
}
|
||||
}
|
||||
|
||||
export interface TreeNodeProps {
|
||||
didSelectNode?: (node: q.TreeNode) => void
|
||||
}
|
||||
|
||||
export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
||||
private socket: SocketIOClient.Socket
|
||||
private renderDuration: number = 300
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
let tree = new q.Tree()
|
||||
this.state = new TreeState(tree, {})
|
||||
this.socket = io('http://localhost:3000');
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
let updateState = throttle((state: any) => {
|
||||
this.setState(state)
|
||||
updateState.cancel()
|
||||
updateState = throttle(() => {
|
||||
this.setState(state)
|
||||
}, Math.max(this.renderDuration * 5, 300), {trailing: true})
|
||||
}, 1000)
|
||||
|
||||
this.socket.on('message', (msg: any) => {
|
||||
const edges = msg.topic.split('/')
|
||||
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, "base64").toString())
|
||||
this.state.tree.updateWithNode(node.firstNode())
|
||||
|
||||
updateState({tree: this.state.tree, msg: msg})
|
||||
})
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.socket.removeAllListeners()
|
||||
}
|
||||
|
||||
private getStyle(): {[s: string]: any} {
|
||||
return {
|
||||
marginTop: '64px'
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div {...this.props}>
|
||||
<List style={this.getStyle()}>
|
||||
<TreeNode
|
||||
didSelectNode={this.props.didSelectNode}
|
||||
treeNode={this.state.tree}
|
||||
name="/" collapsed={false}
|
||||
performanceCallback={(ms) => this.renderDuration = ms}
|
||||
/>
|
||||
</List>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
194
app/src/components/TreeNode.tsx
Normal file
194
app/src/components/TreeNode.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import * as React from "react";
|
||||
import * as io from 'socket.io-client';
|
||||
import * as q from '../../../src/Model'
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
|
||||
export interface TreeNodeProps {
|
||||
treeNode: q.TreeNode,
|
||||
name?: string | undefined
|
||||
collapsed?: boolean | undefined
|
||||
performanceCallback?: ((ms: number) => void) | undefined
|
||||
didSelectNode?: (node: q.TreeNode) => void
|
||||
}
|
||||
|
||||
interface TreeNodeState {
|
||||
title: string | undefined,
|
||||
collapsed: boolean,
|
||||
collapsedOverride: boolean | undefined,
|
||||
edgeCount: number
|
||||
}
|
||||
|
||||
let collapseLimit = 0
|
||||
declare var performance: any
|
||||
|
||||
export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
|
||||
private dirty: boolean = true
|
||||
private willUpdateTime: number = performance.now()
|
||||
|
||||
constructor(props: TreeNodeProps, state: TreeNodeState) {
|
||||
super(props, state)
|
||||
|
||||
let edgeCount = Object.keys(props.treeNode.edges).length
|
||||
let collapsed = edgeCount > collapseLimit
|
||||
|
||||
this.props.treeNode.on('update', () => {
|
||||
this.dirty = true
|
||||
})
|
||||
|
||||
this.state = {collapsed, edgeCount: edgeCount, collapsedOverride: props.collapsed, title: props.name}
|
||||
}
|
||||
|
||||
public setState(state: any) {
|
||||
this.dirty = true
|
||||
super.setState(state)
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
return this.dirty
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.dirty = false
|
||||
if (this.props.performanceCallback) {
|
||||
let renderTime = performance.now()-this.willUpdateTime
|
||||
this.props.performanceCallback(renderTime)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUpdate() {
|
||||
if (this.props.performanceCallback) {
|
||||
this.willUpdateTime = performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
private collapsed() {
|
||||
if (this.state.collapsedOverride !== undefined) {
|
||||
return this.state.collapsedOverride
|
||||
}
|
||||
|
||||
return this.state.collapsed
|
||||
}
|
||||
|
||||
private renderNodes() {
|
||||
const edges = Object.values(this.props.treeNode.edges)
|
||||
const listItemStyle = {
|
||||
padding: '3px 8px 3px 8px'
|
||||
}
|
||||
const listStyle = {
|
||||
padding: '3px 8px 3px 16px'
|
||||
}
|
||||
|
||||
if (edges.length > 0) {
|
||||
const listItems = edges
|
||||
.map(edge => edge.node)
|
||||
.map(node => <ListItem style={listItemStyle} button key={node.hash()}>
|
||||
<TreeNode didSelectNode={this.props.didSelectNode} treeNode={node} />
|
||||
</ListItem>)
|
||||
|
||||
return <Collapse in={!this.collapsed()} timeout="auto" unmountOnExit>
|
||||
<List style={listStyle}>{listItems}</List>
|
||||
</Collapse>
|
||||
}
|
||||
}
|
||||
|
||||
private renderSourceEdge() {
|
||||
const style: React.CSSProperties = {
|
||||
fontWeight: "bold",
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
}
|
||||
let name = this.state.title || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
|
||||
|
||||
return <span style={style} onClick={() => this.toggle()}>{name}</span>
|
||||
}
|
||||
|
||||
private getStyle(): React.CSSProperties {
|
||||
return {
|
||||
display: 'block',
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps() {
|
||||
let edgeCount = Object.keys(this.props.treeNode.edges).length
|
||||
this.setState({collapsed: edgeCount > collapseLimit, edgeCount: edgeCount})
|
||||
}
|
||||
|
||||
private renderValue() {
|
||||
const style: React.CSSProperties = {
|
||||
width: "15em",
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '0',
|
||||
paddingLeft: '5px',
|
||||
display: 'inline-block',
|
||||
}
|
||||
return this.props.treeNode.value
|
||||
? <span
|
||||
style={style}
|
||||
onMouseOver={() => this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)}
|
||||
> = {this.props.treeNode.value.toString()}</span>
|
||||
: null
|
||||
}
|
||||
|
||||
private clear() {
|
||||
return <div style={{clear: 'both'}} />
|
||||
}
|
||||
|
||||
private renderTitleLine() {
|
||||
const style = {
|
||||
lineHeight: '1em'
|
||||
}
|
||||
return <div style={style}>{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}</div>
|
||||
}
|
||||
|
||||
public render() {
|
||||
const nodeStyle: React.CSSProperties = {
|
||||
//marginLeft: '10px',
|
||||
display: 'block',
|
||||
}
|
||||
|
||||
return <div style={this.getStyle()}>
|
||||
{this.renderTitleLine()}
|
||||
<div style={nodeStyle}>
|
||||
{this.clear()}
|
||||
<div style={this.subnodesStyle()}>
|
||||
{this.collapsed() ? null : this.renderNodes()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
private toggle() {
|
||||
this.setState({collapsedOverride: !this.collapsed()})
|
||||
}
|
||||
|
||||
private renderExpander() {
|
||||
if (this.state.edgeCount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.collapsed()
|
||||
? <span onClick={() => this.toggle()}>▶</span>
|
||||
: <span onClick={() => this.toggle()}>▼</span>
|
||||
}
|
||||
|
||||
private renderCollapsedSubnodes() {
|
||||
if (this.state.edgeCount === 0 || !this.collapsed()) {
|
||||
return null
|
||||
}
|
||||
|
||||
let style = {
|
||||
color: '#333'
|
||||
}
|
||||
return <span style={style}>({this.props.treeNode.leafes().length} nodes)</span>
|
||||
}
|
||||
|
||||
private subnodesStyle(): React.CSSProperties {
|
||||
return {
|
||||
display: 'block',
|
||||
}
|
||||
}
|
||||
}
|
||||
67
app/src/components/ValueRenderer.tsx
Normal file
67
app/src/components/ValueRenderer.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as React from "react";
|
||||
import * as q from '../../../src/Model'
|
||||
import ReactJson from 'react-json-view'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode | undefined
|
||||
}
|
||||
|
||||
interface State {
|
||||
node?: q.TreeNode | undefined
|
||||
}
|
||||
|
||||
export class ValueRenderer extends React.Component<Props, State> {
|
||||
private updateNode: (node?: q.TreeNode | undefined) => void
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {}
|
||||
this.updateNode = (node) => {
|
||||
if (!node) {
|
||||
this.setState(this.state)
|
||||
} else {
|
||||
this.setState({node: node})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
this.props.node && this.props.node.removeListener('update', this.updateNode)
|
||||
nextProps.node && nextProps.node.on('update', this.updateNode)
|
||||
nextProps.node && this.updateNode(nextProps.node)
|
||||
}
|
||||
|
||||
public render() {
|
||||
let node = this.props.node
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(node.value)
|
||||
} catch(error) {
|
||||
return this.renderRawValue(node.value)
|
||||
}
|
||||
|
||||
if (typeof json === 'string') {
|
||||
return this.renderRawValue(node.value)
|
||||
} else if (typeof json === 'number') {
|
||||
return this.renderRawValue(node.value)
|
||||
} else {
|
||||
return <ReactJson src={json} />
|
||||
}
|
||||
}
|
||||
|
||||
private renderRawValue(value: string) {
|
||||
let style: React.CSSProperties = {
|
||||
wordBreak: 'break-all',
|
||||
width: '100%',
|
||||
overflow: 'scroll',
|
||||
display: 'block',
|
||||
lineHeight: '1.2em',
|
||||
padding: '12px 5px 12px 5px'
|
||||
}
|
||||
|
||||
return <pre><code style={style}>{value}</code></pre>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user