Add pause feature

This commit is contained in:
Thomas Nordquist
2019-04-03 17:57:19 +02:00
parent 8266a87417
commit 094d795b39
6 changed files with 191 additions and 8 deletions

View File

@@ -57,3 +57,13 @@ export const showTree = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<a
type: ActionTypes.TREE_SHOW_TREE, type: ActionTypes.TREE_SHOW_TREE,
}) })
} }
export const togglePause = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
const paused = getState().tree.paused
const tree = getState().tree.tree
// tree && tree.applyUnmergedChanges()
return dispatch({
type: paused ? ActionTypes.TREE_RESUME_UPDATES : ActionTypes.TREE_PAUSE_UPDATES,
})
}

View File

@@ -0,0 +1,108 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import CustomIconButton from './CustomIconButton'
import Pause from '@material-ui/icons/PauseCircleFilled'
import Resume from '@material-ui/icons/PlayCircleFilled'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { treeActions } from '../actions'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { Tooltip } from '@material-ui/core';
const styles: StyleRulesCallback = theme => ({ })
interface Props {
classes: any
actions: {
tree: typeof treeActions,
}
paused: boolean
tree?: q.Tree<any>
}
class PauseButton extends React.Component<Props, {changes: number}> {
private timer?: any
constructor(props: Props) {
super(props)
this.state = { changes: 0 }
}
public componentDidMount() {
this.timer = setInterval(() => {
if (!this.props.paused || !this.props.tree) {
return
}
const changes = this.props.tree.unmergedChanges()
if (this.state.changes !== changes.length) {
this.setState({ changes: changes.length })
}
}, 300)
}
public componentWillUnmount() {
this.timer && clearInterval(this.timer)
}
public render() {
const { actions, classes } = this.props
return (
<div style={{ display: 'inline-flex' }}>
<span>
<CustomIconButton onClick={this.props.actions.tree.togglePause} >
{this.props.paused ? this.renderResume() : this.renderPause()}
</CustomIconButton>
</span>
{this.props.paused ? this.renderBufferStats() : null}
</div>
)
}
private renderResume() {
return (
<Tooltip title="Resumes updating the tree, after applying all recorded changes">
<Resume />
</Tooltip>
)
}
private renderPause() {
return (
<Tooltip title="Stops all updates, records changes until the buffer is full.">
<Pause />
</Tooltip>
)
}
private renderBufferStats() {
if (!this.props.tree) {
return
}
return (
<span>
{this.state.changes} changes<br />
buffer at {Math.round(this.props.tree.unmergedChanges().fillState() * 10000) / 100}%
</span>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
paused: state.tree.paused,
tree: state.tree.tree,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
tree: bindActionCreators(treeActions, dispatch),
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))

View File

@@ -7,7 +7,7 @@ import Search from '@material-ui/icons/Search'
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { connectionActions, settingsActions } from '../actions' import { connectionActions, settingsActions, treeActions } from '../actions'
import { fade } from '@material-ui/core/styles/colorManipulator' import { fade } from '@material-ui/core/styles/colorManipulator'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles' import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { import {
@@ -18,6 +18,7 @@ import {
Toolbar, Toolbar,
Typography, Typography,
} from '@material-ui/core' } from '@material-ui/core'
import PauseButton from './PauseButton';
const styles: StyleRulesCallback = theme => ({ const styles: StyleRulesCallback = theme => ({
title: { title: {
@@ -72,7 +73,7 @@ const styles: StyleRulesCallback = theme => ({
disconnect: { disconnect: {
margin: 'auto 8px auto auto', margin: 'auto 8px auto auto',
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
} },
}) })
interface Props { interface Props {
@@ -101,10 +102,11 @@ class TitleBar extends React.Component<Props, {}> {
</IconButton> </IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Explorer</Typography> <Typography className={classes.title} variant="h6" color="inherit">MQTT Explorer</Typography>
{this.renderSearch()} {this.renderSearch()}
<PauseButton />
<Button className={classes.disconnect} onClick={actions.connection.disconnect}> <Button className={classes.disconnect} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/> Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button> </Button>
<ConnectionHealthIndicator /> <ConnectionHealthIndicator withBackground={true} />
</Toolbar> </Toolbar>
</AppBar> </AppBar>
) )

View File

@@ -25,6 +25,7 @@ interface Props {
autoExpandLimit: number autoExpandLimit: number
highlightTopicUpdates: boolean highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean selectTopicWithMouseOver: boolean
paused: boolean
} }
interface State { interface State {
@@ -78,7 +79,10 @@ class Tree extends React.PureComponent<Props, State> {
this.updateTimer && clearTimeout(this.updateTimer) this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined this.updateTimer = undefined
this.renderTime = performance.now() this.renderTime = performance.now()
this.props.tree && this.props.tree.applyUnmergedChanges()
if (!this.props.paused) {
this.props.tree && this.props.tree.applyUnmergedChanges()
}
window.requestIdleCallback(() => { window.requestIdleCallback(() => {
this.setState({ lastUpdate: this.renderTime }) this.setState({ lastUpdate: this.renderTime })
}, { timeout: 100 }) }, { timeout: 100 })
@@ -126,6 +130,7 @@ class Tree extends React.PureComponent<Props, State> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
tree: state.tree.tree, tree: state.tree.tree,
paused: state.tree.paused,
filter: state.tree.filter, filter: state.tree.filter,
host: state.connection.host, host: state.connection.host,
autoExpandLimit: state.settings.autoExpandLimit, autoExpandLimit: state.settings.autoExpandLimit,

View File

@@ -7,6 +7,7 @@ export interface TreeState {
tree?: q.Tree<TopicViewModel> tree?: q.Tree<TopicViewModel>
selectedTopic?: q.TreeNode<TopicViewModel> selectedTopic?: q.TreeNode<TopicViewModel>
filter?: string filter?: string
paused: boolean
} }
export type Action = ShowTree | SelectTopic export type Action = ShowTree | SelectTopic
@@ -14,6 +15,8 @@ export type Action = ShowTree | SelectTopic
export enum ActionTypes { export enum ActionTypes {
TREE_SHOW_TREE = 'TREE_SHOW_TREE', TREE_SHOW_TREE = 'TREE_SHOW_TREE',
TREE_SELECT_TOPIC = 'TREE_SELECT_TOPIC', TREE_SELECT_TOPIC = 'TREE_SELECT_TOPIC',
TREE_RESUME_UPDATES = 'TREE_RESUME_UPDATES',
TREE_PAUSE_UPDATES = 'TREE_PAUSE_UPDATES',
} }
export interface ShowTree { export interface ShowTree {
@@ -27,11 +30,26 @@ export interface SelectTopic {
selectedTopic?: q.TreeNode<TopicViewModel> selectedTopic?: q.TreeNode<TopicViewModel>
} }
const initialState: TreeState = { } export interface SetPause {
type: ActionTypes.TREE_PAUSE_UPDATES | ActionTypes.TREE_RESUME_UPDATES
}
const initialState: TreeState = {
paused: false,
}
const setPaused = (pause: boolean) => (state: TreeState, action: ShowTree) => {
return {
...state,
paused: pause,
}
}
export const treeReducer = createReducer(initialState, { export const treeReducer = createReducer(initialState, {
TREE_SHOW_TREE: showTree, TREE_SHOW_TREE: showTree,
TREE_SELECT_TOPIC: selectTopic, TREE_SELECT_TOPIC: selectTopic,
TREE_PAUSE_UPDATES: setPaused(true),
TREE_RESUME_UPDATES: setPaused(false),
}) })
function showTree(state: TreeState, action: ShowTree) { function showTree(state: TreeState, action: ShowTree) {

View File

@@ -2,6 +2,43 @@ import { TreeNode } from './'
import { EventBusInterface, makeConnectionMessageEvent, MqttMessage, EventDispatcher } from '../../../events' import { EventBusInterface, makeConnectionMessageEvent, MqttMessage, EventDispatcher } from '../../../events'
import { TreeNodeFactory } from './TreeNodeFactory' import { TreeNodeFactory } from './TreeNodeFactory'
class ChangeBuffer {
private buffer: MqttMessage[] = []
private size = 0
private maxSize = 100_000_000 // ~100MB
public length = 0
public estimatedMessageOverhead = 24
public push(val: MqttMessage) {
if (!this.isFull()) {
this.buffer.push(val)
this.size += this.estimatedMessageOverhead + (val.payload ? val.payload.length : 0)
this.length += 1
}
}
public getSize() {
return this.size
}
public isFull() {
return this.size >= this.maxSize
}
public fillState() {
return this.size / this.maxSize
}
public popAll(): MqttMessage[] {
const tmpBuffer = this.buffer
this.buffer = []
this.size = 0
this.length = 0
return tmpBuffer
}
}
export class Tree<ViewModel> extends TreeNode<ViewModel> { export class Tree<ViewModel> extends TreeNode<ViewModel> {
public connectionId?: string public connectionId?: string
public updateSource?: EventBusInterface public updateSource?: EventBusInterface
@@ -9,7 +46,7 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
private subscriptionEvent?: any private subscriptionEvent?: any
public isTree = true public isTree = true
private cachedHash = `${Math.random()}` private cachedHash = `${Math.random()}`
private unmergedMessages: MqttMessage[] = [] private unmergedMessages: ChangeBuffer = new ChangeBuffer()
public didReceive = new EventDispatcher<void, Tree<ViewModel>>(this) public didReceive = new EventDispatcher<void, Tree<ViewModel>>(this)
constructor() { constructor() {
@@ -30,7 +67,7 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
} }
public applyUnmergedChanges() { public applyUnmergedChanges() {
this.unmergedMessages.forEach((msg) => { this.unmergedMessages.popAll().forEach((msg) => {
const edges = msg.topic.split('/') const edges = msg.topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue<ViewModel>(edges, msg.payload) const node = TreeNodeFactory.fromEdgesAndValue<ViewModel>(edges, msg.payload)
node.mqttMessage = msg node.mqttMessage = msg
@@ -39,7 +76,10 @@ export class Tree<ViewModel> extends TreeNode<ViewModel> {
this.updateWithNode(node.firstNode()) this.updateWithNode(node.firstNode())
} }
}) })
this.unmergedMessages = [] }
public unmergedChanges(): ChangeBuffer {
return this.unmergedMessages
} }
private handleNewData = (msg: MqttMessage) => { private handleNewData = (msg: MqttMessage) => {