This commit is contained in:
Thomas Nordquist
2019-07-11 13:54:53 +02:00
parent c1e2a4c625
commit df42d75651
9 changed files with 294 additions and 246 deletions

View File

@@ -3,7 +3,8 @@ import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import Message from './Model/Message' import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation' import Navigation from '@material-ui/icons/Navigation'
import PublishHistory from './PublishHistory' import PublishHistory from './PublishHistory'
import React, { useCallback, useState, useMemo } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import RetainSwitch from './RetainSwitch'
import TopicInput from './TopicInput' import TopicInput from './TopicInput'
import { AppState } from '../../../reducers' import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
@@ -12,7 +13,6 @@ import { connect } from 'react-redux'
import { EditorModeSelect } from './EditorModeSelect' import { EditorModeSelect } from './EditorModeSelect'
import { globalActions, publishActions } from '../../../actions' import { globalActions, publishActions } from '../../../actions'
import { KeyCodes } from '../../../utils/KeyCodes' import { KeyCodes } from '../../../utils/KeyCodes'
import RetainSwitch from './RetainSwitch'
interface Props { interface Props {
connectionId?: string connectionId?: string
@@ -41,14 +41,8 @@ function useHistory(): [Array<Message>, (topic: string, payload?: string) => voi
} }
function Publish(props: Props) { function Publish(props: Props) {
console.log(props.connectionId)
const updatePayload = props.actions.setPayload
const [history, amendToHistory] = useHistory() const [history, amendToHistory] = useHistory()
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
props.actions.setEditorMode(value)
}, [])
const publish = useCallback(() => { const publish = useCallback(() => {
if (!props.connectionId) { if (!props.connectionId) {
return return
@@ -63,65 +57,6 @@ function Publish(props: Props) {
} }
}, [props, props.connectionId, props.topic, props.payload, amendToHistory]) }, [props, props.connectionId, props.topic, props.payload, amendToHistory])
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
publish()
},
[publish]
)
const PublishButton = () => {
return (
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
}
const formatJson = () => {
if (props.payload) {
try {
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str)
} catch (error) {
props.globalActions.showError(`Format error: ${error.message}`)
}
}
}
const renderFormatJson = () => {
if (props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
onClick={formatJson}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
}
function EditorMode() {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
{renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>
<PublishButton />
</div>
</div>
</div>
)
}
const handleSubmit = useCallback( const handleSubmit = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (e.keyCode === KeyCodes.enter && (e.metaKey || e.ctrlKey)) { if (e.keyCode === KeyCodes.enter && (e.metaKey || e.ctrlKey)) {
@@ -138,14 +73,97 @@ function Publish(props: Props) {
<div style={{ flexGrow: 1, marginLeft: '8px' }} onKeyDown={handleSubmit}> <div style={{ flexGrow: 1, marginLeft: '8px' }} onKeyDown={handleSubmit}>
<TopicInput /> <TopicInput />
<div style={{ width: '100%', display: 'block' }}> <div style={{ width: '100%', display: 'block' }}>
<EditorMode /> <EditorMode
<Editor value={props.payload} editorMode={props.editorMode} onChange={updatePayload} /> actions={props.actions}
globalActions={props.globalActions}
payload={props.payload}
editorMode={props.editorMode}
publish={publish}
/>
<Editor value={props.payload} editorMode={props.editorMode} onChange={props.actions.setPayload} />
<RetainSwitch /> <RetainSwitch />
</div> </div>
<PublishHistory history={history} /> <PublishHistory history={history} />
</div> </div>
), ),
[props.payload, props.editorMode, history, handleSubmit, updatePayload] [props.payload, props.editorMode, history, handleSubmit, publish]
)
}
function EditorMode(props: {
payload?: string
editorMode: string
actions: typeof publishActions
globalActions: typeof globalActions
publish: () => void
}) {
const updatePayload = props.actions.setPayload
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
props.actions.setEditorMode(value)
}, [])
const formatJson = useCallback(() => {
if (props.payload) {
try {
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str)
} catch (error) {
props.globalActions.showError(`Format error: ${error.message}`)
}
}
}, [props.payload])
const renderFormatJson = useCallback(() => {
if (props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
onClick={formatJson}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
}, [formatJson, props.editorMode, props.publish])
return useMemo(
() => (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
{renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>
<PublishButton publish={props.publish} />
</div>
</div>
</div>
),
[props.editorMode, renderFormatJson]
)
}
const PublishButton = (props: { publish: () => void }) => {
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
props.publish()
},
[props.publish]
)
return useMemo(
() => (
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
),
[handleClickPublish]
) )
} }

View File

@@ -1,5 +1,5 @@
import ClearAdornment from '../../helper/ClearAdornment' import ClearAdornment from '../../helper/ClearAdornment'
import React, { useCallback } from 'react' import React, { useCallback, useMemo } from 'react'
import { FormControl, Input, InputLabel } from '@material-ui/core' import { FormControl, Input, InputLabel } from '@material-ui/core'
import { publishActions } from '../../../actions' import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
@@ -7,7 +7,6 @@ import { AppState } from '../../../reducers'
import { connect } from 'react-redux' import { connect } from 'react-redux'
function TopicInput(props: { actions: typeof publishActions; topic?: string }) { function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
console.log(props.topic)
const updateTopic = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { const updateTopic = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
props.actions.setTopic(e.target.value) props.actions.setTopic(e.target.value)
}, []) }, [])
@@ -23,7 +22,9 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
}, []) }, [])
const topicStr = props.topic || '' const topicStr = props.topic || ''
return (
return useMemo(
() => (
<div> <div>
<FormControl style={{ width: '100%' }}> <FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel> <InputLabel htmlFor="publish-topic">Topic</InputLabel>
@@ -34,11 +35,13 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
endAdornment={<ClearAdornment action={clearTopic} value={topicStr} />} endAdornment={<ClearAdornment action={clearTopic} value={topicStr} />}
onBlur={onTopicBlur} onBlur={onTopicBlur}
onChange={updateTopic} onChange={updateTopic}
multiline={true} multiline={false}
placeholder="example/topic" placeholder="example/topic"
/> />
</FormControl> </FormControl>
</div> </div>
),
[topicStr]
) )
} }

View File

@@ -54,9 +54,7 @@ function Sidebar(props: Props) {
<ValuePanel lastUpdate={node ? node.lastUpdate : 0} /> <ValuePanel lastUpdate={node ? node.lastUpdate : 0} />
<Panel> <Panel>
<span>Publish</span> <span>Publish</span>
<React.Suspense fallback={<div>Loading...</div>}>
<Publish connectionId={props.connectionId} /> <Publish connectionId={props.connectionId} />
</React.Suspense>
</Panel> </Panel>
<Panel detailsHidden={!node}> <Panel detailsHidden={!node}>
<span>Stats</span> <span>Stats</span>

View File

@@ -0,0 +1,79 @@
import React, { useCallback } from 'react'
import Code from '@material-ui/icons/Code'
import Reorder from '@material-ui/icons/Reorder'
import ToggleButton from '@material-ui/lab/ToggleButton'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import { settingsActions } from '../../../actions'
import { Tooltip, withStyles, Theme } from '@material-ui/core'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
function ActionButtons(props: {
actions: { settings: typeof settingsActions }
valueRendererDisplayMode: ValueRendererDisplayMode
classes: any
}) {
const handleValue = useCallback(
(mouseEvent: React.MouseEvent, value: any) => {
if (value === null) {
return
}
props.actions.settings.setValueDisplayMode(value)
},
[props.actions.settings.setValueDisplayMode]
)
return (
<ToggleButtonGroup
id="valueRendererDisplayMode"
value={props.valueRendererDisplayMode}
exclusive={true}
onChange={handleValue}
>
<ToggleButton className={props.classes.toggleButton} value="diff" id="valueRendererDisplayMode-diff">
<Tooltip title="Show difference between the current and the last message">
<span>
<Code className={props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
<ToggleButton className={props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw value">
<span>
<Reorder className={props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
)
}
const styles = (theme: Theme) => ({
toggleButton: {
height: '36px',
},
toggleButtonIcon: {
verticalAlign: 'middle',
},
})
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
settings: bindActionCreators(settingsActions, dispatch),
},
}
}
const mapStateToProps = (state: AppState) => {
return {
valueRendererDisplayMode: state.settings.get('valueRendererDisplayMode'),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ActionButtons))

View File

@@ -0,0 +1,39 @@
import Clear from '@material-ui/icons/Clear'
import React, { useMemo } from 'react'
import { bindActionCreators } from 'redux'
import { Button, Tooltip } from '@material-ui/core'
import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions'
const DeleteSelectedTopicButton = (props: {
actions: {
sidebar: typeof sidebarActions
}
}) =>
useMemo(
() => (
<Tooltip title="Delete retained topic" placement="top">
<Button
size="small"
color="secondary"
variant="contained"
style={{ marginTop: '11px', padding: '0px 4px', minHeight: '24px' }}
onClick={props.actions.sidebar.clearRetainedTopic}
>
retained <Clear style={{ fontSize: '16px', marginLeft: '2px' }} />
</Button>
</Tooltip>
),
[props.actions.sidebar.clearRetainedTopic]
)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: { sidebar: bindActionCreators(sidebarActions, dispatch) },
}
}
export default connect(
undefined,
mapDispatchToProps
)(DeleteSelectedTopicButton)

View File

@@ -27,16 +27,17 @@ interface Props {
interface State { interface State {
displayMessage?: q.Message displayMessage?: q.Message
anchorEl?: HTMLElement anchorEl?: HTMLElement
lastUpdate: number
} }
class MessageHistory extends React.Component<Props, State> { class MessageHistory extends React.PureComponent<Props, State> {
private updateNode = throttle(() => { private updateNode = throttle(() => {
this.setState(this.state) this.setState({ lastUpdate: Date.now() })
}, 300) }, 300)
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.state = {} this.state = { lastUpdate: 0 }
} }
private addNodeToCharts = (event: React.MouseEvent) => { private addNodeToCharts = (event: React.MouseEvent) => {

View File

@@ -1,157 +1,85 @@
import * as q from '../../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import * as React from 'react' import ActionButtons from './ActionButtons'
import Clear from '@material-ui/icons/Clear'
import Code from '@material-ui/icons/Code'
import Copy from '../../helper/Copy' import Copy from '../../helper/Copy'
import DateFormatter from '../../helper/DateFormatter' import DateFormatter from '../../helper/DateFormatter'
import ExpandMore from '@material-ui/icons/ExpandMore'
import MessageHistory from './MessageHistory' import MessageHistory from './MessageHistory'
import Reorder from '@material-ui/icons/Reorder' import Panel from '../Panel'
import ToggleButton from '@material-ui/lab/ToggleButton' import React, { useCallback } from 'react'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import ValueRenderer from './ValueRenderer' import ValueRenderer from './ValueRenderer'
import { AppState } from '../../../reducers' import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { Theme, Typography, withStyles } from '@material-ui/core'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../../actions' import { sidebarActions } from '../../../actions'
import { ValueRendererDisplayMode } from '../../../reducers/Settings' import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
import {
Button,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Tooltip,
Typography,
StyleRulesCallback,
withStyles,
Theme,
} from '@material-ui/core'
interface Props { interface Props {
node?: q.TreeNode<any> node?: q.TreeNode<any>
valueRendererDisplayMode: ValueRendererDisplayMode
sidebarActions: typeof sidebarActions sidebarActions: typeof sidebarActions
settingsActions: typeof settingsActions
classes: any classes: any
lastUpdate: number lastUpdate: number
compareMessage?: q.Message compareMessage?: q.Message
} }
interface State {} function RenderedValue(props: { node?: q.TreeNode<any>; compareMessage?: q.Message }) {
const { node, compareMessage } = props
class ValuePanel extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {}
}
private renderValue() {
const node = this.props.node
if (!node || !node.message) { if (!node || !node.message) {
return null return null
} }
return <ValueRenderer treeNode={node} message={node.message} compareWith={this.props.compareMessage} /> return <ValueRenderer treeNode={node} message={node.message} compareWith={compareMessage} />
} }
private renderViewOptions() { function ValuePanel(props: Props) {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) { const { node, compareMessage } = props
function renderViewOptions() {
if (!props.node || !props.node.message || !props.node.mqttMessage) {
return null return null
} }
const retainedButton = (
<Tooltip title="Delete retained topic" placement="top">
<Button
size="small"
color="secondary"
variant="contained"
style={{ marginTop: '11px', padding: '0px 4px', minHeight: '24px' }}
onClick={this.props.sidebarActions.clearRetainedTopic}
>
retained <Clear style={{ fontSize: '16px', marginLeft: '2px' }} />
</Button>
</Tooltip>
)
return ( return (
<div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}> <div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}>
<span style={{ marginTop: '2px', flexGrow: 1 }}>{this.renderActionButtons()}</span> <span style={{ marginTop: '2px', flexGrow: 1 }}>
<div style={{ flex: 6, textAlign: 'right' }}>{this.props.node.mqttMessage.retain ? retainedButton : null}</div> <ActionButtons />
{this.messageMetaInfo()} </span>
<div style={{ flex: 6, textAlign: 'right' }}>
{props.node.mqttMessage.retain ? <DeleteSelectedTopicButton /> : null}
</div>
{messageMetaInfo()}
</div> </div>
) )
} }
private messageMetaInfo() { function messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) { if (!props.node || !props.node.message || !props.node.mqttMessage) {
return null return null
} }
return ( return (
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}> <span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
<Typography style={{ textAlign: 'right' }}>QoS: {this.props.node.mqttMessage.qos}</Typography> <Typography style={{ textAlign: 'right' }}>QoS: {props.node.mqttMessage.qos}</Typography>
<Typography style={{ textAlign: 'right' }}> <Typography style={{ textAlign: 'right' }}>
<i> <i>
<DateFormatter date={this.props.node.message.received} /> <DateFormatter date={props.node.message.received} />
</i> </i>
</Typography> </Typography>
</span> </span>
) )
} }
private renderActionButtons() { const handleMessageHistorySelect = useCallback(
const handleValue = (mouseEvent: React.MouseEvent, value: any) => { (message: q.Message) => {
if (value === null) { if (message !== compareMessage) {
return props.sidebarActions.setCompareMessage(message)
}
this.props.settingsActions.setValueDisplayMode(value)
}
return (
<ToggleButtonGroup
id="valueRendererDisplayMode"
value={this.props.valueRendererDisplayMode}
exclusive={true}
onChange={handleValue}
>
<ToggleButton className={this.props.classes.toggleButton} value="diff" id="valueRendererDisplayMode-diff">
<Tooltip title="Show difference between the current and the last message">
<span>
<Code className={this.props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
<ToggleButton className={this.props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw value">
<span>
<Reorder className={this.props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
)
}
private panelStyle() {
return {
detailsStyle: { padding: '0px 16px 8px 8px', display: 'block' },
summaryStyle: { minHeight: '0' },
}
}
private handleMessageHistorySelect = (message: q.Message) => {
if (message !== this.props.compareMessage) {
this.props.sidebarActions.setCompareMessage(message)
} else { } else {
this.props.sidebarActions.setCompareMessage(undefined) props.sidebarActions.setCompareMessage(undefined)
} }
} },
[compareMessage]
public render() { )
const { node, classes } = this.props
const { detailsStyle, summaryStyle } = this.panelStyle()
const copyValue = const copyValue =
node && node.message && node.message.value ? ( node && node.message && node.message.value ? (
@@ -159,38 +87,31 @@ class ValuePanel extends React.PureComponent<Props, State> {
) : null ) : null
return ( return (
<ExpansionPanel key="value" defaultExpanded={true}> <Panel>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}> <span>Value {copyValue}</span>
<Typography className={classes.heading}>Value {copyValue}</Typography> <span style={{ width: '100%' }}>
</ExpansionPanelSummary> {renderViewOptions()}
<ExpansionPanelDetails style={detailsStyle}>
{this.renderViewOptions()}
<div style={{ marginBottom: '-8px' }}> <div style={{ marginBottom: '-8px' }}>
<React.Suspense fallback={<div>Loading...</div>}>{this.renderValue()}</React.Suspense> <React.Suspense fallback={<div>Loading...</div>}>
<RenderedValue node={node} compareMessage={compareMessage} />
</React.Suspense>
</div> </div>
<div> <div>
<MessageHistory <MessageHistory onSelect={handleMessageHistorySelect} selected={props.compareMessage} node={props.node} />
onSelect={this.handleMessageHistorySelect}
selected={this.props.compareMessage}
node={this.props.node}
/>
</div> </div>
</ExpansionPanelDetails> </span>
</ExpansionPanel> </Panel>
) )
}
} }
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
return { return {
sidebarActions: bindActionCreators(sidebarActions, dispatch), sidebarActions: bindActionCreators(sidebarActions, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
} }
} }
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
valueRendererDisplayMode: state.settings.get('valueRendererDisplayMode'),
node: state.tree.get('selectedTopic'), node: state.tree.get('selectedTopic'),
compareMessage: state.sidebar.get('compareMessage'), compareMessage: state.sidebar.get('compareMessage'),
} }
@@ -201,12 +122,6 @@ const styles = (theme: Theme) => ({
fontSize: theme.typography.pxToRem(15), fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular, fontWeight: theme.typography.fontWeightRegular,
}, },
toggleButton: {
height: '36px',
},
toggleButtonIcon: {
verticalAlign: 'middle',
},
}) })
export default connect( export default connect(

View File

@@ -56,12 +56,7 @@ class ValueRenderer extends React.Component<Props, State> {
} }
public render() { public render() {
return ( return <div style={{ padding: '0px 0px 8px 8px', width: '100%' }}>{this.renderValue()}</div>
<div style={{ padding: '0px 0px 8px 8px' }}>
<ReactResizeDetector handleWidth={true} onResize={this.updateWidth} />
{this.renderValue()}
</div>
)
} }
public renderValue() { public renderValue() {

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio' import { Browser } from 'webdriverio'
import { clickOn, expandTopic, sleep, writeText } from '../util' import { clickOn } from '../util'
export async function copyValueToClipboard(browser: Browser) { export async function copyValueToClipboard(browser: Browser) {
const copyButton = await browser.$('//p[contains(text(), "Value")]//button') const copyButton = await browser.$('//span[contains(text(), "Value")]//button')
await clickOn(copyButton, browser, 1) await clickOn(copyButton, browser, 1)
} }