Compare commits

..
18 Commits
Author SHA1 Message Date
Thomas Nordquist 901acf2bed Prepare release 2019-07-08 01:42:29 +02:00
Thomas Nordquist 4ec7c8ca75 Revert "Update material-ui"
This reverts commit 75c3619898.
2019-07-08 01:41:42 +02:00
Thomas Nordquist 75c3619898 Update material-ui 2019-07-08 01:31:27 +02:00
Thomas Nordquist 38f8d2e6ee Improve render performance 2019-07-08 01:22:16 +02:00
Thomas Nordquist e3584add7c Fix broker stats 2019-07-08 00:19:34 +02:00
Thomas Nordquist 77dcbccd5c Add time range support to charts 2019-07-07 23:54:28 +02:00
Thomas Nordquist 0ff6359a41 Allow scrolling in profile list 2019-07-07 23:52:51 +02:00
Thomas Nordquist 66bfcab256 Fix test 2019-07-07 23:11:13 +02:00
Thomas Nordquist 195dcf37d4 Add time range setting for charts 2019-07-07 22:14:49 +02:00
Thomas Nordquist 5830d99d45 Quit ui-tests eary if an error occurs 2019-07-07 16:50:29 +02:00
Thomas Nordquist d50cef7bb3 Repair reducer 2019-07-07 16:24:53 +02:00
Thomas Nordquist b79725bdf0 Refactor sidebar 2019-07-07 16:24:18 +02:00
Thomas Nordquist 6bead5b5a6 Fix animation glitch 2019-07-07 12:59:30 +02:00
Thomas Nordquist 183ea9d8c0 Allow publishing topics with ctrl+return 2019-07-07 12:44:34 +02:00
Thomas Nordquist 282736d2f6 Use default domain if plot data is empty 2019-07-07 12:31:07 +02:00
Thomas Nordquist 45b30e5997 Update dependencies 2019-07-07 12:04:27 +02:00
Thomas Nordquist 3d45e8ce6e Disable code signing for windows 2019-07-07 11:52:36 +02:00
Thomas Nordquist 393cf76839 Update dictionary 2019-07-02 15:34:43 +02:00
38 changed files with 760 additions and 446 deletions
+2
View File
@@ -1,6 +1,8 @@
{
"language": "en",
"words": [
"thomasnordquist",
"nowrap",
"subheader",
"basepath",
"webdriverio",
+1 -1
View File
@@ -35,4 +35,4 @@ script:
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- win; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then CSC_LINK="" yarn run package -- win; fi
+1
View File
@@ -35,6 +35,7 @@
"moment": "^2.24.0",
"moving-average": "^1.0.0",
"number-abbreviate": "^2.0.0",
"parse-duration": "^0.1.1",
"prismjs": "^1.15.0",
"react": "16.8",
"react-ace": "^7.0.1",
-1
View File
@@ -11,7 +11,6 @@ const debounce = require('lodash.debounce')
export { clearTopic } from './clearTopic'
export { moveSelectionUpOrDownwards, moveInward, moveOutward } from './visibleTreeTraversal'
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (
dispatch: Dispatch<any>,
+30 -14
View File
@@ -1,7 +1,6 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import React, { useState } from 'react'
import TopicPlot from '../TopicPlot'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
@@ -20,32 +19,48 @@ interface Props {
}
/**
* Subscribes to onMessage of treeNode
* Subscribes to onMessages keeping track of additional data points
*/
function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
const [lastUpdated, setLastUpdate] = React.useState(0)
const [lastUpdated, setLastUpdate] = useState(0)
const [messageHistory, setMessageHistory] = useState<q.MessageHistory | undefined>()
let amendMessageCallback: any
function subscribeToMessageUpdates() {
const onUpdateCallback = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
treeNode && treeNode.onMessage.subscribe(onUpdateCallback)
const throttledUpdate = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
if (treeNode) {
const newMessageHistory = treeNode.messageHistory.clone()
newMessageHistory.setCapacity(500, 2 * 500 * 10000)
amendMessageCallback = (message: q.Message) => {
newMessageHistory.add(message)
throttledUpdate()
}
treeNode.onMessage.subscribe(amendMessageCallback)
setMessageHistory(newMessageHistory)
}
return function cleanup() {
treeNode && treeNode.onMessage.unsubscribe(onUpdateCallback)
treeNode && treeNode.onMessage.unsubscribe(amendMessageCallback)
}
}
React.useEffect(subscribeToMessageUpdates, [treeNode])
return messageHistory
}
function Chart(props: Props) {
const { parameters, treeNode } = props
const [freezedHistory, setHistory] = React.useState<q.MessageHistory | undefined>()
useMessageSubscriptionToUpdate(treeNode)
const [frozenHistory, setFrozenHistory] = React.useState<q.MessageHistory | undefined>()
const messageHistory = useMessageSubscriptionToUpdate(treeNode)
const togglePause = React.useCallback(() => {
if (!props.treeNode) {
return
}
setHistory(freezedHistory ? undefined : props.treeNode.messageHistory.clone())
}, [props.treeNode, freezedHistory])
setFrozenHistory(frozenHistory ? undefined : messageHistory && messageHistory.clone())
}, [props.treeNode, frozenHistory])
const onRemove = React.useCallback(() => {
props.actions.chart.removeChart(props.parameters)
@@ -63,17 +78,18 @@ function Chart(props: Props) {
<ChartActions
parameters={parameters}
onRemove={onRemove}
paused={Boolean(freezedHistory)}
paused={Boolean(frozenHistory)}
togglePause={togglePause}
/>
</div>
</div>
{props.treeNode ? (
{messageHistory ? (
<TopicPlot
color={props.parameters.color}
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
history={freezedHistory ? freezedHistory : props.treeNode.messageHistory}
history={frozenHistory || messageHistory}
dotPath={parameters.dotPath}
/>
) : (
@@ -0,0 +1,102 @@
import React, { ChangeEvent, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { bindActionCreators } from 'redux'
import { Button, Menu, TextField, Typography } from '@material-ui/core'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
const parseDuration = require('parse-duration')
interface Props {
actions: { chart: typeof chartActions }
chart: ChartParameters
anchorEl?: Element
open: boolean
onClose: () => void
}
function TimeRangeSettings(props: Props) {
const dismissClick = useCallback((e: MouseEvent) => e.stopPropagation(), [])
const [value, setValue] = useState<string | undefined>(
props.chart.timeRange ? props.chart.timeRange.until : undefined
)
const ranges = ['all', '10s', '30s', '1m', '5m', '15m', '1h', '6h', '1d']
const manuallySetIntervalHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}, [])
useEffect(() => {
if (!value) {
props.actions.chart.updateChart({
...props.chart,
timeRange: undefined,
})
return
}
const canBeParsed = Boolean(parseDuration(value))
if (canBeParsed) {
props.actions.chart.updateChart({
...props.chart,
timeRange: {
until: value,
},
})
}
}, [value])
return useMemo(() => {
const createRangeHandler = (range: string) => (e: React.MouseEvent) => setValue(range === 'all' ? undefined : range)
return (
<Menu
style={{ textAlign: 'center' }}
keepMounted={true}
anchorEl={props.anchorEl}
open={props.open}
onClose={props.onClose}
>
<Typography>Chart data within a time interval</Typography>
<div style={{ padding: '0 16px', width: '275px', textAlign: 'center' }}>
{ranges.map(r => {
return (
<Button
style={{ margin: '4px', textTransform: 'none' }}
variant="contained"
key={r}
onClick={createRangeHandler(r)}
>
{r}
</Button>
)
})}
</div>
<Typography style={{ fontSize: '0.75em' }}>
<i>Limited to 500 data points</i>
</Typography>
<br />
<TextField
style={{ marginLeft: '8px', marginTop: '0' }}
onClick={dismissClick}
label="custom interval"
value={value || ''}
onChange={manuallySetIntervalHandler}
margin="normal"
/>
</Menu>
)
}, [value, props.open])
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(TimeRangeSettings)
@@ -1,11 +1,12 @@
import * as React from 'react'
import ColorSettings from './ColorSettings'
import InterpolationSettings from './InterpolationSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem } from '@material-ui/core'
import MoveUp from './MoveUp'
import RangeSettings from './RangeSettings'
import Size from './Size'
import MoveUp from './MoveUp'
import ColorSettings from './ColorSettings'
import TimeRangeSettings from './TimeRangeSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem } from '@material-ui/core'
function ChartSettings(props: {
open: boolean
@@ -14,6 +15,7 @@ function ChartSettings(props: {
anchorEl: React.MutableRefObject<undefined>
}) {
const [rangeVisible, setRangeVisible] = React.useState(false)
const [timeRangeVisible, setTimeRangeVisible] = React.useState(false)
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
const [sizeVisible, setSizeVisible] = React.useState(false)
const [colorVisible, setColorVisible] = React.useState(false)
@@ -25,6 +27,13 @@ function ChartSettings(props: {
setRangeVisible(!rangeVisible)
}, [rangeVisible, open])
const toggleTimeRange = React.useCallback(() => {
if (open) {
props.close()
}
setTimeRangeVisible(!timeRangeVisible)
}, [timeRangeVisible, open])
const toggleInterpolation = React.useCallback(() => {
if (open) {
props.close()
@@ -52,6 +61,9 @@ function ChartSettings(props: {
<MenuItem key="range" onClick={toggleRange}>
Set range
</MenuItem>
<MenuItem key="timeRange" onClick={toggleTimeRange}>
Time range
</MenuItem>
<MenuItem key="interpolation" onClick={toggleInterpolation}>
Curve interpolation
</MenuItem>
@@ -64,6 +76,12 @@ function ChartSettings(props: {
<MoveUp chart={props.chart} close={props.close} />
</Menu>
<RangeSettings chart={props.chart} anchorEl={props.anchorEl.current} open={rangeVisible} onClose={toggleRange} />
<TimeRangeSettings
chart={props.chart}
anchorEl={props.anchorEl.current}
open={timeRangeVisible}
onClose={toggleTimeRange}
/>
<InterpolationSettings
chart={props.chart}
anchorEl={props.anchorEl.current}
@@ -1,7 +1,8 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import React from 'react'
import Chart from './Chart'
import { ChartParameters } from '../../reducers/Charts'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
interface Props {
tree?: q.Tree<any>
@@ -14,37 +15,6 @@ export function ChartWithTreeNode(props: Props) {
return null
}
const initialTreeNode = tree.findNode(parameters.topic)
const [treeNode, setTreeNode] = React.useState<q.TreeNode<any> | undefined>(initialTreeNode)
usePollingToFetchTreeNode(treeNode, tree, parameters.topic, setTreeNode)
const treeNode = usePollingToFetchTreeNode(tree, parameters.topic)
return <Chart treeNode={treeNode} parameters={parameters} />
}
/**
* If a node is not available when the plot is shown, keep polling until it has been created
*/
function usePollingToFetchTreeNode(
treeNode: q.TreeNode<any> | undefined,
tree: q.Tree<any>,
path: string,
setTreeNode: React.Dispatch<React.SetStateAction<q.TreeNode<any> | undefined>>
) {
function pollUntilTreeNodeHasBeenFound() {
let intervalTimer: any
if (!treeNode) {
intervalTimer = setInterval(() => {
const node = tree.findNode(path)
if (node) {
setTreeNode(node)
clearInterval(intervalTimer)
}
}, 500)
}
return function cleanup() {
intervalTimer && clearInterval(intervalTimer)
}
}
React.useEffect(pollUntilTreeNodeHasBeenFound, [])
}
@@ -95,9 +95,10 @@ const styles = (theme: Theme) => ({
borderRadius: `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`,
paddingTop: theme.spacing(2),
flex: 3,
overflow: 'hidden',
overflow: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
overflowY: 'auto' as 'auto',
},
right: {
borderRadius: `0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0`,
@@ -6,10 +6,10 @@ import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../../actions'
import { ConnectionOptions } from '../../../model/ConnectionOptions'
import { KeyCodes } from '../../../utils/KeyCodes'
import { List, ListSubheader } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'
import { KeyCodes } from '../../../utils/KeyCodes'
interface Props {
classes: any
@@ -39,10 +39,10 @@ function ProfileList(props: Props) {
useGlobalKeyEventHandler(KeyCodes.arrow_up, selectConnection('previous'))
const createConnectionButton = (
<ListSubheader component="div">
<div style={{ padding: '8px 16px' }}>
<AddButton action={actions.createConnection} />
Connections
</ListSubheader>
</div>
)
return (
@@ -1,13 +1,12 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import React, { useMemo } from 'react'
import { AppState } from '../../reducers'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux'
import { StyleRulesCallback, withStyles, Theme } from '@material-ui/core/styles'
import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import teal from '@material-ui/core/colors/teal'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
const abbreviate = require('number-abbreviate')
interface Stats {
@@ -16,10 +15,6 @@ interface Stats {
}
const styles = (theme: Theme) => ({
flex: {
display: 'flex',
width: '100%',
},
container: {
width: '100%',
height: '224px',
@@ -34,24 +29,12 @@ interface Props {
tree?: q.Tree<TopicViewModel>
}
class BrokerStatistics extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
}
function BrokerStatistics(props: Props) {
const { tree, classes } = props
const sysTopic = usePollingToFetchTreeNode(props.tree, '$SYS')
private renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div className={this.props.classes.flex}>
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{this.renderStat(tree, b)}</div>
</div>
)
}
public render() {
const { tree, classes } = this.props
if (!tree || !tree.findNode('$SYS/broker/clients/total')) {
return useMemo(() => {
if (!Boolean(sysTopic)) {
return null
}
@@ -94,38 +77,20 @@ class BrokerStatistics extends React.Component<Props, {}> {
},
}
return (
<div className={classes.container}>
{this.renderStat(tree, stats.broker)}
{this.renderPair(tree, stats.sent, stats.received)}
{this.renderPair(tree, stats.clients, stats.subscriptions)}
{this.renderPair(tree, stats.sent5m, stats.received5m)}
{this.renderPair(tree, stats.heap, stats.heapMax)}
</div>
)
}
public renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
const node = tree.findNode(stat.topic)
if (!node || !node.message) {
if (!tree) {
return null
}
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
let value = node.message && node.message.value ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str
return (
<div key={stat.title}>
<Typography>
<b>{stat.title}</b>
</Typography>
<Typography style={{ paddingLeft: '8px' }}>
<i>{value}</i>
</Typography>
<div className={classes.container}>
{renderStat(tree, stats.broker)}
{renderPair(tree, stats.sent, stats.received)}
{renderPair(tree, stats.clients, stats.subscriptions)}
{renderPair(tree, stats.sent5m, stats.received5m)}
{renderPair(tree, stats.heap, stats.heapMax)}
</div>
)
}
}, [sysTopic && sysTopic.lastUpdate])
}
const mapStateToProps = (state: AppState) => {
@@ -135,3 +100,39 @@ const mapStateToProps = (state: AppState) => {
}
export default withStyles(styles)(connect(mapStateToProps)(BrokerStatistics))
function renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div
style={{
display: 'flex',
width: '100%',
}}
>
<div style={{ flex: 1 }}>{renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{renderStat(tree, b)}</div>
</div>
)
}
function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
const node = tree.findNode(stat.topic)
if (!node || !node.message) {
return null
}
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
let value = node.message && node.message.value ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str
return (
<div key={stat.title}>
<Typography>
<b>{stat.title}</b>
</Typography>
<Typography style={{ paddingLeft: '8px' }}>
<i>{value}</i>
</Typography>
</div>
)
}
+7 -1
View File
@@ -34,6 +34,12 @@ class HistoryDrawer extends React.Component<Props, State> {
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' })
public renderHistory() {
@@ -51,7 +57,7 @@ class HistoryDrawer extends React.Component<Props, State> {
<div
key={element.key}
style={style(element)}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
onClick={this.createSelectionHandler(index)}
tabIndex={0}
onKeyDown={this.handleCtrlA}
>
+4 -1
View File
@@ -4,7 +4,7 @@ import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
interface Props {
node: q.TreeNode<TopicViewModel>
node?: q.TreeNode<TopicViewModel>
}
class NodeStats extends React.Component<Props, {}> {
@@ -14,6 +14,9 @@ class NodeStats extends React.Component<Props, {}> {
public render() {
const { node } = this.props
if (!node) {
return null
}
return (
<div>
+33
View 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)
+10 -2
View File
@@ -12,6 +12,7 @@ interface Props {
theme: Theme
interpolation?: PlotCurveTypes
range?: [number?, number?]
timeRangeStart?: number
color?: string
}
@@ -55,10 +56,12 @@ export default withTheme((props: Props) => {
return React.useMemo(() => {
const data = props.data
const calculatedDomain = domainForData(data)
let yDomain: [number, number] = props.range
const yDomain: [number, number] = props.range
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
: calculatedDomain
const xDomain = props.timeRangeStart ? [Date.now() - props.timeRangeStart, Date.now()] : undefined
let color: string =
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
if (props.color) {
@@ -67,7 +70,7 @@ export default withTheme((props: Props) => {
return (
<div style={{ height: '150px', overflow: 'hidden' }}>
<XYPlot width={width} height={180} yDomain={yDomain}>
<XYPlot width={width} height={180} yDomain={yDomain} xDomain={xDomain}>
<HorizontalGridLines />
<XAxis />
<YAxis width={45} tickFormat={(num: number) => abbreviate(num)} />
@@ -88,6 +91,11 @@ export default withTheme((props: Props) => {
})
function domainForData(data: Array<{ x: number; y: number }>): [number, number] {
if (!data[0]) {
const defaultDomain: [number, number] = [-1, 1]
return defaultDomain
}
let max = data[0].y
let min = data[0].y
data.forEach(d => {
+80 -152
View File
@@ -1,32 +1,18 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ClearAdornment from '../../helper/ClearAdornment'
import Editor from './Editor'
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import History from '../HistoryDrawer'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import PublishHistory from './PublishHistory'
import React, { useCallback, useState, useMemo } from 'react'
import TopicInput from './TopicInput'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { Button, Fab, Theme, Tooltip, withTheme } from '@material-ui/core'
import { connect } from 'react-redux'
import { globalActions, publishActions } from '../../../actions'
import {
Button,
FormControlLabel,
FormControl,
InputLabel,
Input,
Checkbox,
Tooltip,
Fab,
Theme,
withTheme,
} from '@material-ui/core'
import { EditorModeSelect } from './EditorModeSelect'
import QosSelect from './QosSelect'
import Editor from './Editor'
const sha1 = require('sha1')
import { globalActions, publishActions } from '../../../actions'
import { KeyCodes } from '../../../utils/KeyCodes'
import RetainSwitch from './RetainSwitch'
interface Props {
connectionId?: string
@@ -39,104 +25,73 @@ interface Props {
theme: Theme
}
interface State {
history: Array<Message>
function useHistory(): [Array<Message>, (topic: string, payload?: string) => void] {
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> {
constructor(props: any) {
super(props)
this.state = { history: [] }
}
function Publish(props: Props) {
console.log(props.connectionId)
const updatePayload = props.actions.setPayload
const [history, amendToHistory] = useHistory()
private updatePayload = (payload: string) => {
this.props.actions.setPayload(payload)
}
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
props.actions.setEditorMode(value)
}, [])
private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.actions.setTopic(e.target.value)
}
private updateMode = (e: React.ChangeEvent<{}>, value: string) => {
this.props.actions.setEditorMode(value)
}
private publish = (e: React.MouseEvent) => {
e.stopPropagation()
if (!this.props.connectionId) {
const publish = useCallback(() => {
if (!props.connectionId) {
return
}
this.props.actions.publish(this.props.connectionId)
props.actions.publish(props.connectionId)
const topic = this.props.topic || ''
const payload = this.props.payload
if (this.props.connectionId && topic) {
this.addMessageToHistory(topic, payload)
const topic = props.topic || ''
const payload = props.payload
if (props.connectionId && topic) {
amendToHistory(topic, payload)
}
}
}, [props, props.connectionId, props.topic, props.payload, amendToHistory])
private addMessageToHistory(topic: string, payload?: string) {
// Remove duplicates
let filteredHistory = this.state.history.filter(e => e.payload !== payload || e.topic !== topic)
filteredHistory = filteredHistory.slice(-7)
const history: Array<Message> = [...filteredHistory, { topic, payload, sent: new Date() }]
this.setState({ history })
}
private clearTopic = () => {
this.props.actions.setTopic('')
}
private topic() {
const topicStr = this.props.topic || ''
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
publish()
},
[publish]
)
const PublishButton = () => {
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>) => {
if (!e.target.value) {
this.props.actions.setTopic(undefined)
}
}
private publishButton() {
return (
<Button variant="contained" size="small" color="primary" onClick={this.publish} id="publish-button">
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
}
private formatJson = () => {
if (this.props.payload) {
const formatJson = () => {
if (props.payload) {
try {
const str = JSON.stringify(JSON.parse(this.props.payload), undefined, ' ')
this.updatePayload(str)
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str)
} catch (error) {
this.props.globalActions.showError(`Format error: ${error.message}`)
props.globalActions.showError(`Format error: ${error.message}`)
}
}
}
private renderFormatJson() {
if (this.props.editorMode !== 'json') {
const renderFormatJson = () => {
if (props.editorMode !== 'json') {
return null
}
@@ -144,7 +99,7 @@ class Publish extends React.Component<Props, State> {
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
onClick={this.formatJson}
onClick={formatJson}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
@@ -153,72 +108,45 @@ class Publish extends React.Component<Props, State> {
)
}
private editorMode() {
function EditorMode() {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<EditorModeSelect value={this.props.editorMode} onChange={this.updateMode} />
{this.renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>{this.publishButton()}</div>
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
{renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>
<PublishButton />
</div>
</div>
</div>
)
}
private publishMode() {
const labelStyle = { margin: '0 8px 0 8px' }
const handleSubmit = useCallback(
(e: React.KeyboardEvent) => {
if (e.keyCode === KeyCodes.enter && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
e.stopPropagation()
publish()
}
},
[publish]
)
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)
}
public render() {
return (
<div style={{ flexGrow: 1, marginLeft: '8px' }}>
{this.topic()}
return useMemo(
() => (
<div style={{ flexGrow: 1, marginLeft: '8px' }} onKeyDown={handleSubmit}>
<TopicInput />
<div style={{ width: '100%', display: 'block' }}>
{this.editorMode()}
<Editor value={this.props.payload} editorMode={this.props.editorMode} onChange={this.updatePayload} />
{this.publishMode()}
<EditorMode />
<Editor value={props.payload} editorMode={props.editorMode} onChange={updatePayload} />
<RetainSwitch />
</div>
{this.history()}
<PublishHistory history={history} />
</div>
)
}
),
[props.payload, props.editorMode, history, handleSubmit, updatePayload]
)
}
const mapDispatchToProps = (dispatch: any) => {
@@ -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)
@@ -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)
@@ -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)
+39 -143
View File
@@ -1,11 +1,7 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import Copy from '../helper/Copy'
import CustomIconButton from '../helper/CustomIconButton'
import Delete from '@material-ui/icons/Delete'
import React, { useState, useEffect, useCallback } from 'react'
import ExpandMore from '@material-ui/icons/ExpandMore'
import NodeStats from './NodeStats'
import Topic from './Topic'
import ValuePanel from './ValueRenderer/ValuePanel'
import { AppState } from '../../reducers'
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 { Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import TopicPanel from './TopicPanel/TopicPanel'
import Panel from './Panel'
const throttle = require('lodash.throttle')
@@ -27,148 +25,48 @@ interface Props {
connectionId?: string
}
interface State {
compareMessage?: q.Message
valueRenderWidth: number
}
function Sidebar(props: Props) {
const { classes, node } = props
const [lastUpdate, setLastUpdate] = useState(0)
class Sidebar extends React.Component<Props, State> {
private updateNode = throttle(() => {
this.setState(this.state)
}, 300)
const updateNode = useCallback(
throttle(() => {
setLastUpdate(node ? node.lastUpdate : 0)
}, 300),
[node]
)
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
useEffect(() => {
const updateCallback = updateNode
node && node.onMerge.subscribe(updateCallback)
node && node.onMessage.subscribe(updateCallback)
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 function cleanup() {
node && node.onMerge.unsubscribe(updateCallback)
node && node.onMessage.unsubscribe(updateCallback)
}
}, [node])
return (
<CustomIconButton onClick={() => this.deleteTopic(this.props.node)} tooltip="Clear this topic">
<Delete style={{ marginTop: '-3px' }} />
</CustomIconButton>
)
}
private renderRecursiveTopicDeleteButton() {
const deleteLimit = 50
const topicCount = this.props.node ? this.props.node.childTopicCount() : 0
if (!this.props.node || topicCount === 0 || (this.props.node.message && topicCount === 1)) {
return null
}
return (
<Badge
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 (
return (
<div id="Sidebar" className={props.classes.drawer}>
<div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>
Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
<TopicPanel node={node} updateNode={updateNode} />
<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>
<ExpansionPanelDetails className={props.classes.details}>
<NodeStats node={node} />
</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>}>
<Publish connectionId={this.props.connectionId} />
</React.Suspense>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
{this.renderNodeStats()}
</ExpansionPanel>
</Panel>
</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>
)
}
</div>
)
}
const mapStateToProps = (state: AppState) => {
@@ -188,13 +86,11 @@ const styles = (theme: Theme) => ({
drawer: {
display: 'block' as 'block',
},
badge: {
top: '3px',
right: '3px',
},
valuePaper: {
margin: theme.spacing(1),
},
summary: { minHeight: '0' },
details: { padding: '0px 16px 8px 8px', display: 'block' },
heading: {
fontSize: theme.typography.pxToRem(15),
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 * as q from '../../../../backend/src/Model'
import React from 'react'
import * as q from '../../../../../backend/src/Model'
import Button from '@material-ui/core/Button'
import { withStyles, Theme } from '@material-ui/core/styles'
import { treeActions } from '../../actions'
import { treeActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { TopicViewModel } from '../../model/TopicViewModel'
import { TopicViewModel } from '../../../model/TopicViewModel'
interface Props {
classes: any
@@ -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>
)
}
@@ -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)
+29 -8
View File
@@ -5,18 +5,28 @@ import PlotHistory from './Sidebar/PlotHistory'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './Sidebar/CodeDiff/util'
import { PlotCurveTypes } from '../reducers/Charts'
const parseDuration = require('parse-duration')
interface Props {
history: q.MessageHistory
dotPath?: string
timeInterval?: string
interpolation?: PlotCurveTypes
range?: [number?, number?]
color?: string
}
function nodeToHistory(history: q.MessageHistory) {
return history
.toArray()
function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Message>) {
if (startTime) {
const threshold = new Date(Date.now() - startTime)
return data.filter(d => d.received >= threshold)
}
return data
}
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
return { x: message.received.getTime(), y: toPlottableValue(value) }
@@ -24,9 +34,8 @@ function nodeToHistory(history: q.MessageHistory) {
.filter(data => !isNaN(data.y as any)) as any
}
function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
return history
.toArray()
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
let json = {}
try {
@@ -41,8 +50,20 @@ function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
}
function render(props: Props) {
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
return <PlotHistory color={props.color} range={props.range} interpolation={props.interpolation} data={data} />
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
const data = props.dotPath
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
: nodeToHistory(startOffset, props.history)
return (
<PlotHistory
timeRangeStart={startOffset}
color={props.color}
range={props.range}
interpolation={props.interpolation}
data={data}
/>
)
}
export default render
@@ -63,7 +63,7 @@ function TreeNodeSubnodes(props: Props) {
})
return <span className={props.classes.list}>{listItems}</span>
}, [alreadyAdded, props.lastUpdate, props.theme])
}, [alreadyAdded, props.treeNode.lastUpdate, props.theme])
}
const styles = (theme: Theme) => ({
@@ -9,20 +9,21 @@ export function useAnimationToIndicateTopicUpdate(
) {
useEffect(() => {
if (ref.current && shouldAnimate && Date.now() - lastUpdate < 3000 && !selected) {
let timeout: any
let animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.add(className)
timeout = setTimeout(
() =>
(animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.remove(className)
})),
500
)
})
const timeout = setTimeout(
() =>
(animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.remove(className)
})),
500
)
return function cleanup() {
clearTimeout(timeout)
timeout && clearTimeout(timeout)
animationFrame && cancelAnimationFrame(animationFrame)
}
}
+11 -8
View File
@@ -28,11 +28,11 @@ export interface Props {
function TreeNodeComponent(props: Props) {
const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
const [selected, setSelected] = useState(false)
const nodeRef = useRef<HTMLDivElement>()
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
const animationClass =
props.theme.palette.type === 'light' ? props.classes.animationLight : props.classes.animationDark
@@ -81,12 +81,15 @@ function TreeNodeComponent(props: Props) {
didSelectTopic()
}, [didSelectTopic])
const mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
didSelectTopic()
}
}
const mouseOver = useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
didSelectTopic()
}
},
[didSelectTopic]
)
useEffect(() => {
treeNode.viewModel && treeNode.viewModel.setExpanded(!isCollapsed, false)
@@ -133,7 +136,7 @@ function TreeNodeComponent(props: Props) {
{renderNodes()}
</div>
)
}, [lastUpdate, treeNode, name, isCollapsed, selected, theme])
}, [treeNode.lastUpdate, treeNode, name, isCollapsed, selected, theme])
}
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
+2 -2
View File
@@ -99,8 +99,8 @@ class TreeComponent extends React.PureComponent<Props, State> {
this.updateTimer = undefined
this.renderTime = performance.now()
if (!this.props.paused) {
this.props.tree && this.props.tree.applyUnmergedChanges()
if (!this.props.paused && this.props.tree) {
this.props.tree.applyUnmergedChanges()
}
window.requestIdleCallback(
() => {
@@ -0,0 +1,38 @@
import * as q from '../../../../backend/src/Model'
import { useState, useEffect } from 'react'
/**
* If a node is not available when the plot is shown, keep polling until it has been created
*/
export function usePollingToFetchTreeNode(tree: q.Tree<any> | undefined, path: string) {
const [treeNode, setTreeNode] = useState<q.TreeNode<any> | undefined>()
function pollUntilTreeNodeHasBeenFound() {
if (!tree) {
return
}
const initialTreeNode = tree.findNode(path)
if (initialTreeNode) {
setTreeNode(initialTreeNode)
return
}
let intervalTimer: any
if (!treeNode) {
intervalTimer = setInterval(() => {
const node = tree.findNode(path)
if (node) {
setTreeNode(node)
clearInterval(intervalTimer)
}
}, 500)
}
return function cleanup() {
intervalTimer && clearInterval(intervalTimer)
}
}
useEffect(pollUntilTreeNodeHasBeenFound, [tree])
return treeNode
}
+5 -1
View File
@@ -13,6 +13,9 @@ export interface ChartParameters {
from?: number
to?: number
}
timeRange?: {
until: string
}
width?: 'big' | 'medium' | 'small'
color?: string
}
@@ -84,8 +87,9 @@ function moveUp(state: ChartsState, action: MoveUp) {
const previousItem = charts.get(idx - 1)
if (idx === 0 || !item || !previousItem) {
return
return state // do nothing
}
const newlyOrderedCharts = charts.set(idx - 1, item).set(idx, previousItem)
return state.set('charts', newlyOrderedCharts)
}
+5
View File
@@ -4252,6 +4252,11 @@ parse-asn1@^5.0.0:
pbkdf2 "^3.0.3"
safe-buffer "^5.1.1"
parse-duration@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-0.1.1.tgz#13114ddc9891c1ecd280036244554de43647a226"
integrity sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+5
View File
@@ -54,6 +54,11 @@ export class RingBuffer<T extends Lengthwise> {
this.usage -= freedSpace
}
public setCapacity(items: number, bytes: number) {
this.maxItems = items
this.capacity = bytes
}
public clone(): RingBuffer<T> {
return new RingBuffer(this.capacity, this.maxItems, this)
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "MQTT-Explorer",
"version": "0.3.1",
"version": "0.3.2",
"description": "Explore your message queues",
"main": "dist/src/electron.js",
"scripts": {
+1
View File
@@ -8,6 +8,7 @@ function finish {
}
trap finish EXIT
set -e
DIMENSIONS="1024x720"
SCR=99
+2 -2
View File
@@ -1,7 +1,7 @@
import { Browser, Element } from 'webdriverio'
import { Browser } from 'webdriverio'
import { clickOn } from '../util'
export async function copyTopicToClipboard(browser: Browser) {
const copyButton = await browser.$('//p[contains(text(), "Topic")]/span')
const copyButton = await browser.$('//span[contains(text(), "Topic")]//button')
await clickOn(copyButton, browser, 1)
}
+1 -1
View File
@@ -2,6 +2,6 @@ import { Browser, Element } from 'webdriverio'
import { clickOn, expandTopic, sleep, writeText } from '../util'
export async function copyValueToClipboard(browser: Browser) {
const copyButton = await browser.$('//p[contains(text(), "Value")]/span')
const copyButton = await browser.$('//p[contains(text(), "Value")]//button')
await clickOn(copyButton, browser, 1)
}
+1 -1
View File
@@ -1472,7 +1472,7 @@ electron-publish@21.0.6:
"electron-telemetry@git+https://github.com/thomasnordquist/electron-telemetry.git#dist":
version "1.0.0"
resolved "git+https://github.com/thomasnordquist/electron-telemetry.git#78b140426a31fe6fec75c7fe36daa130445af191"
resolved "git+https://github.com/thomasnordquist/electron-telemetry.git#2d67a18cfe6f5caf2fd0fe35eb35272a4b24439f"
dependencies:
axios "^0.18.0"
pako "^1.0.8"