Refactor
This commit is contained in:
@@ -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% {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
41
app/src/actions/clearTopic.ts
Normal file
41
app/src/actions/clearTopic.ts
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -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>
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
@@ -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])
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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> = []
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user