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 Navigation from '@material-ui/icons/Navigation'
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 { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
@@ -12,7 +13,6 @@ import { connect } from 'react-redux'
import { EditorModeSelect } from './EditorModeSelect'
import { globalActions, publishActions } from '../../../actions'
import { KeyCodes } from '../../../utils/KeyCodes'
import RetainSwitch from './RetainSwitch'
interface Props {
connectionId?: string
@@ -41,14 +41,8 @@ function useHistory(): [Array<Message>, (topic: string, payload?: string) => voi
}
function Publish(props: Props) {
console.log(props.connectionId)
const updatePayload = props.actions.setPayload
const [history, amendToHistory] = useHistory()
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
props.actions.setEditorMode(value)
}, [])
const publish = useCallback(() => {
if (!props.connectionId) {
return
@@ -63,65 +57,6 @@ function Publish(props: Props) {
}
}, [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(
(e: React.KeyboardEvent) => {
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}>
<TopicInput />
<div style={{ width: '100%', display: 'block' }}>
<EditorMode />
<Editor value={props.payload} editorMode={props.editorMode} onChange={updatePayload} />
<EditorMode
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 />
</div>
<PublishHistory history={history} />
</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 React, { useCallback } from 'react'
import React, { useCallback, useMemo } from 'react'
import { FormControl, Input, InputLabel } from '@material-ui/core'
import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux'
@@ -7,7 +7,6 @@ import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
console.log(props.topic)
const updateTopic = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
props.actions.setTopic(e.target.value)
}, [])
@@ -23,7 +22,9 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
}, [])
const topicStr = props.topic || ''
return (
return useMemo(
() => (
<div>
<FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
@@ -34,11 +35,13 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
endAdornment={<ClearAdornment action={clearTopic} value={topicStr} />}
onBlur={onTopicBlur}
onChange={updateTopic}
multiline={true}
multiline={false}
placeholder="example/topic"
/>
</FormControl>
</div>
),
[topicStr]
)
}

View File

@@ -54,9 +54,7 @@ function Sidebar(props: Props) {
<ValuePanel lastUpdate={node ? node.lastUpdate : 0} />
<Panel>
<span>Publish</span>
<React.Suspense fallback={<div>Loading...</div>}>
<Publish connectionId={props.connectionId} />
</React.Suspense>
</Panel>
<Panel detailsHidden={!node}>
<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 {
displayMessage?: q.Message
anchorEl?: HTMLElement
lastUpdate: number
}
class MessageHistory extends React.Component<Props, State> {
class MessageHistory extends React.PureComponent<Props, State> {
private updateNode = throttle(() => {
this.setState(this.state)
this.setState({ lastUpdate: Date.now() })
}, 300)
constructor(props: any) {
super(props)
this.state = {}
this.state = { lastUpdate: 0 }
}
private addNodeToCharts = (event: React.MouseEvent) => {

View File

@@ -1,157 +1,85 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import Code from '@material-ui/icons/Code'
import ActionButtons from './ActionButtons'
import Copy from '../../helper/Copy'
import DateFormatter from '../../helper/DateFormatter'
import ExpandMore from '@material-ui/icons/ExpandMore'
import MessageHistory from './MessageHistory'
import Reorder from '@material-ui/icons/Reorder'
import ToggleButton from '@material-ui/lab/ToggleButton'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import Panel from '../Panel'
import React, { useCallback } from 'react'
import ValueRenderer from './ValueRenderer'
import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { bindActionCreators } from 'redux'
import { Theme, Typography, withStyles } from '@material-ui/core'
import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../../actions'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import {
Button,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Tooltip,
Typography,
StyleRulesCallback,
withStyles,
Theme,
} from '@material-ui/core'
import { sidebarActions } from '../../../actions'
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
interface Props {
node?: q.TreeNode<any>
valueRendererDisplayMode: ValueRendererDisplayMode
sidebarActions: typeof sidebarActions
settingsActions: typeof settingsActions
classes: any
lastUpdate: number
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) {
return null
}
return <ValueRenderer treeNode={node} message={node.message} compareWith={this.props.compareMessage} />
return <ValueRenderer treeNode={node} message={node.message} compareWith={compareMessage} />
}
private renderViewOptions() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
function ValuePanel(props: Props) {
const { node, compareMessage } = props
function renderViewOptions() {
if (!props.node || !props.node.message || !props.node.mqttMessage) {
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 (
<div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}>
<span style={{ marginTop: '2px', flexGrow: 1 }}>{this.renderActionButtons()}</span>
<div style={{ flex: 6, textAlign: 'right' }}>{this.props.node.mqttMessage.retain ? retainedButton : null}</div>
{this.messageMetaInfo()}
<span style={{ marginTop: '2px', flexGrow: 1 }}>
<ActionButtons />
</span>
<div style={{ flex: 6, textAlign: 'right' }}>
{props.node.mqttMessage.retain ? <DeleteSelectedTopicButton /> : null}
</div>
{messageMetaInfo()}
</div>
)
}
private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
function messageMetaInfo() {
if (!props.node || !props.node.message || !props.node.mqttMessage) {
return null
}
return (
<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' }}>
<i>
<DateFormatter date={this.props.node.message.received} />
<DateFormatter date={props.node.message.received} />
</i>
</Typography>
</span>
)
}
private renderActionButtons() {
const handleValue = (mouseEvent: React.MouseEvent, value: any) => {
if (value === null) {
return
}
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)
const handleMessageHistorySelect = useCallback(
(message: q.Message) => {
if (message !== compareMessage) {
props.sidebarActions.setCompareMessage(message)
} else {
this.props.sidebarActions.setCompareMessage(undefined)
props.sidebarActions.setCompareMessage(undefined)
}
}
public render() {
const { node, classes } = this.props
const { detailsStyle, summaryStyle } = this.panelStyle()
},
[compareMessage]
)
const copyValue =
node && node.message && node.message.value ? (
@@ -159,38 +87,31 @@ class ValuePanel extends React.PureComponent<Props, State> {
) : null
return (
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}>
{this.renderViewOptions()}
<Panel>
<span>Value {copyValue}</span>
<span style={{ width: '100%' }}>
{renderViewOptions()}
<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>
<MessageHistory
onSelect={this.handleMessageHistorySelect}
selected={this.props.compareMessage}
node={this.props.node}
/>
<MessageHistory onSelect={handleMessageHistorySelect} selected={props.compareMessage} node={props.node} />
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
</span>
</Panel>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
sidebarActions: bindActionCreators(sidebarActions, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
valueRendererDisplayMode: state.settings.get('valueRendererDisplayMode'),
node: state.tree.get('selectedTopic'),
compareMessage: state.sidebar.get('compareMessage'),
}
@@ -201,12 +122,6 @@ const styles = (theme: Theme) => ({
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
toggleButton: {
height: '36px',
},
toggleButtonIcon: {
verticalAlign: 'middle',
},
})
export default connect(

View File

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

View File

@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio'
import { clickOn, expandTopic, sleep, writeText } from '../util'
import { Browser } from 'webdriverio'
import { clickOn } from '../util'
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)
}