Tweak performance
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
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 { Tree } from './components/Tree'
|
import { Tree } from './components/Tree/Tree'
|
||||||
import TitleBar from './components/TitleBar'
|
import TitleBar from './components/TitleBar'
|
||||||
import Sidebar from './components/Sidebar/Sidebar'
|
import Sidebar from './components/Sidebar/Sidebar'
|
||||||
|
|
||||||
|
|||||||
@@ -22,17 +22,17 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Sidebar extends React.Component<Props, 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) {
|
constructor(props: any) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {}
|
this.state = { node: new q.Tree() }
|
||||||
this.updateNode = (node) => {
|
|
||||||
if (!node) {
|
|
||||||
this.setState(this.state)
|
|
||||||
} else {
|
|
||||||
this.setState({ node })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
|
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
|
||||||
@@ -52,9 +52,19 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public componentWillReceiveProps(nextProps: Props) {
|
public componentWillReceiveProps(nextProps: Props) {
|
||||||
this.props.node && this.props.node.removeListener('update', this.updateNode)
|
this.props.node && this.removeUpdateListener(this.props.node)
|
||||||
nextProps.node && nextProps.node.on('update', this.updateNode)
|
nextProps.node && this.registerUpdateListener(nextProps.node)
|
||||||
nextProps.node && this.updateNode(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 {
|
private open(): boolean {
|
||||||
@@ -79,7 +89,7 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
<Typography className={classes.heading}>Topic</Typography>
|
<Typography className={classes.heading}>Topic</Typography>
|
||||||
</ExpansionPanelSummary>
|
</ExpansionPanelSummary>
|
||||||
<ExpansionPanelDetails>
|
<ExpansionPanelDetails>
|
||||||
<Topic node={this.state.node} />
|
<Topic node={this.props.node} didSelectNode={this.updateNode} />
|
||||||
</ExpansionPanelDetails>
|
</ExpansionPanelDetails>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
<ExpansionPanel key="value" defaultExpanded={true}>
|
<ExpansionPanel key="value" defaultExpanded={true}>
|
||||||
@@ -90,7 +100,7 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
<ValueRenderer node={this.state.node} />
|
<ValueRenderer node={this.state.node} />
|
||||||
</ExpansionPanelDetails>
|
</ExpansionPanelDetails>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
<ExpansionPanel key="stats" defaultExpanded={true}>
|
<ExpansionPanel defaultExpanded={true}>
|
||||||
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
|
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
|
||||||
<Typography className={classes.heading}>Stats</Typography>
|
<Typography className={classes.heading}>Stats</Typography>
|
||||||
</ExpansionPanelSummary>
|
</ExpansionPanelSummary>
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import Button from '@material-ui/core/Button'
|
|||||||
interface Props {
|
interface Props {
|
||||||
classes: any
|
classes: any
|
||||||
theme: Theme
|
theme: Theme
|
||||||
node: q.TreeNode
|
node?: q.TreeNode
|
||||||
selected?: q.TreeNode
|
selected?: q.TreeNode
|
||||||
|
didSelectNode: (node: q.TreeNode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
class Topic extends React.Component<Props, {}> {
|
class Topic extends React.Component<Props, {}> {
|
||||||
@@ -21,7 +22,11 @@ class Topic extends React.Component<Props, {}> {
|
|||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { node } = this.props
|
const { node } = this.props
|
||||||
let i = 0
|
if (!node) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = 0
|
||||||
const breadCrumps = node.branch()
|
const breadCrumps = node.branch()
|
||||||
.map(node => node.sourceEdge)
|
.map(node => node.sourceEdge)
|
||||||
.filter(edge => Boolean(edge))
|
.filter(edge => Boolean(edge))
|
||||||
@@ -42,7 +47,7 @@ class Topic extends React.Component<Props, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const joinedBreadCrumps = breadCrumps.reduce((prev, current) =>
|
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>
|
return <span style={{ lineHeight: '2.2em' }}>{joinedBreadCrumps}</span>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as io from 'socket.io-client'
|
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 TreeNode from './TreeNode'
|
||||||
import List from '@material-ui/core/List'
|
import List from '@material-ui/core/List'
|
||||||
|
|
||||||
const throttle = require('lodash.throttle')
|
|
||||||
|
|
||||||
class TreeState {
|
class TreeState {
|
||||||
public tree: q.Tree
|
public tree: q.Tree
|
||||||
public msg: any
|
public msg: any
|
||||||
@@ -22,7 +20,7 @@ declare const performance: any
|
|||||||
|
|
||||||
export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
||||||
private socket: SocketIOClient.Socket
|
private socket: SocketIOClient.Socket
|
||||||
private renderDuration: number = 200
|
private renderDuration: number = 300
|
||||||
private updateTimer?: any
|
private updateTimer?: any
|
||||||
private lastUpdate: number = 0
|
private lastUpdate: number = 0
|
||||||
private perf:number = 0
|
private perf:number = 0
|
||||||
@@ -46,7 +44,7 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateInterval = Math.max(this.renderDuration * 5, 200)
|
const updateInterval = Math.max(this.renderDuration * 5, 300)
|
||||||
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
|
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
|
||||||
|
|
||||||
this.updateTimer = setTimeout(() => {
|
this.updateTimer = setTimeout(() => {
|
||||||
@@ -75,6 +73,8 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
|
|||||||
return <div>
|
return <div>
|
||||||
<List>
|
<List>
|
||||||
<TreeNode
|
<TreeNode
|
||||||
|
animateChages={true}
|
||||||
|
autoExpandLimit={3}
|
||||||
isRoot={true}
|
isRoot={true}
|
||||||
didSelectNode={this.props.didSelectNode}
|
didSelectNode={this.props.didSelectNode}
|
||||||
treeNode={this.state.tree}
|
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 { Edge, Message } from './'
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
|
|
||||||
|
export enum TreeNodeUpdateEvents {
|
||||||
|
edges = 'edges',
|
||||||
|
message = 'message',
|
||||||
|
merge = 'merge',
|
||||||
|
}
|
||||||
|
|
||||||
export class TreeNode extends EventEmitter {
|
export class TreeNode extends EventEmitter {
|
||||||
public sourceEdge?: Edge
|
public sourceEdge?: Edge
|
||||||
public message?: Message
|
public message?: Message
|
||||||
@@ -19,6 +25,10 @@ export class TreeNode extends EventEmitter {
|
|||||||
this.setMessage(message)
|
this.setMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private propagateUpdate(event: TreeNodeUpdateEvents) {
|
||||||
|
this.emit(event)
|
||||||
|
}
|
||||||
|
|
||||||
public setMessage(value: any) {
|
public setMessage(value: any) {
|
||||||
this.message = value
|
this.message = value
|
||||||
this.messages += 1
|
this.messages += 1
|
||||||
@@ -43,10 +53,13 @@ export class TreeNode extends EventEmitter {
|
|||||||
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
|
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
public addEdge(edge: Edge) {
|
public addEdge(edge: Edge, emitUpdate: boolean = false) {
|
||||||
this.edges[edge.name] = edge
|
this.edges[edge.name] = edge
|
||||||
edge.source = this
|
edge.source = this
|
||||||
this.emit('update')
|
|
||||||
|
if (emitUpdate) {
|
||||||
|
this.propagateUpdate(TreeNodeUpdateEvents.edges)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public branch(): TreeNode[] {
|
public branch(): TreeNode[] {
|
||||||
@@ -61,10 +74,11 @@ export class TreeNode extends EventEmitter {
|
|||||||
public updateWithNode(node: TreeNode) {
|
public updateWithNode(node: TreeNode) {
|
||||||
if (node.message) {
|
if (node.message) {
|
||||||
this.setMessage(node.message)
|
this.setMessage(node.message)
|
||||||
|
this.propagateUpdate(TreeNodeUpdateEvents.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mergeEdges(node)
|
this.mergeEdges(node)
|
||||||
this.emit('update')
|
this.propagateUpdate(TreeNodeUpdateEvents.merge)
|
||||||
}
|
}
|
||||||
|
|
||||||
public leafes(): TreeNode[] {
|
public leafes(): TreeNode[] {
|
||||||
@@ -79,13 +93,20 @@ export class TreeNode extends EventEmitter {
|
|||||||
|
|
||||||
private mergeEdges(node: TreeNode) {
|
private mergeEdges(node: TreeNode) {
|
||||||
const edgeKeys = Object.keys(node.edges)
|
const edgeKeys = Object.keys(node.edges)
|
||||||
|
let edgesDidUpdate = false
|
||||||
|
|
||||||
for (const edgeKey of edgeKeys) {
|
for (const edgeKey of edgeKeys) {
|
||||||
const matchingEdge = this.edges[edgeKey]
|
const matchingEdge = this.edges[edgeKey]
|
||||||
if (matchingEdge) {
|
if (matchingEdge) {
|
||||||
matchingEdge.target.updateWithNode(node.edges[edgeKey].target)
|
matchingEdge.target.updateWithNode(node.edges[edgeKey].target)
|
||||||
} else {
|
} 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 { Edge } from './Edge'
|
||||||
export { TreeNode } from './TreeNode'
|
export { TreeNode, TreeNodeUpdateEvents } from './TreeNode'
|
||||||
export { Message } from './Message'
|
export { Message } from './Message'
|
||||||
export { TreeNodeFactory } from './TreeNodeFactory'
|
export { TreeNodeFactory } from './TreeNodeFactory'
|
||||||
export { Tree } from './Tree'
|
export { Tree } from './Tree'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const http = require('http')
|
|||||||
import { TopicProperties, Tree, TreeNodeFactory } from './Model'
|
import { TopicProperties, Tree, TreeNodeFactory } from './Model'
|
||||||
import { MqttSource, DataSource } from './DataSource'
|
import { MqttSource, DataSource } from './DataSource'
|
||||||
|
|
||||||
const options = { url: 'mqtt://test.mosquitto.org' }
|
const options = { url: 'mqtt://nodered' }
|
||||||
const dataSource = new MqttSource()
|
const dataSource = new MqttSource()
|
||||||
|
|
||||||
const a: any[] = []
|
const a: any[] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user