Add message history to frontend
This commit is contained in:
87
app/src/components/Sidebar/History.tsx
Normal file
87
app/src/components/Sidebar/History.tsx
Normal 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)
|
||||
49
app/src/components/Sidebar/MessageHistory.tsx
Normal file
49
app/src/components/Sidebar/MessageHistory.tsx
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user