Make topics selectable
This commit is contained in:
@@ -22,7 +22,7 @@ interface Props {
|
|||||||
settingsVisible: boolean
|
settingsVisible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends React.Component<Props, {}> {
|
class App extends React.PureComponent<Props, {}> {
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = { }
|
this.state = { }
|
||||||
|
|||||||
19
app/src/TopicViewModel.ts
Normal file
19
app/src/TopicViewModel.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { EventDispatcher } from '../../events'
|
||||||
|
|
||||||
|
export class TopicViewModel {
|
||||||
|
private selected: boolean
|
||||||
|
public change = new EventDispatcher<void, TopicViewModel>(this)
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.selected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSelected() {
|
||||||
|
return this.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSelected(selected: boolean) {
|
||||||
|
this.selected = selected
|
||||||
|
this.change.dispatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { AppState } from '../reducers'
|
|||||||
import * as q from '../../../backend/src/Model'
|
import * as q from '../../../backend/src/Model'
|
||||||
import { showTree } from './Tree'
|
import { showTree } from './Tree'
|
||||||
import * as url from 'url'
|
import * as url from 'url'
|
||||||
|
import { TopicViewModel } from '../TopicViewModel'
|
||||||
|
|
||||||
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||||
dispatch(connecting(connectionId))
|
dispatch(connecting(connectionId))
|
||||||
@@ -15,7 +16,7 @@ export const connect = (options: MqttOptions, connectionId: string) => (dispatch
|
|||||||
|
|
||||||
rendererEvents.subscribe(event, (dataSourceState) => {
|
rendererEvents.subscribe(event, (dataSourceState) => {
|
||||||
if (dataSourceState.connected) {
|
if (dataSourceState.connected) {
|
||||||
const tree = new q.Tree()
|
const tree = new q.Tree<TopicViewModel>()
|
||||||
tree.updateWithConnection(rendererEvents, connectionId)
|
tree.updateWithConnection(rendererEvents, connectionId)
|
||||||
dispatch(connected(tree, host!))
|
dispatch(connected(tree, host!))
|
||||||
dispatch(showTree(tree))
|
dispatch(showTree(tree))
|
||||||
@@ -26,7 +27,7 @@ export const connect = (options: MqttOptions, connectionId: string) => (dispatch
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const connected: (tree: q.Tree, host: string) => Action = (tree: q.Tree, host: string) => ({
|
export const connected: (tree: q.Tree<TopicViewModel>, host: string) => Action = (tree: q.Tree<TopicViewModel>, host: string) => ({
|
||||||
tree,
|
tree,
|
||||||
host,
|
host,
|
||||||
type: ActionTypes.CONNECTION_SET_CONNECTED,
|
type: ActionTypes.CONNECTION_SET_CONNECTED,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { AppState } from '../reducers'
|
|||||||
import * as q from '../../../backend/src/Model'
|
import * as q from '../../../backend/src/Model'
|
||||||
import { batchActions } from 'redux-batched-actions'
|
import { batchActions } from 'redux-batched-actions'
|
||||||
import { autoExpandLimitSet } from '../components/Settings'
|
import { autoExpandLimitSet } from '../components/Settings'
|
||||||
|
import { TopicViewModel } from '../TopicViewModel'
|
||||||
|
|
||||||
export const setAutoExpandLimit = (autoExpandLimit: number = 0): Action => {
|
export const setAutoExpandLimit = (autoExpandLimit: number = 0): Action => {
|
||||||
return {
|
return {
|
||||||
@@ -42,7 +43,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
|
|||||||
|
|
||||||
const topicFilter = filterStr.toLowerCase()
|
const topicFilter = filterStr.toLowerCase()
|
||||||
|
|
||||||
const nodeFilter = (node: q.TreeNode): boolean => {
|
const nodeFilter = (node: q.TreeNode<TopicViewModel>): boolean => {
|
||||||
const topicMatches = node.path().toLowerCase().indexOf(topicFilter) !== -1
|
const topicMatches = node.path().toLowerCase().indexOf(topicFilter) !== -1
|
||||||
if (topicMatches) {
|
if (topicMatches) {
|
||||||
return true
|
return true
|
||||||
@@ -54,17 +55,17 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
|
|||||||
|
|
||||||
const resultTree = tree.childTopics()
|
const resultTree = tree.childTopics()
|
||||||
.filter(nodeFilter)
|
.filter(nodeFilter)
|
||||||
.map((node) => {
|
.map((node: q.TreeNode<TopicViewModel>) => {
|
||||||
const clone = node.unconnectedClone()
|
const clone = node.unconnectedClone()
|
||||||
q.TreeNodeFactory.insertNodeAtPosition(node.path().split('/'), clone)
|
q.TreeNodeFactory.insertNodeAtPosition(node.path().split('/'), clone)
|
||||||
return clone.firstNode()
|
return clone.firstNode()
|
||||||
})
|
})
|
||||||
.reduce((a: q.TreeNode, b: q.TreeNode) => {
|
.reduce((a: q.TreeNode<TopicViewModel>, b: q.TreeNode<TopicViewModel>) => {
|
||||||
a.updateWithNode(b)
|
a.updateWithNode(b)
|
||||||
return a
|
return a
|
||||||
}, new q.Tree())
|
}, new q.Tree<TopicViewModel>())
|
||||||
|
|
||||||
const nextTree: q.Tree = resultTree as q.Tree
|
const nextTree: q.Tree<TopicViewModel> = resultTree as q.Tree<TopicViewModel>
|
||||||
if (tree.updateSource && tree.connectionId) {
|
if (tree.updateSource && tree.connectionId) {
|
||||||
nextTree.updateWithConnection(tree.updateSource, tree.connectionId, nodeFilter)
|
nextTree.updateWithConnection(tree.updateSource, tree.connectionId, nodeFilter)
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
|
|||||||
dispatch(batchActions([setAutoExpandLimit(autoExpandLimitForTree(nextTree)), (showTree(nextTree) as any)]))
|
dispatch(batchActions([setAutoExpandLimit(autoExpandLimitForTree(nextTree)), (showTree(nextTree) as any)]))
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoExpandLimitForTree(tree: q.Tree) {
|
function autoExpandLimitForTree(tree: q.Tree<TopicViewModel>) {
|
||||||
if (!tree) {
|
if (!tree) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,42 @@ import { ActionTypes } from '../reducers/Tree'
|
|||||||
import * as q from '../../../backend/src/Model'
|
import * as q from '../../../backend/src/Model'
|
||||||
import { Dispatch, AnyAction } from 'redux'
|
import { Dispatch, AnyAction } from 'redux'
|
||||||
import { setTopic } from './Publish'
|
import { setTopic } from './Publish'
|
||||||
|
import { TopicViewModel } from '../TopicViewModel'
|
||||||
|
import { batchActions } from 'redux-batched-actions'
|
||||||
|
|
||||||
export const selectTopic = (topic: q.TreeNode) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
|
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||||
const { selectedTopic } = getState().tree
|
const { selectedTopic } = getState().tree
|
||||||
|
if (selectedTopic === topic) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Update publish topic
|
// Update publish topic
|
||||||
|
let setTopicDispatch: any | undefined
|
||||||
if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) {
|
if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) {
|
||||||
dispatch(setTopic(topic.path()))
|
setTopicDispatch = setTopic(topic.path())
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatch({
|
if (selectedTopic && selectedTopic.viewModel) {
|
||||||
|
selectedTopic.viewModel.setSelected(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.viewModel) {
|
||||||
|
topic.viewModel.setSelected(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectTreeTopicDispatch = {
|
||||||
selectedTopic: topic,
|
selectedTopic: topic,
|
||||||
type: ActionTypes.TREE_SELECT_TOPIC,
|
type: ActionTypes.TREE_SELECT_TOPIC,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showTree = (tree?: q.Tree) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
|
if (setTopicDispatch) {
|
||||||
|
dispatch(batchActions([selectTreeTopicDispatch, setTopicDispatch]))
|
||||||
|
} else {
|
||||||
|
dispatch(selectTreeTopicDispatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showTree = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
|
||||||
const visibleTree = getState().tree.tree
|
const visibleTree = getState().tree.tree
|
||||||
const connectionTree = getState().connection.tree
|
const connectionTree = getState().connection.tree
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Typography } from '@material-ui/core'
|
|||||||
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
|
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
|
||||||
|
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { TopicViewModel } from '../TopicViewModel'
|
||||||
const abbreviate = require('number-abbreviate')
|
const abbreviate = require('number-abbreviate')
|
||||||
|
|
||||||
interface Stats {
|
interface Stats {
|
||||||
@@ -30,7 +31,7 @@ const styles: StyleRulesCallback = theme => ({
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
classes: any
|
classes: any
|
||||||
tree?: q.Tree
|
tree?: q.Tree<TopicViewModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrokerStatistics extends React.Component<Props, {}> {
|
class BrokerStatistics extends React.Component<Props, {}> {
|
||||||
@@ -95,7 +96,7 @@ class BrokerStatistics extends React.Component<Props, {}> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPair(tree: q.Tree, a: Stats, b: Stats) {
|
private renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
|
||||||
return (
|
return (
|
||||||
<div className={this.props.classes.flex}>
|
<div className={this.props.classes.flex}>
|
||||||
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
|
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
|
||||||
@@ -104,7 +105,7 @@ class BrokerStatistics extends React.Component<Props, {}> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderStat(tree: q.Tree, stat: Stats) {
|
public renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
|
||||||
const node = tree.findNode(stat.topic)
|
const node = tree.findNode(stat.topic)
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import * as q from '../../../../backend/src/Model'
|
|||||||
import BarChart from '@material-ui/icons/BarChart'
|
import BarChart from '@material-ui/icons/BarChart'
|
||||||
import DateFormatter from '../helper/DateFormatter'
|
import DateFormatter from '../helper/DateFormatter'
|
||||||
import History from './History'
|
import History from './History'
|
||||||
|
import { TopicViewModel } from '../../TopicViewModel'
|
||||||
const PlotHistory = React.lazy(() => import('./PlotHistory'))
|
const PlotHistory = React.lazy(() => import('./PlotHistory'))
|
||||||
|
|
||||||
const throttle = require('lodash.throttle')
|
const throttle = require('lodash.throttle')
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node?: q.TreeNode
|
node?: q.TreeNode<TopicViewModel>
|
||||||
onSelect: (message: q.Message) => void
|
onSelect: (message: q.Message) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import * as React from 'react'
|
|||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../../backend/src/Model'
|
||||||
|
|
||||||
import { Typography } from '@material-ui/core'
|
import { Typography } from '@material-ui/core'
|
||||||
|
import { TopicViewModel } from '../../TopicViewModel'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: q.TreeNode
|
node: q.TreeNode<TopicViewModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeStats extends React.Component<Props, {}> {
|
class NodeStats extends React.Component<Props, {}> {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
TextField,
|
TextField,
|
||||||
IconButton,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Input,
|
Input,
|
||||||
@@ -32,10 +31,11 @@ import Clear from '@material-ui/icons/Clear'
|
|||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { publishActions } from '../../../actions'
|
import { publishActions } from '../../../actions'
|
||||||
import ClearAdornment from '../../helper/ClearAdornment';
|
import ClearAdornment from '../../helper/ClearAdornment'
|
||||||
|
import { TopicViewModel } from '../../../TopicViewModel'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node?: q.TreeNode
|
node?: q.TreeNode<TopicViewModel>
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
topic?: string
|
topic?: string
|
||||||
payload?: string
|
payload?: string
|
||||||
|
|||||||
@@ -28,18 +28,19 @@ import Topic from './Topic'
|
|||||||
const ValueRenderer = React.lazy(() => import('./ValueRenderer'))
|
const ValueRenderer = React.lazy(() => import('./ValueRenderer'))
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { TopicViewModel } from '../../TopicViewModel'
|
||||||
|
|
||||||
const throttle = require('lodash.throttle')
|
const throttle = require('lodash.throttle')
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node?: q.TreeNode,
|
node?: q.TreeNode<TopicViewModel>,
|
||||||
actions: typeof sidebarActons,
|
actions: typeof sidebarActons,
|
||||||
classes: any,
|
classes: any,
|
||||||
connectionId?: string,
|
connectionId?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
node: q.TreeNode,
|
node: q.TreeNode<TopicViewModel>
|
||||||
compareMessage?: q.Message
|
compareMessage?: q.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,12 +70,12 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
this.props.node && this.removeUpdateListener(this.props.node)
|
this.props.node && this.removeUpdateListener(this.props.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerUpdateListener(node: q.TreeNode) {
|
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
|
||||||
node.onMerge.subscribe(this.updateNode)
|
node.onMerge.subscribe(this.updateNode)
|
||||||
node.onMessage.subscribe(this.updateNode)
|
node.onMessage.subscribe(this.updateNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeUpdateListener(node: q.TreeNode) {
|
private removeUpdateListener(node: q.TreeNode<TopicViewModel>) {
|
||||||
node.onMerge.unsubscribe(this.updateNode)
|
node.onMerge.unsubscribe(this.updateNode)
|
||||||
node.onMessage.unsubscribe(this.updateNode)
|
node.onMessage.unsubscribe(this.updateNode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
|
|||||||
import { treeActions } from '../../actions'
|
import { treeActions } from '../../actions'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { TopicViewModel } from '../../TopicViewModel'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
classes: any
|
classes: any
|
||||||
theme: Theme
|
theme: Theme
|
||||||
node?: q.TreeNode
|
node?: q.TreeNode<TopicViewModel>
|
||||||
selected?: q.TreeNode
|
selected?: q.TreeNode<TopicViewModel>
|
||||||
actions: typeof treeActions
|
actions: typeof treeActions
|
||||||
didSelectNode: (node: q.TreeNode) => void
|
didSelectNode: (node: q.TreeNode<TopicViewModel>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles: StyleRulesCallback<string> = (theme: Theme) => ({
|
const styles: StyleRulesCallback<string> = (theme: Theme) => ({
|
||||||
|
|||||||
@@ -4,30 +4,38 @@ import * as q from '../../../../backend/src/Model'
|
|||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
import TreeNode from './TreeNode'
|
import TreeNode from './TreeNode'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { TopicOrder } from '../../reducers/Settings'
|
||||||
|
import { TopicViewModel } from '../../TopicViewModel'
|
||||||
|
|
||||||
const MovingAverage = require('moving-average')
|
const MovingAverage = require('moving-average')
|
||||||
|
|
||||||
const timeInterval = 10 * 1000
|
const averagingTimeInterval = 10 * 1000
|
||||||
const average = MovingAverage(timeInterval)
|
const average = MovingAverage(averagingTimeInterval)
|
||||||
|
|
||||||
declare var window: any
|
declare var window: any
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
autoExpandLimit: number
|
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
tree?: q.Tree
|
tree?: q.Tree<TopicViewModel>
|
||||||
filter: string
|
filter: string
|
||||||
host?: string
|
host?: string
|
||||||
|
|
||||||
|
topicOrder: TopicOrder
|
||||||
|
autoExpandLimit: number
|
||||||
}
|
}
|
||||||
|
|
||||||
class Tree extends React.Component<Props, {}> {
|
interface State {
|
||||||
|
lastUpdate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tree extends React.PureComponent<Props, State> {
|
||||||
private updateTimer?: any
|
private updateTimer?: any
|
||||||
private lastUpdate: number = 0
|
|
||||||
private perf: number = 0
|
private perf: number = 0
|
||||||
|
private renderTime = 0
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = { }
|
this.state = { lastUpdate: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
public time(): number {
|
public time(): number {
|
||||||
@@ -40,17 +48,17 @@ class Tree extends React.Component<Props, {}> {
|
|||||||
public componentWillReceiveProps(nextProps: Props) {
|
public componentWillReceiveProps(nextProps: Props) {
|
||||||
if (this.props.tree !== nextProps.tree) {
|
if (this.props.tree !== nextProps.tree) {
|
||||||
if (this.props.tree) {
|
if (this.props.tree) {
|
||||||
this.props.tree.onMerge.unsubscribe(this.throttledTreeUpdate)
|
this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
|
||||||
}
|
}
|
||||||
if (nextProps.tree) {
|
if (nextProps.tree) {
|
||||||
nextProps.tree.onMerge.subscribe(this.throttledTreeUpdate)
|
nextProps.tree.didReceive.subscribe(this.throttledTreeUpdate)
|
||||||
}
|
}
|
||||||
this.setState(this.state)
|
this.setState(this.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.props.tree && this.props.tree.onMerge.unsubscribe(this.throttledTreeUpdate)
|
this.props.tree && this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
public throttledTreeUpdate = () => {
|
public throttledTreeUpdate = () => {
|
||||||
@@ -60,14 +68,17 @@ class Tree extends React.Component<Props, {}> {
|
|||||||
|
|
||||||
const expectedRenderTime = average.forecast()
|
const expectedRenderTime = average.forecast()
|
||||||
const updateInterval = Math.max(expectedRenderTime * 7, 300)
|
const updateInterval = Math.max(expectedRenderTime * 7, 300)
|
||||||
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
|
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
|
||||||
|
|
||||||
this.updateTimer = setTimeout(() => {
|
this.updateTimer = setTimeout(() => {
|
||||||
window.requestIdleCallback(() => {
|
window.requestIdleCallback(() => {
|
||||||
this.lastUpdate = performance.now()
|
|
||||||
this.updateTimer && clearTimeout(this.updateTimer)
|
this.updateTimer && clearTimeout(this.updateTimer)
|
||||||
this.updateTimer = undefined
|
this.updateTimer = undefined
|
||||||
this.setState(this.state)
|
this.renderTime = performance.now()
|
||||||
|
this.props.tree && this.props.tree.applyUnmergedChanges()
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
this.setState({ lastUpdate: this.renderTime })
|
||||||
|
}, { timeout: 100 })
|
||||||
}, { timeout: 500 })
|
}, { timeout: 500 })
|
||||||
}, Math.max(0, timeUntilNextUpdate))
|
}, Math.max(0, timeUntilNextUpdate))
|
||||||
}
|
}
|
||||||
@@ -91,9 +102,11 @@ class Tree extends React.Component<Props, {}> {
|
|||||||
isRoot={true}
|
isRoot={true}
|
||||||
treeNode={tree}
|
treeNode={tree}
|
||||||
name={this.props.host}
|
name={this.props.host}
|
||||||
lastUpdate={tree.lastUpdate}
|
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
performanceCallback={this.performanceCallback}
|
performanceCallback={this.performanceCallback}
|
||||||
|
autoExpandLimit={this.props.autoExpandLimit}
|
||||||
|
topicOrder={this.props.topicOrder}
|
||||||
|
lastUpdate={tree.lastUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -106,10 +119,11 @@ class Tree extends React.Component<Props, {}> {
|
|||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
return {
|
return {
|
||||||
autoExpandLimit: state.settings.autoExpandLimit,
|
|
||||||
tree: state.tree.tree,
|
tree: state.tree.tree,
|
||||||
filter: state.tree.filter,
|
filter: state.tree.filter,
|
||||||
host: state.connection.host,
|
host: state.connection.host,
|
||||||
|
autoExpandLimit: state.settings.autoExpandLimit,
|
||||||
|
topicOrder: state.settings.topicOrder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import * as q from '../../../../backend/src/Model'
|
|||||||
|
|
||||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||||
|
|
||||||
import LabelImportant from '@material-ui/icons/LabelImportant'
|
|
||||||
import TreeNodeSubnodes from './TreeNodeSubnodes'
|
import TreeNodeSubnodes from './TreeNodeSubnodes'
|
||||||
import TreeNodeTitle from './TreeNodeTitle'
|
import TreeNodeTitle from './TreeNodeTitle'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
@@ -11,6 +10,9 @@ import { connect } from 'react-redux'
|
|||||||
import { isElementInViewport } from '../helper/isElementInViewport'
|
import { isElementInViewport } from '../helper/isElementInViewport'
|
||||||
import { treeActions } from '../../actions'
|
import { treeActions } from '../../actions'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
|
import { TopicOrder } from '../../reducers/Settings'
|
||||||
|
import { TopicViewModel } from '../../TopicViewModel'
|
||||||
|
const debounce = require('lodash.debounce')
|
||||||
|
|
||||||
declare var performance: any
|
declare var performance: any
|
||||||
|
|
||||||
@@ -26,36 +28,40 @@ const styles = (theme: Theme) => {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
marginLeft: '10px',
|
marginLeft: '10px',
|
||||||
},
|
},
|
||||||
// hover: {
|
|
||||||
// '&:hover': {
|
|
||||||
// backgroundColor: 'rgba(80, 80, 80, 0.35)',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
topicSelect: {
|
topicSelect: {
|
||||||
float: 'right' as 'right',
|
float: 'right' as 'right',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
marginTop: '-1px',
|
marginTop: '-1px',
|
||||||
},
|
},
|
||||||
|
selected: {
|
||||||
|
backgroundColor: 'rgba(120, 120, 120, 0.55)',
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
backgroundColor: 'rgba(80, 80, 80, 0.55)',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
actions: typeof treeActions
|
actions: typeof treeActions
|
||||||
lastUpdate: number
|
|
||||||
animateChages: boolean
|
animateChages: boolean
|
||||||
isRoot?: boolean
|
isRoot?: boolean
|
||||||
treeNode: q.TreeNode
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
name?: string | undefined
|
name?: string | undefined
|
||||||
collapsed?: boolean | undefined
|
collapsed?: boolean | undefined
|
||||||
performanceCallback?: ((ms: number) => void) | undefined
|
performanceCallback?: ((ms: number) => void) | undefined
|
||||||
autoExpandLimit: number
|
|
||||||
classes: any
|
classes: any
|
||||||
className?: string
|
className?: string
|
||||||
|
topicOrder: TopicOrder
|
||||||
|
autoExpandLimit: number
|
||||||
|
lastUpdate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
collapsedOverride: boolean | undefined
|
collapsedOverride: boolean | undefined
|
||||||
|
mouseOver: boolean
|
||||||
|
selected: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class TreeNode extends React.Component<Props, State> {
|
class TreeNode extends React.Component<Props, State> {
|
||||||
@@ -63,13 +69,13 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
private dirtyEdges: boolean = true
|
private dirtyEdges: boolean = true
|
||||||
private dirtyMessage: boolean = true
|
private dirtyMessage: boolean = true
|
||||||
private animationDirty: boolean = false
|
private animationDirty: boolean = false
|
||||||
|
private lastRenderTime = 0
|
||||||
|
|
||||||
private cssAnimationWasSetAt?: number
|
private cssAnimationWasSetAt?: number
|
||||||
|
|
||||||
private willUpdateTime: number = performance.now()
|
private willUpdateTime: number = performance.now()
|
||||||
private titleRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
private titleRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
||||||
private nodeRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
private nodeRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
||||||
private topicSelectRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
|
||||||
|
|
||||||
private subnodesDidchange = () => {
|
private subnodesDidchange = () => {
|
||||||
this.dirtySubnodes = true
|
this.dirtySubnodes = true
|
||||||
@@ -88,6 +94,8 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsedOverride: props.collapsed,
|
collapsedOverride: props.collapsed,
|
||||||
|
mouseOver: false,
|
||||||
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,13 +104,21 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
this.addSubscriber(treeNode)
|
this.addSubscriber(treeNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private addSubscriber(treeNode: q.TreeNode) {
|
private addSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
|
||||||
|
treeNode.viewModel = new TopicViewModel()
|
||||||
|
treeNode.viewModel.change.subscribe(this.viewStateHasChanged)
|
||||||
treeNode.onMerge.subscribe(this.subnodesDidchange)
|
treeNode.onMerge.subscribe(this.subnodesDidchange)
|
||||||
treeNode.onEdgesChange.subscribe(this.edgesDidChange)
|
treeNode.onEdgesChange.subscribe(this.edgesDidChange)
|
||||||
treeNode.onMessage.subscribe(this.messageDidChange)
|
treeNode.onMessage.subscribe(this.messageDidChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeSubscriber(treeNode: q.TreeNode) {
|
private viewStateHasChanged = (msg: void, viewModel: TopicViewModel) => {
|
||||||
|
this.setState({ selected: viewModel.isSelected() })
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
|
||||||
|
treeNode.viewModel && treeNode.viewModel.change.unsubscribe(this.viewStateHasChanged)
|
||||||
|
treeNode.viewModel = undefined
|
||||||
treeNode.onMerge.unsubscribe(this.subnodesDidchange)
|
treeNode.onMerge.unsubscribe(this.subnodesDidchange)
|
||||||
treeNode.onEdgesChange.unsubscribe(this.edgesDidChange)
|
treeNode.onEdgesChange.unsubscribe(this.edgesDidChange)
|
||||||
treeNode.onMessage.unsubscribe(this.messageDidChange)
|
treeNode.onMessage.unsubscribe(this.messageDidChange)
|
||||||
@@ -118,13 +134,14 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
const { treeNode } = this.props
|
const { treeNode } = this.props
|
||||||
this.removeSubscriber(treeNode)
|
this.removeSubscriber(treeNode)
|
||||||
this.topicSelectRef = undefined
|
|
||||||
this.titleRef = undefined
|
this.titleRef = undefined
|
||||||
this.nodeRef = undefined
|
this.nodeRef = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
private stateHasChanged(newState: State) {
|
private stateHasChanged(newState: State) {
|
||||||
return this.state.collapsedOverride !== newState.collapsedOverride
|
return this.state.collapsedOverride !== newState.collapsedOverride
|
||||||
|
|| this.state.mouseOver !== newState.mouseOver
|
||||||
|
|| this.state.selected !== newState.selected
|
||||||
}
|
}
|
||||||
|
|
||||||
private propsHasChanged(newProps: Props) {
|
private propsHasChanged(newProps: Props) {
|
||||||
@@ -135,9 +152,7 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
|
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
|
||||||
return this.stateHasChanged(nextState)
|
return this.stateHasChanged(nextState)
|
||||||
|| this.propsHasChanged(nextProps)
|
|| this.propsHasChanged(nextProps)
|
||||||
|| this.dirtyEdges
|
|| (this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes)
|
||||||
|| this.dirtyMessage
|
|
||||||
|| this.dirtySubnodes
|
|
||||||
|| this.animationDirty
|
|| this.animationDirty
|
||||||
|| shouldRenderToRemoveCssAnimation
|
|| shouldRenderToRemoveCssAnimation
|
||||||
}
|
}
|
||||||
@@ -176,10 +191,11 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
const animation = shouldStartAnimation ? { willChange: 'auto', translateZ: 0, animation: 'example 0.5s' } : {}
|
const animation = shouldStartAnimation ? { willChange: 'auto', translateZ: 0, animation: 'example 0.5s' } : {}
|
||||||
this.animationDirty = shouldStartAnimation
|
this.animationDirty = shouldStartAnimation
|
||||||
|
|
||||||
|
const highlightClass = this.state.selected ? this.props.classes.selected : (this.state.mouseOver ? this.props.classes.hover : '')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={this.props.treeNode.hash()}
|
key={this.props.treeNode.hash()}
|
||||||
className={`${classes.node} ${this.props.className}`}
|
className={`${classes.node} ${this.props.className} ${highlightClass}`}
|
||||||
onClick={this.didClickNode}
|
onClick={this.didClickNode}
|
||||||
onMouseOver={this.mouseOver}
|
onMouseOver={this.mouseOver}
|
||||||
onMouseOut={this.mouseOut}
|
onMouseOut={this.mouseOut}
|
||||||
@@ -190,7 +206,7 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
collapsed={this.collapsed()}
|
collapsed={this.collapsed()}
|
||||||
treeNode={this.props.treeNode}
|
treeNode={this.props.treeNode}
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
lastUpdate={this.props.treeNode.lastUpdate}
|
didSelectNode={this.didSelectTopic}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{this.renderNodes()}
|
{this.renderNodes()}
|
||||||
@@ -198,31 +214,27 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private didSelectTopic = () => {
|
||||||
|
this.props.actions.selectTopic(this.props.treeNode)
|
||||||
|
}
|
||||||
|
|
||||||
private mouseOver = (event: React.MouseEvent) => {
|
private mouseOver = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (this.nodeRef && this.nodeRef.current) {
|
this.setHover(true)
|
||||||
this.nodeRef.current.style.backgroundColor = 'rgba(100, 100, 100, 0.55)'
|
|
||||||
}
|
|
||||||
if (this.topicSelectRef && this.topicSelectRef.current) {
|
|
||||||
this.topicSelectRef.current.style.opacity = '1'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private mouseOut = (event: React.MouseEvent) => {
|
private mouseOut = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (this.nodeRef && this.nodeRef.current) {
|
this.setHover(false)
|
||||||
this.nodeRef.current.style.backgroundColor = 'inherit'
|
|
||||||
}
|
|
||||||
if (this.topicSelectRef && this.topicSelectRef.current) {
|
|
||||||
this.topicSelectRef.current.style.opacity = '0'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setHover = debounce((hover: boolean) => {
|
||||||
|
this.setState({ mouseOver: hover })
|
||||||
|
}, 5)
|
||||||
|
|
||||||
private didSelectNode = (event: React.MouseEvent) => {
|
private didSelectNode = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (this.topicSelectRef && this.topicSelectRef.current) {
|
this.didSelectTopic()
|
||||||
this.topicSelectRef.current.style.opacity = '1'
|
|
||||||
}
|
|
||||||
this.props.actions.selectTopic(this.props.treeNode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private didClickNode = (event: React.MouseEvent) => {
|
private didClickNode = (event: React.MouseEvent) => {
|
||||||
@@ -236,8 +248,9 @@ class TreeNode extends React.Component<Props, State> {
|
|||||||
<TreeNodeSubnodes
|
<TreeNodeSubnodes
|
||||||
animateChanges={this.props.animateChages}
|
animateChanges={this.props.animateChages}
|
||||||
collapsed={this.collapsed()}
|
collapsed={this.collapsed()}
|
||||||
autoExpandLimit={this.props.autoExpandLimit}
|
|
||||||
treeNode={this.props.treeNode}
|
treeNode={this.props.treeNode}
|
||||||
|
autoExpandLimit={this.props.autoExpandLimit}
|
||||||
|
topicOrder={this.props.topicOrder}
|
||||||
lastUpdate={this.props.treeNode.lastUpdate}
|
lastUpdate={this.props.treeNode.lastUpdate}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -250,10 +263,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
export default withStyles(styles)(connect(null, mapDispatchToProps)(TreeNode))
|
||||||
return {
|
|
||||||
autoExpandLimit: state.settings.autoExpandLimit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TreeNode))
|
|
||||||
|
|||||||
@@ -7,17 +7,20 @@ import TreeNode from './TreeNode'
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { TopicOrder } from '../../reducers/Settings'
|
import { TopicOrder } from '../../reducers/Settings'
|
||||||
import { Theme, withStyles } from '@material-ui/core'
|
import { Theme, withStyles } from '@material-ui/core'
|
||||||
|
import { TopicViewModel } from '../../TopicViewModel'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
lastUpdate: number
|
|
||||||
topicOrder?: TopicOrder
|
|
||||||
animateChanges: boolean
|
animateChanges: boolean
|
||||||
treeNode: q.TreeNode
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
autoExpandLimit: number
|
|
||||||
filter?: string
|
filter?: string
|
||||||
collapsed?: boolean | undefined
|
collapsed?: boolean | undefined
|
||||||
didSelectNode?: (node: q.TreeNode) => void
|
|
||||||
classes: any
|
classes: any
|
||||||
|
|
||||||
|
lastUpdate: number
|
||||||
|
|
||||||
|
topicOrder: TopicOrder
|
||||||
|
selectedTopic?: q.TreeNode<TopicViewModel>
|
||||||
|
autoExpandLimit: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@@ -31,7 +34,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
|||||||
this.state = { alreadyAdded: 10 }
|
this.state = { alreadyAdded: 10 }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortedNodes(): q.TreeNode[] {
|
private sortedNodes(): q.TreeNode<TopicViewModel>[] {
|
||||||
const { topicOrder, treeNode } = this.props
|
const { topicOrder, treeNode } = this.props
|
||||||
|
|
||||||
let edges = treeNode.edgeArray
|
let edges = treeNode.edgeArray
|
||||||
@@ -72,15 +75,19 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nodes = this.sortedNodes().slice(0, this.state.alreadyAdded)
|
const nodes = this.sortedNodes().slice(0, this.state.alreadyAdded)
|
||||||
const listItems = nodes.map(node => (
|
const listItems = nodes.map((node) => {
|
||||||
|
return (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
key={`${node.hash()}-${this.props.filter}`}
|
key={`${node.hash()}-${this.props.filter}`}
|
||||||
animateChages={this.props.animateChanges}
|
animateChages={this.props.animateChanges}
|
||||||
treeNode={node}
|
treeNode={node}
|
||||||
lastUpdate={node.lastUpdate}
|
|
||||||
className={this.props.classes.listItem}
|
className={this.props.classes.listItem}
|
||||||
|
topicOrder={this.props.topicOrder}
|
||||||
|
autoExpandLimit={this.props.autoExpandLimit}
|
||||||
|
lastUpdate={node.lastUpdate}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={this.props.classes.list}>
|
<span className={this.props.classes.list}>
|
||||||
@@ -90,13 +97,6 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
return {
|
|
||||||
topicOrder: state.settings.topicOrder,
|
|
||||||
filter: state.tree.filter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = (theme: Theme) => ({
|
const styles = (theme: Theme) => ({
|
||||||
list: {
|
list: {
|
||||||
display: 'block' as 'block',
|
display: 'block' as 'block',
|
||||||
@@ -107,4 +107,4 @@ const styles = (theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withStyles(styles)(connect(mapStateToProps)(TreeNodeSubnodes))
|
export default withStyles(styles)(TreeNodeSubnodes)
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { bindActionCreators } from 'redux'
|
|
||||||
import { treeActions } from '../../actions'
|
|
||||||
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 '../../TopicViewModel'
|
||||||
|
const debounce = require('lodash.debounce')
|
||||||
|
|
||||||
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
|
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
treeNode: q.TreeNode
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
actions: any
|
|
||||||
name?: string | undefined
|
name?: string | undefined
|
||||||
collapsed?: boolean | undefined
|
collapsed?: boolean | undefined
|
||||||
lastUpdate: number
|
|
||||||
classes: any
|
classes: any
|
||||||
|
didSelectNode: any
|
||||||
}
|
}
|
||||||
|
|
||||||
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
|
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
|
||||||
private mouseOver = (event: React.MouseEvent) => {
|
private mouseOver = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
this.selectTopic()
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectTopic = debounce(() => {
|
||||||
if (this.props.treeNode.message) {
|
if (this.props.treeNode.message) {
|
||||||
this.props.actions.selectTopic(this.props.treeNode)
|
this.props.didSelectNode(this.props.treeNode)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, 5)
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<span className={this.props.classes.title} onMouseOver={this.mouseOver}>
|
<span className={this.props.classes.title} onMouseOver={this.props.treeNode.message ? this.mouseOver : undefined}>
|
||||||
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
|
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -59,12 +63,6 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: any) => {
|
|
||||||
return {
|
|
||||||
actions: bindActionCreators(treeActions, dispatch),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = (theme: Theme) => ({
|
const styles = (theme: Theme) => ({
|
||||||
value: {
|
value: {
|
||||||
whiteSpace: 'nowrap' as 'nowrap',
|
whiteSpace: 'nowrap' as 'nowrap',
|
||||||
@@ -88,4 +86,4 @@ const styles = (theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withStyles(styles)(connect(null, mapDispatchToProps)(TreeNodeTitle))
|
export default withStyles(styles)(TreeNodeTitle)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import './tracking'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as ReactDOM from 'react-dom'
|
import * as ReactDOM from 'react-dom'
|
||||||
import reduxThunk from 'redux-thunk'
|
import reduxThunk from 'redux-thunk'
|
||||||
import { batchDispatchMiddleware } from 'redux-batched-actions';
|
import { batchDispatchMiddleware } from 'redux-batched-actions'
|
||||||
|
|
||||||
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
|
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
|
||||||
import reducers from './reducers'
|
import reducers from './reducers'
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { Action } from 'redux'
|
|||||||
import { createReducer } from './lib'
|
import { createReducer } from './lib'
|
||||||
import * as q from '../../../backend/src/Model'
|
import * as q from '../../../backend/src/Model'
|
||||||
import { MqttOptions } from '../../../backend/src/DataSource'
|
import { MqttOptions } from '../../../backend/src/DataSource'
|
||||||
|
import { TopicViewModel } from '../TopicViewModel'
|
||||||
|
|
||||||
export interface ConnectionState {
|
export interface ConnectionState {
|
||||||
host?: string
|
host?: string
|
||||||
tree?: q.Tree
|
tree?: q.Tree<TopicViewModel>
|
||||||
connectionOptions?: MqttOptions
|
connectionOptions?: MqttOptions
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
error?: string
|
error?: string
|
||||||
@@ -30,7 +31,7 @@ export interface SetConnecting {
|
|||||||
export interface SetConnected {
|
export interface SetConnected {
|
||||||
type: ActionTypes.CONNECTION_SET_CONNECTED
|
type: ActionTypes.CONNECTION_SET_CONNECTED
|
||||||
host: string
|
host: string
|
||||||
tree: q.Tree
|
tree: q.Tree<TopicViewModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetDisconnected {
|
export interface SetDisconnected {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as q from '../../../backend/src/Model'
|
import * as q from '../../../backend/src/Model'
|
||||||
import { Action } from 'redux'
|
import { Action } from 'redux'
|
||||||
import { createReducer } from './lib'
|
import { createReducer } from './lib'
|
||||||
|
import { TopicViewModel } from '../TopicViewModel'
|
||||||
|
|
||||||
export interface TreeState {
|
export interface TreeState {
|
||||||
tree?: q.Tree
|
tree?: q.Tree<TopicViewModel>
|
||||||
selectedTopic?: q.TreeNode
|
selectedTopic?: q.TreeNode<TopicViewModel>
|
||||||
filter?: string
|
filter?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,13 +18,13 @@ export enum ActionTypes {
|
|||||||
|
|
||||||
export interface ShowTree {
|
export interface ShowTree {
|
||||||
type: ActionTypes.TREE_SHOW_TREE
|
type: ActionTypes.TREE_SHOW_TREE
|
||||||
tree?: q.Tree
|
tree?: q.Tree<TopicViewModel>
|
||||||
filter?: string
|
filter?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectTopic {
|
export interface SelectTopic {
|
||||||
type: ActionTypes.TREE_SELECT_TOPIC
|
type: ActionTypes.TREE_SELECT_TOPIC
|
||||||
selectedTopic?: q.TreeNode
|
selectedTopic?: q.TreeNode<TopicViewModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TreeState = { }
|
const initialState: TreeState = { }
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as q from '../../../backend/src/Model'
|
|
||||||
|
|
||||||
import { Action, Reducer, combineReducers } from 'redux'
|
import { Action, Reducer, combineReducers } from 'redux'
|
||||||
|
|
||||||
import { trackEvent } from '../tracking'
|
import { trackEvent } from '../tracking'
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Hashable, TreeNode } from './'
|
import { Hashable, TreeNode } from './'
|
||||||
const sha1 = require('sha1')
|
const sha1 = require('sha1')
|
||||||
|
|
||||||
export class Edge implements Hashable {
|
export class Edge<ViewModel> implements Hashable {
|
||||||
public name: string
|
public name: string
|
||||||
|
|
||||||
public target!: TreeNode
|
public target!: TreeNode<ViewModel>
|
||||||
public source?: TreeNode | undefined
|
public source?: TreeNode<ViewModel> | undefined
|
||||||
private cachedHash?: string
|
private cachedHash?: string
|
||||||
|
|
||||||
constructor(name: string) {
|
constructor(name: string) {
|
||||||
@@ -32,7 +32,7 @@ export class Edge implements Hashable {
|
|||||||
return this.cachedHash
|
return this.cachedHash
|
||||||
}
|
}
|
||||||
|
|
||||||
public firstEdge(): Edge {
|
public firstEdge(): Edge<ViewModel> {
|
||||||
if (this.source && this.source.sourceEdge) {
|
if (this.source && this.source.sourceEdge) {
|
||||||
return this.source.sourceEdge.firstEdge()
|
return this.source.sourceEdge.firstEdge()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { TreeNode } from './'
|
import { TreeNode } from './'
|
||||||
import { EventBusInterface, makeConnectionMessageEvent, MqttMessage } from '../../../events'
|
import { EventBusInterface, makeConnectionMessageEvent, MqttMessage, EventDispatcher } from '../../../events'
|
||||||
import { TreeNodeFactory } from './TreeNodeFactory'
|
import { TreeNodeFactory } from './TreeNodeFactory'
|
||||||
|
|
||||||
export class Tree extends TreeNode {
|
export class Tree<ViewModel> extends TreeNode<ViewModel> {
|
||||||
public connectionId?: string
|
public connectionId?: string
|
||||||
public updateSource?: EventBusInterface
|
public updateSource?: EventBusInterface
|
||||||
public nodeFilter?: (node: TreeNode) => boolean
|
public nodeFilter?: (node: TreeNode<ViewModel>) => boolean
|
||||||
private subscriptionEvent?: any
|
private subscriptionEvent?: any
|
||||||
public isTree = true
|
public isTree = true
|
||||||
private cachedHash = `${Math.random()}`
|
private cachedHash = `${Math.random()}`
|
||||||
|
private unmergedMessages: MqttMessage[] = []
|
||||||
|
public didReceive = new EventDispatcher<void, Tree<ViewModel>>(this)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(undefined, undefined)
|
super(undefined, undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateWithConnection(emitter: EventBusInterface, connectionId: string, nodeFilter?:(node: TreeNode) => boolean) {
|
public updateWithConnection(emitter: EventBusInterface, connectionId: string, nodeFilter?:(node: TreeNode<ViewModel>) => boolean) {
|
||||||
this.updateSource = emitter
|
this.updateSource = emitter
|
||||||
this.connectionId = connectionId
|
this.connectionId = connectionId
|
||||||
this.nodeFilter = nodeFilter
|
this.nodeFilter = nodeFilter
|
||||||
@@ -27,16 +29,23 @@ export class Tree extends TreeNode {
|
|||||||
return this.cachedHash
|
return this.cachedHash
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleNewData = (msg: MqttMessage) => {
|
public applyUnmergedChanges() {
|
||||||
|
this.unmergedMessages.forEach((msg) => {
|
||||||
const edges = msg.topic.split('/')
|
const edges = msg.topic.split('/')
|
||||||
const node = TreeNodeFactory.fromEdgesAndValue(edges, msg.payload)
|
const node = TreeNodeFactory.fromEdgesAndValue<ViewModel, any>(edges, msg.payload)
|
||||||
node.mqttMessage = msg
|
node.mqttMessage = msg
|
||||||
|
|
||||||
if (this.nodeFilter && !this.nodeFilter(node)) {
|
if (!this.nodeFilter || this.nodeFilter(node)) {
|
||||||
return
|
|
||||||
}
|
|
||||||
this.updateWithNode(node.firstNode())
|
this.updateWithNode(node.firstNode())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
this.unmergedMessages = []
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleNewData = (msg: MqttMessage) => {
|
||||||
|
this.unmergedMessages.push(msg)
|
||||||
|
this.didReceive.dispatch()
|
||||||
|
}
|
||||||
|
|
||||||
public stopUpdating() {
|
public stopUpdating() {
|
||||||
if (this.subscriptionEvent && this.updateSource) {
|
if (this.subscriptionEvent && this.updateSource) {
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import { Edge, Message, RingBuffer } from './'
|
import { Edge, Message, RingBuffer } from './'
|
||||||
import { EventDispatcher, MqttMessage } from '../../../events'
|
import { EventDispatcher, MqttMessage } from '../../../events'
|
||||||
|
|
||||||
export class TreeNode {
|
export class TreeNode<ViewModel> {
|
||||||
public sourceEdge?: Edge
|
public sourceEdge?: Edge<ViewModel>
|
||||||
public message?: Message
|
public message?: Message
|
||||||
public mqttMessage?: MqttMessage
|
public mqttMessage?: MqttMessage
|
||||||
public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100)
|
public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100)
|
||||||
public edges: {[s: string]: Edge} = {}
|
public viewModel?: ViewModel
|
||||||
public edgeArray: Edge[] = []
|
public edges: {[s: string]: Edge<ViewModel>} = {}
|
||||||
|
public edgeArray: Edge<ViewModel>[] = []
|
||||||
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>(this)
|
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>(this)
|
||||||
public onEdgesChange = new EventDispatcher<void, TreeNode>(this)
|
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>(this)
|
||||||
public onMessage = new EventDispatcher<Message, TreeNode>(this)
|
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>(this)
|
||||||
public isTree = false
|
public isTree = false
|
||||||
|
|
||||||
private cachedPath?: string
|
private cachedPath?: string
|
||||||
private cachedChildTopics?: TreeNode[]
|
private cachedChildTopics?: TreeNode<ViewModel>[]
|
||||||
private cachedLeafMessageCount?: number
|
private cachedLeafMessageCount?: number
|
||||||
private cachedChildTopicCount?: number
|
private cachedChildTopicCount?: number
|
||||||
|
|
||||||
public unconnectedClone() {
|
public unconnectedClone() {
|
||||||
const node = new TreeNode()
|
const node = new TreeNode<ViewModel>()
|
||||||
node.message = this.message
|
node.message = this.message
|
||||||
node.mqttMessage = this.mqttMessage
|
node.mqttMessage = this.mqttMessage
|
||||||
node.messageHistory = this.messageHistory.clone()
|
node.messageHistory = this.messageHistory.clone()
|
||||||
@@ -32,7 +33,7 @@ export class TreeNode {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(sourceEdge?: Edge, message?: Message) {
|
constructor(sourceEdge?: Edge<ViewModel>, message?: Message) {
|
||||||
if (sourceEdge) {
|
if (sourceEdge) {
|
||||||
this.sourceEdge = sourceEdge
|
this.sourceEdge = sourceEdge
|
||||||
sourceEdge.target = this
|
sourceEdge.target = this
|
||||||
@@ -63,7 +64,7 @@ export class TreeNode {
|
|||||||
return `N${(this.sourceEdge ? this.sourceEdge.hash() : '')}`
|
return `N${(this.sourceEdge ? this.sourceEdge.hash() : '')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
public firstNode(): TreeNode {
|
public firstNode(): TreeNode<ViewModel> {
|
||||||
return this.sourceEdge && this.sourceEdge.source ? this.sourceEdge.source.firstNode() : this
|
return this.sourceEdge && this.sourceEdge.source ? this.sourceEdge.source.firstNode() : this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +79,11 @@ export class TreeNode {
|
|||||||
return this.cachedPath
|
return this.cachedPath
|
||||||
}
|
}
|
||||||
|
|
||||||
private previous(): TreeNode | undefined {
|
private previous(): TreeNode<ViewModel> | undefined {
|
||||||
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
|
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
public addEdge(edge: Edge, emitUpdate: boolean = false) {
|
public addEdge(edge: Edge<ViewModel>, emitUpdate: boolean = false) {
|
||||||
this.edges[edge.name] = edge
|
this.edges[edge.name] = edge
|
||||||
this.edgeArray.push(edge)
|
this.edgeArray.push(edge)
|
||||||
edge.source = this
|
edge.source = this
|
||||||
@@ -92,7 +93,7 @@ export class TreeNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public branch(): TreeNode[] {
|
public branch(): TreeNode<ViewModel>[] {
|
||||||
const previous = this.previous()
|
const previous = this.previous()
|
||||||
if (!previous) {
|
if (!previous) {
|
||||||
return [this]
|
return [this]
|
||||||
@@ -101,7 +102,7 @@ export class TreeNode {
|
|||||||
return previous.branch().concat([this])
|
return previous.branch().concat([this])
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateWithNode(node: TreeNode) {
|
public updateWithNode(node: TreeNode<ViewModel>) {
|
||||||
if (node.message) {
|
if (node.message) {
|
||||||
this.setMessage(node.message)
|
this.setMessage(node.message)
|
||||||
this.onMessage.dispatch(node.message)
|
this.onMessage.dispatch(node.message)
|
||||||
@@ -119,7 +120,7 @@ export class TreeNode {
|
|||||||
.reduce((a, b) => a + b, 0) + this.messages
|
.reduce((a, b) => a + b, 0) + this.messages
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.cachedLeafMessageCount
|
return this.cachedLeafMessageCount as number
|
||||||
}
|
}
|
||||||
|
|
||||||
public childTopicCount(): number {
|
public childTopicCount(): number {
|
||||||
@@ -129,14 +130,14 @@ export class TreeNode {
|
|||||||
.reduce((a, b) => a + b, this.edgeArray.length === 0 ? 1 : 0)
|
.reduce((a, b) => a + b, this.edgeArray.length === 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.cachedChildTopicCount
|
return this.cachedChildTopicCount as number
|
||||||
}
|
}
|
||||||
|
|
||||||
public edgeCount(): number {
|
public edgeCount(): number {
|
||||||
return this.edgeArray.length
|
return this.edgeArray.length
|
||||||
}
|
}
|
||||||
|
|
||||||
public childTopics(): TreeNode[] {
|
public childTopics(): TreeNode<ViewModel>[] {
|
||||||
if (this.cachedChildTopics === undefined) {
|
if (this.cachedChildTopics === undefined) {
|
||||||
const initialValue = this.message && this.message.value ? [this] : []
|
const initialValue = this.message && this.message.value ? [this] : []
|
||||||
|
|
||||||
@@ -145,16 +146,16 @@ export class TreeNode {
|
|||||||
.reduce((a, b) => a.concat(b), initialValue)
|
.reduce((a, b) => a.concat(b), initialValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.cachedChildTopics
|
return this.cachedChildTopics as TreeNode<ViewModel>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
public findNode (path: String): TreeNode | undefined {
|
public findNode (path: String): TreeNode<ViewModel> | undefined {
|
||||||
const topics = path.split('/')
|
const topics = path.split('/')
|
||||||
|
|
||||||
return this.findChild(topics)
|
return this.findChild(topics)
|
||||||
}
|
}
|
||||||
|
|
||||||
private findChild(edges: string[]): TreeNode | undefined {
|
private findChild(edges: string[]): TreeNode<ViewModel> | undefined {
|
||||||
if (edges.length === 0) {
|
if (edges.length === 0) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -167,7 +168,7 @@ export class TreeNode {
|
|||||||
return nextEdge.target.findChild(edges.slice(1))
|
return nextEdge.target.findChild(edges.slice(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
private mergeEdges(node: TreeNode) {
|
private mergeEdges(node: TreeNode<ViewModel>) {
|
||||||
const edgeKeys = Object.keys(node.edges)
|
const edgeKeys = Object.keys(node.edges)
|
||||||
let edgesDidUpdate = false
|
let edgesDidUpdate = false
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ interface HasLength {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class TreeNodeFactory {
|
export abstract class TreeNodeFactory {
|
||||||
public static insertNodeAtPosition(edgeNames: string[], node: TreeNode) {
|
public static insertNodeAtPosition<ViewModel>(edgeNames: string[], node: TreeNode<ViewModel>) {
|
||||||
let currentNode: TreeNode = new Tree()
|
let currentNode: TreeNode<ViewModel> = new Tree()
|
||||||
let edge
|
let edge
|
||||||
for (const edgeName of edgeNames) {
|
for (const edgeName of edgeNames) {
|
||||||
edge = new Edge(edgeName)
|
edge = new Edge<ViewModel>(edgeName)
|
||||||
currentNode.addEdge(edge)
|
currentNode.addEdge(edge)
|
||||||
currentNode = new TreeNode(edge)
|
currentNode = new TreeNode(edge)
|
||||||
edge.target = currentNode
|
edge.target = currentNode
|
||||||
@@ -18,15 +18,15 @@ export abstract class TreeNodeFactory {
|
|||||||
node.sourceEdge!.target = node
|
node.sourceEdge!.target = node
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromEdgesAndValue<T extends HasLength>(edgeNames: string[], value?: T): TreeNode {
|
public static fromEdgesAndValue<ViewModel, T extends HasLength>(edgeNames: string[], value?: T): TreeNode<ViewModel> {
|
||||||
const node = new TreeNode()
|
const node = new TreeNode<ViewModel>()
|
||||||
node.setMessage({
|
node.setMessage({
|
||||||
value,
|
value,
|
||||||
length: value ? value.length : 0,
|
length: value ? value.length : 0,
|
||||||
received: new Date(),
|
received: new Date(),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.insertNodeAtPosition(edgeNames, node)
|
this.insertNodeAtPosition<ViewModel>(edgeNames, node)
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user