This commit is contained in:
Thomas Nordquist
2019-06-26 17:41:04 +02:00
parent e02091f645
commit f9829c2d5c
24 changed files with 204 additions and 152 deletions

View File

@@ -22,10 +22,10 @@
background-color: none; background-color: none;
} }
25% { 25% {
background-color: #3f51b5; background-color: #4f5781;
} }
50% { 50% {
background-color: #3f51b5; background-color: #4f5781;
} }
100% { 100% {
background-color: none; background-color: none;
@@ -38,11 +38,11 @@
color: inherit; color: inherit;
} }
25% { 25% {
background-color: #bfc9c8; background-color: #c0c8c0;
color: #000; color: #000;
} }
50% { 50% {
background-color: #bfc9c8; background-color: #c0c8c0;
color: #000; color: #000;
} }
100% { 100% {

View File

@@ -2,7 +2,9 @@ import * as q from '../../../backend/src/Model'
import { ActionTypes } from '../reducers/Sidebar' import { ActionTypes } from '../reducers/Sidebar'
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events' import { clearTopic } from './clearTopic'
export { clearTopic } from './clearTopic'
export const clearRetainedTopic = () => (dispatch: Dispatch<any>, getState: () => AppState) => { export const clearRetainedTopic = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const selectedTopic = getState().tree.get('selectedTopic') const selectedTopic = getState().tree.get('selectedTopic')
@@ -19,38 +21,3 @@ export const setCompareMessage = (message?: q.Message) => (dispatch: Dispatch<an
type: ActionTypes.SIDEBAR_SET_COMPARE_MESSAGE, type: ActionTypes.SIDEBAR_SET_COMPARE_MESSAGE,
}) })
} }
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
const { connectionId } = getState().connection
if (!connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
if (recursive) {
topic
.childTopics()
.filter(topic => Boolean(topic.message && topic.message.value))
.slice(0, subtopicClearLimit)
.forEach(topic => {
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
})
}
}

View File

@@ -8,8 +8,10 @@ import { globalActions } from './'
import { setTopic } from './Publish' import { setTopic } from './Publish'
import { TopicViewModel } from '../model/TopicViewModel' import { TopicViewModel } from '../model/TopicViewModel'
const debounce = require('lodash.debounce') const debounce = require('lodash.debounce')
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>,

View File

@@ -0,0 +1,41 @@
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events'
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch(moveSelectionUpOrDownwards('next'))
const { connectionId } = getState().connection
if (!connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
if (recursive) {
topic
.childTopics()
.filter(topic => Boolean(topic.message && topic.message.value))
.slice(0, subtopicClearLimit)
.forEach((topic, idx) => {
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
// Rate limit deletion
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
})
}
}

View File

@@ -13,6 +13,7 @@ export const moveSelectionUpOrDownwards = (direction: 'next' | 'previous') => (
const state = getState() const state = getState()
const selected = state.tree.get('selectedTopic') const selected = state.tree.get('selectedTopic')
const tree = state.tree.get('tree') const tree = state.tree.get('tree')
if (!selected || !tree) { if (!selected || !tree) {
if (tree) { if (tree) {
dispatch(selectTopic(tree)) dispatch(selectTopic(tree))

View File

@@ -1,7 +1,7 @@
import * as React from 'react' import * as React from 'react'
import ChartPanel from '../ChartPanel' import ChartPanel from '../ChartPanel'
import ReactSplitPane from 'react-split-pane' import ReactSplitPane from 'react-split-pane'
import Tree from '../Tree/Tree' import Tree from '../Tree'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import { ChartParameters } from '../../reducers/Charts' import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux' import { connect } from 'react-redux'

View File

@@ -41,7 +41,13 @@ function SearchBar(props: {
const tagElementIsNotBlacklisted = const tagElementIsNotBlacklisted =
document.activeElement && tagNameBlacklist.indexOf(document.activeElement.tagName) === -1 document.activeElement && tagNameBlacklist.indexOf(document.activeElement.tagName) === -1
if ((isCharacter || isAllowedControlCharacter) && !hasFocus && tagElementIsNotBlacklisted && hasConnection) { if (
(isCharacter || isAllowedControlCharacter) &&
!event.defaultPrevented &&
!hasFocus &&
tagElementIsNotBlacklisted &&
hasConnection
) {
// Focus input field, no preventDefault the event will reach the input element after it has been focussed // Focus input field, no preventDefault the event will reach the input element after it has been focussed
inputRef.current && inputRef.current.focus() inputRef.current && inputRef.current.focus()
} }

View File

@@ -1,10 +1,11 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import * as React from 'react' import React from 'react'
import TreeNode from './TreeNode' import TreeNode from '.'
import { SettingsState } from '../../reducers/Settings' import { SettingsState } from '../../../reducers/Settings'
import { sortedNodes } from '../../sortedNodes' import { sortedNodes } from '../../../sortedNodes'
import { Theme, withStyles } from '@material-ui/core' import { Theme, withStyles } from '@material-ui/core'
import { TopicViewModel } from '../../model/TopicViewModel' import { TopicViewModel } from '../../../model/TopicViewModel'
import { treeActions } from '../../../actions'
export interface Props { export interface Props {
treeNode: q.TreeNode<TopicViewModel> treeNode: q.TreeNode<TopicViewModel>
@@ -14,6 +15,7 @@ export interface Props {
selectedTopic?: q.TreeNode<TopicViewModel> selectedTopic?: q.TreeNode<TopicViewModel>
selectTopicAction: (treeNode: q.TreeNode<any>) => void selectTopicAction: (treeNode: q.TreeNode<any>) => void
settings: SettingsState settings: SettingsState
actions: typeof treeActions
} }
interface State { interface State {
@@ -60,6 +62,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
lastUpdate={node.lastUpdate} lastUpdate={node.lastUpdate}
selectTopicAction={this.props.selectTopicAction} selectTopicAction={this.props.selectTopicAction}
settings={this.props.settings} settings={this.props.settings}
actions={this.props.actions}
/> />
) )
}) })

View File

@@ -1,8 +1,8 @@
import * as React from 'react' import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import { withStyles, Theme } from '@material-ui/core' import { withStyles, Theme } from '@material-ui/core'
import { TopicViewModel } from '../../model/TopicViewModel' import { TopicViewModel } from '../../../model/TopicViewModel'
import { Base64Message } from '../../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> { export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode<TopicViewModel> treeNode: q.TreeNode<TopicViewModel>

View File

@@ -0,0 +1,22 @@
import React, { useEffect } from 'react'
export function useAnimationToIndicateTopicUpdate(
lastUpdate: number,
selected: boolean,
setShowUpdateAnimation: React.Dispatch<React.SetStateAction<boolean>>,
showUpdateAnimation: boolean
) {
useEffect(() => {
if (Date.now() - lastUpdate < 3000 && !selected) {
setShowUpdateAnimation(true)
}
}, [lastUpdate, selected])
useEffect(() => {
if (showUpdateAnimation) {
const timeout = setTimeout(() => setShowUpdateAnimation(false), 500)
return function cleanup() {
clearTimeout(timeout)
}
}
}, [showUpdateAnimation])
}

View File

@@ -0,0 +1,17 @@
import * as q from '../../../../../../backend/src/Model'
import React, { useCallback } from 'react'
import { KeyCodes } from '../../../../utils/KeyCodes'
import { treeActions } from '../../../../actions'
export function useDeleteKeyCallback(topic: q.TreeNode<any>, actions: typeof treeActions) {
return useCallback(
(event: React.KeyboardEvent) => {
if (event.keyCode === KeyCodes.delete || event.keyCode === KeyCodes.backspace) {
event.stopPropagation()
event.preventDefault()
actions.clearTopic(topic, true, 50)
}
},
[topic]
)
}

View File

@@ -0,0 +1,14 @@
import { Props } from '..'
import { useEffect, useState } from 'react'
export function useIsAllowedToAutoExpandState(props: Props): boolean {
const { settings, treeNode, isRoot } = props
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
useEffect(() => {
const newIsAllowedToAutoExpand = isRoot || treeNode.edgeCount() <= settings.get('autoExpandLimit')
if (newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
setAllowAutoExpand(newIsAllowedToAutoExpand)
}
}, [treeNode.edgeCount(), settings.get('autoExpandLimit')])
return isAllowedToAutoExpand
}

View File

@@ -1,6 +1,6 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../../../backend/src/Model'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { TopicViewModel } from '../../model/TopicViewModel' import { TopicViewModel } from '../../../../model/TopicViewModel'
export function useViewModelSubscriptions( export function useViewModelSubscriptions(
treeNode: q.TreeNode<TopicViewModel>, treeNode: q.TreeNode<TopicViewModel>,
@@ -11,6 +11,7 @@ export function useViewModelSubscriptions(
const selectionDidChange = () => { const selectionDidChange = () => {
const selected = treeNode.viewModel && treeNode.viewModel.isSelected() const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
treeNode.viewModel && setSelected(Boolean(selected)) treeNode.viewModel && setSelected(Boolean(selected))
if (selected && nodeRef && nodeRef.current) { if (selected && nodeRef && nodeRef.current) {
nodeRef.current.focus({ preventScroll: false }) nodeRef.current.focus({ preventScroll: false })
} }

View File

@@ -1,14 +1,16 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react' import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'
import TreeNodeSubnodes from './TreeNodeSubnodes' import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle' import TreeNodeTitle from './TreeNodeTitle'
import { SettingsState } from '../../reducers/Settings' import { SettingsState } from '../../../reducers/Settings'
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 { useViewModelSubscriptions } from './useViewModelSubscriptions' import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions'
const debounce = require('lodash.debounce') import { treeActions } from '../../../actions'
import { lightBlue, teal, amber, green, deepPurple, blueGrey } from '@material-ui/core/colors'
declare var performance: any import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate'
import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback'
import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState'
const styles = (theme: Theme) => { const styles = (theme: Theme) => {
return { return {
@@ -21,6 +23,9 @@ const styles = (theme: Theme) => {
node: { node: {
display: 'block', display: 'block',
marginLeft: '10px', marginLeft: '10px',
'&:hover': {
backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light,
},
}, },
topicSelect: { topicSelect: {
float: 'right' as 'right', float: 'right' as 'right',
@@ -32,11 +37,9 @@ const styles = (theme: Theme) => {
marginLeft: theme.spacing(1.5), marginLeft: theme.spacing(1.5),
}, },
selected: { selected: {
backgroundColor: theme.palette.type === 'dark' ? 'rgba(170, 170, 170, 0.55)' : 'rgba(170, 170, 170, 0.55)', backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important',
},
hover: {
backgroundColor: theme.palette.type === 'dark' ? 'rgba(100, 100, 100, 0.55)' : 'rgba(200, 200, 200, 0.55)',
}, },
hover: {},
title: { title: {
borderRadius: '4px', borderRadius: '4px',
lineHeight: '1em', lineHeight: '1em',
@@ -49,7 +52,7 @@ const styles = (theme: Theme) => {
} }
} }
interface Props { export interface Props {
isRoot?: boolean isRoot?: boolean
treeNode: q.TreeNode<TopicViewModel> treeNode: q.TreeNode<TopicViewModel>
name?: string | undefined name?: string | undefined
@@ -57,44 +60,26 @@ interface Props {
classes: any classes: any
className?: string className?: string
lastUpdate: number lastUpdate: number
actions: typeof treeActions
selectTopicAction: (treeNode: q.TreeNode<any>) => void selectTopicAction: (treeNode: q.TreeNode<any>) => void
theme: Theme theme: Theme
settings: SettingsState settings: SettingsState
} }
function useIsAllowedToAutoExpandState(props: Props): boolean {
const { settings, treeNode, isRoot } = props
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
useEffect(() => {
const newIsAllowedToAutoExpand = isRoot || treeNode.edgeCount() <= settings.get('autoExpandLimit')
if (newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
setAllowAutoExpand(newIsAllowedToAutoExpand)
}
}, [treeNode.edgeCount(), settings.get('autoExpandLimit')])
return isAllowedToAutoExpand
}
function TreeNodeComponent(props: Props) { function TreeNodeComponent(props: Props) {
const { classes, className, settings, theme, treeNode, lastUpdate, name } = props const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
const [showUpdateAnimation, setShowUpdateAnimation] = useState(false) const [showUpdateAnimation, setShowUpdateAnimation] = useState(false)
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined) const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
const [isHovering, setIsHovering] = useState(false)
const [selected, setSelected] = useState(false) const [selected, setSelected] = useState(false)
const nodeRef = useRef<HTMLDivElement>() const nodeRef = useRef<HTMLDivElement>()
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props) const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride) useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
useAnimationToIndicateTopicUpdate(lastUpdate, setShowUpdateAnimation, showUpdateAnimation) useAnimationToIndicateTopicUpdate(lastUpdate, selected, setShowUpdateAnimation, showUpdateAnimation)
const isCollapsed = const isCollapsed =
Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand
const setHover = debounce((hover: boolean) => {
setIsHovering(hover)
}, 45)
const toggle = useCallback(() => { const toggle = useCallback(() => {
setCollapsedOverride(!isCollapsed) setCollapsedOverride(!isCollapsed)
}, [isCollapsed]) }, [isCollapsed])
@@ -130,17 +115,11 @@ function TreeNodeComponent(props: Props) {
const mouseOver = (event: React.MouseEvent) => { const mouseOver = (event: React.MouseEvent) => {
event.stopPropagation() event.stopPropagation()
setHover(true)
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) { if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
didSelectTopic() didSelectTopic()
} }
} }
const mouseOut = (event: React.MouseEvent) => {
event.stopPropagation()
setHover(false)
}
useEffect(() => { useEffect(() => {
treeNode.viewModel && treeNode.viewModel.setExpanded(!isCollapsed, false) treeNode.viewModel && treeNode.viewModel.setExpanded(!isCollapsed, false)
}, [isCollapsed]) }, [isCollapsed])
@@ -157,6 +136,7 @@ function TreeNodeComponent(props: Props) {
lastUpdate={treeNode.lastUpdate} lastUpdate={treeNode.lastUpdate}
selectTopicAction={props.selectTopicAction} selectTopicAction={props.selectTopicAction}
settings={settings} settings={settings}
actions={props.actions}
/> />
) )
} }
@@ -167,19 +147,19 @@ function TreeNodeComponent(props: Props) {
? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` } ? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` }
: {} : {}
const highlightClass = selected ? classes.selected : isHovering ? classes.hover : '' const highlightClass = selected ? classes.selected : ''
return ( return (
<div> <div>
<div <div
key={treeNode.hash()} key={treeNode.hash()}
className={`${classes.node} ${className} ${highlightClass} ${classes.title}`} className={`${classes.node} ${className} ${highlightClass} ${classes.title}`}
style={animation} style={animation}
onMouseOver={mouseOver} onMouseEnter={mouseOver}
onMouseOut={mouseOut}
onFocus={didObtainFocus} onFocus={didObtainFocus}
onClick={didClickTitle} onClick={didClickTitle}
ref={nodeRef as any} ref={nodeRef as any}
tabIndex={-1} tabIndex={-1}
onKeyDown={deleteTopicCallback}
> >
<TreeNodeTitle <TreeNodeTitle
toggleCollapsed={toggleCollapsed} toggleCollapsed={toggleCollapsed}
@@ -192,26 +172,7 @@ function TreeNodeComponent(props: Props) {
{renderNodes()} {renderNodes()}
</div> </div>
) )
}, [lastUpdate, treeNode, name, isCollapsed, selected, showUpdateAnimation, isHovering]) }, [lastUpdate, treeNode, name, isCollapsed, selected, theme, showUpdateAnimation])
} }
export default withStyles(styles, { withTheme: true })(TreeNodeComponent) export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
function useAnimationToIndicateTopicUpdate(
lastUpdate: number,
setShowUpdateAnimation: React.Dispatch<React.SetStateAction<boolean>>,
showUpdateAnimation: boolean
) {
useEffect(() => {
if (Date.now() - lastUpdate < 3000) {
setShowUpdateAnimation(true)
}
}, [lastUpdate])
useEffect(() => {
if (showUpdateAnimation) {
const timeout = setTimeout(() => setShowUpdateAnimation(false), 500)
return function cleanup() {
clearTimeout(timeout)
}
}
}, [showUpdateAnimation])
}

View File

@@ -1,5 +1,5 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import React from 'react' import React, { useCallback } from 'react'
import TreeNode from './TreeNode' import TreeNode from './TreeNode'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
@@ -7,7 +7,6 @@ import { connect } from 'react-redux'
import { SettingsState } from '../../reducers/Settings' import { SettingsState } from '../../reducers/Settings'
import { TopicViewModel } from '../../model/TopicViewModel' import { TopicViewModel } from '../../model/TopicViewModel'
import { treeActions } from '../../actions' import { treeActions } from '../../actions'
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
import { KeyCodes } from '../../utils/KeyCodes' import { KeyCodes } from '../../utils/KeyCodes'
const MovingAverage = require('moving-average') const MovingAverage = require('moving-average')
@@ -30,16 +29,27 @@ interface State {
lastUpdate: number lastUpdate: number
} }
function ArrowKeyHandler(props: { function useArrowKeyEventHandler(actions: typeof treeActions) {
action: (direction: 'next' | 'previous') => any return (event: React.KeyboardEvent) => {
leftAction: () => void switch (event.keyCode) {
rightAction: () => void case KeyCodes.arrow_down:
}) { actions.moveSelectionUpOrDownwards('next')
useGlobalKeyEventHandler(KeyCodes.arrow_down, () => props.action('next'), [props.action]) event.preventDefault()
useGlobalKeyEventHandler(KeyCodes.arrow_up, () => props.action('previous'), [props.action]) break
useGlobalKeyEventHandler(KeyCodes.arrow_right, props.rightAction, [props.action]) case KeyCodes.arrow_up:
useGlobalKeyEventHandler(KeyCodes.arrow_left, props.leftAction, [props.action]) actions.moveSelectionUpOrDownwards('previous')
return <div /> event.preventDefault()
break
case KeyCodes.arrow_left:
actions.moveOutward()
event.preventDefault()
break
case KeyCodes.arrow_right:
actions.moveInward()
event.preventDefault()
break
}
}
} }
class TreeComponent extends React.PureComponent<Props, State> { class TreeComponent extends React.PureComponent<Props, State> {
@@ -52,6 +62,7 @@ class TreeComponent extends React.PureComponent<Props, State> {
this.state = { lastUpdate: 0 } this.state = { lastUpdate: 0 }
} }
private keyEventHandler = useArrowKeyEventHandler(this.props.actions)
private performanceCallback = (ms: number) => { private performanceCallback = (ms: number) => {
average.push(Date.now(), ms) average.push(Date.now(), ms)
} }
@@ -124,16 +135,12 @@ class TreeComponent extends React.PureComponent<Props, State> {
overflowX: 'hidden', overflowX: 'hidden',
height: '100%', height: '100%',
width: '100%', width: '100%',
outline: '24px black !important',
paddingBottom: '16px', // avoid conflict with chart panel Resizer paddingBottom: '16px', // avoid conflict with chart panel Resizer
} }
return ( return (
<div style={style}> <div style={style} tabIndex={0} onKeyDown={this.keyEventHandler}>
<ArrowKeyHandler
action={this.props.actions.moveSelectionUpOrDownwards}
leftAction={this.props.actions.moveOutward}
rightAction={this.props.actions.moveInward}
/>
<TreeNode <TreeNode
key={tree.hash()} key={tree.hash()}
isRoot={true} isRoot={true}
@@ -142,6 +149,7 @@ class TreeComponent extends React.PureComponent<Props, State> {
collapsed={false} collapsed={false}
settings={this.props.settings} settings={this.props.settings}
lastUpdate={tree.lastUpdate} lastUpdate={tree.lastUpdate}
actions={this.props.actions}
selectTopicAction={this.props.actions.selectTopic} selectTopicAction={this.props.actions.selectTopic}
/> />
</div> </div>

View File

@@ -4,8 +4,8 @@ import { EventDispatcher } from '../../../events'
export class TopicViewModel implements Destroyable { export class TopicViewModel implements Destroyable {
private selected: boolean private selected: boolean
private expanded: boolean private expanded: boolean
public selectionChange = new EventDispatcher<void, TopicViewModel>() public selectionChange = new EventDispatcher<void>()
public expandedChange = new EventDispatcher<void, TopicViewModel>() public expandedChange = new EventDispatcher<void>()
public constructor() { public constructor() {
this.selected = false this.selected = false

View File

@@ -44,7 +44,6 @@ export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> =
action action
): GlobalState => { ): GlobalState => {
trackEvent(action.type) trackEvent(action.type)
console.log(action.type)
switch (action.type) { switch (action.type) {
case ActionTypes.showUpdateNotification: case ActionTypes.showUpdateNotification:

View File

@@ -7,7 +7,7 @@ export interface DataSourceState {
} }
export class DataSourceStateMachine { export class DataSourceStateMachine {
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>() public onUpdate = new EventDispatcher<DataSourceState>()
private state: DataSourceState = { private state: DataSourceState = {
error: undefined, error: undefined,
connected: false, connected: false,

View File

@@ -12,7 +12,7 @@ export class Tree<ViewModel extends Destroyable> extends TreeNode<ViewModel> {
public isTree = true public isTree = true
private cachedHash = `${Math.random()}` private cachedHash = `${Math.random()}`
private unmergedMessages: ChangeBuffer = new ChangeBuffer() private unmergedMessages: ChangeBuffer = new ChangeBuffer()
public didReceive = new EventDispatcher<void, Tree<ViewModel>>() public didReceive = new EventDispatcher<void>()
constructor() { constructor() {
super(undefined, undefined) super(undefined, undefined)

View File

@@ -13,9 +13,10 @@ export class TreeNode<ViewModel extends Destroyable> {
public collapsed = false public collapsed = false
public messages: number = 0 public messages: number = 0
public lastUpdate: number = Date.now() public lastUpdate: number = Date.now()
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>() public onMerge = new EventDispatcher<void>()
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>() public onEdgesChange = new EventDispatcher<void>()
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>() public onMessage = new EventDispatcher<Message>()
public onDestroy = new EventDispatcher<TreeNode<ViewModel>>()
public isTree = false public isTree = false
private cachedPath?: string private cachedPath?: string
@@ -65,7 +66,11 @@ export class TreeNode<ViewModel extends Destroyable> {
if (!previous || !this.sourceEdge) { if (!previous || !this.sourceEdge) {
return return
} }
this.lastUpdate = Date.now()
previous.removeEdge(this.sourceEdge) previous.removeEdge(this.sourceEdge)
if (!this.isTree) {
this.destroy()
}
} }
private findChild(edges: Array<string>): TreeNode<ViewModel> | undefined { private findChild(edges: Array<string>): TreeNode<ViewModel> | undefined {
@@ -101,6 +106,9 @@ export class TreeNode<ViewModel extends Destroyable> {
} }
public destroy() { public destroy() {
this.onDestroy.dispatch(this)
this.onDestroy.removeAllListeners()
for (const edge of this.edgeArray) { for (const edge of this.edgeArray) {
edge.target.destroy() edge.target.destroy()
} }
@@ -158,6 +166,8 @@ export class TreeNode<ViewModel extends Destroyable> {
this.edgeArray.push(edge) this.edgeArray.push(edge)
edge.source = this edge.source = this
edge.target && edge.target.removeFromTreeIfEmpty()
if (emitUpdate) { if (emitUpdate) {
this.onEdgesChange.dispatch() this.onEdgesChange.dispatch()
} }
@@ -175,8 +185,12 @@ export class TreeNode<ViewModel extends Destroyable> {
public removeEdge(edge: Edge<any>) { public removeEdge(edge: Edge<any>) {
delete this.edges[edge.name] delete this.edges[edge.name]
this.edgeArray = Object.values(this.edges) this.edgeArray = Object.values(this.edges)
this.onMerge.dispatch()
this.removeFromTreeIfEmpty()
this.onMerge.dispatch()
}
public removeFromTreeIfEmpty() {
if (this.isTopicEmptyLeaf()) { if (this.isTopicEmptyLeaf()) {
this.removeFromParent() this.removeFromParent()
} }
@@ -189,10 +203,7 @@ export class TreeNode<ViewModel extends Destroyable> {
this.mqttMessage = node.mqttMessage this.mqttMessage = node.mqttMessage
} }
if (this.isTopicEmptyLeaf()) { this.removeFromTreeIfEmpty()
this.removeFromParent()
}
this.mergeEdges(node) this.mergeEdges(node)
this.onMerge.dispatch() this.onMerge.dispatch()
} }

View File

@@ -4,7 +4,7 @@ import 'mocha'
describe('EventDispatcher', async () => { describe('EventDispatcher', async () => {
it('should dispatch', async function() { it('should dispatch', async function() {
const dispatcher = new EventDispatcher<string, string>() const dispatcher = new EventDispatcher<string>()
this.timeout(300) this.timeout(300)
setTimeout(() => dispatcher.dispatch('hello'), 5) setTimeout(() => dispatcher.dispatch('hello'), 5)
@@ -18,7 +18,7 @@ describe('EventDispatcher', async () => {
}) })
it('should unsubscribe', async function() { it('should unsubscribe', async function() {
const dispatcher = new EventDispatcher<string, string>() const dispatcher = new EventDispatcher<string>()
this.timeout(300) this.timeout(300)
let callbackCounter = 0 let callbackCounter = 0
const callback = (msg: any) => { const callback = (msg: any) => {

View File

@@ -45,7 +45,6 @@ export class ConnectionManager {
if (buffer.length > 20000) { if (buffer.length > 20000) {
buffer = buffer.slice(0, 20000) buffer = buffer.slice(0, 20000)
} }
backendEvents.emit(messageEvent, { backendEvents.emit(messageEvent, {
topic, topic,
payload: Base64Message.fromBuffer(buffer), payload: Base64Message.fromBuffer(buffer),

View File

@@ -5,7 +5,7 @@ interface CallbackStore {
callback: any callback: any
} }
export class EventDispatcher<Message, Dispatcher> { export class EventDispatcher<Message> {
private emitter = new EventEmitter() private emitter = new EventEmitter()
private callbacks: Array<CallbackStore> = [] private callbacks: Array<CallbackStore> = []

View File

@@ -10,7 +10,7 @@ const applicationMenu: MenuItemConstructorOptions = {
click: () => { click: () => {
openAboutWindow({ openAboutWindow({
icon_path: path.join(__dirname, '..', '..', 'icon.png'), icon_path: path.join(__dirname, '..', '..', 'icon.png'),
license: 'AGPL-3.0', license: 'CC-BY-ND-4.0',
homepage: 'https://thomasnordquist.github.io/MQTT-Explorer/', homepage: 'https://thomasnordquist.github.io/MQTT-Explorer/',
bug_report_url: 'https://github.com/thomasnordquist/MQTT-Explorer/issues', bug_report_url: 'https://github.com/thomasnordquist/MQTT-Explorer/issues',
description: 'Author: Thomas Nordquist', description: 'Author: Thomas Nordquist',