Refactor connection reducer & tree

This commit is contained in:
Thomas Nordquist
2019-01-20 22:34:15 +01:00
parent ecd3a23311
commit ba4efbe30d
10 changed files with 163 additions and 107 deletions

View File

@@ -87,7 +87,7 @@ class App extends React.Component<Props, {}> {
<TitleBar /> <TitleBar />
<div style={centerContent}> <div style={centerContent}>
<div style={this.getStyles().left}> <div style={this.getStyles().left}>
<Tree connectionId={this.props.connectionId} /> <Tree />
</div> </div>
<div style={this.getStyles().right}> <div style={this.getStyles().right}>
<Sidebar connectionId={this.props.connectionId} /> <Sidebar connectionId={this.props.connectionId} />
@@ -105,7 +105,7 @@ class App extends React.Component<Props, {}> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
settingsVisible: state.tooBigReducer.settings.visible, settingsVisible: state.tooBigReducer.settings.visible,
connectionId: state.tooBigReducer.connectionId, connectionId: state.connection.connectionId,
} }
} }

View File

@@ -1,7 +1,9 @@
import { ActionTypes, NodeOrder, CustomAction, AppState } from '../reducers' import { ActionTypes, Action, ConnectionState } from '../reducers/Connection'
import { MqttOptions } from '../../../backend/src/DataSource' import { MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events' import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events'
import { AppState } from '../reducers'
import * as q from '../../../backend/src/Model'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => { export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId)) dispatch(connecting(connectionId))
@@ -9,7 +11,9 @@ export const connect = (options: MqttOptions, connectionId: string) => (dispatch
const event = makeConnectionStateEvent(connectionId) const event = makeConnectionStateEvent(connectionId)
rendererEvents.subscribe(event, (dataSourceState) => { rendererEvents.subscribe(event, (dataSourceState) => {
if (dataSourceState.connected) { if (dataSourceState.connected) {
dispatch(connected()) const tree = new q.Tree()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(connected(tree))
} else if (dataSourceState.error) { } else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error)) dispatch(showError(dataSourceState.error))
dispatch(disconnect()) dispatch(disconnect())
@@ -17,24 +21,27 @@ export const connect = (options: MqttOptions, connectionId: string) => (dispatch
}) })
} }
export const connected: () => CustomAction = () => ({ export const connected: (tree: q.Tree) => Action = (tree: q.Tree) => ({
type: ActionTypes.connected, tree,
type: ActionTypes.CONNECTION_SET_CONNECTED,
}) })
export const connecting: (connectionId: string) => CustomAction = (connectionId: string) => ({ export const connecting: (connectionId: string) => Action = (connectionId: string) => ({
connectionId, connectionId,
type: ActionTypes.connecting, type: ActionTypes.CONNECTION_SET_CONNECTING,
}) })
export const showError = (error?: string) => ({ export const showError = (error?: string) => ({
error, error,
type: ActionTypes.showError, type: ActionTypes.CONNECTION_SET_SHOW_ERROR,
}) })
export const disconnect = () => (dispatch: Dispatch<CustomAction>, getState: () => AppState) => { export const disconnect = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
rendererEvents.emit(removeConnection, getState().tooBigReducer.connectionId) const { connectionId, tree } = getState().connection
rendererEvents.emit(removeConnection, connectionId)
tree && tree.stopUpdating()
dispatch({ dispatch({
type: ActionTypes.disconnect, type: ActionTypes.CONNECTION_SET_DISCONNECTED,
}) })
} }

View File

@@ -3,7 +3,9 @@ import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events' import { makePublishEvent, rendererEvents } from '../../../events'
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => { export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic, connectionId } = getState().tooBigReducer const { selectedTopic } = getState().tooBigReducer
const { connectionId } = getState().connection
if (!selectedTopic || !connectionId) { if (!selectedTopic || !connectionId) {
return return
} }

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.tooBigReducer.connected, visible: !state.connection.connected,
connected: state.tooBigReducer.connected, connected: state.connection.connected,
connecting: state.tooBigReducer.connecting, connecting: state.connection.connecting,
error: state.tooBigReducer.error, error: state.connection.error,
} }
} }

View File

@@ -1,5 +1,4 @@
import * as React from 'react' import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Badge, Typography } from '@material-ui/core' import { Badge, Typography } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'

View File

@@ -18,22 +18,17 @@ interface Props {
autoExpandLimit: number autoExpandLimit: number
didSelectNode?: (node: q.TreeNode) => void didSelectNode?: (node: q.TreeNode) => void
connectionId?: string connectionId?: string
connected: boolean tree?: q.Tree
} }
interface TreeState { class Tree extends React.Component<Props, {}> {
tree: q.Tree
msg: any
}
class Tree extends React.Component<Props, TreeState> {
private updateTimer?: any private updateTimer?: any
private lastUpdate: number = 0 private lastUpdate: number = 0
private perf: number = 0 private perf: number = 0
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.state = { tree: new q.Tree(), msg: {} } this.state = { }
} }
public time(): number { public time(): number {
@@ -43,11 +38,18 @@ class Tree extends React.Component<Props, TreeState> {
return time return time
} }
private performanceCallback = (ms: number) => { public componentWillReceiveProps(nextProps: Props) {
average.push(Date.now(), ms) if (this.props.tree !== nextProps.tree) {
if (this.props.tree) {
this.props.tree.onMerge.unsubscribe(this.throttledTreeUpdate)
}
if (nextProps.tree) {
nextProps.tree.onMerge.subscribe(this.throttledTreeUpdate)
}
}
} }
public throttledTreeUpdate() { public throttledTreeUpdate = () => {
if (this.updateTimer) { if (this.updateTimer) {
return return
} }
@@ -66,23 +68,6 @@ class Tree extends React.Component<Props, TreeState> {
}, Math.max(0, timeUntilNextUpdate)) }, Math.max(0, timeUntilNextUpdate))
} }
public componentWillReceiveProps(nextProps: Props) {
this.registerAndUnregisterEventSubscriptionsForNewProps(nextProps)
}
private registerAndUnregisterEventSubscriptionsForNewProps(nextProps: Props) {
if (this.props.connectionId !== nextProps.connectionId) {
if (this.props.connectionId) {
this.setState({ tree: new q.Tree() })
rendererEvents.unsubscribeAll(makeConnectionMessageEvent(this.props.connectionId))
this.updateTimer && clearTimeout(this.updateTimer)
}
if (nextProps.connectionId) {
rendererEvents.subscribe(makeConnectionMessageEvent(nextProps.connectionId), this.handleNewData)
}
}
}
public componentWillUnmount() { public componentWillUnmount() {
if (this.props.connectionId) { if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId) const event = makeConnectionMessageEvent(this.props.connectionId)
@@ -90,17 +75,8 @@ class Tree extends React.Component<Props, TreeState> {
} }
} }
private handleNewData = (msg: MqttMessage) => {
const edges = msg.topic.split('/')
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, msg.payload)
node.mqttMessage = msg
this.state.tree.updateWithNode(node.firstNode())
this.throttledTreeUpdate()
}
public render() { public render() {
if (!this.props.connected) { if (!this.props.tree) {
return null return null
} }
@@ -116,22 +92,26 @@ class Tree extends React.Component<Props, TreeState> {
autoExpandLimit={this.props.autoExpandLimit} autoExpandLimit={this.props.autoExpandLimit}
isRoot={true} isRoot={true}
didSelectNode={this.props.didSelectNode} didSelectNode={this.props.didSelectNode}
treeNode={this.state.tree} treeNode={this.props.tree}
name="/" name="/"
collapsed={false} collapsed={false}
key="rootNode" key="rootNode"
lastUpdate={this.state.tree.lastUpdate} lastUpdate={this.props.tree.lastUpdate}
performanceCallback={this.performanceCallback} performanceCallback={this.performanceCallback}
/> />
</div> </div>
) )
} }
private performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
} }
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit, autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
connected: state.tooBigReducer.connected, tree: state.connection.tree,
} }
} }

View File

@@ -0,0 +1,88 @@
import { Action } from 'redux'
import { createReducer } from './lib'
import * as q from '../../../backend/src/Model'
import { MqttOptions } from '../../../backend/src/DataSource'
export interface ConnectionState {
tree?: q.Tree
connectionOptions?: MqttOptions
connectionId?: string
error?: string
connected: boolean
connecting: boolean
}
export type Action = SetConnecting | SetConnected | SetDisconnected | ShowError
export enum ActionTypes {
CONNECTION_SET_CONNECTING = 'CONNECTION_SET_CONNECTING',
CONNECTION_SET_CONNECTED = 'CONNECTION_SET_CONNECTED',
CONNECTION_SET_DISCONNECTED = 'CONNECTION_SET_DISCONNECTED',
CONNECTION_SET_SHOW_ERROR = 'CONNECTION_SET_SHOW_ERROR',
}
export interface SetConnecting {
type: ActionTypes.CONNECTION_SET_CONNECTING,
connectionId: string
}
export interface SetConnected {
type: ActionTypes.CONNECTION_SET_CONNECTED
tree: q.Tree
}
export interface SetDisconnected {
type: ActionTypes.CONNECTION_SET_DISCONNECTED
}
export interface ShowError {
type: ActionTypes.CONNECTION_SET_SHOW_ERROR
error?: Error
}
const initialState: ConnectionState = {
connected: false,
connecting: false,
}
export const connectionReducer = createReducer(initialState, {
CONNECTION_SET_CONNECTING: setConnecting,
CONNECTION_SET_CONNECTED: setConnected,
CONNECTION_SET_DISCONNECTED: setDisconnected,
CONNECTION_SET_SHOW_ERROR: showError,
})
function setConnecting(state: ConnectionState, action: SetConnecting) {
return {
...state,
connecting: true,
connected: false,
connectionId: action.connectionId,
}
}
function setConnected(state: ConnectionState, action: SetConnected) {
return {
...state,
connecting: false,
connected: true,
tree: action.tree,
}
}
function setDisconnected(state: ConnectionState, action: SetDisconnected) {
return {
...state,
connecting: false,
connected: false,
connectionId: undefined,
tree: undefined,
}
}
function showError(state: ConnectionState, action: ShowError) {
return {
...state,
error: action.error,
}
}

View File

@@ -3,20 +3,18 @@ import * as q from '../../../backend/src/Model'
import { Action, Reducer, combineReducers } from 'redux' import { Action, Reducer, combineReducers } from 'redux'
import { trackEvent } from '../tracking' import { trackEvent } from '../tracking'
import { MqttOptions } from '../../../backend/src/DataSource'
import { PublishState, publishReducer } from './Publish' import { PublishState, publishReducer } from './Publish'
import { ConnectionState, connectionReducer } from './Connection'
export enum ActionTypes { export enum ActionTypes {
disconnect = 'DISCONNECT',
showError = 'SHOW_ERROR',
setAutoExpandLimit = 'SET_AUTO_EXPAND_LIMIT', setAutoExpandLimit = 'SET_AUTO_EXPAND_LIMIT',
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY', toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
setNodeOrder = 'SET_NODE_ORDER', setNodeOrder = 'SET_NODE_ORDER',
selectTopic = 'SELECT_TOPIC', selectTopic = 'SELECT_TOPIC',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION', showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS', showUpdateDetails = 'SHOW_UPDATE_DETAILS',
connecting = 'CONNECTING',
connected = 'CONNECTED',
} }
export interface CustomAction extends Action { export interface CustomAction extends Action {
@@ -26,14 +24,12 @@ export interface CustomAction extends Action {
selectedTopic?: q.TreeNode selectedTopic?: q.TreeNode
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails?: boolean showUpdateDetails?: boolean
connectionOptions?: MqttOptions
connectionId?: string
error?: string
} }
export interface AppState { export interface AppState {
tooBigReducer: TooBigOfState tooBigReducer: TooBigOfState
publish: PublishState publish: PublishState
connection: ConnectionState
} }
export interface TooBigOfState { export interface TooBigOfState {
@@ -41,10 +37,6 @@ export interface TooBigOfState {
selectedTopic?: q.TreeNode selectedTopic?: q.TreeNode
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails: boolean showUpdateDetails: boolean
connecting: boolean
connected: boolean
error?: string
connectionId?: string
} }
export interface SettingsState { export interface SettingsState {
@@ -68,9 +60,6 @@ const initialBigState: TooBigOfState = {
}, },
selectedTopic: undefined, selectedTopic: undefined,
showUpdateDetails: false, showUpdateDetails: false,
connected: false,
connecting: false,
error: undefined,
} }
const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => { const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => {
@@ -134,38 +123,6 @@ const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state =
showUpdateDetails: action.showUpdateDetails, showUpdateDetails: action.showUpdateDetails,
} }
case ActionTypes.connecting:
if (!action.connectionId) {
return state
}
return {
...state,
connected: false,
connecting: true,
connectionId: action.connectionId,
}
case ActionTypes.disconnect:
return {
...state,
connected: false,
connecting: false,
connectionId: undefined,
}
case ActionTypes.connected:
return {
...state,
connected: true,
connecting: false,
}
case ActionTypes.showError:
return {
...state,
error: action.error,
}
default: default:
return state return state
} }
@@ -174,6 +131,7 @@ const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state =
const reducer = combineReducers({ const reducer = combineReducers({
tooBigReducer, tooBigReducer,
publish: publishReducer, publish: publishReducer,
connection: connectionReducer,
}) })
export default reducer export default reducer

View File

@@ -1,7 +1,29 @@
import { Edge, TreeNode } from './' import { Edge, TreeNode } from './'
import { EventBusInterface, makeConnectionMessageEvent, MqttMessage } from '../../../events'
import { TreeNodeFactory } from './TreeNodeFactory'
export class Tree extends TreeNode { export class Tree extends TreeNode {
private connectionId?: string
private updateSource?: EventBusInterface
constructor() { constructor() {
super(undefined, undefined) super(undefined, undefined)
} }
public updateWithConnection(emitter: EventBusInterface, connectionId: string) {
this.updateSource = emitter
this.updateSource.subscribe(makeConnectionMessageEvent(connectionId), this.handleNewData)
}
private handleNewData = (msg: MqttMessage) => {
const edges = msg.topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue(edges, msg.payload)
node.mqttMessage = msg
this.updateWithNode(node.firstNode())
}
public stopUpdating() {
if (this.updateSource && this.connectionId) {
this.updateSource.unsubscribeAll(makeConnectionMessageEvent(this.connectionId))
}
}
} }

View File

@@ -2,7 +2,7 @@ import { IpcMain, IpcRenderer, ipcMain, ipcRenderer } from 'electron'
import { Event } from './Events' import { Event } from './Events'
interface EventBusInterface { export interface EventBusInterface {
subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void): void subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void): void
unsubscribeAll<MessageType>(event: Event<MessageType>): void unsubscribeAll<MessageType>(event: Event<MessageType>): void
emit<MessageType>(event: Event<MessageType>, msg: MessageType): void emit<MessageType>(event: Event<MessageType>, msg: MessageType): void