Tweak performance
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import * as q from '../../backend/src/Model'
|
||||
|
||||
import { Tree } from './components/Tree'
|
||||
import { Tree } from './components/Tree/Tree'
|
||||
import TitleBar from './components/TitleBar'
|
||||
import Sidebar from './components/Sidebar/Sidebar'
|
||||
|
||||
|
||||
@@ -22,17 +22,17 @@ interface State {
|
||||
}
|
||||
|
||||
class Sidebar extends React.Component<Props, State> {
|
||||
private updateNode: (node?: q.TreeNode | undefined) => void
|
||||
private updateNode = (node: q.TreeNode) => {
|
||||
if (!node) {
|
||||
this.setState(this.state)
|
||||
} else {
|
||||
this.setState({ node })
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
this.updateNode = (node) => {
|
||||
if (!node) {
|
||||
this.setState(this.state)
|
||||
} else {
|
||||
this.setState({ node })
|
||||
}
|
||||
}
|
||||
this.state = { node: new q.Tree() }
|
||||
}
|
||||
|
||||
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
|
||||
@@ -52,9 +52,19 @@ class Sidebar extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
this.props.node && this.props.node.removeListener('update', this.updateNode)
|
||||
nextProps.node && nextProps.node.on('update', this.updateNode)
|
||||
nextProps.node && this.updateNode(nextProps.node)
|
||||
this.props.node && this.removeUpdateListener(this.props.node)
|
||||
nextProps.node && this.registerUpdateListener(nextProps.node)
|
||||
this.props.node && this.setState({ node: this.props.node })
|
||||
}
|
||||
|
||||
private registerUpdateListener(node: q.TreeNode) {
|
||||
node.on(q.TreeNodeUpdateEvents.merge, this.updateNode)
|
||||
node.on(q.TreeNodeUpdateEvents.message, this.updateNode)
|
||||
}
|
||||
|
||||
private removeUpdateListener(node: q.TreeNode) {
|
||||
node.removeListener(q.TreeNodeUpdateEvents.merge, this.updateNode)
|
||||
node.removeListener(q.TreeNodeUpdateEvents.message, this.updateNode)
|
||||
}
|
||||
|
||||
private open(): boolean {
|
||||
@@ -79,7 +89,7 @@ class Sidebar extends React.Component<Props, State> {
|
||||
<Typography className={classes.heading}>Topic</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails>
|
||||
<Topic node={this.state.node} />
|
||||
<Topic node={this.props.node} didSelectNode={this.updateNode} />
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel key="value" defaultExpanded={true}>
|
||||
@@ -90,7 +100,7 @@ class Sidebar extends React.Component<Props, State> {
|
||||
<ValueRenderer node={this.state.node} />
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel key="stats" defaultExpanded={true}>
|
||||
<ExpansionPanel defaultExpanded={true}>
|
||||
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
|
||||
<Typography className={classes.heading}>Stats</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
|
||||
@@ -6,8 +6,9 @@ import Button from '@material-ui/core/Button'
|
||||
interface Props {
|
||||
classes: any
|
||||
theme: Theme
|
||||
node: q.TreeNode
|
||||
node?: q.TreeNode
|
||||
selected?: q.TreeNode
|
||||
didSelectNode: (node: q.TreeNode) => void
|
||||
}
|
||||
|
||||
class Topic extends React.Component<Props, {}> {
|
||||
@@ -21,7 +22,11 @@ class Topic extends React.Component<Props, {}> {
|
||||
|
||||
public render() {
|
||||
const { node } = this.props
|
||||
let i = 0
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
let key = 0
|
||||
const breadCrumps = node.branch()
|
||||
.map(node => node.sourceEdge)
|
||||
.filter(edge => Boolean(edge))
|
||||
@@ -42,7 +47,7 @@ class Topic extends React.Component<Props, {}> {
|
||||
}
|
||||
|
||||
const joinedBreadCrumps = breadCrumps.reduce((prev, current) =>
|
||||
prev.concat([<span key={i += 1}>/</span>]).concat(current),
|
||||
prev.concat([<span key={key += 1}>/</span>]).concat(current),
|
||||
)
|
||||
|
||||
return <span style={{ lineHeight: '2.2em' }}>{joinedBreadCrumps}</span>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import * as io from 'socket.io-client'
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import TreeNode from './TreeNode'
|
||||
import List from '@material-ui/core/List'
|
||||
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
class TreeState {
|
||||
public tree: q.Tree
|
||||
public msg: any
|
||||
@@ -22,7 +20,7 @@ declare const performance: any
|
||||
|
||||
export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
||||
private socket: SocketIOClient.Socket
|
||||
private renderDuration: number = 200
|
||||
private renderDuration: number = 300
|
||||
private updateTimer?: any
|
||||
private lastUpdate: number = 0
|
||||
private perf:number = 0
|
||||
@@ -46,7 +44,7 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
||||
return
|
||||
}
|
||||
|
||||
const updateInterval = Math.max(this.renderDuration * 5, 200)
|
||||
const updateInterval = Math.max(this.renderDuration * 5, 300)
|
||||
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
@@ -75,6 +73,8 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
||||
return <div>
|
||||
<List>
|
||||
<TreeNode
|
||||
animateChages={true}
|
||||
autoExpandLimit={3}
|
||||
isRoot={true}
|
||||
didSelectNode={this.props.didSelectNode}
|
||||
treeNode={this.state.tree}
|
||||
194
app/src/components/Tree/TreeNode.tsx
Normal file
194
app/src/components/Tree/TreeNode.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import * as React from 'react'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { withTheme, Theme } from '@material-ui/core/styles'
|
||||
const throttle = require('lodash.throttle')
|
||||
import { isElementInViewport } from '../helper/isElementInViewport'
|
||||
import TreeNodeTitle from './TreeNodeTitle'
|
||||
import TreeNodeSubnodes from './TreeNodeSubnodes'
|
||||
|
||||
declare var performance: any
|
||||
declare var window: any
|
||||
|
||||
export interface TreeNodeProps {
|
||||
animateChages: boolean
|
||||
isRoot?: boolean
|
||||
treeNode: q.TreeNode
|
||||
name?: string | undefined
|
||||
collapsed?: boolean | undefined
|
||||
performanceCallback?: ((ms: number) => void) | undefined
|
||||
didSelectNode?: (node: q.TreeNode) => void
|
||||
theme: Theme
|
||||
autoExpandLimit: number
|
||||
}
|
||||
|
||||
interface TreeNodeState {
|
||||
title: string | undefined
|
||||
collapsed: boolean
|
||||
collapsedOverride: boolean | undefined
|
||||
edgeCount: number
|
||||
}
|
||||
|
||||
class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
|
||||
private dirtySubnodes: boolean = true
|
||||
private dirtyState: boolean = true
|
||||
private dirtyEdges: boolean = true
|
||||
private dirtyMessage: boolean = true
|
||||
|
||||
private cssAnimationWasSetAt?: number
|
||||
|
||||
private willUpdateTime: number = performance.now()
|
||||
private titleRef = React.createRef<HTMLElement>()
|
||||
|
||||
private subnodesDidchange = () => {
|
||||
this.dirtySubnodes = true
|
||||
}
|
||||
|
||||
private messageDidChange = () => {
|
||||
this.dirtyMessage = true
|
||||
}
|
||||
|
||||
private edgesDidChange = () => {
|
||||
this.dirtyMessage = true
|
||||
}
|
||||
|
||||
constructor(props: TreeNodeProps) {
|
||||
super(props)
|
||||
const edgeCount = Object.keys(props.treeNode.edges).length
|
||||
const collapsed = edgeCount > this.props.autoExpandLimit
|
||||
this.state = { collapsed, edgeCount, collapsedOverride: props.collapsed, title: props.name }
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.treeNode.on(q.TreeNodeUpdateEvents.merge, this.subnodesDidchange)
|
||||
this.props.treeNode.on(q.TreeNodeUpdateEvents.edges, this.edgesDidChange)
|
||||
this.props.treeNode.on(q.TreeNodeUpdateEvents.message, this.messageDidChange)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.treeNode.removeListener(q.TreeNodeUpdateEvents.merge, this.subnodesDidchange)
|
||||
this.props.treeNode.removeListener(q.TreeNodeUpdateEvents.edges, this.edgesDidChange)
|
||||
this.props.treeNode.removeListener(q.TreeNodeUpdateEvents.message, this.messageDidChange)
|
||||
}
|
||||
|
||||
private getStyles() {
|
||||
return {
|
||||
collapsedSubnodes: {
|
||||
color: this.props.theme.palette.text.secondary,
|
||||
},
|
||||
displayBlock: {
|
||||
display: 'block',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
public setState(newState: any) {
|
||||
this.dirtyState = this.stateHasChanged(newState)
|
||||
|
||||
super.setState(newState)
|
||||
}
|
||||
|
||||
private stateHasChanged(newState: any) {
|
||||
return this.state.collapsed !== newState.collapsed
|
||||
|| this.state.collapsedOverride !== newState.collapsedOverride
|
||||
|| this.state.edgeCount !== newState.edgeCount
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
|
||||
return this.dirtyState
|
||||
|| this.dirtyEdges
|
||||
|| this.dirtyMessage
|
||||
|| this.dirtySubnodes
|
||||
|| shouldRenderToRemoveCssAnimation
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.props.performanceCallback) {
|
||||
const renderTime = performance.now() - this.willUpdateTime
|
||||
this.props.performanceCallback(renderTime)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUpdate() {
|
||||
if (this.props.performanceCallback) {
|
||||
this.willUpdateTime = performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
private toggle() {
|
||||
this.setState({ collapsedOverride: !this.collapsed() })
|
||||
}
|
||||
|
||||
private collapsed() {
|
||||
if (this.state.collapsedOverride !== undefined) {
|
||||
return this.state.collapsedOverride
|
||||
}
|
||||
|
||||
return this.state.collapsed
|
||||
}
|
||||
|
||||
public componentWillReceiveProps() {
|
||||
const edgeCount = Object.keys(this.props.treeNode.edges).length
|
||||
this.setState({ edgeCount, collapsed: edgeCount > this.props.autoExpandLimit })
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { displayBlock } = this.getStyles()
|
||||
const animationStyle = this.indicatingChangeAnimationStyle()
|
||||
|
||||
this.dirtyState = this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
|
||||
|
||||
return <div key={this.props.treeNode.hash()} style={ displayBlock }>
|
||||
<div style={animationStyle} ref={this.titleRef}>
|
||||
<TreeNodeTitle
|
||||
edgeCount={this.state.edgeCount}
|
||||
collapsed={this.collapsed()}
|
||||
treeNode={this.props.treeNode}
|
||||
name={this.props.name}
|
||||
didSelectNode={this.props.didSelectNode}
|
||||
toggleCollapsed={() => this.toggle()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ this.clear() }
|
||||
<div style = { displayBlock }>
|
||||
{this.renderNodes()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
private clear() {
|
||||
return <div style={{ clear: 'both' }} />
|
||||
}
|
||||
|
||||
private renderNodes() {
|
||||
return <TreeNodeSubnodes
|
||||
animateChanges={this.props.animateChages}
|
||||
collapsed={this.collapsed()}
|
||||
autoExpandLimit={this.props.autoExpandLimit}
|
||||
didSelectNode={this.props.didSelectNode}
|
||||
toggleCollapsed={() => this.toggle()}
|
||||
treeNode={this.props.treeNode}
|
||||
/>
|
||||
}
|
||||
|
||||
private indicatingChangeAnimationStyle() {
|
||||
if (this.props.isRoot) {
|
||||
return {}
|
||||
}
|
||||
if (this.cssAnimationWasSetAt && (performance.now() - this.cssAnimationWasSetAt) > 500) {
|
||||
this.cssAnimationWasSetAt = undefined
|
||||
return {}
|
||||
}
|
||||
const isInViewPort = this.titleRef.current && isElementInViewport(this.titleRef.current)
|
||||
const isDirty = this.dirtyMessage || this.dirtyEdges || this.collapsed()
|
||||
if (this.props.animateChages && isDirty && isInViewPort) {
|
||||
if (!this.cssAnimationWasSetAt) {
|
||||
this.cssAnimationWasSetAt = performance.now()
|
||||
}
|
||||
return { animation: 'example 0.5s' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme()(TreeNode)
|
||||
51
app/src/components/Tree/TreeNodeSubnodes.tsx
Normal file
51
app/src/components/Tree/TreeNodeSubnodes.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { withTheme, Theme } from '@material-ui/core/styles'
|
||||
import { List, ListItem, Collapse } from '@material-ui/core'
|
||||
import TreeNode from './TreeNode'
|
||||
|
||||
export interface Props {
|
||||
animateChanges: boolean
|
||||
treeNode: q.TreeNode
|
||||
autoExpandLimit: number
|
||||
collapsed?: boolean | undefined
|
||||
didSelectNode?: (node: q.TreeNode) => void
|
||||
toggleCollapsed: () => void
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
class TreeNodeSubnodes extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
const edges = Object.values(this.props.treeNode.edges)
|
||||
const listItemStyle = {
|
||||
padding: '3px 8px 0px 8px',
|
||||
}
|
||||
|
||||
const listStyle = {
|
||||
padding: '3px 8px 0px 8px',
|
||||
}
|
||||
|
||||
if (edges.length > 0 && !this.props.collapsed) {
|
||||
const listItems = edges
|
||||
.map(edge => edge.target)
|
||||
.map(node => (
|
||||
<ListItem key={node.hash()} style={listItemStyle} button>
|
||||
<TreeNode
|
||||
animateChages={this.props.animateChanges}
|
||||
treeNode={node}
|
||||
didSelectNode={this.props.didSelectNode}
|
||||
autoExpandLimit={this.props.autoExpandLimit}
|
||||
/>
|
||||
</ListItem>
|
||||
))
|
||||
|
||||
return <Collapse in={!this.props.collapsed} timeout="auto" unmountOnExit>
|
||||
<List style={listStyle}>{listItems}</List>
|
||||
</Collapse>
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme()(TreeNodeSubnodes)
|
||||
95
app/src/components/Tree/TreeNodeTitle.tsx
Normal file
95
app/src/components/Tree/TreeNodeTitle.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as React from 'react'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { withTheme, Theme } from '@material-ui/core/styles'
|
||||
|
||||
export interface TreeNodeProps {
|
||||
treeNode: q.TreeNode
|
||||
name?: string | undefined
|
||||
collapsed?: boolean | undefined
|
||||
toggleCollapsed: () => void
|
||||
didSelectNode?: (node: q.TreeNode) => void
|
||||
edgeCount: number
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
|
||||
private getStyles() {
|
||||
const { theme } = this.props
|
||||
return {
|
||||
collapsedSubnodes: {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
container: {
|
||||
display: 'block',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const style: React.CSSProperties = {
|
||||
lineHeight: '1em',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
return <Typography style={style}>
|
||||
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
private renderSourceEdge() {
|
||||
const style: React.CSSProperties = {
|
||||
fontWeight: 'bold',
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
}
|
||||
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
|
||||
|
||||
return <span style={style} onClick={() => {
|
||||
this.toggle()
|
||||
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
|
||||
}}>{name}</span>
|
||||
}
|
||||
|
||||
private renderValue() {
|
||||
const style: React.CSSProperties = {
|
||||
width: '15em',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '0',
|
||||
paddingLeft: '5px',
|
||||
display: 'inline-block',
|
||||
}
|
||||
return this.props.treeNode.message
|
||||
? <span
|
||||
style={style}
|
||||
onMouseOver={() => this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)}
|
||||
> = {this.props.treeNode.message.value.toString()}</span>
|
||||
: null
|
||||
}
|
||||
|
||||
private toggle() {
|
||||
this.props.toggleCollapsed()
|
||||
}
|
||||
|
||||
private renderExpander() {
|
||||
if (this.props.edgeCount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.props.collapsed
|
||||
? <span onClick={() => this.toggle()}>▶</span>
|
||||
: <span onClick={() => this.toggle()}>▼</span>
|
||||
}
|
||||
|
||||
private renderCollapsedSubnodes() {
|
||||
if (this.props.edgeCount === 0 || !this.props.collapsed) {
|
||||
return null
|
||||
}
|
||||
|
||||
const messages = this.props.treeNode.leafes().map(leaf => leaf.messages).reduce((a, b) => a + b)
|
||||
return <span style={this.getStyles().collapsedSubnodes}>({this.props.treeNode.leafes().length} nodes, {messages} messages)</span>
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme()(TreeNodeTitle)
|
||||
@@ -1,242 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { List, ListItem, Collapse, Typography } from '@material-ui/core'
|
||||
import { withTheme, Theme } from '@material-ui/core/styles'
|
||||
const throttle = require('lodash.throttle')
|
||||
import Slide from '@material-ui/core/Slide'
|
||||
import { isElementInViewport } from './helper/isElementInViewport'
|
||||
const collapseLimit = 3
|
||||
declare var performance: any
|
||||
|
||||
export interface TreeNodeProps {
|
||||
isRoot?: boolean
|
||||
treeNode: q.TreeNode
|
||||
name?: string | undefined
|
||||
collapsed?: boolean | undefined
|
||||
performanceCallback?: ((ms: number) => void) | undefined
|
||||
didSelectNode?: (node: q.TreeNode) => void
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
interface TreeNodeState {
|
||||
title: string | undefined
|
||||
collapsed: boolean
|
||||
collapsedOverride: boolean | undefined
|
||||
edgeCount: number
|
||||
}
|
||||
|
||||
class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
|
||||
private dirty: boolean = true
|
||||
private willUpdateTime: number = performance.now()
|
||||
private titleRef = React.createRef<HTMLElement>()
|
||||
private markAsDirty = () => {
|
||||
this.dirty = true
|
||||
if (!this.props.isRoot) {
|
||||
this.indicateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private indicateUpdate = throttle(() => {
|
||||
const title: any = this.titleRef.current
|
||||
if (title && isElementInViewport(title)) {
|
||||
title.style.animation = 'example 0.5s'
|
||||
setTimeout(() => {
|
||||
title.style.animation = ''
|
||||
}, 500)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
constructor(props: TreeNodeProps) {
|
||||
super(props)
|
||||
const edgeCount = Object.keys(props.treeNode.edges).length
|
||||
const collapsed = edgeCount > collapseLimit
|
||||
this.state = { collapsed, edgeCount, collapsedOverride: props.collapsed, title: props.name }
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.treeNode.on('update', this.markAsDirty)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.treeNode.removeListener('update', this.markAsDirty)
|
||||
}
|
||||
|
||||
private getStyles() {
|
||||
const { theme } = this.props
|
||||
return {
|
||||
collapsedSubnodes: {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
container: {
|
||||
display: 'block',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
public setState(state: any) {
|
||||
this.dirty = this.state.collapsed !== state.collapsed
|
||||
|| this.state.collapsedOverride !== state.collapsedOverride
|
||||
|| this.state.edgeCount !== state.edgeCount
|
||||
|
||||
super.setState(state)
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
return this.dirty
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.props.performanceCallback) {
|
||||
const renderTime = performance.now() - this.willUpdateTime
|
||||
this.props.performanceCallback(renderTime)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUpdate() {
|
||||
if (this.props.performanceCallback) {
|
||||
this.willUpdateTime = performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
private collapsed() {
|
||||
if (this.state.collapsedOverride !== undefined) {
|
||||
return this.state.collapsedOverride
|
||||
}
|
||||
|
||||
return this.state.collapsed
|
||||
}
|
||||
|
||||
private renderNodes() {
|
||||
const edges = Object.values(this.props.treeNode.edges)
|
||||
const listItemStyle = {
|
||||
padding: '3px 8px 0px 8px',
|
||||
}
|
||||
|
||||
const listStyle = {
|
||||
padding: '3px 8px 0px 16px',
|
||||
}
|
||||
|
||||
if (edges.length > 0) {
|
||||
const listItems = edges
|
||||
.map(edge => edge.target)
|
||||
.map(node => (
|
||||
<ListItem key={node.hash()} style={listItemStyle} button>
|
||||
<TreeNode
|
||||
theme={this.props.theme}
|
||||
didSelectNode={this.props.didSelectNode}
|
||||
treeNode={node}
|
||||
/>
|
||||
</ListItem>
|
||||
))
|
||||
|
||||
return <Collapse in={!this.collapsed()} timeout="auto" unmountOnExit>
|
||||
<List style={listStyle}>{listItems}</List>
|
||||
</Collapse>
|
||||
}
|
||||
}
|
||||
|
||||
private renderSourceEdge() {
|
||||
const style: React.CSSProperties = {
|
||||
fontWeight: 'bold',
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
}
|
||||
const name = this.state.title || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
|
||||
|
||||
return <span style={style} onClick={() => {
|
||||
this.toggle()
|
||||
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
|
||||
}>{name}</span>
|
||||
}
|
||||
|
||||
public componentWillReceiveProps() {
|
||||
const edgeCount = Object.keys(this.props.treeNode.edges).length
|
||||
this.setState({ edgeCount, collapsed: edgeCount > collapseLimit })
|
||||
}
|
||||
|
||||
private renderValue() {
|
||||
const style: React.CSSProperties = {
|
||||
width: '15em',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '0',
|
||||
paddingLeft: '5px',
|
||||
display: 'inline-block',
|
||||
}
|
||||
return this.props.treeNode.message
|
||||
? <span
|
||||
style={style}
|
||||
onMouseOver={() => this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)}
|
||||
> = {this.props.treeNode.message.value.toString()}</span>
|
||||
: null
|
||||
}
|
||||
|
||||
private clear() {
|
||||
return <div style={{ clear: 'both' }} />
|
||||
}
|
||||
|
||||
private renderTitleLine() {
|
||||
const style: React.CSSProperties = {
|
||||
lineHeight: '1em',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '15em',
|
||||
}
|
||||
return <div
|
||||
ref={this.titleRef}
|
||||
>
|
||||
<Typography style={style}>
|
||||
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
|
||||
public render() {
|
||||
this.dirty = false
|
||||
|
||||
const nodeStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
}
|
||||
|
||||
return <div key={this.props.treeNode.hash()} style={ this.getStyles().container }>
|
||||
{ this.renderTitleLine() }
|
||||
<div style = { nodeStyle }>
|
||||
{ this.clear() }
|
||||
<div style = { this.subnodesStyle() }>
|
||||
{ this.collapsed() ? null : this.renderNodes() }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
private toggle() {
|
||||
this.setState({ collapsedOverride: !this.collapsed() })
|
||||
}
|
||||
|
||||
private renderExpander() {
|
||||
if (this.state.edgeCount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.collapsed()
|
||||
? <span onClick={() => this.toggle()}>▶</span>
|
||||
: <span onClick={() => this.toggle()}>▼</span>
|
||||
}
|
||||
|
||||
private renderCollapsedSubnodes() {
|
||||
if (this.state.edgeCount === 0 || !this.collapsed()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const messages = this.props.treeNode.leafes().map(leaf => leaf.messages).reduce((a, b) => a + b)
|
||||
return <span style={this.getStyles().collapsedSubnodes}>({this.props.treeNode.leafes().length} nodes, {messages} messages)</span>
|
||||
}
|
||||
|
||||
private subnodesStyle(): React.CSSProperties {
|
||||
return {
|
||||
display: 'block',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme()(TreeNode)
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Edge, Message } from './'
|
||||
import { EventEmitter } from 'events'
|
||||
|
||||
export enum TreeNodeUpdateEvents {
|
||||
edges = 'edges',
|
||||
message = 'message',
|
||||
merge = 'merge',
|
||||
}
|
||||
|
||||
export class TreeNode extends EventEmitter {
|
||||
public sourceEdge?: Edge
|
||||
public message?: Message
|
||||
@@ -19,6 +25,10 @@ export class TreeNode extends EventEmitter {
|
||||
this.setMessage(message)
|
||||
}
|
||||
|
||||
private propagateUpdate(event: TreeNodeUpdateEvents) {
|
||||
this.emit(event)
|
||||
}
|
||||
|
||||
public setMessage(value: any) {
|
||||
this.message = value
|
||||
this.messages += 1
|
||||
@@ -43,10 +53,13 @@ export class TreeNode extends EventEmitter {
|
||||
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
|
||||
}
|
||||
|
||||
public addEdge(edge: Edge) {
|
||||
public addEdge(edge: Edge, emitUpdate: boolean = false) {
|
||||
this.edges[edge.name] = edge
|
||||
edge.source = this
|
||||
this.emit('update')
|
||||
|
||||
if (emitUpdate) {
|
||||
this.propagateUpdate(TreeNodeUpdateEvents.edges)
|
||||
}
|
||||
}
|
||||
|
||||
public branch(): TreeNode[] {
|
||||
@@ -61,10 +74,11 @@ export class TreeNode extends EventEmitter {
|
||||
public updateWithNode(node: TreeNode) {
|
||||
if (node.message) {
|
||||
this.setMessage(node.message)
|
||||
this.propagateUpdate(TreeNodeUpdateEvents.message)
|
||||
}
|
||||
|
||||
this.mergeEdges(node)
|
||||
this.emit('update')
|
||||
this.propagateUpdate(TreeNodeUpdateEvents.merge)
|
||||
}
|
||||
|
||||
public leafes(): TreeNode[] {
|
||||
@@ -79,13 +93,20 @@ export class TreeNode extends EventEmitter {
|
||||
|
||||
private mergeEdges(node: TreeNode) {
|
||||
const edgeKeys = Object.keys(node.edges)
|
||||
let edgesDidUpdate = false
|
||||
|
||||
for (const edgeKey of edgeKeys) {
|
||||
const matchingEdge = this.edges[edgeKey]
|
||||
if (matchingEdge) {
|
||||
matchingEdge.target.updateWithNode(node.edges[edgeKey].target)
|
||||
} else {
|
||||
this.addEdge(node.edges[edgeKey])
|
||||
this.addEdge(node.edges[edgeKey], false)
|
||||
edgesDidUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
if (edgesDidUpdate) {
|
||||
this.propagateUpdate(TreeNodeUpdateEvents.edges)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { Edge } from './Edge'
|
||||
export { TreeNode } from './TreeNode'
|
||||
export { TreeNode, TreeNodeUpdateEvents } from './TreeNode'
|
||||
export { Message } from './Message'
|
||||
export { TreeNodeFactory } from './TreeNodeFactory'
|
||||
export { Tree } from './Tree'
|
||||
|
||||
@@ -4,7 +4,7 @@ const http = require('http')
|
||||
import { TopicProperties, Tree, TreeNodeFactory } from './Model'
|
||||
import { MqttSource, DataSource } from './DataSource'
|
||||
|
||||
const options = { url: 'mqtt://test.mosquitto.org' }
|
||||
const options = { url: 'mqtt://nodered' }
|
||||
const dataSource = new MqttSource()
|
||||
|
||||
const a: any[] = []
|
||||
|
||||
Reference in New Issue
Block a user