diff --git a/app/index.html b/app/index.html index fc57f92..2779afa 100644 --- a/app/index.html +++ b/app/index.html @@ -22,10 +22,10 @@ background-color: none; } 25% { - background-color: #3f51b5; + background-color: #4f5781; } 50% { - background-color: #3f51b5; + background-color: #4f5781; } 100% { background-color: none; @@ -38,11 +38,11 @@ color: inherit; } 25% { - background-color: #bfc9c8; + background-color: #c0c8c0; color: #000; } 50% { - background-color: #bfc9c8; + background-color: #c0c8c0; color: #000; } 100% { diff --git a/app/src/actions/Sidebar.ts b/app/src/actions/Sidebar.ts index 4d2dbd5..18eec74 100644 --- a/app/src/actions/Sidebar.ts +++ b/app/src/actions/Sidebar.ts @@ -2,7 +2,9 @@ import * as q from '../../../backend/src/Model' import { ActionTypes } from '../reducers/Sidebar' import { AppState } from '../reducers' import { Dispatch } from 'redux' -import { makePublishEvent, rendererEvents } from '../../../events' +import { clearTopic } from './clearTopic' + +export { clearTopic } from './clearTopic' export const clearRetainedTopic = () => (dispatch: Dispatch, getState: () => AppState) => { const selectedTopic = getState().tree.get('selectedTopic') @@ -19,38 +21,3 @@ export const setCompareMessage = (message?: q.Message) => (dispatch: Dispatch, recursive: boolean, subtopicClearLimit = 50) => ( - dispatch: Dispatch, - 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) - }) - } -} diff --git a/app/src/actions/Tree.ts b/app/src/actions/Tree.ts index 0e88fbf..d6fa567 100644 --- a/app/src/actions/Tree.ts +++ b/app/src/actions/Tree.ts @@ -8,8 +8,10 @@ import { globalActions } from './' import { setTopic } from './Publish' import { TopicViewModel } from '../model/TopicViewModel' 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) => ( dispatch: Dispatch, diff --git a/app/src/actions/clearTopic.ts b/app/src/actions/clearTopic.ts new file mode 100644 index 0000000..afdcf20 --- /dev/null +++ b/app/src/actions/clearTopic.ts @@ -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, recursive: boolean, subtopicClearLimit = 50) => ( + dispatch: Dispatch, + 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) + }) + } +} diff --git a/app/src/actions/visibleTreeTraversal.ts b/app/src/actions/visibleTreeTraversal.ts index 446ec1d..233e22e 100644 --- a/app/src/actions/visibleTreeTraversal.ts +++ b/app/src/actions/visibleTreeTraversal.ts @@ -13,6 +13,7 @@ export const moveSelectionUpOrDownwards = (direction: 'next' | 'previous') => ( const state = getState() const selected = state.tree.get('selectedTopic') const tree = state.tree.get('tree') + if (!selected || !tree) { if (tree) { dispatch(selectTopic(tree)) diff --git a/app/src/components/Layout/ContentView.tsx b/app/src/components/Layout/ContentView.tsx index 5e4f5c2..bec1f83 100644 --- a/app/src/components/Layout/ContentView.tsx +++ b/app/src/components/Layout/ContentView.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import ChartPanel from '../ChartPanel' import ReactSplitPane from 'react-split-pane' -import Tree from '../Tree/Tree' +import Tree from '../Tree' import { AppState } from '../../reducers' import { ChartParameters } from '../../reducers/Charts' import { connect } from 'react-redux' diff --git a/app/src/components/Layout/SearchBar.tsx b/app/src/components/Layout/SearchBar.tsx index 3c47cb9..b9c40b8 100644 --- a/app/src/components/Layout/SearchBar.tsx +++ b/app/src/components/Layout/SearchBar.tsx @@ -41,7 +41,13 @@ function SearchBar(props: { const tagElementIsNotBlacklisted = 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 inputRef.current && inputRef.current.focus() } diff --git a/app/src/components/Tree/TreeNodeSubnodes.tsx b/app/src/components/Tree/TreeNode/TreeNodeSubnodes.tsx similarity index 82% rename from app/src/components/Tree/TreeNodeSubnodes.tsx rename to app/src/components/Tree/TreeNode/TreeNodeSubnodes.tsx index e1291ac..635351a 100644 --- a/app/src/components/Tree/TreeNodeSubnodes.tsx +++ b/app/src/components/Tree/TreeNode/TreeNodeSubnodes.tsx @@ -1,10 +1,11 @@ -import * as q from '../../../../backend/src/Model' -import * as React from 'react' -import TreeNode from './TreeNode' -import { SettingsState } from '../../reducers/Settings' -import { sortedNodes } from '../../sortedNodes' +import * as q from '../../../../../backend/src/Model' +import React from 'react' +import TreeNode from '.' +import { SettingsState } from '../../../reducers/Settings' +import { sortedNodes } from '../../../sortedNodes' import { Theme, withStyles } from '@material-ui/core' -import { TopicViewModel } from '../../model/TopicViewModel' +import { TopicViewModel } from '../../../model/TopicViewModel' +import { treeActions } from '../../../actions' export interface Props { treeNode: q.TreeNode @@ -14,6 +15,7 @@ export interface Props { selectedTopic?: q.TreeNode selectTopicAction: (treeNode: q.TreeNode) => void settings: SettingsState + actions: typeof treeActions } interface State { @@ -60,6 +62,7 @@ class TreeNodeSubnodes extends React.Component { lastUpdate={node.lastUpdate} selectTopicAction={this.props.selectTopicAction} settings={this.props.settings} + actions={this.props.actions} /> ) }) diff --git a/app/src/components/Tree/TreeNodeTitle.tsx b/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx similarity index 92% rename from app/src/components/Tree/TreeNodeTitle.tsx rename to app/src/components/Tree/TreeNode/TreeNodeTitle.tsx index 04114a1..d5bad8e 100644 --- a/app/src/components/Tree/TreeNodeTitle.tsx +++ b/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx @@ -1,8 +1,8 @@ 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 { TopicViewModel } from '../../model/TopicViewModel' -import { Base64Message } from '../../../../backend/src/Model/Base64Message' +import { TopicViewModel } from '../../../model/TopicViewModel' +import { Base64Message } from '../../../../../backend/src/Model/Base64Message' export interface TreeNodeProps extends React.HTMLAttributes { treeNode: q.TreeNode diff --git a/app/src/components/Tree/TreeNode/effects/useAnimationToIndicateTopicUpdate.tsx b/app/src/components/Tree/TreeNode/effects/useAnimationToIndicateTopicUpdate.tsx new file mode 100644 index 0000000..d23a505 --- /dev/null +++ b/app/src/components/Tree/TreeNode/effects/useAnimationToIndicateTopicUpdate.tsx @@ -0,0 +1,22 @@ +import React, { useEffect } from 'react' + +export function useAnimationToIndicateTopicUpdate( + lastUpdate: number, + selected: boolean, + setShowUpdateAnimation: React.Dispatch>, + 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]) +} diff --git a/app/src/components/Tree/TreeNode/effects/useDeleteKeyCallback.tsx b/app/src/components/Tree/TreeNode/effects/useDeleteKeyCallback.tsx new file mode 100644 index 0000000..6eefe0d --- /dev/null +++ b/app/src/components/Tree/TreeNode/effects/useDeleteKeyCallback.tsx @@ -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, 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] + ) +} diff --git a/app/src/components/Tree/TreeNode/effects/useIsAllowedToAutoExpandState.tsx b/app/src/components/Tree/TreeNode/effects/useIsAllowedToAutoExpandState.tsx new file mode 100644 index 0000000..5556ae2 --- /dev/null +++ b/app/src/components/Tree/TreeNode/effects/useIsAllowedToAutoExpandState.tsx @@ -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 +} diff --git a/app/src/components/Tree/useViewModelSubscriptions.tsx b/app/src/components/Tree/TreeNode/effects/useViewModelSubscriptions.tsx similarity index 91% rename from app/src/components/Tree/useViewModelSubscriptions.tsx rename to app/src/components/Tree/TreeNode/effects/useViewModelSubscriptions.tsx index c231d4e..2dc94cb 100644 --- a/app/src/components/Tree/useViewModelSubscriptions.tsx +++ b/app/src/components/Tree/TreeNode/effects/useViewModelSubscriptions.tsx @@ -1,6 +1,6 @@ -import * as q from '../../../../backend/src/Model' +import * as q from '../../../../../../backend/src/Model' import React, { useEffect } from 'react' -import { TopicViewModel } from '../../model/TopicViewModel' +import { TopicViewModel } from '../../../../model/TopicViewModel' export function useViewModelSubscriptions( treeNode: q.TreeNode, @@ -11,6 +11,7 @@ export function useViewModelSubscriptions( const selectionDidChange = () => { const selected = treeNode.viewModel && treeNode.viewModel.isSelected() treeNode.viewModel && setSelected(Boolean(selected)) + if (selected && nodeRef && nodeRef.current) { nodeRef.current.focus({ preventScroll: false }) } diff --git a/app/src/components/Tree/TreeNode.tsx b/app/src/components/Tree/TreeNode/index.tsx similarity index 64% rename from app/src/components/Tree/TreeNode.tsx rename to app/src/components/Tree/TreeNode/index.tsx index 6d8e56c..51903f5 100644 --- a/app/src/components/Tree/TreeNode.tsx +++ b/app/src/components/Tree/TreeNode/index.tsx @@ -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 TreeNodeSubnodes from './TreeNodeSubnodes' import TreeNodeTitle from './TreeNodeTitle' -import { SettingsState } from '../../reducers/Settings' +import { SettingsState } from '../../../reducers/Settings' import { Theme, withStyles } from '@material-ui/core/styles' -import { TopicViewModel } from '../../model/TopicViewModel' -import { useViewModelSubscriptions } from './useViewModelSubscriptions' -const debounce = require('lodash.debounce') - -declare var performance: any +import { TopicViewModel } from '../../../model/TopicViewModel' +import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions' +import { treeActions } from '../../../actions' +import { lightBlue, teal, amber, green, deepPurple, blueGrey } from '@material-ui/core/colors' +import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate' +import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback' +import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState' const styles = (theme: Theme) => { return { @@ -21,6 +23,9 @@ const styles = (theme: Theme) => { node: { display: 'block', marginLeft: '10px', + '&:hover': { + backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light, + }, }, topicSelect: { float: 'right' as 'right', @@ -32,11 +37,9 @@ const styles = (theme: Theme) => { marginLeft: theme.spacing(1.5), }, selected: { - backgroundColor: theme.palette.type === 'dark' ? 'rgba(170, 170, 170, 0.55)' : 'rgba(170, 170, 170, 0.55)', - }, - hover: { - backgroundColor: theme.palette.type === 'dark' ? 'rgba(100, 100, 100, 0.55)' : 'rgba(200, 200, 200, 0.55)', + backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important', }, + hover: {}, title: { borderRadius: '4px', lineHeight: '1em', @@ -49,7 +52,7 @@ const styles = (theme: Theme) => { } } -interface Props { +export interface Props { isRoot?: boolean treeNode: q.TreeNode name?: string | undefined @@ -57,44 +60,26 @@ interface Props { classes: any className?: string lastUpdate: number + actions: typeof treeActions selectTopicAction: (treeNode: q.TreeNode) => void theme: Theme 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) { - 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 [collapsedOverride, setCollapsedOverride] = useState(undefined) - const [isHovering, setIsHovering] = useState(false) const [selected, setSelected] = useState(false) const nodeRef = useRef() const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props) useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride) - useAnimationToIndicateTopicUpdate(lastUpdate, setShowUpdateAnimation, showUpdateAnimation) + useAnimationToIndicateTopicUpdate(lastUpdate, selected, setShowUpdateAnimation, showUpdateAnimation) const isCollapsed = Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand - const setHover = debounce((hover: boolean) => { - setIsHovering(hover) - }, 45) - const toggle = useCallback(() => { setCollapsedOverride(!isCollapsed) }, [isCollapsed]) @@ -130,17 +115,11 @@ function TreeNodeComponent(props: Props) { const mouseOver = (event: React.MouseEvent) => { event.stopPropagation() - setHover(true) if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) { didSelectTopic() } } - const mouseOut = (event: React.MouseEvent) => { - event.stopPropagation() - setHover(false) - } - useEffect(() => { treeNode.viewModel && treeNode.viewModel.setExpanded(!isCollapsed, false) }, [isCollapsed]) @@ -157,6 +136,7 @@ function TreeNodeComponent(props: Props) { lastUpdate={treeNode.lastUpdate} selectTopicAction={props.selectTopicAction} settings={settings} + actions={props.actions} /> ) } @@ -167,19 +147,19 @@ function TreeNodeComponent(props: Props) { ? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` } : {} - const highlightClass = selected ? classes.selected : isHovering ? classes.hover : '' + const highlightClass = selected ? classes.selected : '' return (
) - }, [lastUpdate, treeNode, name, isCollapsed, selected, showUpdateAnimation, isHovering]) + }, [lastUpdate, treeNode, name, isCollapsed, selected, theme, showUpdateAnimation]) } export default withStyles(styles, { withTheme: true })(TreeNodeComponent) -function useAnimationToIndicateTopicUpdate( - lastUpdate: number, - setShowUpdateAnimation: React.Dispatch>, - 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]) -} diff --git a/app/src/components/Tree/Tree.tsx b/app/src/components/Tree/index.tsx similarity index 81% rename from app/src/components/Tree/Tree.tsx rename to app/src/components/Tree/index.tsx index 28a1846..a40029f 100644 --- a/app/src/components/Tree/Tree.tsx +++ b/app/src/components/Tree/index.tsx @@ -1,5 +1,5 @@ import * as q from '../../../../backend/src/Model' -import React from 'react' +import React, { useCallback } from 'react' import TreeNode from './TreeNode' import { AppState } from '../../reducers' import { bindActionCreators } from 'redux' @@ -7,7 +7,6 @@ import { connect } from 'react-redux' import { SettingsState } from '../../reducers/Settings' import { TopicViewModel } from '../../model/TopicViewModel' import { treeActions } from '../../actions' -import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler' import { KeyCodes } from '../../utils/KeyCodes' const MovingAverage = require('moving-average') @@ -30,16 +29,27 @@ interface State { lastUpdate: number } -function ArrowKeyHandler(props: { - action: (direction: 'next' | 'previous') => any - leftAction: () => void - rightAction: () => void -}) { - useGlobalKeyEventHandler(KeyCodes.arrow_down, () => props.action('next'), [props.action]) - useGlobalKeyEventHandler(KeyCodes.arrow_up, () => props.action('previous'), [props.action]) - useGlobalKeyEventHandler(KeyCodes.arrow_right, props.rightAction, [props.action]) - useGlobalKeyEventHandler(KeyCodes.arrow_left, props.leftAction, [props.action]) - return
+function useArrowKeyEventHandler(actions: typeof treeActions) { + return (event: React.KeyboardEvent) => { + switch (event.keyCode) { + case KeyCodes.arrow_down: + actions.moveSelectionUpOrDownwards('next') + event.preventDefault() + break + case KeyCodes.arrow_up: + actions.moveSelectionUpOrDownwards('previous') + 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 { @@ -52,6 +62,7 @@ class TreeComponent extends React.PureComponent { this.state = { lastUpdate: 0 } } + private keyEventHandler = useArrowKeyEventHandler(this.props.actions) private performanceCallback = (ms: number) => { average.push(Date.now(), ms) } @@ -124,16 +135,12 @@ class TreeComponent extends React.PureComponent { overflowX: 'hidden', height: '100%', width: '100%', + outline: '24px black !important', paddingBottom: '16px', // avoid conflict with chart panel Resizer } return ( -
- +
{ collapsed={false} settings={this.props.settings} lastUpdate={tree.lastUpdate} + actions={this.props.actions} selectTopicAction={this.props.actions.selectTopic} />
diff --git a/app/src/model/TopicViewModel.ts b/app/src/model/TopicViewModel.ts index f3a8da5..3c7ef54 100644 --- a/app/src/model/TopicViewModel.ts +++ b/app/src/model/TopicViewModel.ts @@ -4,8 +4,8 @@ import { EventDispatcher } from '../../../events' export class TopicViewModel implements Destroyable { private selected: boolean private expanded: boolean - public selectionChange = new EventDispatcher() - public expandedChange = new EventDispatcher() + public selectionChange = new EventDispatcher() + public expandedChange = new EventDispatcher() public constructor() { this.selected = false diff --git a/app/src/reducers/Global.ts b/app/src/reducers/Global.ts index 3c2ef6e..c61808b 100644 --- a/app/src/reducers/Global.ts +++ b/app/src/reducers/Global.ts @@ -44,7 +44,6 @@ export const globalState: Reducer, GlobalAction> = action ): GlobalState => { trackEvent(action.type) - console.log(action.type) switch (action.type) { case ActionTypes.showUpdateNotification: diff --git a/backend/src/DataSource/DataSourceState.ts b/backend/src/DataSource/DataSourceState.ts index abffb36..d69bfd6 100644 --- a/backend/src/DataSource/DataSourceState.ts +++ b/backend/src/DataSource/DataSourceState.ts @@ -7,7 +7,7 @@ export interface DataSourceState { } export class DataSourceStateMachine { - public onUpdate = new EventDispatcher() + public onUpdate = new EventDispatcher() private state: DataSourceState = { error: undefined, connected: false, diff --git a/backend/src/Model/Tree.ts b/backend/src/Model/Tree.ts index 09232fc..ca8514f 100644 --- a/backend/src/Model/Tree.ts +++ b/backend/src/Model/Tree.ts @@ -12,7 +12,7 @@ export class Tree extends TreeNode { public isTree = true private cachedHash = `${Math.random()}` private unmergedMessages: ChangeBuffer = new ChangeBuffer() - public didReceive = new EventDispatcher>() + public didReceive = new EventDispatcher() constructor() { super(undefined, undefined) diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index 458cc36..c2b6c11 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -13,9 +13,10 @@ export class TreeNode { public collapsed = false public messages: number = 0 public lastUpdate: number = Date.now() - public onMerge = new EventDispatcher>() - public onEdgesChange = new EventDispatcher>() - public onMessage = new EventDispatcher>() + public onMerge = new EventDispatcher() + public onEdgesChange = new EventDispatcher() + public onMessage = new EventDispatcher() + public onDestroy = new EventDispatcher>() public isTree = false private cachedPath?: string @@ -65,7 +66,11 @@ export class TreeNode { if (!previous || !this.sourceEdge) { return } + this.lastUpdate = Date.now() previous.removeEdge(this.sourceEdge) + if (!this.isTree) { + this.destroy() + } } private findChild(edges: Array): TreeNode | undefined { @@ -101,6 +106,9 @@ export class TreeNode { } public destroy() { + this.onDestroy.dispatch(this) + this.onDestroy.removeAllListeners() + for (const edge of this.edgeArray) { edge.target.destroy() } @@ -158,6 +166,8 @@ export class TreeNode { this.edgeArray.push(edge) edge.source = this + edge.target && edge.target.removeFromTreeIfEmpty() + if (emitUpdate) { this.onEdgesChange.dispatch() } @@ -175,8 +185,12 @@ export class TreeNode { public removeEdge(edge: Edge) { delete this.edges[edge.name] this.edgeArray = Object.values(this.edges) - this.onMerge.dispatch() + this.removeFromTreeIfEmpty() + this.onMerge.dispatch() + } + + public removeFromTreeIfEmpty() { if (this.isTopicEmptyLeaf()) { this.removeFromParent() } @@ -189,10 +203,7 @@ export class TreeNode { this.mqttMessage = node.mqttMessage } - if (this.isTopicEmptyLeaf()) { - this.removeFromParent() - } - + this.removeFromTreeIfEmpty() this.mergeEdges(node) this.onMerge.dispatch() } diff --git a/backend/src/Model/spec/EventDispatcher.spec.ts b/backend/src/Model/spec/EventDispatcher.spec.ts index 153e774..1276bae 100644 --- a/backend/src/Model/spec/EventDispatcher.spec.ts +++ b/backend/src/Model/spec/EventDispatcher.spec.ts @@ -4,7 +4,7 @@ import 'mocha' describe('EventDispatcher', async () => { it('should dispatch', async function() { - const dispatcher = new EventDispatcher() + const dispatcher = new EventDispatcher() this.timeout(300) setTimeout(() => dispatcher.dispatch('hello'), 5) @@ -18,7 +18,7 @@ describe('EventDispatcher', async () => { }) it('should unsubscribe', async function() { - const dispatcher = new EventDispatcher() + const dispatcher = new EventDispatcher() this.timeout(300) let callbackCounter = 0 const callback = (msg: any) => { diff --git a/backend/src/index.ts b/backend/src/index.ts index 0f73129..48a0b2c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -45,7 +45,6 @@ export class ConnectionManager { if (buffer.length > 20000) { buffer = buffer.slice(0, 20000) } - backendEvents.emit(messageEvent, { topic, payload: Base64Message.fromBuffer(buffer), diff --git a/events/EventDispatcher.ts b/events/EventDispatcher.ts index 2cabbf3..a3152b0 100644 --- a/events/EventDispatcher.ts +++ b/events/EventDispatcher.ts @@ -5,7 +5,7 @@ interface CallbackStore { callback: any } -export class EventDispatcher { +export class EventDispatcher { private emitter = new EventEmitter() private callbacks: Array = [] diff --git a/src/MenuTemplate.ts b/src/MenuTemplate.ts index 38ddb4b..5faa41f 100644 --- a/src/MenuTemplate.ts +++ b/src/MenuTemplate.ts @@ -10,7 +10,7 @@ const applicationMenu: MenuItemConstructorOptions = { click: () => { openAboutWindow({ icon_path: path.join(__dirname, '..', '..', 'icon.png'), - license: 'AGPL-3.0', + license: 'CC-BY-ND-4.0', homepage: 'https://thomasnordquist.github.io/MQTT-Explorer/', bug_report_url: 'https://github.com/thomasnordquist/MQTT-Explorer/issues', description: 'Author: Thomas Nordquist',