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

23
app/index.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<style>
body, html {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="example"></div>
<!-- Dependencies -->
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
<!-- Main -->
<script src="./build/bundle.js"></script>
</body>
</html>

6141
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
app/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "mqtt-explorer-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"awesome-typescript-loader": "^5.2.1",
"source-map-loader": "^0.2.4",
"typescript": "^3.2.2",
"webpack": "^4.28.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
},
"dependencies": {
"@material-ui/core": "^3.7.1",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/socket.io-client": "^1.4.32",
"@types/vis": "^4.21.9",
"cytoscape": "^3.3.1",
"cytoscape-dagre": "^2.2.2",
"cytoscape-expand-collapse": "^3.1.2",
"jquery": "^3.3.1",
"lodash.throttle": "^4.1.1",
"react": "^16.3.2",
"react-cytoscape": "^1.0.6",
"react-dom": "^16.3.3",
"react-json-view": "^1.19.1",
"socket.io-client": "^2.2.0",
"vis": "^4.21.0"
}
}

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")
);

22
app/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strict": true,
"lib": ["es2017"],
"outDir": "./build/",
"sourceMap": true,
"module": "commonjs",
"target": "es5",
"jsx": "react"
},
"include": [
"./src/**/*"
],
"awesomeTypescriptLoaderOptions": {
"useCache": true,
"transpileModule": true,
"errorsAsWarnings": true
}
}

36
app/webpack.config.js Normal file
View File

@@ -0,0 +1,36 @@
module.exports = {
entry: "./src/index.tsx",
output: {
filename: "bundle.js",
path: __dirname + "/build"
},
mode: 'production',
// Enable sourcemaps for debugging webpack's output.
devtool: "source-map",
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: [".ts", ".tsx", ".js", ".json"]
},
module: {
rules: [
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
{ test: /\.tsx?$/, loader: "awesome-typescript-loader" },
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
]
},
// When importing a module whose path matches one of the following, just
// assume a corresponding global variable exists and use that instead.
// This is important because it allows us to avoid bundling all of our
// dependencies, which allows browsers to cache those libraries between builds.
externals: {
// "react": "React",
// "react-dom": "ReactDOM"
}
};