From 094d795b39f7e2d32e98fe20b7feac2881cd8432 Mon Sep 17 00:00:00 2001 From: Thomas Nordquist Date: Wed, 3 Apr 2019 17:57:19 +0200 Subject: [PATCH] Add pause feature --- app/src/actions/Tree.ts | 10 +++ app/src/components/PauseButton.tsx | 108 +++++++++++++++++++++++++++++ app/src/components/TitleBar.tsx | 8 ++- app/src/components/Tree/Tree.tsx | 7 +- app/src/reducers/Tree.ts | 20 +++++- backend/src/Model/Tree.ts | 46 +++++++++++- 6 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 app/src/components/PauseButton.tsx diff --git a/app/src/actions/Tree.ts b/app/src/actions/Tree.ts index 0d51279..8998d78 100644 --- a/app/src/actions/Tree.ts +++ b/app/src/actions/Tree.ts @@ -57,3 +57,13 @@ export const showTree = (tree?: q.Tree) => (dispatch: Dispatch) => (dispatch: Dispatch, 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, + }) +} diff --git a/app/src/components/PauseButton.tsx b/app/src/components/PauseButton.tsx new file mode 100644 index 0000000..fcdb06b --- /dev/null +++ b/app/src/components/PauseButton.tsx @@ -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 +} + +class PauseButton extends React.Component { + 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 ( +
+ + + {this.props.paused ? this.renderResume() : this.renderPause()} + + + {this.props.paused ? this.renderBufferStats() : null} +
+ ) + } + + private renderResume() { + return ( + + + + ) + } + + private renderPause() { + return ( + + + + ) + } + + private renderBufferStats() { + if (!this.props.tree) { + return + } + + return ( + + {this.state.changes} changes
+ buffer at {Math.round(this.props.tree.unmergedChanges().fillState() * 10000) / 100}% +
+ ) + } +} + +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)) diff --git a/app/src/components/TitleBar.tsx b/app/src/components/TitleBar.tsx index 50a3bb4..d2d436e 100644 --- a/app/src/components/TitleBar.tsx +++ b/app/src/components/TitleBar.tsx @@ -7,7 +7,7 @@ import Search from '@material-ui/icons/Search' import { AppState } from '../reducers' import { bindActionCreators } from '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 { StyleRulesCallback, withStyles } from '@material-ui/core/styles' import { @@ -18,6 +18,7 @@ import { Toolbar, Typography, } from '@material-ui/core' +import PauseButton from './PauseButton'; const styles: StyleRulesCallback = theme => ({ title: { @@ -72,7 +73,7 @@ const styles: StyleRulesCallback = theme => ({ disconnect: { margin: 'auto 8px auto auto', color: theme.palette.primary.contrastText, - } + }, }) interface Props { @@ -101,10 +102,11 @@ class TitleBar extends React.Component { MQTT Explorer {this.renderSearch()} + - + ) diff --git a/app/src/components/Tree/Tree.tsx b/app/src/components/Tree/Tree.tsx index 57cac5a..b761393 100644 --- a/app/src/components/Tree/Tree.tsx +++ b/app/src/components/Tree/Tree.tsx @@ -25,6 +25,7 @@ interface Props { autoExpandLimit: number highlightTopicUpdates: boolean selectTopicWithMouseOver: boolean + paused: boolean } interface State { @@ -78,7 +79,10 @@ class Tree extends React.PureComponent { this.updateTimer && clearTimeout(this.updateTimer) this.updateTimer = undefined this.renderTime = performance.now() - this.props.tree && this.props.tree.applyUnmergedChanges() + + if (!this.props.paused) { + this.props.tree && this.props.tree.applyUnmergedChanges() + } window.requestIdleCallback(() => { this.setState({ lastUpdate: this.renderTime }) }, { timeout: 100 }) @@ -126,6 +130,7 @@ class Tree extends React.PureComponent { const mapStateToProps = (state: AppState) => { return { tree: state.tree.tree, + paused: state.tree.paused, filter: state.tree.filter, host: state.connection.host, autoExpandLimit: state.settings.autoExpandLimit, diff --git a/app/src/reducers/Tree.ts b/app/src/reducers/Tree.ts index 4dedadd..f308fce 100644 --- a/app/src/reducers/Tree.ts +++ b/app/src/reducers/Tree.ts @@ -7,6 +7,7 @@ export interface TreeState { tree?: q.Tree selectedTopic?: q.TreeNode filter?: string + paused: boolean } export type Action = ShowTree | SelectTopic @@ -14,6 +15,8 @@ export type Action = ShowTree | SelectTopic export enum ActionTypes { TREE_SHOW_TREE = 'TREE_SHOW_TREE', TREE_SELECT_TOPIC = 'TREE_SELECT_TOPIC', + TREE_RESUME_UPDATES = 'TREE_RESUME_UPDATES', + TREE_PAUSE_UPDATES = 'TREE_PAUSE_UPDATES', } export interface ShowTree { @@ -27,11 +30,26 @@ export interface SelectTopic { selectedTopic?: q.TreeNode } -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, { TREE_SHOW_TREE: showTree, TREE_SELECT_TOPIC: selectTopic, + TREE_PAUSE_UPDATES: setPaused(true), + TREE_RESUME_UPDATES: setPaused(false), }) function showTree(state: TreeState, action: ShowTree) { diff --git a/backend/src/Model/Tree.ts b/backend/src/Model/Tree.ts index 33b2b35..0a852ab 100644 --- a/backend/src/Model/Tree.ts +++ b/backend/src/Model/Tree.ts @@ -2,6 +2,43 @@ import { TreeNode } from './' import { EventBusInterface, makeConnectionMessageEvent, MqttMessage, EventDispatcher } from '../../../events' 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 extends TreeNode { public connectionId?: string public updateSource?: EventBusInterface @@ -9,7 +46,7 @@ export class Tree extends TreeNode { private subscriptionEvent?: any public isTree = true private cachedHash = `${Math.random()}` - private unmergedMessages: MqttMessage[] = [] + private unmergedMessages: ChangeBuffer = new ChangeBuffer() public didReceive = new EventDispatcher>(this) constructor() { @@ -30,7 +67,7 @@ export class Tree extends TreeNode { } public applyUnmergedChanges() { - this.unmergedMessages.forEach((msg) => { + this.unmergedMessages.popAll().forEach((msg) => { const edges = msg.topic.split('/') const node = TreeNodeFactory.fromEdgesAndValue(edges, msg.payload) node.mqttMessage = msg @@ -39,7 +76,10 @@ export class Tree extends TreeNode { this.updateWithNode(node.firstNode()) } }) - this.unmergedMessages = [] + } + + public unmergedChanges(): ChangeBuffer { + return this.unmergedMessages } private handleNewData = (msg: MqttMessage) => {