From a677fb7a0c4828a380db365787b0350e5d7836fc Mon Sep 17 00:00:00 2001 From: Thomas Nordquist Date: Sat, 12 Jan 2019 20:25:52 +0100 Subject: [PATCH] Add message history to frontend --- app/src/components/Sidebar/History.tsx | 87 +++++++++++++++++++ app/src/components/Sidebar/MessageHistory.tsx | 49 +++++++++++ .../Sidebar/Publish/HistoryEntry.tsx | 39 --------- .../components/Sidebar/Publish/Publish.tsx | 70 ++++++++------- app/src/components/Sidebar/Sidebar.tsx | 44 ++++++---- app/src/components/Sidebar/ValueRenderer.tsx | 35 +++++--- app/src/components/Tree/Tree.tsx | 4 +- app/src/components/Tree/TreeNodeTitle.tsx | 2 +- app/src/index.tsx | 2 +- backend/src/Model/TreeNode.ts | 2 +- 10 files changed, 233 insertions(+), 101 deletions(-) create mode 100644 app/src/components/Sidebar/History.tsx create mode 100644 app/src/components/Sidebar/MessageHistory.tsx delete mode 100644 app/src/components/Sidebar/Publish/HistoryEntry.tsx diff --git a/app/src/components/Sidebar/History.tsx b/app/src/components/Sidebar/History.tsx new file mode 100644 index 0000000..9597e8d --- /dev/null +++ b/app/src/components/Sidebar/History.tsx @@ -0,0 +1,87 @@ +import * as React from 'react' +import * as q from '../../../../backend/src/Model' + +import { Badge, Typography } from '@material-ui/core' +import { Theme, withStyles } from '@material-ui/core/styles' + +interface HistoryItem { + title: string + value: string +} + +interface Props { + items: HistoryItem[] + onClick?: (index: number) => void + classes: any +} + +interface State { + collapsed: boolean +} + +class MessageHistory extends React.Component { + constructor(props: any) { + super(props) + this.state = { + collapsed: true, + } + } + + public renderHistory() { + const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' } + const elements = this.props.items.map((element, index) => ( +
this.props.onClick && this.props.onClick(index)} + > +
{element.title}
+
+
{element.value}
+
+
+ )) + + return ( +
+ + {this.state.collapsed ? '▶' : '▼'} History + + {this.state.collapsed ? null : elements} +
+ ) + } + + public render() { + const visible = this.props.items.length > 0 && this.state.collapsed + return ( + + {this.renderHistory()} + + ) + } + + private toggle = () => { + this.setState({ collapsed: !this.state.collapsed }) + } +} + +const styles = (theme: Theme) => ({ + badge: {top: '-8px', left:'64px'} +}); + +export default withStyles(styles)(MessageHistory) diff --git a/app/src/components/Sidebar/MessageHistory.tsx b/app/src/components/Sidebar/MessageHistory.tsx new file mode 100644 index 0000000..fff7a64 --- /dev/null +++ b/app/src/components/Sidebar/MessageHistory.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import * as q from '../../../../backend/src/Model' + +import { Theme, withTheme } from '@material-ui/core/styles' + +import History from './History' + +interface Props { + node?: q.TreeNode + theme: Theme +} + +class MessageHistory extends React.Component { + constructor(props: any) { + super(props) + } + + private updateNode = () => { + this.setState(this.state) + } + + public componentWillReceiveProps(nextProps: Props) { + this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode) + nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode) + } + + public componentWillMount() { + this.props.node && this.props.node.onMessage.subscribe(this.updateNode) + } + + public componentWillUnMount() { + this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode) + } + + public render() { + if (!this.props.node) { + return null + } + const history = this.props.node.messageHistory.toArray() + const historyElements = history.map(message => ({ + title: message.received.toGMTString(), + value: message.value, + })) + + return + } +} + +export default withTheme()(MessageHistory) diff --git a/app/src/components/Sidebar/Publish/HistoryEntry.tsx b/app/src/components/Sidebar/Publish/HistoryEntry.tsx deleted file mode 100644 index 42e6917..0000000 --- a/app/src/components/Sidebar/Publish/HistoryEntry.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react' -import { connect } from 'react-redux' -import { bindActionCreators } from 'redux' -import { sidebarActions } from '../../../actions' -import { Typography } from '@material-ui/core' -import Message from './Model/Message' - -interface Props { - message: Message - actions: any -} - -class HystoryEntry extends React.Component { - public render() { - const { message } = this.props - return ( - -
-
{message.topic}
-
{message.payload}
-
-
- ) - } - - private setPublishPreset = (e: React.MouseEvent) => { - e.stopPropagation() - this.props.actions.setPublishTopic(this.props.message.topic) - this.props.actions.setPublishPayload(this.props.message.payload) - } -} - -const mapDispatchToProps = (dispatch: any) => { - return { - actions: bindActionCreators(sidebarActions, dispatch), - } -} - -export default connect(null, mapDispatchToProps)(HystoryEntry) diff --git a/app/src/components/Sidebar/Publish/Publish.tsx b/app/src/components/Sidebar/Publish/Publish.tsx index 5766d33..3128c84 100644 --- a/app/src/components/Sidebar/Publish/Publish.tsx +++ b/app/src/components/Sidebar/Publish/Publish.tsx @@ -1,20 +1,3 @@ -import * as React from 'react' -import { connect } from 'react-redux' -import { bindActionCreators } from 'redux' -import * as q from '../../../../../backend/src/Model' -import { AppState } from '../../../reducers' -import { rendererEvents, makePublishEvent } from '../../../../../events' -import { sidebarActions } from '../../../actions' -import Navigation from '@material-ui/icons/Navigation' -import { - Button, Fab, InputAdornment, FormControlLabel, Radio, - RadioGroup, TextField, Typography, -} from '@material-ui/core' -import Message from './Model/Message' -import HistoryEntry from './HistoryEntry' - -import * as brace from 'brace' -import { default as AceEditor } from 'react-ace' // tslint:disable-next-line import 'react-ace' import 'brace/mode/json' @@ -22,6 +5,31 @@ import 'brace/mode/text' 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 { + Button, + Fab, + FormControlLabel, + InputAdornment, + Radio, + RadioGroup, + TextField, + Typography, +} from '@material-ui/core' +import { makePublishEvent, rendererEvents } from '../../../../../events' + +import { default as AceEditor } from 'react-ace' +import { AppState } from '../../../reducers' +import History from '../History' +import Message from './Model/Message' +import Navigation from '@material-ui/icons/Navigation' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { sidebarActions } from '../../../actions' + interface Props { node?: q.TreeNode connectionId?: string @@ -35,7 +43,7 @@ interface State { history: Message[] } -class Publisher extends React.Component { +class Publish extends React.Component { constructor(props: any) { super(props) this.state = { mode: 'json', history: [] } @@ -140,7 +148,7 @@ class Publisher extends React.Component { const labelStyle = { margin: '0 8px 0 8px' } return (
- +
{
{this.publishButton()}
- +
) } private history() { - const entries = this.state.history.map(message => ( - - )) + const items = this.state.history.map(message => ({ + title: message.topic, + value: message.payload, + })) - return ( -
- History - {entries} -
- ) + return + } + + private didSelectHistoryEntry = (index: number) => { + let message = this.state.history[index] + this.props.actions.setPublishTopic(message.topic) + this.props.actions.setPublishPayload(message.payload) } private editor() { @@ -224,4 +234,4 @@ const mapStateToProps = (state: AppState) => { } } -export default connect(mapStateToProps, mapDispatchToProps)(Publisher) +export default connect(mapStateToProps, mapDispatchToProps)(Publish) diff --git a/app/src/components/Sidebar/Sidebar.tsx b/app/src/components/Sidebar/Sidebar.tsx index d685821..905c542 100644 --- a/app/src/components/Sidebar/Sidebar.tsx +++ b/app/src/components/Sidebar/Sidebar.tsx @@ -1,16 +1,17 @@ import * as React from 'react' -import { connect } from 'react-redux' -import { AppState } from '../../reducers' import * as q from '../../../../backend/src/Model' -import { ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core' -import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles' -import ExpandMore from '@material-ui/icons/ExpandMore' -import Publish from './Publish/Publish' +import { ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core' +import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles' + +import { AppState } from '../../reducers' import Copy from '../Copy' -import ValueRenderer from './ValueRenderer' +import ExpandMore from '@material-ui/icons/ExpandMore' import NodeStats from './NodeStats' +import Publish from './Publish/Publish' import Topic from './Topic' +import ValueRenderer from './ValueRenderer' +import { connect } from 'react-redux' interface Props { node?: q.TreeNode, @@ -74,20 +75,21 @@ class Sidebar extends React.Component { ) } + private detailsStyle = { padding: '0px 16px 8px 8px' } + private renderNode() { const { classes, node } = this.props const copyTopic = node ? : null const copyValue = node && node.message ? : null const summeryStyle = { minHeight: '0' } - const detailsStyle = { padding: '0px 16px 8px 8px' } return (
- + } style={summeryStyle}> Topic {copyTopic} - + @@ -95,7 +97,7 @@ class Sidebar extends React.Component { } style={summeryStyle}> Value {copyValue} - + @@ -103,21 +105,31 @@ class Sidebar extends React.Component { } style={summeryStyle}> Publish - + - + } style={summeryStyle}> Stats - - {this.props.node ? : null} - + {this.renderNodeStats()}
) } + + private renderNodeStats() { + if (!this.props.node) { + return null + } + + return ( + + + + ) + } } const mapStateToProps = (state: AppState) => { diff --git a/app/src/components/Sidebar/ValueRenderer.tsx b/app/src/components/Sidebar/ValueRenderer.tsx index 68f6136..c2826de 100644 --- a/app/src/components/Sidebar/ValueRenderer.tsx +++ b/app/src/components/Sidebar/ValueRenderer.tsx @@ -1,15 +1,18 @@ import * as React from 'react' import * as q from '../../../../backend/src/Model' + +import { Theme, withTheme } from '@material-ui/core/styles' + +import MessageHistory from './MessageHistory' import { default as ReactJson } from 'react-json-view' -import { withTheme, Theme } from '@material-ui/core/styles' interface Props { - node?: q.TreeNode | undefined + node?: q.TreeNode theme: Theme } interface State { - node?: q.TreeNode | undefined + node?: q.TreeNode } class ValueRenderer extends React.Component { @@ -28,6 +31,15 @@ class ValueRenderer extends React.Component { } public render() { + return ( +
+ {this.renderValue()} + +
+ ) + } + + public renderValue() { const node = this.props.node if (!node || !node.message) { return null @@ -47,19 +59,20 @@ class ValueRenderer extends React.Component { } else if (typeof json === 'boolean') { return this.renderRawValue(node.message.value) } else { - const theme = this.props.theme.palette.type === 'dark' ? 'monokai' : 'bright:inverted' - return { - console.log(val) - }} /> + const theme = (this.props.theme.palette.type === 'dark') ? 'monokai' : 'bright:inverted' + return ( + + ) } } private renderRawValue(value: string) { const style: React.CSSProperties = { + backgroundColor: 'rgba(80, 80, 80, 0.6)', wordBreak: 'break-all', width: '100%', overflow: 'scroll', diff --git a/app/src/components/Tree/Tree.tsx b/app/src/components/Tree/Tree.tsx index c9e2427..542d01f 100644 --- a/app/src/components/Tree/Tree.tsx +++ b/app/src/components/Tree/Tree.tsx @@ -108,7 +108,7 @@ class Tree extends React.Component { } return ( - +
{ key="rootNode" lastUpdate={this.state.tree.lastUpdate} /> - +
) } } diff --git a/app/src/components/Tree/TreeNodeTitle.tsx b/app/src/components/Tree/TreeNodeTitle.tsx index aa9be38..6dea8c6 100644 --- a/app/src/components/Tree/TreeNodeTitle.tsx +++ b/app/src/components/Tree/TreeNodeTitle.tsx @@ -59,7 +59,7 @@ class TreeNodeTitle extends React.Component { private renderValue() { const style: React.CSSProperties = { - width: '15em', + maxWidth: '15em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', diff --git a/app/src/index.tsx b/app/src/index.tsx index 658b168..34559cf 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -30,5 +30,5 @@ ReactDOM.render( , - document.getElementById('example'), + document.getElementById('app'), ) diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index 767347d..eb415f0 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -4,7 +4,7 @@ import { EventDispatcher } from '../../../events' export class TreeNode { public sourceEdge?: Edge public message?: Message - public messageHistory = new RingBuffer(3000, 100) + public messageHistory: RingBuffer = new RingBuffer(3000, 100) public edges: {[s: string]: Edge} = {} public collapsed = false public messages: number = 0