Refactor sidebar
This commit is contained in:
@@ -11,7 +11,6 @@ const debounce = require('lodash.debounce')
|
|||||||
export { clearTopic } from './clearTopic'
|
export { clearTopic } from './clearTopic'
|
||||||
|
|
||||||
export { moveSelectionUpOrDownwards, moveInward, moveOutward } from './visibleTreeTraversal'
|
export { moveSelectionUpOrDownwards, moveInward, moveOutward } from './visibleTreeTraversal'
|
||||||
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
|
|
||||||
|
|
||||||
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (
|
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (
|
||||||
dispatch: Dispatch<any>,
|
dispatch: Dispatch<any>,
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ class HistoryDrawer extends React.Component<Props, State> {
|
|||||||
this.setState({ collapsed: !this.state.collapsed })
|
this.setState({ collapsed: !this.state.collapsed })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createSelectionHandler = (index: number) => (event: React.MouseEvent) => {
|
||||||
|
this.props.onClick && this.props.onClick(index, event.target)
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' })
|
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' })
|
||||||
|
|
||||||
public renderHistory() {
|
public renderHistory() {
|
||||||
@@ -51,7 +57,7 @@ class HistoryDrawer extends React.Component<Props, State> {
|
|||||||
<div
|
<div
|
||||||
key={element.key}
|
key={element.key}
|
||||||
style={style(element)}
|
style={style(element)}
|
||||||
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
|
onClick={this.createSelectionHandler(index)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={this.handleCtrlA}
|
onKeyDown={this.handleCtrlA}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TopicViewModel } from '../../model/TopicViewModel'
|
|||||||
import { Typography } from '@material-ui/core'
|
import { Typography } from '@material-ui/core'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: q.TreeNode<TopicViewModel>
|
node?: q.TreeNode<TopicViewModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeStats extends React.Component<Props, {}> {
|
class NodeStats extends React.Component<Props, {}> {
|
||||||
@@ -14,6 +14,9 @@ class NodeStats extends React.Component<Props, {}> {
|
|||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { node } = this.props
|
const { node } = this.props
|
||||||
|
if (!node) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
33
app/src/components/Sidebar/Panel.tsx
Normal file
33
app/src/components/Sidebar/Panel.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||||
|
import { ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography, Theme } from '@material-ui/core'
|
||||||
|
import { withStyles } from '@material-ui/styles'
|
||||||
|
|
||||||
|
const styles = (theme: Theme) => ({
|
||||||
|
summary: { minHeight: '0' },
|
||||||
|
details: { padding: '0px 16px 8px 8px', display: 'block' },
|
||||||
|
heading: {
|
||||||
|
fontSize: theme.typography.pxToRem(15),
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Panel = (props: {
|
||||||
|
classes: any
|
||||||
|
children: [React.ReactElement, React.ReactElement]
|
||||||
|
disabled?: boolean
|
||||||
|
detailsHidden?: boolean
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ExpansionPanel defaultExpanded={true} disabled={props.disabled}>
|
||||||
|
<ExpansionPanelSummary expandIcon={<ExpandMore />} className={props.classes.summary}>
|
||||||
|
<Typography className={props.classes.heading}>{props.children[0]}</Typography>
|
||||||
|
</ExpansionPanelSummary>
|
||||||
|
{props.detailsHidden ? null : (
|
||||||
|
<ExpansionPanelDetails className={props.classes.detail}>{props.children[1]}</ExpansionPanelDetails>
|
||||||
|
)}
|
||||||
|
</ExpansionPanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(Panel)
|
||||||
@@ -1,30 +1,18 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import ClearAdornment from '../../helper/ClearAdornment'
|
|
||||||
import Editor from './Editor'
|
import Editor from './Editor'
|
||||||
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
|
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
|
||||||
import History from '../HistoryDrawer'
|
|
||||||
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 QosSelect from './QosSelect'
|
import PublishHistory from './PublishHistory'
|
||||||
|
import React, { useCallback, useState, useMemo } from 'react'
|
||||||
|
import TopicInput from './TopicInput'
|
||||||
import { AppState } from '../../../reducers'
|
import { AppState } from '../../../reducers'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { Button, Fab, Theme, Tooltip, withTheme } from '@material-ui/core'
|
||||||
import { connect } from 'react-redux'
|
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 {
|
import RetainSwitch from './RetainSwitch'
|
||||||
Button,
|
|
||||||
FormControlLabel,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Input,
|
|
||||||
Checkbox,
|
|
||||||
Tooltip,
|
|
||||||
Fab,
|
|
||||||
Theme,
|
|
||||||
withTheme,
|
|
||||||
} from '@material-ui/core'
|
|
||||||
const sha1 = require('sha1')
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
@@ -37,108 +25,73 @@ interface Props {
|
|||||||
theme: Theme
|
theme: Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
function useHistory(): [Array<Message>, (topic: string, payload?: string) => void] {
|
||||||
history: Array<Message>
|
const [history, setHistory] = useState<Array<Message>>([])
|
||||||
|
const amendToHistory = useCallback(
|
||||||
|
(topic: string, payload?: string) => {
|
||||||
|
// Remove duplicates
|
||||||
|
let filteredHistory = history.filter(e => e.payload !== payload || e.topic !== topic)
|
||||||
|
filteredHistory = filteredHistory.slice(-7)
|
||||||
|
setHistory([...filteredHistory, { topic, payload, sent: new Date() }])
|
||||||
|
},
|
||||||
|
[history]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [history, amendToHistory]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Publish extends React.Component<Props, State> {
|
function Publish(props: Props) {
|
||||||
constructor(props: any) {
|
console.log(props.connectionId)
|
||||||
super(props)
|
const updatePayload = props.actions.setPayload
|
||||||
this.state = { history: [] }
|
const [history, amendToHistory] = useHistory()
|
||||||
}
|
|
||||||
|
|
||||||
private updatePayload = (payload: string) => {
|
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
|
||||||
this.props.actions.setPayload(payload)
|
props.actions.setEditorMode(value)
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const publish = useCallback(() => {
|
||||||
this.props.actions.setTopic(e.target.value)
|
if (!props.connectionId) {
|
||||||
}
|
|
||||||
|
|
||||||
private updateMode = (e: React.ChangeEvent<{}>, value: string) => {
|
|
||||||
this.props.actions.setEditorMode(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleClickPublish = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.publish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private publish() {
|
|
||||||
if (!this.props.connectionId) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.actions.publish(this.props.connectionId)
|
props.actions.publish(props.connectionId)
|
||||||
|
|
||||||
const topic = this.props.topic || ''
|
const topic = props.topic || ''
|
||||||
const payload = this.props.payload
|
const payload = props.payload
|
||||||
if (this.props.connectionId && topic) {
|
if (props.connectionId && topic) {
|
||||||
this.addMessageToHistory(topic, payload)
|
amendToHistory(topic, payload)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [props, props.connectionId, props.topic, props.payload, amendToHistory])
|
||||||
|
|
||||||
private addMessageToHistory(topic: string, payload?: string) {
|
const handleClickPublish = useCallback(
|
||||||
// Remove duplicates
|
(e: React.MouseEvent) => {
|
||||||
let filteredHistory = this.state.history.filter(e => e.payload !== payload || e.topic !== topic)
|
e.stopPropagation()
|
||||||
filteredHistory = filteredHistory.slice(-7)
|
publish()
|
||||||
const history: Array<Message> = [...filteredHistory, { topic, payload, sent: new Date() }]
|
},
|
||||||
this.setState({ history })
|
[publish]
|
||||||
}
|
|
||||||
|
|
||||||
private clearTopic = () => {
|
|
||||||
this.props.actions.setTopic('')
|
|
||||||
}
|
|
||||||
|
|
||||||
private topic() {
|
|
||||||
const topicStr = this.props.topic || ''
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormControl style={{ width: '100%' }}>
|
|
||||||
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
|
|
||||||
<Input
|
|
||||||
id="publish-topic"
|
|
||||||
value={topicStr}
|
|
||||||
startAdornment={<span />}
|
|
||||||
endAdornment={<ClearAdornment action={this.clearTopic} value={topicStr} />}
|
|
||||||
onBlur={this.onTopicBlur}
|
|
||||||
onChange={this.updateTopic}
|
|
||||||
multiline={true}
|
|
||||||
placeholder="example/topic"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
private onTopicBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
const PublishButton = () => {
|
||||||
if (!e.target.value) {
|
|
||||||
this.props.actions.setTopic(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private publishButton() {
|
|
||||||
return (
|
return (
|
||||||
<Button variant="contained" size="small" color="primary" onClick={this.handleClickPublish} id="publish-button">
|
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
|
||||||
<Navigation style={{ marginRight: '8px' }} /> Publish
|
<Navigation style={{ marginRight: '8px' }} /> Publish
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatJson = () => {
|
const formatJson = () => {
|
||||||
if (this.props.payload) {
|
if (props.payload) {
|
||||||
try {
|
try {
|
||||||
const str = JSON.stringify(JSON.parse(this.props.payload), undefined, ' ')
|
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
|
||||||
this.updatePayload(str)
|
updatePayload(str)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.props.globalActions.showError(`Format error: ${error.message}`)
|
props.globalActions.showError(`Format error: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderFormatJson() {
|
const renderFormatJson = () => {
|
||||||
if (this.props.editorMode !== 'json') {
|
if (props.editorMode !== 'json') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +99,7 @@ class Publish extends React.Component<Props, State> {
|
|||||||
<Tooltip title="Format JSON">
|
<Tooltip title="Format JSON">
|
||||||
<Fab
|
<Fab
|
||||||
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
|
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
|
||||||
onClick={this.formatJson}
|
onClick={formatJson}
|
||||||
id="sidebar-publish-format-json"
|
id="sidebar-publish-format-json"
|
||||||
>
|
>
|
||||||
<FormatAlignLeft style={{ fontSize: '20px' }} />
|
<FormatAlignLeft style={{ fontSize: '20px' }} />
|
||||||
@@ -155,80 +108,45 @@ class Publish extends React.Component<Props, State> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private editorMode() {
|
function EditorMode() {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '16px' }}>
|
<div style={{ marginTop: '16px' }}>
|
||||||
<div style={{ width: '100%', lineHeight: '64px' }}>
|
<div style={{ width: '100%', lineHeight: '64px' }}>
|
||||||
<EditorModeSelect value={this.props.editorMode} onChange={this.updateMode} />
|
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
|
||||||
{this.renderFormatJson()}
|
{renderFormatJson()}
|
||||||
<div style={{ float: 'right', marginRight: '16px' }}>{this.publishButton()}</div>
|
<div style={{ float: 'right', marginRight: '16px' }}>
|
||||||
|
<PublishButton />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private publishMode() {
|
const handleSubmit = useCallback(
|
||||||
const labelStyle = { margin: '0 8px 0 8px' }
|
(e: React.KeyboardEvent) => {
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: '8px', clear: 'both' }}>
|
|
||||||
<div style={{ width: '100%', textAlign: 'right' }}>
|
|
||||||
<FormControlLabel style={labelStyle} control={<QosSelect />} label="QoS" labelPlacement="start" />
|
|
||||||
<Tooltip
|
|
||||||
title="Retained messages only appear to be retained, when client subscribes after the initial publish."
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<FormControlLabel
|
|
||||||
value="retain"
|
|
||||||
style={labelStyle}
|
|
||||||
control={
|
|
||||||
<Checkbox color="primary" checked={this.props.retain} onChange={this.props.actions.toggleRetain} />
|
|
||||||
}
|
|
||||||
label="retain"
|
|
||||||
labelPlacement="end"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private history() {
|
|
||||||
const items = [...this.state.history].reverse().map(message => ({
|
|
||||||
key: sha1(message.topic + message.payload),
|
|
||||||
title: message.topic,
|
|
||||||
value: message.payload || '',
|
|
||||||
}))
|
|
||||||
return <History items={items} onClick={this.didSelectHistoryEntry} />
|
|
||||||
}
|
|
||||||
|
|
||||||
private didSelectHistoryEntry = (index: number) => {
|
|
||||||
const message = this.state.history[index]
|
|
||||||
this.props.actions.setTopic(message.topic)
|
|
||||||
this.props.actions.setPayload(message.payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSubmit = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.keyCode === KeyCodes.enter && (e.metaKey || e.ctrlKey)) {
|
if (e.keyCode === KeyCodes.enter && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.publish()
|
publish()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[publish]
|
||||||
public render() {
|
)
|
||||||
return (
|
|
||||||
<div style={{ flexGrow: 1, marginLeft: '8px' }} onKeyDown={this.handleSubmit}>
|
return useMemo(
|
||||||
{this.topic()}
|
() => (
|
||||||
<div style={{ width: '100%', display: 'block' }}>
|
<div style={{ flexGrow: 1, marginLeft: '8px' }} onKeyDown={handleSubmit}>
|
||||||
{this.editorMode()}
|
<TopicInput />
|
||||||
<Editor value={this.props.payload} editorMode={this.props.editorMode} onChange={this.updatePayload} />
|
<div style={{ width: '100%', display: 'block' }}>
|
||||||
{this.publishMode()}
|
<EditorMode />
|
||||||
</div>
|
<Editor value={props.payload} editorMode={props.editorMode} onChange={updatePayload} />
|
||||||
{this.history()}
|
<RetainSwitch />
|
||||||
</div>
|
</div>
|
||||||
|
<PublishHistory history={history} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[props.payload, props.editorMode, history, handleSubmit, updatePayload]
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: any) => {
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
|||||||
40
app/src/components/Sidebar/Publish/PublishHistory.tsx
Normal file
40
app/src/components/Sidebar/Publish/PublishHistory.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import History from '../HistoryDrawer'
|
||||||
|
import Message from './Model/Message'
|
||||||
|
import React, { useCallback, useMemo } from 'react'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { publishActions } from '../../../actions'
|
||||||
|
const sha1 = require('sha1')
|
||||||
|
|
||||||
|
function PublishHistory(props: { history: Array<Message>; actions: typeof publishActions }) {
|
||||||
|
const didSelectHistoryEntry = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const items = [...props.history].reverse()
|
||||||
|
const message = items[index]
|
||||||
|
props.actions.setTopic(message.topic)
|
||||||
|
props.actions.setPayload(message.payload)
|
||||||
|
},
|
||||||
|
[props.history]
|
||||||
|
)
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const items = [...props.history].reverse().map(message => ({
|
||||||
|
key: sha1(message.topic + message.payload),
|
||||||
|
title: message.topic,
|
||||||
|
value: message.payload || '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
return <History items={items} onClick={didSelectHistoryEntry} />
|
||||||
|
}, [props.history])
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(publishActions, dispatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
undefined,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(PublishHistory)
|
||||||
47
app/src/components/Sidebar/Publish/RetainSwitch.tsx
Normal file
47
app/src/components/Sidebar/Publish/RetainSwitch.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import QosSelect from './QosSelect'
|
||||||
|
import React from 'react'
|
||||||
|
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
|
||||||
|
import { publishActions } from '../../../actions'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { AppState } from '../../../reducers'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
|
export function RetainSwitch(props: { retain: boolean; actions: typeof publishActions }) {
|
||||||
|
const labelStyle = { margin: '0 8px 0 8px' }
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: '8px', clear: 'both' }}>
|
||||||
|
<div style={{ width: '100%', textAlign: 'right' }}>
|
||||||
|
<FormControlLabel style={labelStyle} control={<QosSelect />} label="QoS" labelPlacement="start" />
|
||||||
|
<Tooltip
|
||||||
|
title="Retained messages only appear to be retained, when client subscribes after the initial publish."
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value="retain"
|
||||||
|
style={labelStyle}
|
||||||
|
control={<Checkbox color="primary" checked={props.retain} onChange={props.actions.toggleRetain} />}
|
||||||
|
label="retain"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(publishActions, dispatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
return {
|
||||||
|
retain: state.publish.retain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(RetainSwitch)
|
||||||
60
app/src/components/Sidebar/Publish/TopicInput.tsx
Normal file
60
app/src/components/Sidebar/Publish/TopicInput.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import ClearAdornment from '../../helper/ClearAdornment'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { FormControl, Input, InputLabel } from '@material-ui/core'
|
||||||
|
import { publishActions } from '../../../actions'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
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)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const clearTopic = useCallback(() => {
|
||||||
|
props.actions.setTopic('')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onTopicBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.value) {
|
||||||
|
props.actions.setTopic(undefined)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const topicStr = props.topic || ''
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormControl style={{ width: '100%' }}>
|
||||||
|
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
|
||||||
|
<Input
|
||||||
|
id="publish-topic"
|
||||||
|
value={topicStr}
|
||||||
|
startAdornment={<span />}
|
||||||
|
endAdornment={<ClearAdornment action={clearTopic} value={topicStr} />}
|
||||||
|
onBlur={onTopicBlur}
|
||||||
|
onChange={updateTopic}
|
||||||
|
multiline={true}
|
||||||
|
placeholder="example/topic"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(publishActions, dispatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
return {
|
||||||
|
topic: state.publish.topic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TopicInput)
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../../backend/src/Model'
|
||||||
import * as React from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import Copy from '../helper/Copy'
|
|
||||||
import CustomIconButton from '../helper/CustomIconButton'
|
|
||||||
import Delete from '@material-ui/icons/Delete'
|
|
||||||
import ExpandMore from '@material-ui/icons/ExpandMore'
|
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||||
import NodeStats from './NodeStats'
|
import NodeStats from './NodeStats'
|
||||||
import Topic from './Topic'
|
|
||||||
import ValuePanel from './ValueRenderer/ValuePanel'
|
import ValuePanel from './ValueRenderer/ValuePanel'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
|
import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
|
||||||
@@ -14,6 +10,8 @@ import { connect } from 'react-redux'
|
|||||||
import { settingsActions, sidebarActions } from '../../actions'
|
import { settingsActions, sidebarActions } from '../../actions'
|
||||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||||
|
import TopicPanel from './TopicPanel/TopicPanel'
|
||||||
|
import Panel from './Panel'
|
||||||
|
|
||||||
const throttle = require('lodash.throttle')
|
const throttle = require('lodash.throttle')
|
||||||
|
|
||||||
@@ -27,148 +25,48 @@ interface Props {
|
|||||||
connectionId?: string
|
connectionId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
function Sidebar(props: Props) {
|
||||||
compareMessage?: q.Message
|
const { classes, node } = props
|
||||||
valueRenderWidth: number
|
const [lastUpdate, setLastUpdate] = useState(0)
|
||||||
}
|
|
||||||
|
|
||||||
class Sidebar extends React.Component<Props, State> {
|
const updateNode = useCallback(
|
||||||
private updateNode = throttle(() => {
|
throttle(() => {
|
||||||
this.setState(this.state)
|
setLastUpdate(node ? node.lastUpdate : 0)
|
||||||
}, 300)
|
}, 300),
|
||||||
|
[node]
|
||||||
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props)
|
|
||||||
this.state = { valueRenderWidth: 300 }
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
|
|
||||||
node.onMerge.subscribe(this.updateNode)
|
|
||||||
node.onMessage.subscribe(this.updateNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeUpdateListener(node: q.TreeNode<TopicViewModel>) {
|
|
||||||
node.onMerge.unsubscribe(this.updateNode)
|
|
||||||
node.onMessage.unsubscribe(this.updateNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTopicDeleteButton() {
|
|
||||||
if (!this.props.node || (!this.props.node.message || !this.props.node.message.value)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomIconButton onClick={() => this.deleteTopic(this.props.node)} tooltip="Clear this topic">
|
|
||||||
<Delete style={{ marginTop: '-3px' }} />
|
|
||||||
</CustomIconButton>
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
private renderRecursiveTopicDeleteButton() {
|
useEffect(() => {
|
||||||
const deleteLimit = 50
|
const updateCallback = updateNode
|
||||||
const topicCount = this.props.node ? this.props.node.childTopicCount() : 0
|
node && node.onMerge.subscribe(updateCallback)
|
||||||
if (!this.props.node || topicCount === 0 || (this.props.node.message && topicCount === 1)) {
|
node && node.onMessage.subscribe(updateCallback)
|
||||||
return null
|
|
||||||
|
return function cleanup() {
|
||||||
|
node && node.onMerge.unsubscribe(updateCallback)
|
||||||
|
node && node.onMessage.unsubscribe(updateCallback)
|
||||||
}
|
}
|
||||||
|
}, [node])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<div id="Sidebar" className={props.classes.drawer}>
|
||||||
classes={{ badge: this.props.classes.badge }}
|
|
||||||
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
<CustomIconButton
|
|
||||||
onClick={() => this.deleteTopic(this.props.node, true, deleteLimit)}
|
|
||||||
tooltip={`Deletes up to ${deleteLimit} sub-topics with a single click`}
|
|
||||||
>
|
|
||||||
<Delete style={{ marginTop: '-3px' }} color="action" />
|
|
||||||
</CustomIconButton>
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private deleteTopic = (topic?: q.TreeNode<TopicViewModel>, recursive: boolean = false, maxCount = 50) => {
|
|
||||||
if (!topic) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.actions.clearTopic(topic, recursive, maxCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderNode() {
|
|
||||||
const { classes, node } = this.props
|
|
||||||
|
|
||||||
const copyTopic = node ? <Copy value={node.path()} /> : null
|
|
||||||
const deleteTopic = this.renderTopicDeleteButton()
|
|
||||||
const deleteRecursiveTopic = this.renderRecursiveTopicDeleteButton()
|
|
||||||
const summaryStyle = { minHeight: '0' }
|
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
|
<TopicPanel node={node} updateNode={updateNode} />
|
||||||
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
|
<ValuePanel lastUpdate={node ? node.lastUpdate : 0} />
|
||||||
<Typography className={classes.heading}>
|
<Panel>
|
||||||
Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}
|
<span>Publish</span>
|
||||||
</Typography>
|
|
||||||
</ExpansionPanelSummary>
|
|
||||||
<ExpansionPanelDetails style={this.detailsStyle}>
|
|
||||||
<Topic node={this.props.node} didSelectNode={this.updateNode} />
|
|
||||||
</ExpansionPanelDetails>
|
|
||||||
</ExpansionPanel>
|
|
||||||
<ValuePanel lastUpdate={this.props.node ? this.props.node.lastUpdate : 0} />
|
|
||||||
<ExpansionPanel defaultExpanded={true}>
|
|
||||||
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
|
|
||||||
<Typography className={classes.heading}>Publish</Typography>
|
|
||||||
</ExpansionPanelSummary>
|
|
||||||
<ExpansionPanelDetails style={this.detailsStyle}>
|
|
||||||
<React.Suspense fallback={<div>Loading...</div>}>
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
<Publish connectionId={this.props.connectionId} />
|
<Publish connectionId={props.connectionId} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
</Panel>
|
||||||
|
<Panel detailsHidden={!node}>
|
||||||
|
<span>Stats</span>
|
||||||
|
<ExpansionPanelDetails className={props.classes.details}>
|
||||||
|
<NodeStats node={node} />
|
||||||
</ExpansionPanelDetails>
|
</ExpansionPanelDetails>
|
||||||
</ExpansionPanel>
|
</Panel>
|
||||||
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
|
</div>
|
||||||
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
|
|
||||||
<Typography className={classes.heading}>Stats</Typography>
|
|
||||||
</ExpansionPanelSummary>
|
|
||||||
{this.renderNodeStats()}
|
|
||||||
</ExpansionPanel>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
private renderNodeStats() {
|
|
||||||
if (!this.props.node) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ExpansionPanelDetails style={this.detailsStyle}>
|
|
||||||
<NodeStats node={this.props.node} />
|
|
||||||
</ExpansionPanelDetails>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillReceiveProps(nextProps: Props) {
|
|
||||||
this.props.node && this.removeUpdateListener(this.props.node)
|
|
||||||
nextProps.node && this.registerUpdateListener(nextProps.node)
|
|
||||||
|
|
||||||
if (this.props.node !== nextProps.node) {
|
|
||||||
this.setState({ compareMessage: undefined })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
this.props.node && this.removeUpdateListener(this.props.node)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<div id="Sidebar" className={this.props.classes.drawer}>
|
|
||||||
{this.renderNode()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
@@ -188,13 +86,11 @@ const styles = (theme: Theme) => ({
|
|||||||
drawer: {
|
drawer: {
|
||||||
display: 'block' as 'block',
|
display: 'block' as 'block',
|
||||||
},
|
},
|
||||||
badge: {
|
|
||||||
top: '3px',
|
|
||||||
right: '3px',
|
|
||||||
},
|
|
||||||
valuePaper: {
|
valuePaper: {
|
||||||
margin: theme.spacing(1),
|
margin: theme.spacing(1),
|
||||||
},
|
},
|
||||||
|
summary: { minHeight: '0' },
|
||||||
|
details: { padding: '0px 16px 8px 8px', display: 'block' },
|
||||||
heading: {
|
heading: {
|
||||||
fontSize: theme.typography.pxToRem(15),
|
fontSize: theme.typography.pxToRem(15),
|
||||||
fontWeight: theme.typography.fontWeightRegular,
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import * as q from '../../../../../backend/src/Model'
|
||||||
|
import CustomIconButton from '../../helper/CustomIconButton'
|
||||||
|
import Delete from '@material-ui/icons/Delete'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { Badge } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const RecursiveTopicDeleteButton = (props: {
|
||||||
|
node?: q.TreeNode<any>
|
||||||
|
deleteTopicAction: (node: q.TreeNode<any>, a: boolean, limit: number) => void
|
||||||
|
}) => {
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (props.node) {
|
||||||
|
props.deleteTopicAction(props.node, true, deleteLimit)
|
||||||
|
}
|
||||||
|
}, [props.node])
|
||||||
|
if (!props.node) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const deleteLimit = 50
|
||||||
|
const topicCount = props.node ? props.node.childTopicCount() : 0
|
||||||
|
if (topicCount === 0 || (props.node.message && topicCount === 1)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
style={{
|
||||||
|
top: '3px',
|
||||||
|
right: '3px',
|
||||||
|
}}
|
||||||
|
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
<CustomIconButton onClick={onClick} tooltip={`Deletes up to ${deleteLimit} sub-topics with a single click`}>
|
||||||
|
<Delete style={{ marginTop: '-3px' }} color="action" />
|
||||||
|
</CustomIconButton>
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from 'react'
|
import React from 'react'
|
||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../../../backend/src/Model'
|
||||||
import Button from '@material-ui/core/Button'
|
import Button from '@material-ui/core/Button'
|
||||||
import { withStyles, Theme } from '@material-ui/core/styles'
|
import { withStyles, Theme } from '@material-ui/core/styles'
|
||||||
import { treeActions } from '../../actions'
|
import { treeActions } from '../../../actions'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
classes: any
|
classes: any
|
||||||
19
app/src/components/Sidebar/TopicPanel/TopicDeleteButton.tsx
Normal file
19
app/src/components/Sidebar/TopicPanel/TopicDeleteButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import * as q from '../../../../../backend/src/Model'
|
||||||
|
import CustomIconButton from '../../helper/CustomIconButton'
|
||||||
|
import Delete from '@material-ui/icons/Delete'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const TopicDeleteButton = (props: {
|
||||||
|
node?: q.TreeNode<any>
|
||||||
|
deleteTopicAction: (node: q.TreeNode<any>) => void
|
||||||
|
}) => {
|
||||||
|
const { node } = props
|
||||||
|
if (!node || !node.message || !node.message.value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CustomIconButton onClick={() => props.deleteTopicAction(node)} tooltip="Clear this topic">
|
||||||
|
<Delete style={{ marginTop: '-3px' }} />
|
||||||
|
</CustomIconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
app/src/components/Sidebar/TopicPanel/TopicPanel.tsx
Normal file
48
app/src/components/Sidebar/TopicPanel/TopicPanel.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import * as q from '../../../../../backend/src/Model'
|
||||||
|
import Copy from '../../helper/Copy'
|
||||||
|
import Panel from '../Panel'
|
||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import Topic from './Topic'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
|
||||||
|
import { sidebarActions } from '../../../actions'
|
||||||
|
import { TopicDeleteButton } from './TopicDeleteButton'
|
||||||
|
|
||||||
|
const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions; updateNode: () => void }) => {
|
||||||
|
const { node, updateNode } = props
|
||||||
|
const copyTopic = node ? <Copy value={node.path()} /> : null
|
||||||
|
|
||||||
|
const deleteTopic = (topic?: q.TreeNode<any>, recursive: boolean = false, maxCount = 50) => {
|
||||||
|
if (!topic) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
props.actions.clearTopic(topic, recursive, maxCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => (
|
||||||
|
<Panel disabled={!Boolean(node)}>
|
||||||
|
<span>
|
||||||
|
Topic {copyTopic}
|
||||||
|
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
|
||||||
|
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
|
||||||
|
</span>
|
||||||
|
<Topic node={node} didSelectNode={updateNode} />
|
||||||
|
</Panel>
|
||||||
|
),
|
||||||
|
[node, node && node.childTopicCount()]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(sidebarActions, dispatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
undefined,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TopicPanel)
|
||||||
@@ -63,7 +63,7 @@ function TreeNodeSubnodes(props: Props) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return <span className={props.classes.list}>{listItems}</span>
|
return <span className={props.classes.list}>{listItems}</span>
|
||||||
}, [alreadyAdded, props.lastUpdate, props.theme])
|
}, [alreadyAdded, props.treeNode.lastUpdate, props.theme])
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = (theme: Theme) => ({
|
const styles = (theme: Theme) => ({
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function TreeNodeComponent(props: Props) {
|
|||||||
{renderNodes()}
|
{renderNodes()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [lastUpdate, treeNode, name, isCollapsed, selected, theme])
|
}, [treeNode.lastUpdate, treeNode, name, isCollapsed, selected, theme])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
|
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ class TreeComponent extends React.PureComponent<Props, State> {
|
|||||||
this.updateTimer = undefined
|
this.updateTimer = undefined
|
||||||
this.renderTime = performance.now()
|
this.renderTime = performance.now()
|
||||||
|
|
||||||
if (!this.props.paused) {
|
if (!this.props.paused && this.props.tree) {
|
||||||
this.props.tree && this.props.tree.applyUnmergedChanges()
|
this.props.tree.applyUnmergedChanges()
|
||||||
}
|
}
|
||||||
window.requestIdleCallback(
|
window.requestIdleCallback(
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user