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
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<Props, State> {
class Publish extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { mode: 'json', history: [] }
@@ -140,7 +148,7 @@ class Publisher extends React.Component<Props, State> {
const labelStyle = { margin: '0 8px 0 8px' }
return (
<div style={{ marginTop: '16px' }}>
<Typography style={{ width: '100%', lineHeight: '64px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={this.state.mode}
@@ -172,22 +180,24 @@ class Publisher extends React.Component<Props, State> {
<div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()}
</div>
</Typography>
</div>
</div>
)
}
private history() {
const entries = this.state.history.map(message => (
<HistoryEntry message={message} />
))
const items = this.state.history.map(message => ({
title: message.topic,
value: message.payload,
}))
return (
<div style={{ marginTop: '8px' }}>
<Typography>History</Typography>
{entries}
</div>
)
return <History items={items} onClick={this.didSelectHistoryEntry} />
}
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)

View File

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

View File

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

View File

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

View File

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