Add pause feature
This commit is contained in:
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
108
app/src/components/PauseButton.tsx
Normal file
108
app/src/components/PauseButton.tsx
Normal 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))
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user