From f893d5ce60a45d87cb442f7cbaf92fe56f09a626 Mon Sep 17 00:00:00 2001 From: Thomas Nordquist Date: Sun, 20 Jan 2019 05:30:21 +0100 Subject: [PATCH] Refactor communication Add QoS andd retain flag Refactor reducer --- app/src/App.tsx | 19 +- app/src/UpdateNotifier.tsx | 4 +- app/src/actions/Connection.ts | 2 +- app/src/actions/Publish.ts | 61 +++++ app/src/actions/Sidebar.ts | 15 -- app/src/actions/index.ts | 4 +- .../components/ConnectionSetup/Connection.tsx | 8 +- app/src/components/Settings.tsx | 6 +- .../components/Sidebar/Publish/Publish.tsx | 214 +++++++++++++----- app/src/components/Sidebar/Sidebar.tsx | 38 +++- app/src/components/TitleBar.tsx | 14 +- app/src/components/Tree/Tree.tsx | 11 +- app/src/components/Tree/TreeNodeSubnodes.tsx | 2 +- app/src/reducers/Publish.ts | 108 +++++++++ app/src/reducers/index.ts | 45 ++-- app/src/reducers/lib.ts | 9 + backend/src/DataSource/DataSource.ts | 3 +- backend/src/DataSource/MqttSource.ts | 11 +- backend/src/Model/TreeNode.ts | 4 +- backend/src/index.ts | 11 +- events/Events.ts | 10 +- 21 files changed, 433 insertions(+), 166 deletions(-) create mode 100644 app/src/actions/Publish.ts create mode 100644 app/src/reducers/Publish.ts create mode 100644 app/src/reducers/lib.ts diff --git a/app/src/App.tsx b/app/src/App.tsx index 17e7eb0..ce8ed0c 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -14,11 +14,6 @@ import UpdateNotifier from './UpdateNotifier' import { connect } from 'react-redux' import ErrorBoundary from './ErrorBoundary' -interface State { - selectedNode?: q.TreeNode, - connectionId?: string -} - interface Props { name: string connectionId: string @@ -26,12 +21,10 @@ interface Props { settingsVisible: boolean } -class App extends React.Component { +class App extends React.Component { constructor(props: any) { super(props) - this.state = { - selectedNode: undefined, - } + this.state = { } } private getStyles(): {[s: string]: React.CSSProperties} { @@ -94,9 +87,7 @@ class App extends React.Component {
- { - this.setState({ selectedNode: node }) - }} /> +
@@ -113,8 +104,8 @@ class App extends React.Component { const mapStateToProps = (state: AppState) => { return { - settingsVisible: state.settings.visible, - connectionId: state.connectionId, + settingsVisible: state.tooBigReducer.settings.visible, + connectionId: state.tooBigReducer.connectionId, } } diff --git a/app/src/UpdateNotifier.tsx b/app/src/UpdateNotifier.tsx index bf9cdbb..8314778 100644 --- a/app/src/UpdateNotifier.tsx +++ b/app/src/UpdateNotifier.tsx @@ -212,8 +212,8 @@ const styles = (theme: Theme) => ({ const mapStateToProps = (state: AppState) => { return { - showUpdateNotification: state.showUpdateNotification, - showUpdateDetails: state.showUpdateDetails, + showUpdateNotification: state.tooBigReducer.showUpdateNotification, + showUpdateDetails: state.tooBigReducer.showUpdateDetails, } } diff --git a/app/src/actions/Connection.ts b/app/src/actions/Connection.ts index 7057997..60f76c7 100644 --- a/app/src/actions/Connection.ts +++ b/app/src/actions/Connection.ts @@ -32,7 +32,7 @@ export const showError = (error?: string) => ({ }) export const disconnect = () => (dispatch: Dispatch, getState: () => AppState) => { - rendererEvents.emit(removeConnection, getState().connectionId) + rendererEvents.emit(removeConnection, getState().tooBigReducer.connectionId) dispatch({ type: ActionTypes.disconnect, diff --git a/app/src/actions/Publish.ts b/app/src/actions/Publish.ts new file mode 100644 index 0000000..a4eb339 --- /dev/null +++ b/app/src/actions/Publish.ts @@ -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, 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, + } +} diff --git a/app/src/actions/Sidebar.ts b/app/src/actions/Sidebar.ts index bbaaff4..e69de29 100644 --- a/app/src/actions/Sidebar.ts +++ b/app/src/actions/Sidebar.ts @@ -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, - } -} diff --git a/app/src/actions/index.ts b/app/src/actions/index.ts index 7c67ff5..bdf2ea1 100644 --- a/app/src/actions/index.ts +++ b/app/src/actions/index.ts @@ -1,7 +1,7 @@ import * as settingsActions from './Settings' -import * as sidebarActions from './Sidebar' +import * as publishActions from './Publish' import * as treeActions from './Tree' import * as updateNotifierActions from './UpdateNotifier' import * as connectionActions from './Connection' -export { settingsActions, treeActions, sidebarActions, updateNotifierActions, connectionActions } +export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions } diff --git a/app/src/components/ConnectionSetup/Connection.tsx b/app/src/components/ConnectionSetup/Connection.tsx index f02ed47..ba08abf 100644 --- a/app/src/components/ConnectionSetup/Connection.tsx +++ b/app/src/components/ConnectionSetup/Connection.tsx @@ -380,10 +380,10 @@ class Connection extends React.Component { const mapStateToProps = (state: AppState) => { return { - visible: !state.connected, - connected: state.connected, - connecting: state.connecting, - error: state.error, + visible: !state.tooBigReducer.connected, + connected: state.tooBigReducer.connected, + connecting: state.tooBigReducer.connecting, + error: state.tooBigReducer.error, } } diff --git a/app/src/components/Settings.tsx b/app/src/components/Settings.tsx index d465747..2a5114c 100644 --- a/app/src/components/Settings.tsx +++ b/app/src/components/Settings.tsx @@ -138,9 +138,9 @@ class Settings extends React.Component { const mapStateToProps = (state: AppState) => { return { - autoExpandLimit: state.settings.autoExpandLimit, - nodeOrder: state.settings.nodeOrder, - visible: state.settings.visible, + autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit, + nodeOrder: state.tooBigReducer.settings.nodeOrder, + visible: state.tooBigReducer.settings.visible, } } diff --git a/app/src/components/Sidebar/Publish/Publish.tsx b/app/src/components/Sidebar/Publish/Publish.tsx index 7b3262d..025aecd 100644 --- a/app/src/components/Sidebar/Publish/Publish.tsx +++ b/app/src/components/Sidebar/Publish/Publish.tsx @@ -1,4 +1,3 @@ -// tslint:disable-next-line import 'react-ace' import 'brace/mode/json' import 'brace/mode/text' @@ -6,7 +5,6 @@ import 'brace/mode/xml' import 'brace/theme/monokai' import * as React from 'react' -import * as brace from 'brace' import * as q from '../../../../../backend/src/Model' import { @@ -15,14 +13,16 @@ import { Radio, RadioGroup, TextField, - Typography, IconButton, FormControl, InputLabel, Input, + Checkbox, + MenuItem, + Tooltip, } from '@material-ui/core' -import { makePublishEvent, rendererEvents } from '../../../../../events' +// tslint:disable-next-line import { default as AceEditor } from 'react-ace' import { AppState } from '../../../reducers' import History from '../History' @@ -31,47 +31,53 @@ import Navigation from '@material-ui/icons/Navigation' import Clear from '@material-ui/icons/Clear' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import { sidebarActions } from '../../../actions' +import { publishActions } from '../../../actions' interface Props { node?: q.TreeNode connectionId?: string topic?: string payload?: string - actions: any + actions: typeof publishActions + emptyPayload: boolean + retain: boolean + editorMode: string + qos: 0 | 1 | 2 } interface State { - mode: string history: Message[] } class Publish extends React.Component { constructor(props: any) { super(props) - this.state = { mode: 'json', history: [] } + this.state = { history: [] } } - private updatePayload = (value: string, event?: any) => { - this.props.actions.setPublishPayload(value) + private updatePayload = (payload: string, event?: any) => { + this.props.actions.setPayload(payload) } private updateTopic = (e: React.ChangeEvent) => { - console.log(e.target.value) - this.props.actions.setPublishTopic(e.target.value) + this.props.actions.setTopic(e.target.value) } private updateMode = (e: React.ChangeEvent<{}>, value: string) => { - this.setState({ mode: value }) + this.props.actions.setEditorMode(value) } private publish = (e: React.MouseEvent) => { e.stopPropagation() + if (!this.props.connectionId) { + return + } + + this.props.actions.publish(this.props.connectionId) const topic = this.currentTopic() || '' const payload = this.props.payload if (this.props.connectionId && topic) { - rendererEvents.emit(makePublishEvent(this.props.connectionId), { topic, payload }) this.addMessageToHistory(topic, payload) } } @@ -102,7 +108,7 @@ class Publish extends React.Component { } private clearTopic = () => { - this.props.actions.setPublishTopic(undefined) + this.props.actions.setTopic('') } private topic() { @@ -129,7 +135,7 @@ class Publish extends React.Component { private onTopicBlur = (e: React.FocusEvent) => { if (!e.target.value) { - this.props.actions.setPublishTopic(undefined) + this.props.actions.setTopic(undefined) } } @@ -152,38 +158,10 @@ class Publish extends React.Component { } private editorMode() { - const labelStyle = { margin: '0 8px 0 8px' } return (
- - } - label="raw" - labelPlacement="top" - /> - } - label="xml" - labelPlacement="top" - /> - } - label="json" - labelPlacement="top" - /> - + {this.renderEditorModeSelection()}
{this.publishButton()}
@@ -192,6 +170,103 @@ class Publish extends React.Component { ) } + private renderEditorModeSelection() { + if (this.props.emptyPayload) { + return null + } + const labelStyle = { margin: '0 8px 0 8px' } + return ( + + } + label="raw" + labelPlacement="top" + /> + } + label="xml" + labelPlacement="top" + /> + } + label="json" + labelPlacement="top" + /> + + ) + } + + private onChangeQoS = (event: React.ChangeEvent) => { + 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 = ( + + +
0
+
+ +
1
+
+ +
2
+
+
+ ) + return ( +
+
+ + } + label="no message" + labelPlacement="end" + /> + } + label="retain" + labelPlacement="end" + /> +
+
+ ) + } + private history() { const items = this.state.history.reverse().map(message => ({ title: message.topic, @@ -203,41 +278,56 @@ class Publish extends React.Component { private didSelectHistoryEntry = (index: number) => { const message = this.state.history[index] - this.props.actions.setPublishTopic(message.topic) - this.props.actions.setPublishPayload(message.payload) + this.props.actions.setTopic(message.topic) + this.props.actions.setPayload(message.payload) } private editor() { return (
{this.editorMode()} - + {this.renderEditor()} + {this.publishMode()}
) } + + private renderEditor() { + if (this.props.emptyPayload) { + return null + } + + return ( + + ) + } } const mapDispatchToProps = (dispatch: any) => { return { - actions: bindActionCreators(sidebarActions, dispatch), + actions: bindActionCreators(publishActions, dispatch), } } const mapStateToProps = (state: AppState) => { return { - topic: state.sidebar.publishTopic, - payload: state.sidebar.publishPayload, + topic: state.publish.topic, + payload: state.publish.payload, + emptyPayload: state.publish.emptyPayload, + editorMode: state.publish.editorMode, + retain: state.publish.retain, + qos: state.publish.qos, } } diff --git a/app/src/components/Sidebar/Sidebar.tsx b/app/src/components/Sidebar/Sidebar.tsx index f255552..209e0de 100644 --- a/app/src/components/Sidebar/Sidebar.tsx +++ b/app/src/components/Sidebar/Sidebar.tsx @@ -116,21 +116,13 @@ class Sidebar extends React.Component { Value {copyValue} -
- {this.props.node && this.props.node.message && } -
+ {this.messageMetaInfo()}
- {({ TransitionProps }) => ( - - - - - - )} + {this.showValueComparison}
@@ -152,6 +144,30 @@ class Sidebar extends React.Component { ) } + private showValueComparison = (a: any) => ( + + + + + + ) + + private messageMetaInfo() { + if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) { + return null + } + + return ( +
+
QoS: {this.props.node.mqttMessage.qos}
+
+ {this.props.node.mqttMessage.retain ? 'retained' : null} +
+
+
+ ) + } + private handleMessageHistorySelect = (message: q.Message) => { if (message !== this.state.compareMessage) { this.setState({ compareMessage: message }) @@ -175,7 +191,7 @@ class Sidebar extends React.Component { const mapStateToProps = (state: AppState) => { return { - node: state.selectedTopic, + node: state.tooBigReducer.selectedTopic, } } diff --git a/app/src/components/TitleBar.tsx b/app/src/components/TitleBar.tsx index 14794c7..d631fe2 100644 --- a/app/src/components/TitleBar.tsx +++ b/app/src/components/TitleBar.tsx @@ -66,7 +66,10 @@ const styles: StyleRulesCallback = theme => ({ interface Props { classes: any - actions: any + actions: { + settings: typeof settingsActions, + connection: typeof connectionActions, + } } interface State { @@ -91,11 +94,11 @@ class TitleBar extends React.Component { return ( - + MQTT-Explorer - @@ -122,7 +125,10 @@ class TitleBar extends React.Component { const mapDispatchToProps = (dispatch: any) => { return { - actions: { ...bindActionCreators(connectionActions, dispatch), ...bindActionCreators(settingsActions, dispatch) }, + actions: { + settings: bindActionCreators(settingsActions, dispatch), + connection: bindActionCreators(connectionActions, dispatch), + }, } } diff --git a/app/src/components/Tree/Tree.tsx b/app/src/components/Tree/Tree.tsx index a89df38..bd51322 100644 --- a/app/src/components/Tree/Tree.tsx +++ b/app/src/components/Tree/Tree.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import * as q from '../../../../backend/src/Model' -import { makeConnectionMessageEvent, rendererEvents } from '../../../../events' +import { makeConnectionMessageEvent, rendererEvents, MqttMessage } from '../../../../events' import { AppState } from '../../reducers' import TreeNode from './TreeNode' @@ -89,9 +89,10 @@ class Tree extends React.Component { } } - private handleNewData = (msg: any) => { + private handleNewData = (msg: MqttMessage) => { 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.throttledStateUpdate({ msg, tree: this.state.tree }) @@ -128,8 +129,8 @@ class Tree extends React.Component { const mapStateToProps = (state: AppState) => { return { - autoExpandLimit: state.settings.autoExpandLimit, - connected: state.connected, + autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit, + connected: state.tooBigReducer.connected, } } diff --git a/app/src/components/Tree/TreeNodeSubnodes.tsx b/app/src/components/Tree/TreeNodeSubnodes.tsx index a7ea3ec..150ca1d 100644 --- a/app/src/components/Tree/TreeNodeSubnodes.tsx +++ b/app/src/components/Tree/TreeNodeSubnodes.tsx @@ -72,7 +72,7 @@ class TreeNodeSubnodes extends React.Component { const mapStateToProps = (state: AppState) => { return { - nodeOrder: state.settings.nodeOrder, + nodeOrder: state.tooBigReducer.settings.nodeOrder, } } diff --git a/app/src/reducers/Publish.ts b/app/src/reducers/Publish.ts new file mode 100644 index 0000000..531267b --- /dev/null +++ b/app/src/reducers/Publish.ts @@ -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, + } +} diff --git a/app/src/reducers/index.ts b/app/src/reducers/index.ts index 31e56ca..020530a 100644 --- a/app/src/reducers/index.ts +++ b/app/src/reducers/index.ts @@ -1,10 +1,10 @@ import * as q from '../../../backend/src/Model' -import { Action, Reducer } from 'redux' +import { Action, Reducer, combineReducers } from 'redux' import { trackEvent } from '../tracking' -import { MqttOptions, DataSourceStateMachine } from '../../../backend/src/DataSource' -import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent } from '../../../events' +import { MqttOptions } from '../../../backend/src/DataSource' +import { PublishState, publishReducer } from './Publish' export enum ActionTypes { disconnect = 'DISCONNECT', @@ -13,8 +13,6 @@ export enum ActionTypes { toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY', setNodeOrder = 'SET_NODE_ORDER', selectTopic = 'SELECT_TOPIC', - setPublishTopic = 'SET_PUBLISH_TOPIC', - setPublishPayload = 'SET_PUBLISH_PAYLOAD', showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION', showUpdateDetails = 'SHOW_UPDATE_DETAILS', connecting = 'CONNECTING', @@ -26,8 +24,6 @@ export interface CustomAction extends Action { autoExpandLimit?: number nodeOrder?: NodeOrder selectedTopic?: q.TreeNode - publishTopic?: string - publishPayload?: string showUpdateNotification?: boolean showUpdateDetails?: boolean connectionOptions?: MqttOptions @@ -35,15 +31,14 @@ export interface CustomAction extends Action { error?: string } -export interface SidebarState { - publishTopic?: string - publishPayload?: string +export interface AppState { + tooBigReducer: TooBigOfState + publish: PublishState } -export interface AppState { +export interface TooBigOfState { settings: SettingsState, selectedTopic?: q.TreeNode - sidebar: SidebarState showUpdateNotification?: boolean showUpdateDetails: boolean connecting: boolean @@ -65,13 +60,12 @@ export enum NodeOrder { topics = '#topics', } -const initialAppState: AppState = { +const initialBigState: TooBigOfState = { settings: { autoExpandLimit: 0, nodeOrder: NodeOrder.none, visible: false, }, - sidebar: {}, selectedTopic: undefined, showUpdateDetails: false, connected: false, @@ -79,7 +73,7 @@ const initialAppState: AppState = { error: undefined, } -const reducer: Reducer = (state = initialAppState, action) => { +const tooBigReducer: Reducer = (state = initialBigState, action) => { if (!state) { throw Error('No initial state') } @@ -97,16 +91,7 @@ const reducer: Reducer = (state = initialApp 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: return { ...state, @@ -115,6 +100,7 @@ const reducer: Reducer = (state = initialApp visible: !state.settings.visible, }, } + case ActionTypes.selectTopic: if (!action.selectedTopic) { return state @@ -123,6 +109,7 @@ const reducer: Reducer = (state = initialApp ...state, selectedTopic: action.selectedTopic, } + case ActionTypes.setNodeOrder: if (!action.nodeOrder) { return state @@ -131,11 +118,13 @@ const reducer: Reducer = (state = initialApp ...state, settings: { ...state.settings, nodeOrder: action.nodeOrder }, } + case ActionTypes.showUpdateNotification: return { ...state, showUpdateNotification: action.showUpdateNotification, } + case ActionTypes.showUpdateDetails: if (action.showUpdateDetails === undefined) { return state @@ -144,6 +133,7 @@ const reducer: Reducer = (state = initialApp ...state, showUpdateDetails: action.showUpdateDetails, } + case ActionTypes.connecting: if (!action.connectionId) { return state @@ -181,4 +171,9 @@ const reducer: Reducer = (state = initialApp } } +const reducer = combineReducers({ + tooBigReducer, + publish: publishReducer, +}) + export default reducer diff --git a/app/src/reducers/lib.ts b/app/src/reducers/lib.ts new file mode 100644 index 0000000..3c0770d --- /dev/null +++ b/app/src/reducers/lib.ts @@ -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 + } + } +} diff --git a/backend/src/DataSource/DataSource.ts b/backend/src/DataSource/DataSource.ts index 56bc650..d8a8a53 100644 --- a/backend/src/DataSource/DataSource.ts +++ b/backend/src/DataSource/DataSource.ts @@ -1,4 +1,5 @@ import { DataSourceStateMachine } from './' +import { MqttMessage } from '../../../events' type MessageCallback = (topic: string, payload: Buffer) => void @@ -7,7 +8,7 @@ interface DataSource { connect(options: DataSourceOptions): DataSourceStateMachine disconnect(): void onMessage(messageCallback: MessageCallback): void - publish(topic: string, payload: any): void + publish(msg: MqttMessage): void topicSeparator: string stateMachine: DataSourceStateMachine } diff --git a/backend/src/DataSource/MqttSource.ts b/backend/src/DataSource/MqttSource.ts index 1e6e821..f43d048 100644 --- a/backend/src/DataSource/MqttSource.ts +++ b/backend/src/DataSource/MqttSource.ts @@ -2,6 +2,7 @@ import * as Url from 'url' import { Client, connect as mqttConnect } from 'mqtt' import { DataSource, DataSourceStateMachine } from './' +import { MqttMessage } from '../../../events' export interface MqttOptions { url: string @@ -15,11 +16,11 @@ export interface MqttOptions { export class MqttSource implements DataSource { public stateMachine: DataSourceStateMachine = new DataSourceStateMachine() private client: Client | undefined - private messageCallback?: (topic: string, message: Buffer) => void + private messageCallback?: (topic: string, message: Buffer, packet: any) => void private rootSubscription = '#' public topicSeparator = '/' - public onMessage(messageCallback: (topic: string, message: Buffer) => void) { + public onMessage(messageCallback: (topic: string, message: Buffer, packet: any) => void) { this.messageCallback = messageCallback } @@ -73,14 +74,14 @@ export class MqttSource implements DataSource { }) client.on('message', (topic, message, packet) => { - this.messageCallback && this.messageCallback(topic, message) + this.messageCallback && this.messageCallback(topic, message, packet) }) return this.stateMachine } - public publish(topic: string, payload: any) { - this.client && this.client.publish(topic, payload) + public publish(msg: MqttMessage) { + this.client && this.client.publish(msg.topic, msg.payload, { qos: msg.qos, retain: msg.retain }) } public disconnect() { diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index eb415f0..f868d73 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -1,15 +1,15 @@ import { Edge, Message, RingBuffer } from './' -import { EventDispatcher } from '../../../events' +import { EventDispatcher, MqttMessage } from '../../../events' export class TreeNode { public sourceEdge?: Edge public message?: Message + public mqttMessage?: MqttMessage public messageHistory: RingBuffer = new RingBuffer(3000, 100) public edges: {[s: string]: Edge} = {} public collapsed = false public messages: number = 0 public lastUpdate: number = Date.now() - public onMerge = new EventDispatcher(this) public onEdgesChange = new EventDispatcher(this) public onMessage = new EventDispatcher(this) diff --git a/backend/src/index.ts b/backend/src/index.ts index e37aadd..87ae3f9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,7 +1,7 @@ import { AddMqttConnection, EventDispatcher, - Message, + MqttMessage, addMqttConnectionEvent, backendEvents, checkForUpdates, @@ -36,19 +36,20 @@ export class ConnectionManager { connection.connect(options) this.handleNewMessagesForConnection(connectionId, connection) - backendEvents.subscribe(makePublishEvent(connectionId), (msg: Message) => { - this.connections[connectionId].publish(msg.topic, msg.payload) + backendEvents.subscribe(makePublishEvent(connectionId), (msg: MqttMessage) => { + this.connections[connectionId].publish(msg) }) } private handleNewMessagesForConnection(connectionId: string, connection: MqttSource) { const messageEvent = makeConnectionMessageEvent(connectionId) - connection.onMessage((topic: string, payload: Buffer) => { + connection.onMessage((topic: string, payload: Buffer, packet: any) => { let buffer = payload if (buffer.length > 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 }) }) } diff --git a/events/Events.ts b/events/Events.ts index 0284ff3..c1e270e 100644 --- a/events/Events.ts +++ b/events/Events.ts @@ -35,18 +35,20 @@ export const updateAvailable: Event = { topic: 'app/update/available', } -export interface Message { +export interface MqttMessage { topic: string, - payload: any + payload: any, + qos: 0 | 1 | 2, + retain: boolean } -export function makePublishEvent(connectionId: string): Event { +export function makePublishEvent(connectionId: string): Event { return { topic: `conn/publish/${connectionId}`, } } -export function makeConnectionMessageEvent(connectionId: string): Event { +export function makeConnectionMessageEvent(connectionId: string): Event { return { topic: `conn/${connectionId}`, }