Refactor communication
Add QoS andd retain flag Refactor reducer
This commit is contained in:
@@ -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<Props, State> {
|
||||
class App extends React.Component<Props, {}> {
|
||||
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<Props, State> {
|
||||
<TitleBar />
|
||||
<div style={centerContent}>
|
||||
<div style={this.getStyles().left}>
|
||||
<Tree connectionId={this.props.connectionId} didSelectNode={(node: q.TreeNode) => {
|
||||
this.setState({ selectedNode: node })
|
||||
}} />
|
||||
<Tree connectionId={this.props.connectionId} />
|
||||
</div>
|
||||
<div style={this.getStyles().right}>
|
||||
<Sidebar connectionId={this.props.connectionId} />
|
||||
@@ -113,8 +104,8 @@ class App extends React.Component<Props, State> {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
settingsVisible: state.settings.visible,
|
||||
connectionId: state.connectionId,
|
||||
settingsVisible: state.tooBigReducer.settings.visible,
|
||||
connectionId: state.tooBigReducer.connectionId,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const showError = (error?: string) => ({
|
||||
})
|
||||
|
||||
export const disconnect = () => (dispatch: Dispatch<CustomAction>, getState: () => AppState) => {
|
||||
rendererEvents.emit(removeConnection, getState().connectionId)
|
||||
rendererEvents.emit(removeConnection, getState().tooBigReducer.connectionId)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.disconnect,
|
||||
|
||||
61
app/src/actions/Publish.ts
Normal file
61
app/src/actions/Publish.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -380,10 +380,10 @@ class Connection extends React.Component<Props, State> {
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,9 +138,9 @@ class Settings extends React.Component<Props, {}> {
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Props, State> {
|
||||
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<HTMLInputElement>) => {
|
||||
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<Props, State> {
|
||||
}
|
||||
|
||||
private clearTopic = () => {
|
||||
this.props.actions.setPublishTopic(undefined)
|
||||
this.props.actions.setTopic('')
|
||||
}
|
||||
|
||||
private topic() {
|
||||
@@ -129,7 +135,7 @@ class Publish extends React.Component<Props, State> {
|
||||
|
||||
private onTopicBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (!e.target.value) {
|
||||
this.props.actions.setPublishTopic(undefined)
|
||||
this.props.actions.setTopic(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,13 +158,27 @@ class Publish extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
private editorMode() {
|
||||
const labelStyle = { margin: '0 8px 0 8px' }
|
||||
return (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<div style={{ width: '100%', lineHeight: '64px' }}>
|
||||
{this.renderEditorModeSelection()}
|
||||
<div style={{ float: 'right', marginRight: '16px' }}>
|
||||
{this.publishButton()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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.state.mode}
|
||||
value={this.props.editorMode}
|
||||
onChange={this.updateMode}
|
||||
row={true}
|
||||
>
|
||||
@@ -184,9 +204,64 @@ class Publish extends React.Component<Props, State> {
|
||||
labelPlacement="top"
|
||||
/>
|
||||
</RadioGroup>
|
||||
<div style={{ float: 'right', marginRight: '16px' }}>
|
||||
{this.publishButton()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
@@ -203,16 +278,28 @@ class Publish extends React.Component<Props, State> {
|
||||
|
||||
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 (
|
||||
<div style={{ width: '100%', display: 'block' }}>
|
||||
{this.editorMode()}
|
||||
{this.renderEditor()}
|
||||
{this.publishMode()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderEditor() {
|
||||
if (this.props.emptyPayload) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<AceEditor
|
||||
mode={this.state.mode}
|
||||
mode={this.props.editorMode}
|
||||
theme="monokai"
|
||||
name="UNIQUE_ID_OF_DIV"
|
||||
width="100%"
|
||||
@@ -223,21 +310,24 @@ class Publish extends React.Component<Props, State> {
|
||||
setOptions={this.editorOptions}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,21 +116,13 @@ class Sidebar extends React.Component<Props, State> {
|
||||
<Typography className={classes.heading}>Value {copyValue}</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails style={this.detailsStyle}>
|
||||
<div style={{ width: '100%', textAlign:'right' }}>
|
||||
{this.props.node && this.props.node.message && <i><DateFormatter date={this.props.node.message.received} /></i>}
|
||||
</div>
|
||||
{this.messageMetaInfo()}
|
||||
<div ref={this.valueRef}>
|
||||
<ValueRenderer message={this.props.node && this.props.node.message} />
|
||||
</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}>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade {...TransitionProps} timeout={350}>
|
||||
<Paper>
|
||||
<ValueRenderer message={this.state.compareMessage} />
|
||||
</Paper>
|
||||
</Fade>
|
||||
)}
|
||||
{this.showValueComparison}
|
||||
</Popper>
|
||||
</ExpansionPanelDetails>
|
||||
</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) => {
|
||||
if (message !== this.state.compareMessage) {
|
||||
this.setState({ compareMessage: message })
|
||||
@@ -175,7 +191,7 @@ class Sidebar extends React.Component<Props, State> {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
node: state.selectedTopic,
|
||||
node: state.tooBigReducer.selectedTopic,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Props, State> {
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<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 />
|
||||
</IconButton>
|
||||
<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' }}/>
|
||||
</Button>
|
||||
</Toolbar>
|
||||
@@ -122,7 +125,10 @@ class TitleBar extends React.Component<Props, State> {
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: { ...bindActionCreators(connectionActions, dispatch), ...bindActionCreators(settingsActions, dispatch) },
|
||||
actions: {
|
||||
settings: bindActionCreators(settingsActions, dispatch),
|
||||
connection: bindActionCreators(connectionActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Props, TreeState> {
|
||||
}
|
||||
}
|
||||
|
||||
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<Props, TreeState> {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
autoExpandLimit: state.settings.autoExpandLimit,
|
||||
connected: state.connected,
|
||||
autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
|
||||
connected: state.tooBigReducer.connected,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class TreeNodeSubnodes extends React.Component<Props, {}> {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
nodeOrder: state.settings.nodeOrder,
|
||||
nodeOrder: state.tooBigReducer.settings.nodeOrder,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
108
app/src/reducers/Publish.ts
Normal file
108
app/src/reducers/Publish.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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<AppState | undefined, CustomAction> = (state = initialAppState, action) => {
|
||||
const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => {
|
||||
if (!state) {
|
||||
throw Error('No initial state')
|
||||
}
|
||||
@@ -97,16 +91,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (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<AppState | undefined, CustomAction> = (state = initialApp
|
||||
visible: !state.settings.visible,
|
||||
},
|
||||
}
|
||||
|
||||
case ActionTypes.selectTopic:
|
||||
if (!action.selectedTopic) {
|
||||
return state
|
||||
@@ -123,6 +109,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
|
||||
...state,
|
||||
selectedTopic: action.selectedTopic,
|
||||
}
|
||||
|
||||
case ActionTypes.setNodeOrder:
|
||||
if (!action.nodeOrder) {
|
||||
return state
|
||||
@@ -131,11 +118,13 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (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<AppState | undefined, CustomAction> = (state = initialApp
|
||||
...state,
|
||||
showUpdateDetails: action.showUpdateDetails,
|
||||
}
|
||||
|
||||
case ActionTypes.connecting:
|
||||
if (!action.connectionId) {
|
||||
return state
|
||||
@@ -181,4 +171,9 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialApp
|
||||
}
|
||||
}
|
||||
|
||||
const reducer = combineReducers({
|
||||
tooBigReducer,
|
||||
publish: publishReducer,
|
||||
})
|
||||
|
||||
export default reducer
|
||||
|
||||
9
app/src/reducers/lib.ts
Normal file
9
app/src/reducers/lib.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataSourceStateMachine } from './'
|
||||
import { MqttMessage } from '../../../events'
|
||||
|
||||
type MessageCallback = (topic: string, payload: Buffer) => void
|
||||
|
||||
@@ -7,7 +8,7 @@ interface DataSource<DataSourceOptions> {
|
||||
connect(options: DataSourceOptions): DataSourceStateMachine
|
||||
disconnect(): void
|
||||
onMessage(messageCallback: MessageCallback): void
|
||||
publish(topic: string, payload: any): void
|
||||
publish(msg: MqttMessage): void
|
||||
topicSeparator: string
|
||||
stateMachine: DataSourceStateMachine
|
||||
}
|
||||
|
||||
@@ -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<MqttOptions> {
|
||||
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<MqttOptions> {
|
||||
})
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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<Message> = new RingBuffer<Message>(3000, 100)
|
||||
public edges: {[s: string]: Edge} = {}
|
||||
public collapsed = false
|
||||
public messages: number = 0
|
||||
public lastUpdate: number = Date.now()
|
||||
|
||||
public onMerge = new EventDispatcher<void, TreeNode>(this)
|
||||
public onEdgesChange = new EventDispatcher<void, TreeNode>(this)
|
||||
public onMessage = new EventDispatcher<Message, TreeNode>(this)
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -35,18 +35,20 @@ export const updateAvailable: Event<UpdateInfo> = {
|
||||
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<Message> {
|
||||
export function makePublishEvent(connectionId: string): Event<MqttMessage> {
|
||||
return {
|
||||
topic: `conn/publish/${connectionId}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeConnectionMessageEvent(connectionId: string): Event<Message> {
|
||||
export function makeConnectionMessageEvent(connectionId: string): Event<MqttMessage> {
|
||||
return {
|
||||
topic: `conn/${connectionId}`,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user