Add message history to frontend

This commit is contained in:
Thomas Nordquist
2019-01-12 20:25:52 +01:00
parent 6fbf04cd00
commit a677fb7a0c
10 changed files with 233 additions and 101 deletions

View File

@@ -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<Props, State> {
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) => (
<div
key={index}
style={{
backgroundColor: 'rgba(80, 80, 80, 0.6)',
margin: '8px',
padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit',
}}
onClick={() => this.props.onClick && this.props.onClick(index)}
>
<div><i>{element.title}</i></div>
<div style={messageStyle}>
<span><pre>{element.value}</pre></span>
</div>
</div>
))
return (
<div style={{ backgroundColor: 'rgba(60, 60, 60, 0.6)', marginTop: '16px' }}>
<Typography
onClick={this.toggle}
style={{ cursor: 'pointer' }}
>
{this.state.collapsed ? '▶' : '▼'} History
</Typography>
{this.state.collapsed ? null : elements}
</div>
)
}
public render() {
const visible = this.props.items.length > 0 && this.state.collapsed
return (
<Badge
style={{display: 'block', width: '100%'}}
invisible={!visible}
badgeContent={this.props.items.length}
color="primary"
classes={{badge: this.props.classes.badge}}
>
{this.renderHistory()}
</Badge>
)
}
private toggle = () => {
this.setState({ collapsed: !this.state.collapsed })
}
}
const styles = (theme: Theme) => ({
badge: {top: '-8px', left:'64px'}
});
export default withStyles(styles)(MessageHistory)

View File

@@ -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<Props, {}> {
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 <History items={historyElements} />
}
}
export default withTheme()(MessageHistory)

View File

@@ -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<Props, {}> {
public render() {
const { message } = this.props
return (
<Typography onClick={this.setPublishPreset}>
<div style={{ width: '100%', cursor: 'pointer', marginTop: '8px' }}>
<div><b>{message.topic}</b></div>
<div><i>{message.payload}</i></div>
</div>
</Typography>
)
}
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)

View File

@@ -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 // tslint:disable-next-line
import 'react-ace' import 'react-ace'
import 'brace/mode/json' import 'brace/mode/json'
@@ -22,6 +5,31 @@ import 'brace/mode/text'
import 'brace/mode/xml' import 'brace/mode/xml'
import 'brace/theme/monokai' 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 { interface Props {
node?: q.TreeNode node?: q.TreeNode
connectionId?: string connectionId?: string
@@ -35,7 +43,7 @@ interface State {
history: Message[] history: Message[]
} }
class Publisher extends React.Component<Props, State> { class Publish extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.state = { mode: 'json', history: [] } this.state = { mode: 'json', history: [] }
@@ -140,7 +148,7 @@ class Publisher extends React.Component<Props, State> {
const labelStyle = { margin: '0 8px 0 8px' } const labelStyle = { margin: '0 8px 0 8px' }
return ( return (
<div style={{ marginTop: '16px' }}> <div style={{ marginTop: '16px' }}>
<Typography style={{ width: '100%', lineHeight: '64px' }}> <div style={{ width: '100%', lineHeight: '64px' }}>
<RadioGroup <RadioGroup
style={{ display: 'inline-block', float: 'left' }} style={{ display: 'inline-block', float: 'left' }}
value={this.state.mode} value={this.state.mode}
@@ -172,22 +180,24 @@ class Publisher extends React.Component<Props, State> {
<div style={{ float: 'right', marginRight: '16px' }}> <div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()} {this.publishButton()}
</div> </div>
</Typography> </div>
</div> </div>
) )
} }
private history() { private history() {
const entries = this.state.history.map(message => ( const items = this.state.history.map(message => ({
<HistoryEntry message={message} /> title: message.topic,
)) value: message.payload,
}))
return ( return <History items={items} onClick={this.didSelectHistoryEntry} />
<div style={{ marginTop: '8px' }}> }
<Typography>History</Typography>
{entries} private didSelectHistoryEntry = (index: number) => {
</div> let message = this.state.history[index]
) this.props.actions.setPublishTopic(message.topic)
this.props.actions.setPublishPayload(message.payload)
} }
private editor() { private editor() {
@@ -224,4 +234,4 @@ const mapStateToProps = (state: AppState) => {
} }
} }
export default connect(mapStateToProps, mapDispatchToProps)(Publisher) export default connect(mapStateToProps, mapDispatchToProps)(Publish)

View File

@@ -1,16 +1,17 @@
import * as React from 'react' import * as React from 'react'
import { connect } from 'react-redux'
import { AppState } from '../../reducers'
import * as q from '../../../../backend/src/Model' 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 Copy from '../Copy'
import ValueRenderer from './ValueRenderer' import ExpandMore from '@material-ui/icons/ExpandMore'
import NodeStats from './NodeStats' import NodeStats from './NodeStats'
import Publish from './Publish/Publish'
import Topic from './Topic' import Topic from './Topic'
import ValueRenderer from './ValueRenderer'
import { connect } from 'react-redux'
interface Props { interface Props {
node?: q.TreeNode, node?: q.TreeNode,
@@ -74,20 +75,21 @@ class Sidebar extends React.Component<Props, State> {
) )
} }
private detailsStyle = { padding: '0px 16px 8px 8px' }
private renderNode() { private renderNode() {
const { classes, node } = this.props const { classes, node } = this.props
const copyTopic = node ? <Copy value={node.path()} /> : null const copyTopic = node ? <Copy value={node.path()} /> : null
const copyValue = node && node.message ? <Copy value={node.message.value} /> : null const copyValue = node && node.message ? <Copy value={node.message.value} /> : null
const summeryStyle = { minHeight: '0' } const summeryStyle = { minHeight: '0' }
const detailsStyle = { padding: '0px 16px 8px 8px' }
return ( return (
<div> <div>
<ExpansionPanel key="topic" defaultExpanded={true}> <ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Topic {copyTopic}</Typography> <Typography className={classes.heading}>Topic {copyTopic}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}> <ExpansionPanelDetails style={this.detailsStyle}>
<Topic node={this.props.node} didSelectNode={this.updateNode} /> <Topic node={this.props.node} didSelectNode={this.updateNode} />
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
@@ -95,7 +97,7 @@ class Sidebar extends React.Component<Props, State> {
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography> <Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}> <ExpansionPanelDetails style={this.detailsStyle}>
<ValueRenderer node={this.props.node} /> <ValueRenderer node={this.props.node} />
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
@@ -103,21 +105,31 @@ class Sidebar extends React.Component<Props, State> {
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Publish</Typography> <Typography className={classes.heading}>Publish</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}> <ExpansionPanelDetails style={this.detailsStyle}>
<Publish node={this.props.node} connectionId={this.props.connectionId} /> <Publish node={this.props.node} connectionId={this.props.connectionId} />
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel defaultExpanded={true}> <ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Stats</Typography> <Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}> {this.renderNodeStats()}
{this.props.node ? <NodeStats node={this.props.node} /> : null}
</ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
</div> </div>
) )
} }
private renderNodeStats() {
if (!this.props.node) {
return null
}
return (
<ExpansionPanelDetails style={this.detailsStyle}>
<NodeStats node={this.props.node} />
</ExpansionPanelDetails>
)
}
} }
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {

View File

@@ -1,15 +1,18 @@
import * as React from 'react' import * as React from 'react'
import * as q from '../../../../backend/src/Model' 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 { default as ReactJson } from 'react-json-view'
import { withTheme, Theme } from '@material-ui/core/styles'
interface Props { interface Props {
node?: q.TreeNode | undefined node?: q.TreeNode
theme: Theme theme: Theme
} }
interface State { interface State {
node?: q.TreeNode | undefined node?: q.TreeNode
} }
class ValueRenderer extends React.Component<Props, State> { class ValueRenderer extends React.Component<Props, State> {
@@ -28,6 +31,15 @@ class ValueRenderer extends React.Component<Props, State> {
} }
public render() { public render() {
return (
<div style={{width: '100%'}}>
{this.renderValue()}
<MessageHistory node={this.props.node} />
</div>
)
}
public renderValue() {
const node = this.props.node const node = this.props.node
if (!node || !node.message) { if (!node || !node.message) {
return null return null
@@ -47,19 +59,20 @@ class ValueRenderer extends React.Component<Props, State> {
} else if (typeof json === 'boolean') { } else if (typeof json === 'boolean') {
return this.renderRawValue(node.message.value) return this.renderRawValue(node.message.value)
} else { } else {
const theme = this.props.theme.palette.type === 'dark' ? 'monokai' : 'bright:inverted' const theme = (this.props.theme.palette.type === 'dark') ? 'monokai' : 'bright:inverted'
return <ReactJson return (
style={{ width: '100%' }} <ReactJson
src={json} style={{ width: '100%' }}
theme={theme} src={json}
onEdit={(val) => { theme={theme}
console.log(val) />
}} /> )
} }
} }
private renderRawValue(value: string) { private renderRawValue(value: string) {
const style: React.CSSProperties = { const style: React.CSSProperties = {
backgroundColor: 'rgba(80, 80, 80, 0.6)',
wordBreak: 'break-all', wordBreak: 'break-all',
width: '100%', width: '100%',
overflow: 'scroll', overflow: 'scroll',

View File

@@ -108,7 +108,7 @@ class Tree extends React.Component<Props, TreeState> {
} }
return ( return (
<Typography style={style}> <div style={style}>
<TreeNode <TreeNode
animateChages={true} animateChages={true}
autoExpandLimit={this.props.autoExpandLimit} autoExpandLimit={this.props.autoExpandLimit}
@@ -120,7 +120,7 @@ class Tree extends React.Component<Props, TreeState> {
key="rootNode" key="rootNode"
lastUpdate={this.state.tree.lastUpdate} lastUpdate={this.state.tree.lastUpdate}
/> />
</Typography> </div>
) )
} }
} }

View File

@@ -59,7 +59,7 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
private renderValue() { private renderValue() {
const style: React.CSSProperties = { const style: React.CSSProperties = {
width: '15em', maxWidth: '15em',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',

View File

@@ -30,5 +30,5 @@ ReactDOM.render(
<App name="" /> <App name="" />
</Provider> </Provider>
</MuiThemeProvider>, </MuiThemeProvider>,
document.getElementById('example'), document.getElementById('app'),
) )

View File

@@ -4,7 +4,7 @@ import { EventDispatcher } from '../../../events'
export class TreeNode { export class TreeNode {
public sourceEdge?: Edge public sourceEdge?: Edge
public message?: Message public message?: Message
public messageHistory = new RingBuffer<Message>(3000, 100) public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100)
public edges: {[s: string]: Edge} = {} public edges: {[s: string]: Edge} = {}
public collapsed = false public collapsed = false
public messages: number = 0 public messages: number = 0