Clean up & Add connection setup

This commit is contained in:
Thomas Nordquist
2019-01-06 13:30:35 +01:00
parent ad7794b30d
commit 32c3079821
26 changed files with 809 additions and 356 deletions

27
app/package-lock.json generated
View File

@@ -141,6 +141,14 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/sha1": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/sha1/-/sha1-1.1.1.tgz",
"integrity": "sha512-Yrz4TPsm/xaw7c39aTISskNirnRJj2W9OVeHv8ooOR9SG8NHEfh4lwvGeN9euzxDyPfBdFkvL/VHIY3kM45OpQ==",
"requires": {
"@types/node": "*"
}
},
"@types/socket.io-client": { "@types/socket.io-client": {
"version": "1.4.32", "version": "1.4.32",
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.32.tgz", "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.32.tgz",
@@ -924,6 +932,11 @@
"resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz",
"integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU="
}, },
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
"chokidar": { "chokidar": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
@@ -1210,6 +1223,11 @@
"which": "^1.2.9" "which": "^1.2.9"
} }
}, },
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
},
"crypto-browserify": { "crypto-browserify": {
"version": "3.12.0", "version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -4475,6 +4493,15 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"sha1": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz",
"integrity": "sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg=",
"requires": {
"charenc": ">= 0.0.1",
"crypt": ">= 0.0.1"
}
},
"shebang-command": { "shebang-command": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",

View File

@@ -18,6 +18,7 @@
"@types/node": "^10.12.18", "@types/node": "^10.12.18",
"@types/react": "^16.7.18", "@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11", "@types/react-dom": "^16.0.11",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^1.4.32", "@types/socket.io-client": "^1.4.32",
"@types/vis": "^4.21.9", "@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1", "awesome-typescript-loader": "^5.2.1",
@@ -26,6 +27,7 @@
"react": "^16.3.2", "react": "^16.3.2",
"react-dom": "^16.3.3", "react-dom": "^16.3.3",
"react-json-view": "^1.19.1", "react-json-view": "^1.19.1",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"typescript": "^3.2.2", "typescript": "^3.2.2",

View File

@@ -5,11 +5,13 @@ import { Tree } from './components/Tree/Tree'
import TitleBar from './components/TitleBar' import TitleBar from './components/TitleBar'
import Sidebar from './components/Sidebar/Sidebar' import Sidebar from './components/Sidebar/Sidebar'
import Connection from './components/ConnectionSetup/Connection' import Connection from './components/ConnectionSetup/Connection'
// import { default as EventBus } from '../../events'
import { withTheme, Theme } from '@material-ui/core/styles' import { withTheme, Theme } from '@material-ui/core/styles'
class State { interface State {
public selectedNode?: q.TreeNode | undefined selectedNode?: q.TreeNode,
connectionId?: string
} }
interface Props { interface Props {
@@ -55,7 +57,7 @@ class App extends React.Component<Props, State> {
<TitleBar /> <TitleBar />
<div> <div>
<div style={this.getStyles().left}> <div style={this.getStyles().left}>
<Tree didSelectNode={(node: q.TreeNode) => { <Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node }) this.setState({ selectedNode: node })
}} /> }} />
</div> </div>
@@ -63,7 +65,7 @@ class App extends React.Component<Props, State> {
<Sidebar node={this.state.selectedNode} /> <Sidebar node={this.state.selectedNode} />
</div> </div>
</div> </div>
<Connection /> <Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
</div > </div >
} }
} }

View File

@@ -1,12 +1,15 @@
import * as React from 'react' import * as React from 'react'
import { Typography, Toolbar, Modal, MenuItem, Button, Grid, Paper, TextField, Switch, FormControlLabel } from '@material-ui/core' import { Typography, Toolbar, Modal, MenuItem, Button, Grid, Paper, TextField, Switch, FormControlLabel } from '@material-ui/core'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles' import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
import { MqttOptions, DataSourceState } from '../../../../backend/src/DataSource'
import { addMqttConnectionEvent, makeConnectionStateEvent, rendererEvents } from '../../../../events'
import sha1 = require('sha1')
interface Props { interface Props {
classes: {[s: string]: string} classes: {[s: string]: string}
theme: Theme theme: Theme
onAbort: () => void onAbort: () => void,
onConnection: (connectionId: string) => void
} }
const protocols = [ const protocols = [
@@ -16,7 +19,6 @@ const protocols = [
interface State { interface State {
visible: boolean visible: boolean
name: string
host: string host: string
protocol: string protocol: string
port: number port: number
@@ -27,13 +29,22 @@ interface State {
password: string password: string
} }
declare var window: any
class Connection extends React.Component<Props, State> { class Connection extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.state = { const storedSettingsString = window.localStorage.getItem('connectionSettings')
let storedSettings
try {
storedSettings = storedSettingsString ? JSON.parse(storedSettingsString) : undefined
} catch {
window.localStorage.setItem('connectionSettings', undefined)
}
const defaultState = {
visible: true, visible: true,
name: '', host: 'nodered',
host: '',
protocol: protocols[0], protocol: protocols[0],
port: 1883, port: 1883,
ssl: false, ssl: false,
@@ -42,6 +53,38 @@ class Connection extends React.Component<Props, State> {
username: '', username: '',
password: '', password: '',
} }
this.state = Object.assign({}, defaultState, storedSettings)
}
private saveConnectionSettings() {
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state))
}
private optionsFromState(): MqttOptions {
const protocol = this.state.protocol === 'tcp://' ? 'mqtt://' : this.state.protocol
const url = `${protocol}${this.state.host}:${this.state.port}`
return {
url,
username: this.state.username || undefined,
password: this.state.username || undefined,
ssl: this.state.ssl,
sslValidation: this.state.sslValidation,
}
}
private connect() {
const options = this.optionsFromState()
const connectionId = (sha1(Math.random() + JSON.stringify(options)).slice(0, 8)) as string
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
rendererEvents.subscribe(makeConnectionStateEvent(connectionId), (state: DataSourceState) => {
console.log(state)
if (state.connected) {
this.props.onConnection(connectionId)
this.setState({ visible: false })
}
})
} }
public static styles: StyleRulesCallback<string> = (theme: Theme) => { public static styles: StyleRulesCallback<string> = (theme: Theme) => {
@@ -91,46 +134,6 @@ class Connection extends React.Component<Props, State> {
<form className={classes.container} noValidate autoComplete="off"> <form className={classes.container} noValidate autoComplete="off">
<Grid container spacing={24}> <Grid container spacing={24}>
<Grid item xs={5}>
<TextField
label="Name"
className={classes.textField}
value={this.state.name}
onChange={this.handleChange('name')}
margin="normal"
/>
</Grid>
<Grid item xs={1}></Grid>
<Grid item xs={3}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.sslValidation}
onChange={() => this.setState({ sslValidation: !this.state.sslValidation })}
color="primary"
/>
)}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
</Grid>
<Grid item xs={3}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.ssl}
onChange={() => this.setState({ ssl: !this.state.ssl })}
color="primary"
/>
)}
label="Encryption"
labelPlacement="bottom"
/>
</div>
</Grid>
<Grid item xs={2}> <Grid item xs={2}>
<TextField <TextField
select select
@@ -184,19 +187,49 @@ class Connection extends React.Component<Props, State> {
margin="normal" margin="normal"
/> />
</Grid> </Grid>
<Grid item xs={4}></Grid> <Grid item xs={3}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.sslValidation}
onChange={() => this.setState({ sslValidation: !this.state.sslValidation })}
color="primary"
/>
)}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
</Grid>
<Grid item xs={3}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.ssl}
onChange={() => this.setState({ ssl: !this.state.ssl })}
color="primary"
/>
)}
label="Encryption"
labelPlacement="bottom"
/>
</div>
</Grid>
<Grid item xs={6}></Grid>
</Grid> </Grid>
<br /> <br />
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
<Button variant="contained" className={classes.button}> <Button variant="contained" className={classes.button}>
Test Connection Test Connection
</Button> </Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={() => this.setState({ visible: false })}> <Button variant="contained" color="secondary" className={classes.button} onClick={() => this.saveConnectionSettings()}>
Cancel
</Button>
<Button variant="contained" color="primary" className={classes.button}>
Save Save
</Button> </Button>
<Button variant="contained" color="primary" className={classes.button} onClick={() => this.connect()}>
Connect
</Button>
</div> </div>
</form> </form>
</Paper> </Paper>

View File

@@ -53,6 +53,8 @@ class ValueRenderer extends React.Component<Props, State> {
return this.renderRawValue(node.message.value) return this.renderRawValue(node.message.value)
} else if (typeof json === 'number') { } else if (typeof json === 'number') {
return this.renderRawValue(node.message.value) return this.renderRawValue(node.message.value)
} else if (typeof json === 'boolean') {
return this.renderRawValue(node.message.value)
} else { } else {
const theme = this.props.theme.palette.type === 'dark' ? 'monokai' : 'bright:inverted' const theme = this.props.theme.palette.type === 'dark' ? 'monokai' : 'bright:inverted'
return <ReactJson return <ReactJson

View File

@@ -1,25 +1,22 @@
import * as React from 'react' import * as React from 'react'
import * as io from 'socket.io-client'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import TreeNode from './TreeNode' import TreeNode from './TreeNode'
import List from '@material-ui/core/List' import List from '@material-ui/core/List'
import { makeConnectionMessageEvent, rendererEvents } from '../../../../events'
class TreeState { import { } from '../../../../events/Events'
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
}
declare const performance: any declare const performance: any
export class Tree extends React.Component<TreeNodeProps, TreeState> { interface Props{
private socket: SocketIOClient.Socket didSelectNode?: (node: q.TreeNode) => void
connectionId?: string
}
interface TreeState {
tree: q.Tree
msg: any
}
export class Tree extends React.Component<Props, TreeState> {
private renderDuration: number = 300 private renderDuration: number = 300
private updateTimer?: any private updateTimer?: any
private lastUpdate: number = 0 private lastUpdate: number = 0
@@ -28,8 +25,7 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
const tree = new q.Tree() const tree = new q.Tree()
this.state = new TreeState(tree, {}) this.state = { tree, msg: {} }
this.socket = io('http://localhost:3000')
} }
public time(): number { public time(): number {
@@ -55,18 +51,37 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
}, Math.max(0, timeUntilNextUpdate)) }, Math.max(0, timeUntilNextUpdate))
} }
public componentDidMount() { public componentWillReceiveProps(nextProps: Props) {
this.socket.on('message', (msg: any) => { if (this.props.connectionId) {
const edges = msg.topic.split('/') const event = makeConnectionMessageEvent(this.props.connectionId)
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString()) rendererEvents.unsubscribeAll(event)
this.state.tree.updateWithNode(node.firstNode()) }
if (nextProps.connectionId) {
const event = makeConnectionMessageEvent(nextProps.connectionId)
rendererEvents.subscribe(event, this.handleNewData)
}
}
this.throttledStateUpdate({ msg, tree: this.state.tree }) public componentDidMount() {
}) if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId)
rendererEvents.subscribe(event, this.handleNewData)
}
} }
public componentWillUnmount() { public componentWillUnmount() {
this.socket.removeAllListeners() if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId)
rendererEvents.unsubscribeAll(event)
}
}
private handleNewData = (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())
this.throttledStateUpdate({ msg, tree: this.state.tree })
} }
public render() { public render() {
@@ -74,7 +89,7 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
<List> <List>
<TreeNode <TreeNode
animateChages={true} animateChages={true}
autoExpandLimit={3} autoExpandLimit={0}
isRoot={true} isRoot={true}
didSelectNode={this.props.didSelectNode} didSelectNode={this.props.didSelectNode}
treeNode={this.state.tree} treeNode={this.state.tree}

View File

@@ -1,13 +1,11 @@
import * as React from 'react' import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import { withTheme, Theme } from '@material-ui/core/styles' import { withTheme, Theme } from '@material-ui/core/styles'
const throttle = require('lodash.throttle')
import { isElementInViewport } from '../helper/isElementInViewport' import { isElementInViewport } from '../helper/isElementInViewport'
import TreeNodeTitle from './TreeNodeTitle' import TreeNodeTitle from './TreeNodeTitle'
import TreeNodeSubnodes from './TreeNodeSubnodes' import TreeNodeSubnodes from './TreeNodeSubnodes'
declare var performance: any declare var performance: any
declare var window: any
export interface TreeNodeProps { export interface TreeNodeProps {
animateChages: boolean animateChages: boolean
@@ -37,7 +35,7 @@ class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
private cssAnimationWasSetAt?: number private cssAnimationWasSetAt?: number
private willUpdateTime: number = performance.now() private willUpdateTime: number = performance.now()
private titleRef = React.createRef<HTMLElement>() private titleRef = React.createRef<HTMLDivElement>()
private subnodesDidchange = () => { private subnodesDidchange = () => {
this.dirtySubnodes = true this.dirtySubnodes = true
@@ -139,7 +137,7 @@ class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
this.dirtyState = this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false this.dirtyState = this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
return <div key={this.props.treeNode.hash()} style={ displayBlock }> return <div key={this.props.treeNode.hash()} style={ displayBlock }>
<div style={animationStyle} ref={this.titleRef}> <div style={animationStyle} ref={this.titleRef} onClick={() => this.toggle()}>
<TreeNodeTitle <TreeNodeTitle
edgeCount={this.state.edgeCount} edgeCount={this.state.edgeCount}
collapsed={this.collapsed()} collapsed={this.collapsed()}

View File

@@ -29,7 +29,11 @@ class TreeNodeSubnodes extends React.Component<Props, {}> {
const listItems = edges const listItems = edges
.map(edge => edge.target) .map(edge => edge.target)
.map(node => ( .map(node => (
<ListItem key={node.hash()} style={listItemStyle} button> <ListItem
key={node.hash()}
style={listItemStyle}
button
>
<TreeNode <TreeNode
animateChages={this.props.animateChanges} animateChages={this.props.animateChanges}
treeNode={node} treeNode={node}

View File

@@ -4,7 +4,7 @@
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true, "strictNullChecks": true,
"strict": true, "strict": true,
"lib": ["es2017"], "lib": ["es2017", "dom"],
"outDir": "./build/", "outDir": "./build/",
"sourceMap": true, "sourceMap": true,
"module": "commonjs", "module": "commonjs",
@@ -14,6 +14,9 @@
"include": [ "include": [
"./src/**/*" "./src/**/*"
], ],
"exclude": [
"**/*.d.ts"
],
"awesomeTypescriptLoaderOptions": { "awesomeTypescriptLoaderOptions": {
"useCache": true, "useCache": true,
"transpileModule": true, "transpileModule": true,

View File

@@ -12,6 +12,8 @@ module.exports = {
splitChunks: false, splitChunks: false,
}, },
target: 'electron-renderer',
mode: 'production', mode: 'production',
// Enable sourcemaps for debugging webpack's output. // Enable sourcemaps for debugging webpack's output.

View File

@@ -8,6 +8,7 @@ interface DataSource<DataSourceOptions> {
disconnect(): void disconnect(): void
onMessage(messageCallback: MessageCallback): void onMessage(messageCallback: MessageCallback): void
topicSeparator: string topicSeparator: string
stateMachine: DataSourceStateMachine
} }
export { DataSource, MessageCallback } export { DataSource, MessageCallback }

View File

@@ -1,4 +1,4 @@
import { EventEmitter } from 'events' import { EventDispatcher } from '../../../events'
export interface DataSourceState { export interface DataSourceState {
connecting: boolean connecting: boolean
@@ -6,7 +6,8 @@ export interface DataSourceState {
error?: Error error?: Error
} }
export class DataSourceStateMachine extends EventEmitter { export class DataSourceStateMachine {
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>(this)
private state: DataSourceState = { private state: DataSourceState = {
error: undefined, error: undefined,
connected: false, connected: false,
@@ -19,6 +20,7 @@ export class DataSourceStateMachine extends EventEmitter {
error: undefined, error: undefined,
connecting: false, connecting: false,
} }
this.onUpdate.dispatch(this.state)
} }
public setError(error: Error) { public setError(error: Error) {
@@ -27,6 +29,7 @@ export class DataSourceStateMachine extends EventEmitter {
connected: false, connected: false,
connecting: false, connecting: false,
} }
this.onUpdate.dispatch(this.state)
} }
public setConnecting() { public setConnecting() {
@@ -35,6 +38,7 @@ export class DataSourceStateMachine extends EventEmitter {
connected: false, connected: false,
connecting: true, connecting: true,
} }
this.onUpdate.dispatch(this.state)
} }
public toJSON() { public toJSON() {

View File

@@ -3,9 +3,14 @@ import { DataSource, DataSourceStateMachine } from './'
export interface MqttOptions { export interface MqttOptions {
url: string url: string
username?: string
password?: string
ssl: boolean
sslValidation: boolean
} }
export class MqttSource implements DataSource<MqttOptions> { export class MqttSource implements DataSource<MqttOptions> {
public stateMachine: DataSourceStateMachine = new DataSourceStateMachine()
private client: Client | undefined private client: Client | undefined
private messageCallback?: (topic: string, message: Buffer) => void private messageCallback?: (topic: string, message: Buffer) => void
private rootSubscription = '#' private rootSubscription = '#'
@@ -16,8 +21,7 @@ export class MqttSource implements DataSource<MqttOptions> {
} }
public connect(options: MqttOptions): DataSourceStateMachine { public connect(options: MqttOptions): DataSourceStateMachine {
const state = new DataSourceStateMachine() this.stateMachine.setConnecting()
const client = mqttConnect(options.url, { const client = mqttConnect(options.url, {
resubscribe: false, resubscribe: false,
}) })
@@ -25,19 +29,19 @@ export class MqttSource implements DataSource<MqttOptions> {
this.client = client this.client = client
client.on('error', (error: Error) => { client.on('error', (error: Error) => {
state.setError(error) this.stateMachine.setError(error)
}) })
client.on('close', () => { client.on('close', () => {
state.setConnected(false) this.stateMachine.setConnected(false)
}) })
client.on('reconnect', () => { client.on('reconnect', () => {
state.setConnecting() this.stateMachine.setConnecting()
}) })
client.on('connect', () => { client.on('connect', () => {
state.setConnected(true) this.stateMachine.setConnected(true)
client.subscribe(this.rootSubscription, (err: Error) => { client.subscribe(this.rootSubscription, (err: Error) => {
if (err) { if (err) {
throw new Error('mqtt subscription failed') throw new Error('mqtt subscription failed')
@@ -49,7 +53,7 @@ export class MqttSource implements DataSource<MqttOptions> {
this.messageCallback && this.messageCallback(topic, message) this.messageCallback && this.messageCallback(topic, message)
}) })
return state return this.stateMachine
} }
public disconnect() { public disconnect() {

View File

@@ -1,4 +0,0 @@
export class TopicProperties {
public topicSeparator: string = '/'
public multilevelWildcard: string | null = '#'
}

View File

@@ -3,5 +3,4 @@ export { TreeNode, TreeNodeUpdateEvents } from './TreeNode'
export { Message } from './Message' export { Message } from './Message'
export { TreeNodeFactory } from './TreeNodeFactory' export { TreeNodeFactory } from './TreeNodeFactory'
export { Tree } from './Tree' export { Tree } from './Tree'
export { TopicProperties } from './TopicProperties'
export { Hashable } from './Hashable' export { Hashable } from './Hashable'

View File

@@ -1,34 +1,46 @@
import * as socketIO from 'socket.io' import { addMqttConnectionEvent, backendEvents, makeConnectionStateEvent, makeConnectionMessageEvent, AddMqttConnection } from '../../events'
const http = require('http')
import { TopicProperties, Tree, TreeNodeFactory } from './Model'
import { MqttSource, DataSource } from './DataSource' import { MqttSource, DataSource } from './DataSource'
const options = { url: 'mqtt://nodered' } class ConnectionManager {
const dataSource = new MqttSource() private connections: {[s: string]: DataSource<any>} = {}
const a: any[] = [] public manageConnections() {
backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest)
const server = http.createServer()
const io = socketIO(server)
io.on('connection', (client) => {
console.log('connection')
a.forEach((b) => {
io.emit('message', b)
})
client.on('disconnect', () => { /* … */ })
})
server.listen(3000)
const state = dataSource.connect(options)
dataSource.onMessage((topic: string, payload: Buffer) => {
let buffer = payload
if (a.length < 30) {
a.push({ topic, payload: buffer.toString('base64') })
}
if (buffer.length > 10000) {
buffer = buffer.slice(0, 10000)
} }
io.emit('message', { topic, payload: buffer.toString('base64') }) private handleConnectionRequest = (event: AddMqttConnection) => {
}) console.log(event)
const connectionId = event.id
const options = event.options
const connection = new MqttSource()
this.connections[connectionId] = connection
connection.stateMachine.onUpdate.subscribe((state) => {
backendEvents.emit(makeConnectionStateEvent(connectionId), state)
})
connection.connect(options)
this.handleNewMessagesForConnection(connectionId, connection)
}
private handleNewMessagesForConnection(connectionId: string, connection: MqttSource) {
const messageEvent = makeConnectionMessageEvent(connectionId)
connection.onMessage((topic: string, payload: Buffer) => {
let buffer = payload
if (buffer.length > 10000) {
buffer = buffer.slice(0, 10000)
}
backendEvents.emit(messageEvent, { topic, payload: buffer.toString('base64') })
})
}
public removeConnection(hash: string) {
const connection = this.connections[hash]
connection.stateMachine
connection.disconnect()
delete this.connections[hash]
}
}
const connectionManager = new ConnectionManager()
connectionManager.manageConnections()

View File

@@ -5,7 +5,7 @@
"strictNullChecks": true, "strictNullChecks": true,
"outDir": "./build", "outDir": "./build",
"strict": true, "strict": true,
"lib": ["es2017"], "lib": ["es2017", "dom"],
"sourceMap": true "sourceMap": true
}, },
"includes": [ "includes": [
@@ -14,6 +14,8 @@
"exclude": [ "exclude": [
"app", "app",
"node_modules", "node_modules",
"src/**/*.spec.ts" "src/**/*.spec.ts",
"**/*.d.ts",
"typings"
] ]
} }

View File

@@ -1,6 +1,6 @@
// Modules to control application life and create native browser window // Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron') const {app, BrowserWindow} = require('electron')
require('mqtt-explorer-backend') require('./backend/build/backend/src/index.js')
// Keep a global reference of the window object, if you don't, the window will // Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected. // be closed automatically when the JavaScript object is garbage collected.
let mainWindow let mainWindow
@@ -27,6 +27,7 @@ function createWindow () {
// in an array if your app supports multi windows, this is the time // in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element. // when you should delete the corresponding element.
mainWindow = null mainWindow = null
app.quit()
}) })
} }

55
events/EventBus.ts Normal file
View File

@@ -0,0 +1,55 @@
import { Event } from './Events'
import { ipcMain, ipcRenderer, IpcRenderer, IpcMain } from 'electron'
interface EventBusInterface {
subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void): void
unsubscribeAll<MessageType>(event: Event<MessageType>): void
emit<MessageType>(event: Event<MessageType>, msg: MessageType): void
}
class IpcMainEventBus implements EventBusInterface {
private ipc: IpcMain
private client: any
constructor(ipc: IpcMain) {
this.ipc = ipc
}
public subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void) {
console.log('subscribing', event.topic)
this.ipc.on(event.topic, (event: any, arg: any) => {
this.client = event.sender
callback(arg)
})
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
this.ipc.removeAllListeners(event.topic)
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
this.client.send(event.topic, msg)
}
}
class IpcRendererEventBus implements EventBusInterface {
private ipc: IpcRenderer
constructor(ipc: IpcRenderer) {
this.ipc = ipc
}
public subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void) {
this.ipc.on(event.topic, (_event: any, arg: any) => callback(arg))
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
this.ipc.removeAllListeners(event.topic)
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
console.log(event.topic, msg)
this.ipc.send(event.topic, msg)
}
}
export const rendererEvents = new IpcRendererEventBus(ipcRenderer)
export const backendEvents = new IpcMainEventBus(ipcMain)

47
events/EventDispatcher.ts Normal file
View File

@@ -0,0 +1,47 @@
import { EventEmitter } from 'events'
interface CallbackStore {
wrappedCallback: any
callback: any
}
export class EventDispatcher<Message, Dispatcher> {
private emitter = new EventEmitter()
private dispatcher: Dispatcher
private callbacks: CallbackStore[] = []
constructor(dispatcher: Dispatcher) {
this.dispatcher = dispatcher
}
public dispatch(msg: Message) {
this.emitter.emit('event', msg)
}
public subscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
const wrappedCallback = (msg: Message) => {
callback(msg, this.dispatcher)
}
this.emitter.on('event', wrappedCallback)
this.callbacks.push({
callback,
wrappedCallback,
})
}
public unsubscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
const item = this.callbacks.find(store => store.callback === callback)
if (!item) {
return
}
this.emitter.removeListener('event', item.wrappedCallback)
this.callbacks = this.callbacks.filter(a => a !== item)
}
public removeAllListeners() {
this.emitter.removeAllListeners()
this.callbacks = []
}
}

39
events/Events.ts Normal file
View File

@@ -0,0 +1,39 @@
import { MqttOptions, DataSourceState } from '../backend/src/DataSource'
export interface Event<MessageType> {
topic: string
}
export interface AddMqttConnection {
id: string,
options: MqttOptions
}
export const addMqttConnectionEvent: Event<AddMqttConnection> = {
topic: 'connection/add/mqtt',
}
interface RemoveConnection {
connectionId: string,
}
export const removeConnection: Event<string> = {
topic: 'connection/remove',
}
export function makeConnectionStateEvent(connectionId: string): Event<DataSourceState> {
return {
topic: `conn/state/${connectionId}`,
}
}
interface Message {
topic: string,
payload: any
}
export function makeConnectionMessageEvent(connectionId: string): Event<Message> {
return {
topic: `conn/${connectionId}`,
}
}

0
events/Server/index.ts Normal file
View File

3
events/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './Events'
export * from './EventDispatcher'
export * from './EventBus'

9
events/test.js Normal file
View File

@@ -0,0 +1,9 @@
const { EventEmitter } = require('events');
const a = new EventEmitter()
a.on('test', () => console.log('test'))
a.on('test2', () => console.log('test2'))
a.removeAllListeners('test')
a.emit('test')
a.emit('test2')

631
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"test": "npm run test-backend", "test": "npm run test-backend",
"build": "cd app; npm run build; cd ..; cd backend; npm run build; cd ..",
"test-backend": "cd backend && npm run test && cd ..", "test-backend": "cd backend && npm run test && cd ..",
"release": "npm run test && ./release.sh" "release": "npm run test && ./release.sh"
}, },
@@ -38,6 +39,9 @@
"typescript": "^3.2.2" "typescript": "^3.2.2"
}, },
"dependencies": { "dependencies": {
"mqtt-explorer-backend": "file:backend" "@types/electron": "^1.6.10",
"@types/socket.io": "^2.1.2",
"mqtt-explorer-backend": "file:backend",
"socket.io": "^2.2.0"
} }
} }