diff --git a/app/package-lock.json b/app/package-lock.json index e533125..11f2758 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1167,6 +1167,11 @@ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, + "copy-text-to-clipboard": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-1.0.4.tgz", + "integrity": "sha512-4hDE+0bgqm4G/nXnt91CP3rc0vOptaePPU5WfVZuhv2AYNJogdLHR4pF1XPgXDAGY4QCzj9pD7zKATa+50sQPg==" + }, "core-js": { "version": "1.2.7", "resolved": "http://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", @@ -3522,6 +3527,11 @@ "run-queue": "^1.0.3" } }, + "moving-average": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/moving-average/-/moving-average-1.0.0.tgz", + "integrity": "sha512-97cgMz0U2zciiDp4xRl/n+MYgrm9l7UiYbtsBLPr0rhw6KH3m4LyK2w4d96V6+UwKo+ph7KtQSoL2qgnqZVgvA==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/app/package.json b/app/package.json index 7b0fe0a..1957c89 100644 --- a/app/package.json +++ b/app/package.json @@ -22,8 +22,10 @@ "@types/socket.io-client": "^1.4.32", "@types/vis": "^4.21.9", "awesome-typescript-loader": "^5.2.1", + "copy-text-to-clipboard": "^1.0.4", "jquery": "^3.3.1", "lodash.throttle": "^4.1.1", + "moving-average": "^1.0.0", "react": "^16.3.2", "react-dom": "^16.3.3", "react-json-view": "^1.19.1", diff --git a/app/src/components/Sidebar/NodeStats.tsx b/app/src/components/Sidebar/NodeStats.tsx index 93299c6..f31af58 100644 --- a/app/src/components/Sidebar/NodeStats.tsx +++ b/app/src/components/Sidebar/NodeStats.tsx @@ -25,15 +25,12 @@ class NodeStats extends React.Component { } public render() { - const leafes = this.props.node.leafes() - const leafMessages = leafes - .map(leaf => leaf.messages) - .reduce((a, b) => a + b) + const { node } = this.props return
- Messages: #{this.props.node.messages} - Subtopics: {leafes.length} - Messages Subtopics: #{leafMessages} + Messages: #{node.messages} + Subtopics: {node.leafCount()} + Messages Subtopics: #{node.leafMessageCount()}
} } diff --git a/app/src/components/Sidebar/Topic.tsx b/app/src/components/Sidebar/Topic.tsx index 0a80644..4359648 100644 --- a/app/src/components/Sidebar/Topic.tsx +++ b/app/src/components/Sidebar/Topic.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import * as q from '../../../../backend/src/Model' import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles' import Button from '@material-ui/core/Button' +const copy = require('copy-text-to-clipboard') interface Props { classes: any @@ -50,7 +51,10 @@ class Topic extends React.Component { prev.concat([/]).concat(current), ) - return {joinedBreadCrumps} + return + copy(this.props.node && this.props.node.path())}>📋 + {joinedBreadCrumps} + } } diff --git a/app/src/components/Tree/Tree.tsx b/app/src/components/Tree/Tree.tsx index b34e837..f89869c 100644 --- a/app/src/components/Tree/Tree.tsx +++ b/app/src/components/Tree/Tree.tsx @@ -1,11 +1,16 @@ import * as React from 'react' import * as q from '../../../../backend/src/Model' import TreeNode from './TreeNode' -import List from '@material-ui/core/List' +import { Typography } from '@material-ui/core' import { makeConnectionMessageEvent, rendererEvents } from '../../../../events' import { } from '../../../../events/Events' +const MovingAvaerage = require('moving-average') + declare const performance: any +const timeInterval = 10 * 1000 +const average = MovingAvaerage(timeInterval) + interface Props{ didSelectNode?: (node: q.TreeNode) => void connectionId?: string @@ -20,7 +25,7 @@ export class Tree extends React.Component { private renderDuration: number = 300 private updateTimer?: any private lastUpdate: number = 0 - private perf:number = 0 + private perf: number = 0 constructor(props: any) { super(props) @@ -40,7 +45,8 @@ export class Tree extends React.Component { return } - const updateInterval = Math.max(this.renderDuration * 5, 300) + const expectedRenderTime = average.forecast() + const updateInterval = Math.max(expectedRenderTime * 5, 300) const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate) this.updateTimer = setTimeout(() => { @@ -85,21 +91,20 @@ export class Tree extends React.Component { } public render() { - return
- + return { + average.push(Date.now(), ms) this.renderDuration = ms }} /> - -
+ } } diff --git a/app/src/components/Tree/TreeNode.tsx b/app/src/components/Tree/TreeNode.tsx index 7c1edda..60befdf 100644 --- a/app/src/components/Tree/TreeNode.tsx +++ b/app/src/components/Tree/TreeNode.tsx @@ -138,9 +138,10 @@ class TreeNode extends React.Component { this.dirtyState = this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false - return
-
this.toggle()}> + return
+ this.toggle()} edgeCount={this.state.edgeCount} collapsed={this.collapsed()} treeNode={this.props.treeNode} @@ -148,19 +149,11 @@ class TreeNode extends React.Component { didSelectNode={this.props.didSelectNode} toggleCollapsed={() => this.toggle()} /> -
- - { this.clear() } -
- {this.renderNodes()} -
+ + { this.renderNodes() }
} - private clear() { - return
- } - private renderNodes() { return { /> } - private indicatingChangeAnimationStyle() { + private indicatingChangeAnimationStyle(): React.CSSProperties { if (this.props.isRoot) { return {} } @@ -188,6 +181,8 @@ class TreeNode extends React.Component { } return { animation: 'example 0.5s' } } + + return {} } } diff --git a/app/src/components/Tree/TreeNodeSubnodes.tsx b/app/src/components/Tree/TreeNodeSubnodes.tsx index e540852..b794ebc 100644 --- a/app/src/components/Tree/TreeNodeSubnodes.tsx +++ b/app/src/components/Tree/TreeNodeSubnodes.tsx @@ -29,10 +29,9 @@ class TreeNodeSubnodes extends React.Component { const listItems = edges .map(edge => edge.target) .map(node => ( - { didSelectNode={this.props.didSelectNode} autoExpandLimit={this.props.autoExpandLimit} /> - +
)) - return - {listItems} - + return + {this.props.collapsed ? null : listItems} + } return null diff --git a/app/src/components/Tree/TreeNodeTitle.tsx b/app/src/components/Tree/TreeNodeTitle.tsx index 60e8eee..30054c4 100644 --- a/app/src/components/Tree/TreeNodeTitle.tsx +++ b/app/src/components/Tree/TreeNodeTitle.tsx @@ -3,8 +3,9 @@ import * as q from '../../../../backend/src/Model' import { Typography } from '@material-ui/core' import { withTheme, Theme } from '@material-ui/core/styles' -export interface TreeNodeProps { +export interface TreeNodeProps extends React.HTMLAttributes { treeNode: q.TreeNode + // ref: React.Ref name?: string | undefined collapsed?: boolean | undefined toggleCollapsed: () => void @@ -31,9 +32,14 @@ class TreeNodeTitle extends React.Component { lineHeight: '1em', whiteSpace: 'nowrap', } - return + return { + this.toggle() + this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode) + }}> {this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()} - + } private renderSourceEdge() { @@ -44,10 +50,7 @@ class TreeNodeTitle extends React.Component { } const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name) - return { - this.toggle() - this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode) - }}>{name} + return {name} } private renderValue() { @@ -77,9 +80,7 @@ class TreeNodeTitle extends React.Component { return null } - return this.props.collapsed - ? this.toggle()}>â–¶ - : this.toggle()}>â–¼ + return this.props.collapsed ? 'â–¶' : 'â–¼' } private renderCollapsedSubnodes() { @@ -87,8 +88,8 @@ class TreeNodeTitle extends React.Component { return null } - const messages = this.props.treeNode.leafes().map(leaf => leaf.messages).reduce((a, b) => a + b) - return ({this.props.treeNode.leafes().length} nodes, {messages} messages) + const messages = this.props.treeNode.leafMessageCount() + return ({this.props.treeNode.leafCount()} nodes, {messages} messages) } } diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index ec2a458..53338ca 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -11,6 +11,8 @@ export class TreeNode { public onMerge = new EventDispatcher(this) public onEdgesChange = new EventDispatcher(this) public onMessage = new EventDispatcher(this) + private cachedLeafes?: TreeNode[] + private cachedLeafMessageCount?: number constructor(sourceEdge?: Edge, message?: Message) { if (sourceEdge) { @@ -19,6 +21,10 @@ export class TreeNode { } this.setMessage(message) + this.onMerge.subscribe(() => { + this.cachedLeafes = undefined + this.cachedLeafMessageCount = undefined + }) } public setMessage(value: any) { @@ -73,14 +79,32 @@ export class TreeNode { this.onMerge.dispatch() } - public leafes(): TreeNode[] { - if (Object.values(this.edges).length === 0) { - return [this] + public leafMessageCount() { + if (this.cachedLeafMessageCount === undefined) { + this.cachedLeafMessageCount = this.leafes() + .map(leaf => leaf.messages) + .reduce((a, b) => a + b) } - return Object.values(this.edges) - .map(e => e.target.leafes()) - .reduce((a, b) => a.concat(b), []) + return this.cachedLeafMessageCount + } + + public leafCount(): number { + return this.leafes().length + } + + public leafes(): TreeNode[] { + if (this.cachedLeafes === undefined) { + if (Object.values(this.edges).length === 0) { + return [this] + } + + this.cachedLeafes = Object.values(this.edges) + .map(e => e.target.leafes()) + .reduce((a, b) => a.concat(b), []) + } + + return this.cachedLeafes } private mergeEdges(node: TreeNode) {