Refactor communication

Add QoS andd retain flag
Refactor reducer
This commit is contained in:
Thomas Nordquist
2019-01-20 05:30:21 +01:00
parent 1839b551c0
commit f893d5ce60
21 changed files with 433 additions and 166 deletions

View File

@@ -14,11 +14,6 @@ import UpdateNotifier from './UpdateNotifier'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import ErrorBoundary from './ErrorBoundary' import ErrorBoundary from './ErrorBoundary'
interface State {
selectedNode?: q.TreeNode,
connectionId?: string
}
interface Props { interface Props {
name: string name: string
connectionId: string connectionId: string
@@ -26,12 +21,10 @@ interface Props {
settingsVisible: boolean settingsVisible: boolean
} }
class App extends React.Component<Props, State> { class App extends React.Component<Props, {}> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.state = { this.state = { }
selectedNode: undefined,
}
} }
private getStyles(): {[s: string]: React.CSSProperties} { private getStyles(): {[s: string]: React.CSSProperties} {
@@ -94,9 +87,7 @@ class App extends React.Component<Props, State> {
<TitleBar /> <TitleBar />
<div style={centerContent}> <div style={centerContent}>
<div style={this.getStyles().left}> <div style={this.getStyles().left}>
<Tree connectionId={this.props.connectionId} didSelectNode={(node: q.TreeNode) => { <Tree connectionId={this.props.connectionId} />
this.setState({ selectedNode: node })
}} />
</div> </div>
<div style={this.getStyles().right}> <div style={this.getStyles().right}>
<Sidebar connectionId={this.props.connectionId} /> <Sidebar connectionId={this.props.connectionId} />
@@ -113,8 +104,8 @@ class App extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
settingsVisible: state.settings.visible, settingsVisible: state.tooBigReducer.settings.visible,
connectionId: state.connectionId, connectionId: state.tooBigReducer.connectionId,
} }
} }

View File

@@ -212,8 +212,8 @@ const styles = (theme: Theme) => ({
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
showUpdateNotification: state.showUpdateNotification, showUpdateNotification: state.tooBigReducer.showUpdateNotification,
showUpdateDetails: state.showUpdateDetails, showUpdateDetails: state.tooBigReducer.showUpdateDetails,
} }
} }

View File

@@ -32,7 +32,7 @@ export const showError = (error?: string) => ({
}) })
export const disconnect = () => (dispatch: Dispatch<CustomAction>, getState: () => AppState) => { export const disconnect = () => (dispatch: Dispatch<CustomAction>, getState: () => AppState) => {
rendererEvents.emit(removeConnection, getState().connectionId) rendererEvents.emit(removeConnection, getState().tooBigReducer.connectionId)
dispatch({ dispatch({
type: ActionTypes.disconnect, type: ActionTypes.disconnect,

View File

@@ -0,0 +1,61 @@
import { ActionTypes, Action } from '../reducers/Publish'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { rendererEvents, makePublishEvent } from '../../../events'
export const setTopic = (topic?: string): Action => {
return {
topic,
type: ActionTypes.PUBLISH_SET_TOPIC,
}
}
export const setPayload = (payload?: string): Action => {
return {
payload,
type: ActionTypes.PUBLISH_SET_PAYLOAD,
}
}
export const toggleEmptyPayload = (): Action => {
return {
type: ActionTypes.PUBLISH_TOGGLE_EMPTY_PAYLOAD,
}
}
export const setQoS = (qos: 0 | 1 | 2): Action => {
return {
qos,
type: ActionTypes.PUBLISH_SET_QOS,
}
}
export const setEditorMode = (editorMode: string): Action => {
return {
editorMode,
type: ActionTypes.PUBLISH_SET_EDITOR_MODE,
}
}
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const state = getState()
const topic = state.publish.topic
if (!topic) {
return
}
const publishEvent = makePublishEvent(connectionId)
rendererEvents.emit(publishEvent, {
topic,
payload: state.publish.payload,
retain: state.publish.retain,
qos: state.publish.qos,
})
}
export const toggleRetain = (): Action => {
return {
type: ActionTypes.PUBLISH_TOGGLE_RETAIN,
}
}

View File

@@ -1,15 +0,0 @@
import { ActionTypes, CustomAction } from '../reducers'
export const setPublishTopic = (topic: string): CustomAction => {
return {
publishTopic: topic,
type: ActionTypes.setPublishTopic,
}
}
export const setPublishPayload = (payload: string): CustomAction => {
return {
publishPayload: payload,
type: ActionTypes.setPublishPayload,
}
}

View File

@@ -1,7 +1,7 @@
import * as settingsActions from './Settings' import * as settingsActions from './Settings'
import * as sidebarActions from './Sidebar' import * as publishActions from './Publish'
import * as treeActions from './Tree' import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier' import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection' import * as connectionActions from './Connection'
export { settingsActions, treeActions, sidebarActions, updateNotifierActions, connectionActions } export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions }

View File

@@ -380,10 +380,10 @@ class Connection extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
visible: !state.connected, visible: !state.tooBigReducer.connected,
connected: state.connected, connected: state.tooBigReducer.connected,
connecting: state.connecting, connecting: state.tooBigReducer.connecting,
error: state.error, error: state.tooBigReducer.error,
} }
} }

View File

@@ -138,9 +138,9 @@ class Settings extends React.Component<Props, {}> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
autoExpandLimit: state.settings.autoExpandLimit, autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
nodeOrder: state.settings.nodeOrder, nodeOrder: state.tooBigReducer.settings.nodeOrder,
visible: state.settings.visible, visible: state.tooBigReducer.settings.visible,
} }
} }

View File

@@ -1,4 +1,3 @@
// tslint:disable-next-line
import 'react-ace' import 'react-ace'
import 'brace/mode/json' import 'brace/mode/json'
import 'brace/mode/text' import 'brace/mode/text'
@@ -6,7 +5,6 @@ import 'brace/mode/xml'
import 'brace/theme/monokai' import 'brace/theme/monokai'
import * as React from 'react' import * as React from 'react'
import * as brace from 'brace'
import * as q from '../../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import { import {
@@ -15,14 +13,16 @@ import {
Radio, Radio,
RadioGroup, RadioGroup,
TextField, TextField,
Typography,
IconButton, IconButton,
FormControl, FormControl,
InputLabel, InputLabel,
Input, Input,
Checkbox,
MenuItem,
Tooltip,
} from '@material-ui/core' } from '@material-ui/core'
import { makePublishEvent, rendererEvents } from '../../../../../events'
// tslint:disable-next-line
import { default as AceEditor } from 'react-ace' import { default as AceEditor } from 'react-ace'
import { AppState } from '../../../reducers' import { AppState } from '../../../reducers'
import History from '../History' import History from '../History'
@@ -31,47 +31,53 @@ import Navigation from '@material-ui/icons/Navigation'
import Clear from '@material-ui/icons/Clear' import Clear from '@material-ui/icons/Clear'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions' import { publishActions } from '../../../actions'
interface Props { interface Props {
node?: q.TreeNode node?: q.TreeNode
connectionId?: string connectionId?: string
topic?: string topic?: string
payload?: string payload?: string
actions: any actions: typeof publishActions
emptyPayload: boolean
retain: boolean
editorMode: string
qos: 0 | 1 | 2
} }
interface State { interface State {
mode: string
history: Message[] history: Message[]
} }
class Publish extends React.Component<Props, State> { class Publish extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.state = { mode: 'json', history: [] } this.state = { history: [] }
} }
private updatePayload = (value: string, event?: any) => { private updatePayload = (payload: string, event?: any) => {
this.props.actions.setPublishPayload(value) this.props.actions.setPayload(payload)
} }
private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => { private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value) this.props.actions.setTopic(e.target.value)
this.props.actions.setPublishTopic(e.target.value)
} }
private updateMode = (e: React.ChangeEvent<{}>, value: string) => { private updateMode = (e: React.ChangeEvent<{}>, value: string) => {
this.setState({ mode: value }) this.props.actions.setEditorMode(value)
} }
private publish = (e: React.MouseEvent) => { private publish = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
if (!this.props.connectionId) {
return
}
this.props.actions.publish(this.props.connectionId)
const topic = this.currentTopic() || '' const topic = this.currentTopic() || ''
const payload = this.props.payload const payload = this.props.payload
if (this.props.connectionId && topic) { if (this.props.connectionId && topic) {
rendererEvents.emit(makePublishEvent(this.props.connectionId), { topic, payload })
this.addMessageToHistory(topic, payload) this.addMessageToHistory(topic, payload)
} }
} }
@@ -102,7 +108,7 @@ class Publish extends React.Component<Props, State> {
} }
private clearTopic = () => { private clearTopic = () => {
this.props.actions.setPublishTopic(undefined) this.props.actions.setTopic('')
} }
private topic() { private topic() {
@@ -129,7 +135,7 @@ class Publish extends React.Component<Props, State> {
private onTopicBlur = (e: React.FocusEvent<HTMLInputElement>) => { private onTopicBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) { if (!e.target.value) {
this.props.actions.setPublishTopic(undefined) this.props.actions.setTopic(undefined)
} }
} }
@@ -152,38 +158,10 @@ class Publish extends React.Component<Props, State> {
} }
private editorMode() { private editorMode() {
const labelStyle = { margin: '0 8px 0 8px' }
return ( return (
<div style={{ marginTop: '16px' }}> <div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}> <div style={{ width: '100%', lineHeight: '64px' }}>
<RadioGroup {this.renderEditorModeSelection()}
style={{ display: 'inline-block', float: 'left' }}
value={this.state.mode}
onChange={this.updateMode}
row={true}
>
<FormControlLabel
value="text"
style={labelStyle}
control={<Radio color="primary" />}
label="raw"
labelPlacement="top"
/>
<FormControlLabel
value="xml"
style={labelStyle}
control={<Radio color="primary" />}
label="xml"
labelPlacement="top"
/>
<FormControlLabel
value="json"
style={labelStyle}
control={<Radio color="primary" />}
label="json"
labelPlacement="top"
/>
</RadioGroup>
<div style={{ float: 'right', marginRight: '16px' }}> <div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()} {this.publishButton()}
</div> </div>
@@ -192,6 +170,103 @@ class Publish extends React.Component<Props, State> {
) )
} }
private renderEditorModeSelection() {
if (this.props.emptyPayload) {
return null
}
const labelStyle = { margin: '0 8px 0 8px' }
return (
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={this.props.editorMode}
onChange={this.updateMode}
row={true}
>
<FormControlLabel
value="text"
style={labelStyle}
control={<Radio color="primary" />}
label="raw"
labelPlacement="top"
/>
<FormControlLabel
value="xml"
style={labelStyle}
control={<Radio color="primary" />}
label="xml"
labelPlacement="top"
/>
<FormControlLabel
value="json"
style={labelStyle}
control={<Radio color="primary" />}
label="json"
labelPlacement="top"
/>
</RadioGroup>
)
}
private onChangeQoS = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
if (value !== 0 && value !== 1 && value !== 2) {
return
}
this.props.actions.setQoS(value)
}
private publishMode() {
const labelStyle = { margin: '0 8px 0 8px' }
const itemStyle = { padding: '0' }
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
const qosSelect = (
<TextField
select={true}
value={this.props.qos}
margin="normal"
style={{ margin: '8px 0 8px 8px' }}
onChange={this.onChangeQoS}
>
<MenuItem key={0} value={0} style={itemStyle}>
<Tooltip title="At most once"><div style={tooltipStyle}>0</div></Tooltip>
</MenuItem>
<MenuItem key={1} value={1} style={itemStyle}>
<Tooltip title="At least once"><div style={tooltipStyle}>1</div></Tooltip>
</MenuItem>
<MenuItem key={2} value={2} style={itemStyle}>
<Tooltip title="Exactly once"><div style={tooltipStyle}>2</div></Tooltip>
</MenuItem>
</TextField>
)
return (
<div style={{ marginTop: '8px', clear: 'both' }}>
<div style={{ width: '100%' }}>
<FormControlLabel
style={labelStyle}
control={qosSelect}
label="QoS"
labelPlacement="start"
/>
<FormControlLabel
value="empty"
style={labelStyle}
control={<Checkbox color="primary" checked={this.props.emptyPayload} onChange={this.props.actions.toggleEmptyPayload} />}
label="no message"
labelPlacement="end"
/>
<FormControlLabel
value="retain"
style={labelStyle}
control={<Checkbox color="primary" checked={this.props.retain} onChange={this.props.actions.toggleRetain} />}
label="retain"
labelPlacement="end"
/>
</div>
</div>
)
}
private history() { private history() {
const items = this.state.history.reverse().map(message => ({ const items = this.state.history.reverse().map(message => ({
title: message.topic, title: message.topic,
@@ -203,41 +278,56 @@ class Publish extends React.Component<Props, State> {
private didSelectHistoryEntry = (index: number) => { private didSelectHistoryEntry = (index: number) => {
const message = this.state.history[index] const message = this.state.history[index]
this.props.actions.setPublishTopic(message.topic) this.props.actions.setTopic(message.topic)
this.props.actions.setPublishPayload(message.payload) this.props.actions.setPayload(message.payload)
} }
private editor() { private editor() {
return ( return (
<div style={{ width: '100%', display: 'block' }}> <div style={{ width: '100%', display: 'block' }}>
{this.editorMode()} {this.editorMode()}
<AceEditor {this.renderEditor()}
mode={this.state.mode} {this.publishMode()}
theme="monokai"
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
showGutter={true}
value={this.props.payload}
onChange={this.updatePayload}
setOptions={this.editorOptions}
editorProps={{ $blockScrolling: true }}
/>
</div> </div>
) )
} }
private renderEditor() {
if (this.props.emptyPayload) {
return null
}
return (
<AceEditor
mode={this.props.editorMode}
theme="monokai"
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
showGutter={true}
value={this.props.payload}
onChange={this.updatePayload}
setOptions={this.editorOptions}
editorProps={{ $blockScrolling: true }}
/>
)
}
} }
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
return { return {
actions: bindActionCreators(sidebarActions, dispatch), actions: bindActionCreators(publishActions, dispatch),
} }
} }
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
topic: state.sidebar.publishTopic, topic: state.publish.topic,
payload: state.sidebar.publishPayload, payload: state.publish.payload,
emptyPayload: state.publish.emptyPayload,
editorMode: state.publish.editorMode,
retain: state.publish.retain,
qos: state.publish.qos,
} }
} }

View File

@@ -116,21 +116,13 @@ class Sidebar extends React.Component<Props, State> {
<Typography className={classes.heading}>Value {copyValue}</Typography> <Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}> <ExpansionPanelDetails style={this.detailsStyle}>
<div style={{ width: '100%', textAlign:'right' }}> {this.messageMetaInfo()}
{this.props.node && this.props.node.message && <i><DateFormatter date={this.props.node.message.received} /></i>}
</div>
<div ref={this.valueRef}> <div ref={this.valueRef}>
<ValueRenderer message={this.props.node && this.props.node.message} /> <ValueRenderer message={this.props.node && this.props.node.message} />
</div> </div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} node={this.props.node} /></div> <div><MessageHistory onSelect={this.handleMessageHistorySelect} node={this.props.node} /></div>
<Popper open={Boolean(this.state.compareMessage)} anchorEl={this.valueRef.current} placement="left" transition={true}> <Popper open={Boolean(this.state.compareMessage)} anchorEl={this.valueRef.current} placement="left" transition={true}>
{({ TransitionProps }) => ( {this.showValueComparison}
<Fade {...TransitionProps} timeout={350}>
<Paper>
<ValueRenderer message={this.state.compareMessage} />
</Paper>
</Fade>
)}
</Popper> </Popper>
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
@@ -152,6 +144,30 @@ class Sidebar extends React.Component<Props, State> {
) )
} }
private showValueComparison = (a: any) => (
<Fade {...a.TransitionProps} timeout={350}>
<Paper>
<ValueRenderer message={this.state.compareMessage} />
</Paper>
</Fade>
)
private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
return null
}
return (
<div style={{ width: '100%', display: 'flex' }}>
<div style={{ flex: 1 }}><Typography>QoS: {this.props.node.mqttMessage.qos}</Typography></div>
<div style={{ flex: 1, textAlign: 'center' }}>
<Typography style={{ color: this.props.theme.palette.secondary.main }}><b>{this.props.node.mqttMessage.retain ? 'retained' : null}</b></Typography>
</div>
<div style={{ flex: 1, textAlign: 'right' }}><Typography><i><DateFormatter date={this.props.node.message.received} /></i></Typography></div>
</div>
)
}
private handleMessageHistorySelect = (message: q.Message) => { private handleMessageHistorySelect = (message: q.Message) => {
if (message !== this.state.compareMessage) { if (message !== this.state.compareMessage) {
this.setState({ compareMessage: message }) this.setState({ compareMessage: message })
@@ -175,7 +191,7 @@ class Sidebar extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
node: state.selectedTopic, node: state.tooBigReducer.selectedTopic,
} }
} }

View File

@@ -66,7 +66,10 @@ const styles: StyleRulesCallback = theme => ({
interface Props { interface Props {
classes: any classes: any
actions: any actions: {
settings: typeof settingsActions,
connection: typeof connectionActions,
}
} }
interface State { interface State {
@@ -91,11 +94,11 @@ class TitleBar extends React.Component<Props, State> {
return ( return (
<AppBar position="static"> <AppBar position="static">
<Toolbar> <Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={this.props.actions.toggleSettingsVisibility}> <IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={actions.settings.toggleSettingsVisibility}>
<Menu /> <Menu />
</IconButton> </IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography> <Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
<Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.disconnect}> <Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/> Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button> </Button>
</Toolbar> </Toolbar>
@@ -122,7 +125,10 @@ class TitleBar extends React.Component<Props, State> {
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
return { return {
actions: { ...bindActionCreators(connectionActions, dispatch), ...bindActionCreators(settingsActions, dispatch) }, actions: {
settings: bindActionCreators(settingsActions, dispatch),
connection: bindActionCreators(connectionActions, dispatch),
},
} }
} }

View File

@@ -1,7 +1,7 @@
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 { makeConnectionMessageEvent, rendererEvents } from '../../../../events' import { makeConnectionMessageEvent, rendererEvents, MqttMessage } from '../../../../events'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import TreeNode from './TreeNode' import TreeNode from './TreeNode'
@@ -89,9 +89,10 @@ class Tree extends React.Component<Props, TreeState> {
} }
} }
private handleNewData = (msg: any) => { private handleNewData = (msg: MqttMessage) => {
const edges = msg.topic.split('/') const edges = msg.topic.split('/')
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString()) const node = q.TreeNodeFactory.fromEdgesAndValue(edges, msg.payload)
node.mqttMessage = msg
this.state.tree.updateWithNode(node.firstNode()) this.state.tree.updateWithNode(node.firstNode())
this.throttledStateUpdate({ msg, tree: this.state.tree }) this.throttledStateUpdate({ msg, tree: this.state.tree })
@@ -128,8 +129,8 @@ class Tree extends React.Component<Props, TreeState> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
autoExpandLimit: state.settings.autoExpandLimit, autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
connected: state.connected, connected: state.tooBigReducer.connected,
} }
} }

View File

@@ -72,7 +72,7 @@ class TreeNodeSubnodes extends React.Component<Props, {}> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
nodeOrder: state.settings.nodeOrder, nodeOrder: state.tooBigReducer.settings.nodeOrder,
} }
} }

108
app/src/reducers/Publish.ts Normal file
View File

@@ -0,0 +1,108 @@
import { Action } from 'redux'
import { createReducer } from './lib'
export interface PublishState {
topic?: string
payload?: string
emptyPayload: boolean
retain: boolean
editorMode: string
qos: 0 | 1 | 2
}
export type Action = SetPayload | SetTopic | ToggleEmptyPayload | ToggleRetain | SetEditorMode | SetQoS
export enum ActionTypes {
PUBLISH_SET_TOPIC = 'PUBLISH_SET_TOPIC',
PUBLISH_SET_PAYLOAD = 'PUBLISH_SET_PAYLOAD',
PUBLISH_TOGGLE_EMPTY_PAYLOAD = 'PUBLISH_TOGGLE_EMPTY_PAYLOAD',
PUBLISH_TOGGLE_RETAIN = 'PUBLISH_TOGGLE_RETAIN',
PUBLISH_SET_EDITOR_MODE = 'PUBLISH_SET_EDITOR_MODE',
PUBLISH_SET_QOS = 'PUBLISH_SET_QOS',
}
export interface SetPayload {
type: ActionTypes.PUBLISH_SET_PAYLOAD
payload?: string
}
export interface SetTopic {
type: ActionTypes.PUBLISH_SET_TOPIC
topic?: string
}
export interface SetQoS {
type: ActionTypes.PUBLISH_SET_QOS
qos: 0 | 1 | 2
}
export interface ToggleEmptyPayload {
type: ActionTypes.PUBLISH_TOGGLE_EMPTY_PAYLOAD
}
export interface SetEditorMode {
type: ActionTypes.PUBLISH_SET_EDITOR_MODE
editorMode: string
}
export interface ToggleRetain {
type: ActionTypes.PUBLISH_TOGGLE_RETAIN
}
const initialState: PublishState = {
editorMode: 'text',
emptyPayload: false,
retain: false,
qos: 0,
}
export const publishReducer = createReducer(initialState, {
PUBLISH_SET_TOPIC: setTopic,
PUBLISH_SET_PAYLOAD: setPayload,
PUBLISH_TOGGLE_EMPTY_PAYLOAD: toggleEmptyPayload,
PUBLISH_TOGGLE_RETAIN: toggleRetain,
PUBLISH_SET_EDITOR_MODE: setEditorMode,
PUBLISH_SET_QOS: setQoS,
})
function setTopic(state: PublishState, action: SetTopic) {
return {
...state,
topic: action.topic,
}
}
function setPayload(state: PublishState, action: SetPayload) {
return {
...state,
payload: action.payload,
}
}
function setQoS(state: PublishState, action: SetQoS) {
return {
...state,
qos: action.qos,
}
}
function setEditorMode(state: PublishState, action: SetEditorMode) {
return {
...state,
editorMode: action.editorMode,
}
}
function toggleEmptyPayload(state: PublishState) {
return {
...state,
emptyPayload: !state.emptyPayload,
}
}
function toggleRetain(state: PublishState) {
return {
...state,
retain: !state.retain,
}
}

View File

@@ -1,10 +1,10 @@
import * as q from '../../../backend/src/Model' import * as q from '../../../backend/src/Model'
import { Action, Reducer } from 'redux' import { Action, Reducer, combineReducers } from 'redux'
import { trackEvent } from '../tracking' import { trackEvent } from '../tracking'
import { MqttOptions, DataSourceStateMachine } from '../../../backend/src/DataSource' import { MqttOptions } from '../../../backend/src/DataSource'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent } from '../../../events' import { PublishState, publishReducer } from './Publish'
export enum ActionTypes { export enum ActionTypes {
disconnect = 'DISCONNECT', disconnect = 'DISCONNECT',
@@ -13,8 +13,6 @@ export enum ActionTypes {
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY', toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
setNodeOrder = 'SET_NODE_ORDER', setNodeOrder = 'SET_NODE_ORDER',
selectTopic = 'SELECT_TOPIC', selectTopic = 'SELECT_TOPIC',
setPublishTopic = 'SET_PUBLISH_TOPIC',
setPublishPayload = 'SET_PUBLISH_PAYLOAD',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION', showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS', showUpdateDetails = 'SHOW_UPDATE_DETAILS',
connecting = 'CONNECTING', connecting = 'CONNECTING',
@@ -26,8 +24,6 @@ export interface CustomAction extends Action {
autoExpandLimit?: number autoExpandLimit?: number
nodeOrder?: NodeOrder nodeOrder?: NodeOrder
selectedTopic?: q.TreeNode selectedTopic?: q.TreeNode
publishTopic?: string
publishPayload?: string
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails?: boolean showUpdateDetails?: boolean
connectionOptions?: MqttOptions connectionOptions?: MqttOptions
@@ -35,15 +31,14 @@ export interface CustomAction extends Action {
error?: string error?: string
} }
export interface SidebarState { export interface AppState {
publishTopic?: string tooBigReducer: TooBigOfState
publishPayload?: string publish: PublishState
} }
export interface AppState { export interface TooBigOfState {
settings: SettingsState, settings: SettingsState,
selectedTopic?: q.TreeNode selectedTopic?: q.TreeNode
sidebar: SidebarState
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails: boolean showUpdateDetails: boolean
connecting: boolean connecting: boolean
@@ -65,13 +60,12 @@ export enum NodeOrder {
topics = '#topics', topics = '#topics',
} }
const initialAppState: AppState = { const initialBigState: TooBigOfState = {
settings: { settings: {
autoExpandLimit: 0, autoExpandLimit: 0,
nodeOrder: NodeOrder.none, nodeOrder: NodeOrder.none,
visible: false, visible: false,
}, },
sidebar: {},
selectedTopic: undefined, selectedTopic: undefined,
showUpdateDetails: false, showUpdateDetails: false,
connected: false, connected: false,
@@ -79,7 +73,7 @@ const initialAppState: AppState = {
error: undefined, error: undefined,
} }
const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialAppState, action) => { const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => {
if (!state) { if (!state) {
throw Error('No initial state') throw Error('No initial state')
} }
@@ -97,16 +91,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
autoExpandLimit: action.autoExpandLimit, autoExpandLimit: action.autoExpandLimit,
}, },
} }
case ActionTypes.setPublishTopic:
return {
...state,
sidebar: { ...state.sidebar, publishTopic: action.publishTopic },
}
case ActionTypes.setPublishPayload:
return {
...state,
sidebar: { ...state.sidebar, publishPayload: action.publishPayload },
}
case ActionTypes.toggleSettingsVisibility: case ActionTypes.toggleSettingsVisibility:
return { return {
...state, ...state,
@@ -115,6 +100,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
visible: !state.settings.visible, visible: !state.settings.visible,
}, },
} }
case ActionTypes.selectTopic: case ActionTypes.selectTopic:
if (!action.selectedTopic) { if (!action.selectedTopic) {
return state return state
@@ -123,6 +109,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
...state, ...state,
selectedTopic: action.selectedTopic, selectedTopic: action.selectedTopic,
} }
case ActionTypes.setNodeOrder: case ActionTypes.setNodeOrder:
if (!action.nodeOrder) { if (!action.nodeOrder) {
return state return state
@@ -131,11 +118,13 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
...state, ...state,
settings: { ...state.settings, nodeOrder: action.nodeOrder }, settings: { ...state.settings, nodeOrder: action.nodeOrder },
} }
case ActionTypes.showUpdateNotification: case ActionTypes.showUpdateNotification:
return { return {
...state, ...state,
showUpdateNotification: action.showUpdateNotification, showUpdateNotification: action.showUpdateNotification,
} }
case ActionTypes.showUpdateDetails: case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) { if (action.showUpdateDetails === undefined) {
return state return state
@@ -144,6 +133,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
...state, ...state,
showUpdateDetails: action.showUpdateDetails, showUpdateDetails: action.showUpdateDetails,
} }
case ActionTypes.connecting: case ActionTypes.connecting:
if (!action.connectionId) { if (!action.connectionId) {
return state return state
@@ -181,4 +171,9 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
} }
} }
const reducer = combineReducers({
tooBigReducer,
publish: publishReducer,
})
export default reducer export default reducer

9
app/src/reducers/lib.ts Normal file
View File

@@ -0,0 +1,9 @@
export const createReducer = (initialState: any, handlers: any) => {
return (state = initialState, action: any) => {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}

View File

@@ -1,4 +1,5 @@
import { DataSourceStateMachine } from './' import { DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
type MessageCallback = (topic: string, payload: Buffer) => void type MessageCallback = (topic: string, payload: Buffer) => void
@@ -7,7 +8,7 @@ interface DataSource<DataSourceOptions> {
connect(options: DataSourceOptions): DataSourceStateMachine connect(options: DataSourceOptions): DataSourceStateMachine
disconnect(): void disconnect(): void
onMessage(messageCallback: MessageCallback): void onMessage(messageCallback: MessageCallback): void
publish(topic: string, payload: any): void publish(msg: MqttMessage): void
topicSeparator: string topicSeparator: string
stateMachine: DataSourceStateMachine stateMachine: DataSourceStateMachine
} }

View File

@@ -2,6 +2,7 @@ import * as Url from 'url'
import { Client, connect as mqttConnect } from 'mqtt' import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource, DataSourceStateMachine } from './' import { DataSource, DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
export interface MqttOptions { export interface MqttOptions {
url: string url: string
@@ -15,11 +16,11 @@ export interface MqttOptions {
export class MqttSource implements DataSource<MqttOptions> { export class MqttSource implements DataSource<MqttOptions> {
public stateMachine: DataSourceStateMachine = new DataSourceStateMachine() 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, packet: any) => void
private rootSubscription = '#' private rootSubscription = '#'
public topicSeparator = '/' public topicSeparator = '/'
public onMessage(messageCallback: (topic: string, message: Buffer) => void) { public onMessage(messageCallback: (topic: string, message: Buffer, packet: any) => void) {
this.messageCallback = messageCallback this.messageCallback = messageCallback
} }
@@ -73,14 +74,14 @@ export class MqttSource implements DataSource<MqttOptions> {
}) })
client.on('message', (topic, message, packet) => { client.on('message', (topic, message, packet) => {
this.messageCallback && this.messageCallback(topic, message) this.messageCallback && this.messageCallback(topic, message, packet)
}) })
return this.stateMachine return this.stateMachine
} }
public publish(topic: string, payload: any) { public publish(msg: MqttMessage) {
this.client && this.client.publish(topic, payload) this.client && this.client.publish(msg.topic, msg.payload, { qos: msg.qos, retain: msg.retain })
} }
public disconnect() { public disconnect() {

View File

@@ -1,15 +1,15 @@
import { Edge, Message, RingBuffer } from './' import { Edge, Message, RingBuffer } from './'
import { EventDispatcher } from '../../../events' import { EventDispatcher, MqttMessage } from '../../../events'
export class TreeNode { export class TreeNode {
public sourceEdge?: Edge public sourceEdge?: Edge
public message?: Message public message?: Message
public mqttMessage?: MqttMessage
public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100) public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100)
public edges: {[s: string]: Edge} = {} public edges: {[s: string]: Edge} = {}
public collapsed = false public collapsed = false
public messages: number = 0 public messages: number = 0
public lastUpdate: number = Date.now() public lastUpdate: number = Date.now()
public onMerge = new EventDispatcher<void, TreeNode>(this) public onMerge = new EventDispatcher<void, TreeNode>(this)
public onEdgesChange = new EventDispatcher<void, TreeNode>(this) public onEdgesChange = new EventDispatcher<void, TreeNode>(this)
public onMessage = new EventDispatcher<Message, TreeNode>(this) public onMessage = new EventDispatcher<Message, TreeNode>(this)

View File

@@ -1,7 +1,7 @@
import { import {
AddMqttConnection, AddMqttConnection,
EventDispatcher, EventDispatcher,
Message, MqttMessage,
addMqttConnectionEvent, addMqttConnectionEvent,
backendEvents, backendEvents,
checkForUpdates, checkForUpdates,
@@ -36,19 +36,20 @@ export class ConnectionManager {
connection.connect(options) connection.connect(options)
this.handleNewMessagesForConnection(connectionId, connection) this.handleNewMessagesForConnection(connectionId, connection)
backendEvents.subscribe(makePublishEvent(connectionId), (msg: Message) => { backendEvents.subscribe(makePublishEvent(connectionId), (msg: MqttMessage) => {
this.connections[connectionId].publish(msg.topic, msg.payload) this.connections[connectionId].publish(msg)
}) })
} }
private handleNewMessagesForConnection(connectionId: string, connection: MqttSource) { private handleNewMessagesForConnection(connectionId: string, connection: MqttSource) {
const messageEvent = makeConnectionMessageEvent(connectionId) const messageEvent = makeConnectionMessageEvent(connectionId)
connection.onMessage((topic: string, payload: Buffer) => { connection.onMessage((topic: string, payload: Buffer, packet: any) => {
let buffer = payload let buffer = payload
if (buffer.length > 10000) { if (buffer.length > 10000) {
buffer = buffer.slice(0, 10000) buffer = buffer.slice(0, 10000)
} }
backendEvents.emit(messageEvent, { topic, payload: buffer.toString('base64') })
backendEvents.emit(messageEvent, { topic, payload: buffer.toString(), qos: packet.qos, retain: packet.retain })
}) })
} }

View File

@@ -35,18 +35,20 @@ export const updateAvailable: Event<UpdateInfo> = {
topic: 'app/update/available', topic: 'app/update/available',
} }
export interface Message { export interface MqttMessage {
topic: string, topic: string,
payload: any payload: any,
qos: 0 | 1 | 2,
retain: boolean
} }
export function makePublishEvent(connectionId: string): Event<Message> { export function makePublishEvent(connectionId: string): Event<MqttMessage> {
return { return {
topic: `conn/publish/${connectionId}`, topic: `conn/publish/${connectionId}`,
} }
} }
export function makeConnectionMessageEvent(connectionId: string): Event<Message> { export function makeConnectionMessageEvent(connectionId: string): Event<MqttMessage> {
return { return {
topic: `conn/${connectionId}`, topic: `conn/${connectionId}`,
} }