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

3
.gitignore vendored
View File

@@ -2,6 +2,7 @@ node_modules
coverage coverage
build build
.nyc_output .nyc_output
.DS_Store
test.dot test.dot
test.png test.png
.awcache

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

297
package-lock.json generated
View File

@@ -169,6 +169,28 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/socket.io": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.2.tgz",
"integrity": "sha512-Ind+4qMNfQ62llyB4IMs1D8znMEBsMKohZBPqfBUIXqLQ9bdtWIbNTBWwtdcBWJKnokMZGcmWOOKslatni5vtA==",
"requires": {
"@types/node": "*"
}
},
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"requires": {
"mime-types": "~2.1.18",
"negotiator": "0.6.1"
}
},
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
},
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@@ -187,6 +209,11 @@
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
},
"arrify": { "arrify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
@@ -228,11 +255,34 @@
} }
} }
}, },
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
},
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
}, },
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
},
"base64id": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
"integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
},
"better-assert": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
"integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
"requires": {
"callsite": "1.0.0"
}
},
"bl": { "bl": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
@@ -242,6 +292,11 @@
"safe-buffer": "^5.1.1" "safe-buffer": "^5.1.1"
} }
}, },
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -276,6 +331,11 @@
"readable-stream": "> 1.0.0 < 3.0.0" "readable-stream": "> 1.0.0 < 3.0.0"
} }
}, },
"callsite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
},
"chai": { "chai": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
@@ -356,6 +416,21 @@
"minimist": "^1.1.0" "minimist": "^1.1.0"
} }
}, },
"component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
"integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
},
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"component-inherit": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -372,6 +447,11 @@
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -394,7 +474,6 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": { "requires": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -432,6 +511,69 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"engine.io": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz",
"integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==",
"requires": {
"accepts": "~1.3.4",
"base64id": "1.0.0",
"cookie": "0.3.1",
"debug": "~3.1.0",
"engine.io-parser": "~2.1.0",
"ws": "~6.1.0"
},
"dependencies": {
"ws": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz",
"integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==",
"requires": {
"async-limiter": "~1.0.0"
}
}
}
},
"engine.io-client": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.1.tgz",
"integrity": "sha512-q66JBFuQcy7CSlfAz9L3jH+v7DTT3i6ZEadYcVj2pOs8/0uJHLxKX3WBkGTvULJMdz0tUCyJag0aKT/dpXL9BQ==",
"requires": {
"component-emitter": "1.2.1",
"component-inherit": "0.0.3",
"debug": "~3.1.0",
"engine.io-parser": "~2.1.1",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"ws": "~6.1.0",
"xmlhttprequest-ssl": "~1.5.4",
"yeast": "0.1.2"
},
"dependencies": {
"ws": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz",
"integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==",
"requires": {
"async-limiter": "~1.0.0"
}
}
}
},
"engine.io-parser": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
"integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
"requires": {
"after": "0.8.2",
"arraybuffer.slice": "~0.0.7",
"base64-arraybuffer": "0.1.5",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
}
},
"es5-ext": { "es5-ext": {
"version": "0.10.46", "version": "0.10.46",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz",
@@ -585,6 +727,26 @@
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
}, },
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
"requires": {
"isarray": "2.0.1"
},
"dependencies": {
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
}
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
},
"has-flag": { "has-flag": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -607,6 +769,11 @@
"xtend": "^4.0.0" "xtend": "^4.0.0"
} }
}, },
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
},
"inflight": { "inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -737,6 +904,19 @@
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
"dev": true "dev": true
}, },
"mime-db": {
"version": "1.37.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
},
"mime-types": {
"version": "2.1.21",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
"integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
"requires": {
"mime-db": "~1.37.0"
}
},
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -852,8 +1032,12 @@
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"dev": true },
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
}, },
"next-tick": { "next-tick": {
"version": "1.0.0", "version": "1.0.0",
@@ -1999,6 +2183,11 @@
} }
} }
}, },
"object-component": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
"integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
},
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -2015,6 +2204,22 @@
"readable-stream": "^2.0.1" "readable-stream": "^2.0.1"
} }
}, },
"parseqs": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
"integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
"requires": {
"better-assert": "~1.0.0"
}
},
"parseuri": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
"integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
"requires": {
"better-assert": "~1.0.0"
}
},
"path-dirname": { "path-dirname": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
@@ -2122,6 +2327,77 @@
"crypt": ">= 0.0.1" "crypt": ">= 0.0.1"
} }
}, },
"socket.io": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz",
"integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==",
"requires": {
"debug": "~4.1.0",
"engine.io": "~3.3.1",
"has-binary2": "~1.0.2",
"socket.io-adapter": "~1.1.0",
"socket.io-client": "2.2.0",
"socket.io-parser": "~3.3.0"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"socket.io-adapter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
"integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
},
"socket.io-client": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz",
"integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==",
"requires": {
"backo2": "1.0.2",
"base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
"component-emitter": "1.2.1",
"debug": "~3.1.0",
"engine.io-client": "~3.3.1",
"has-binary2": "~1.0.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
}
},
"socket.io-parser": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
"integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
"requires": {
"component-emitter": "1.2.1",
"debug": "~3.1.0",
"isarray": "2.0.1"
},
"dependencies": {
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
}
}
},
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -2204,6 +2480,11 @@
"is-negated-glob": "^1.0.0" "is-negated-glob": "^1.0.0"
} }
}, },
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
"integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
},
"to-fast-properties": { "to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -2338,11 +2619,21 @@
"ultron": "~1.1.0" "ultron": "~1.1.0"
} }
}, },
"xmlhttprequest-ssl": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
"integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
},
"xtend": { "xtend": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
}, },
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": { "yn": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",

View File

@@ -35,8 +35,10 @@
}, },
"dependencies": { "dependencies": {
"@types/sha1": "^1.1.1", "@types/sha1": "^1.1.1",
"@types/socket.io": "^2.1.2",
"mqtt": "^2.18.8", "mqtt": "^2.18.8",
"sha1": "^1.1.1", "sha1": "^1.1.1",
"socket.io": "^2.2.0",
"tslint": "^5.12.0", "tslint": "^5.12.0",
"typescript": "^3.2.2" "typescript": "^3.2.2"
}, },

66
src/CytoscapeExport.ts Normal file
View File

@@ -0,0 +1,66 @@
import { Tree, TreeNode } from './Model'
export class CytoscapeExport {
public static renderNodeInformation(node: TreeNode): any {
return {
data: {
id: node.sourceEdge.hash(),
label: this.renderLabel(node.value)
}
}
}
public static toDot(tree: Tree): string {
let i = 1
let leaveEdges = Object.values(tree.edges)
.map(e => e.node)
.map(node => node.leafes())
.reduce((a, b) => a.concat(b), [])
.map(leave => leave.branch())
const allEdges: Array<any> = []
const nodeInformation: {[s: string]: any} = {}
leaveEdges.map(edges => edges.reduce( (prev, current) => {
let currentHash = current.sourceEdge.hash()
nodeInformation[currentHash] = this.renderNodeInformation(current)
if (current && prev) {
allEdges.push({
data: {
id: prev.sourceEdge.hash()+currentHash,
source: prev.sourceEdge.hash(),
target: currentHash,
label: this.renderLabel(current.sourceEdge.name)
}
})
}
return current
}))
return JSON.stringify(
[this.renderNodeInformation(tree)]
.concat(Object.values(nodeInformation))
.concat(allEdges), undefined, ' '
)
}
private static renderLabel(value: any): string {
let str;
if(!isNaN(value)) {
str = value
} else {
str = JSON.stringify(value)
if(str && str.length > 0) {
str = str.slice(1, -1)
}
}
if (!str) {
return ""
}
if(str.length > 20) {
str = str.slice(0, 20)+'…'
}
return str
}
}

View File

@@ -0,0 +1,10 @@
type ReadyCallback = () => void
type MessageCallback = (topic: string, payload: Buffer) => void
interface DataSource {
connect({readyCallback, messageCallback}: { readyCallback: ReadyCallback, messageCallback: MessageCallback }): void
disconnect(): void
topicSeparator: string
}
export { DataSource, ReadyCallback, MessageCallback }

View File

@@ -0,0 +1,42 @@
import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource } from './'
export class MqttSource implements DataSource {
private client: Client | undefined
private url: string
private subscription: string
public topicSeparator = '/'
constructor({url, subscription}: {url: string, subscription: string}) {
this.url = url
this.subscription = subscription
}
public connect({
readyCallback,
messageCallback
}: {
readyCallback: () => void,
messageCallback: (topic: string, message: Buffer) => void
}) {
const client = mqttConnect(this.url)
this.client = client
client.on('connect', () => {
readyCallback()
client.subscribe(this.subscription, (err: Error) => {
if (err) {
throw new Error('mqtt connection failed')
}
})
})
client.on('message', (topic, message) => {
messageCallback(topic, message)
})
}
public disconnect() {
this.client && this.client.end()
}
}

7
src/DataSource/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import { DataSource } from './DataSource'
import { MqttSource } from './MqttSource'
export {
DataSource,
MqttSource,
}

View File

@@ -8,7 +8,7 @@ export class DotExport {
let i = 1 let i = 1
let leaveEdges = Object.values(tree.edges) let leaveEdges = Object.values(tree.edges)
.map(e => e.node) .map(e => e.node)
.map(node => node.leaves()) .map(node => node.leafes())
.reduce((a, b) => a.concat(b), []) .reduce((a, b) => a.concat(b), [])
.map(leave => leave.branch()) .map(leave => leave.branch())
@@ -18,7 +18,7 @@ export class DotExport {
let currentHash = current.sourceEdge.hash() let currentHash = current.sourceEdge.hash()
nodeInformation[currentHash] = this.renderNodeInformation(current) nodeInformation[currentHash] = this.renderNodeInformation(current)
if (current && prev) { if (current && prev) {
allEdges.push(`\t${prev.sourceEdge.hash()} -> ${currentHash} [label="${current.sourceEdge.name}"]`) allEdges.push(`\t${prev.sourceEdge.hash()} -> ${currentHash} [label=${this.renderLabel(current.sourceEdge.name)}]`)
} }
return current return current
})) }))
@@ -39,10 +39,8 @@ export class DotExport {
str = value str = value
} else { } else {
str = JSON.stringify(value) str = JSON.stringify(value)
console.log(str)
if(str && str.length > 0) { if(str && str.length > 0) {
str = str.slice(1, -1) str = str.slice(1, -1)
console.log(str)
} }
} }

View File

@@ -28,10 +28,4 @@ export class Edge {
return this return this
} }
} }
// public merge(edge: Edge) {
// if (this.node && edge.node) {
// this.node.updateWithNode(edge.node)
// }
// }
} }

View File

@@ -1,27 +1,43 @@
import { Edge } from './' import { Edge } from './'
import { EventEmitter } from 'events'
export class TreeNode { export class TreeNode extends EventEmitter {
public sourceEdge: Edge public sourceEdge?: Edge
public value: any | null | undefined = undefined public value: any | null | undefined = undefined
public edges: {[s: string]: Edge} = {} public edges: {[s: string]: Edge} = {}
public collapsed = false
constructor(sourceEdge: Edge, value: any) { constructor(sourceEdge: Edge, value: any) {
super()
this.sourceEdge = sourceEdge this.sourceEdge = sourceEdge
sourceEdge.target = this sourceEdge.target = this
this.value = value this.value = value
} }
public hash(): string {
return 'N' + (this.sourceEdge ? this.sourceEdge.hash() : '')
}
public firstNode(): TreeNode { public firstNode(): TreeNode {
return this.sourceEdge.firstEdge().node return this.sourceEdge ? this.sourceEdge.firstEdge().node : this
}
public path(): string {
return this.branch()
.map(node => (node.sourceEdge && node.sourceEdge.name))
.filter(name => name !== undefined)
.join('/')
} }
private previous(): TreeNode | undefined { private previous(): TreeNode | undefined {
return this.sourceEdge.source || undefined return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
} }
public addEdge(edge: Edge) { public addEdge(edge: Edge) {
this.edges[edge.name] = edge this.edges[edge.name] = edge
edge.source = this edge.source = this
this.emit('update')
} }
public branch(): Array<TreeNode> { public branch(): Array<TreeNode> {
@@ -34,20 +50,20 @@ export class TreeNode {
} }
public updateWithNode(node: TreeNode) { public updateWithNode(node: TreeNode) {
debugger
if (node.value !== undefined) { if (node.value !== undefined) {
this.value = node.value this.value = node.value
} }
this.mergeEdges(node) this.mergeEdges(node)
this.emit('update')
} }
public leaves(): Array<TreeNode> { public leafes(): Array<TreeNode> {
if (Object.values(this.edges).length === 0) { if (Object.values(this.edges).length === 0) {
return [this] return [this]
} }
return Object.values(this.edges) return Object.values(this.edges)
.map(e => e.node.leaves()) .map(e => e.node.leafes())
.reduce((a, b) => a.concat(b), []) .reduce((a, b) => a.concat(b), [])
} }

62
src/VisExport.ts Normal file
View File

@@ -0,0 +1,62 @@
import { Tree, TreeNode } from './Model'
export class VisExport {
public static renderNodeInformation(node: TreeNode): any {
return {
id: node.sourceEdge.hash(),
label: this.renderLabel(node.value)
}
}
public static toDot(tree: Tree): string {
let i = 1
let leaveEdges = Object.values(tree.edges)
.map(e => e.node)
.map(node => node.leafes())
.reduce((a, b) => a.concat(b), [])
.map(leave => leave.branch())
const allEdges: {[s: string]: any} = {}
const nodeInformation: {[s: string]: any} = {}
leaveEdges.map(edges => edges.reduce( (prev, current) => {
let currentHash = current.sourceEdge.hash()
nodeInformation[currentHash] = this.renderNodeInformation(current)
if (current && prev) {
let edgeId = prev.sourceEdge.hash()+currentHash
allEdges[prev.sourceEdge.hash()+currentHash] = {
id: prev.sourceEdge.hash()+currentHash,
from: prev.sourceEdge.hash(),
to: currentHash,
label: this.renderLabel(current.sourceEdge.name)
}
}
return current
}))
return JSON.stringify({
nodes: [this.renderNodeInformation(tree)].concat(Object.values(nodeInformation)),
edges: Object.values(allEdges)
}, undefined, ' ')
}
private static renderLabel(value: any): string {
let str;
if(!isNaN(value)) {
str = value
} else {
str = JSON.stringify(value)
if(str && str.length > 0) {
str = str.slice(1, -1)
}
}
if (!str) {
return ""
}
if(str.length > 20) {
str = str.slice(0, 20)+'…'
}
return str
}
}

View File

@@ -1,41 +1,62 @@
import { connect as mqttConnect } from 'mqtt'
import { TopicProperties, Tree, TreeNodeFactory } from './Model' import { TopicProperties, Tree, TreeNodeFactory } from './Model'
import { MqttSource, DataSource } from './DataSource'
import { DotExport } from './DotExport' import { DotExport } from './DotExport'
// import { CytoscapeExport } from './CytoscapeExport'
// import { VisExport } from './VisExport'
import { writeFileSync } from 'fs' import { writeFileSync } from 'fs'
import { spawn } from 'child_process' import { spawn } from 'child_process'
import * as socketIO from 'socket.io'
var client = mqttConnect('mqtt://test.mosquitto.org') const server = require('http').createServer();
const topicSeparator = '/'
client.on('connect', function () {
console.log('connected')
client.subscribe('#', (err: Error) => {
if (!err) {}
})
})
let tree = new Tree() let tree = new Tree()
let dataSource: DataSource = new MqttSource({url: 'mqtt://iot.eclipse.org', subscription: '#'})
let count = 200
client.on('message', function (topic, message) { const a: Array<any> = []
// message is Buffer
const edges = topic.split(topicSeparator) const io = socketIO(server)
let value = message.toString() io.on('connection', client => {
let node = TreeNodeFactory.fromEdgesAndValue(edges, value) console.log('connection')
tree.updateWithNode(node.firstNode()) a.forEach(b => {
io.emit('message', b)
})
client.on('event', data => { /* … */ });
client.on('disconnect', () => { /* … */ });
});
server.listen(3000);
dataSource.connect({
readyCallback: () => {
console.log('connected')
},
messageCallback: (topic, payload) => {
// a.push({topic, payload})
if (payload.length > 10000) {
payload = payload.slice(0, 10000)
}
io.emit('message', {topic, payload: payload.toString('base64')})
// console.log(topic)
const edges = topic.split('/')
let value = payload.toString()
let node = TreeNodeFactory.fromEdgesAndValue(edges, value)
tree.updateWithNode(node.firstNode())
}
}) })
function writeTree() { // function writeTree() {
writeFileSync('./test.dot', DotExport.toDot(tree)) // // writeFileSync('./test.dot', DotExport.toDot(tree))
let p = spawn('dot', '-Tpng test.dot -o test.png'.split(' ')) // // writeFileSync('./test.json', CytoscapeExport.toDot(tree))
} // // writeFileSync('./vis.json', VisExport.toDot(tree))
// // let p = spawn('dot', '-Tpng test.dot -o test2.png'.split(' '))
setInterval(() => { // }
writeTree() //
console.log(tree) // setInterval(() => {
}, 2000) // writeTree()
// }, 2000)
setTimeout(() => { setTimeout(() => {
console.log(tree) dataSource.disconnect()
client.end()
}, 1000000) }, 1000000)

View File

@@ -7,5 +7,13 @@
"strict": true, "strict": true,
"lib": ["es2017"], "lib": ["es2017"],
"sourceMap": true "sourceMap": true
} },
"includes": [
"src/**/*.ts"
],
"exclude": [
"app",
"node_modules",
"**/*.spec.ts"
]
} }