Work in progress

This commit is contained in:
Thomas Nordquist
2019-01-01 13:29:04 +01:00
parent 0af3a2ede3
commit f1a60659e8
24 changed files with 7282 additions and 50 deletions

37
app/src/App.tsx Normal file
View File

@@ -0,0 +1,37 @@
import * as React from "react";
import * as q from '../../src/Model'
import { AppBar, Toolbar, Typography, InputBase } from '@material-ui/core';
import { Tree } from "./components/Tree";
import { Sidebar } from "./components/Sidebar";
import { withStyles } from '@material-ui/core/styles';
class State {
public selectedNode?: q.TreeNode | undefined
}
export class App extends React.Component<{}, State> {
constructor(props: any) {
super(props);
this.state = {
selectedNode: undefined
}
}
public render() {
return <div>
<AppBar>
<Toolbar>
<Typography variant="h6" color="inherit">MQTT-Xplorer</Typography>
</Toolbar>
<InputBase />
</AppBar>
<Tree didSelectNode={(node: q.TreeNode) => {
this.setState({selectedNode: node})
console.log('did select', node)
}}/>
// <Sidebar node={this.state.selectedNode} />
</div>
}
}

View 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>
}
}

View 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>;
}
}

View 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',
}
}
}

View 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>
}
}

10
app/src/index.tsx Normal file
View File

@@ -0,0 +1,10 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { App } from './App'
declare var document: any
ReactDOM.render(
<App />,
document.getElementById("example")
);