Improve ui & performance

This commit is contained in:
Thomas Nordquist
2019-01-03 11:14:38 +01:00
parent 2b7e9a5ef7
commit 87dafc9c89
18 changed files with 676 additions and 364 deletions

View File

@@ -3,7 +3,7 @@ import * as q from '../../backend/src/Model'
import { Tree } from './components/Tree'
import TitleBar from './components/TitleBar'
import { Sidebar } from './components/Sidebar'
import Sidebar from './components/Sidebar/Sidebar'
import { withTheme, Theme } from '@material-ui/core/styles'
@@ -12,6 +12,7 @@ class State {
}
interface Props {
name: string
theme: Theme
}
@@ -21,34 +22,47 @@ class App extends React.Component<Props, State> {
this.state = {
selectedNode: undefined,
}
}
console.log('asd', this.props)
this.theme = this.props.theme
this.styles = {
primaryText: {
backgroundColor: this.theme.palette.background.default,
padding: `${this.theme.spacing.unit}px ${this.theme.spacing.unit * 2}px`,
color: this.theme.palette.text.primary,
private getStyles(): {[s: string]: React.CSSProperties} {
const { theme } = this.props
return {
left: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
height: 'calc(100vh - 64px)',
float: 'left',
width: '60vw',
overflowY: 'scroll',
overflowX: 'hidden',
display: 'block',
},
primaryColor: {
backgroundColor: this.theme.palette.background.default,
// padding: `${this.theme.spacing.unit}px ${this.theme.spacing.unit * 2}px`,
color: this.theme.palette.common.white,
right: {
height: 'calc(100vh - 64px)',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
float: 'right', width: '40vw',
overflowY: 'scroll',
overflowX: 'hidden',
display: 'block',
},
}
}
private theme: Theme
private styles: any
public render() {
return <div style={this.styles.primaryColor}>
return <div>
<TitleBar />
<Tree didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node })
console.log('did select', node)
}} />
</div>
<div>
<div style={this.getStyles().left}>
<Tree didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node })
}} />
</div>
<div style={this.getStyles().right}>
<Sidebar node={this.state.selectedNode} />
</div>
</div>
</div >
}
}

View File

@@ -1,66 +0,0 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import Drawer from '@material-ui/core/Drawer'
import TextField from '@material-ui/core/TextField'
import Paper from '@material-ui/core/Paper'
import { ValueRenderer } from './ValueRenderer'
interface Props {
node?: q.TreeNode | undefined
}
interface State {
node?: q.TreeNode | undefined
}
export class Sidebar extends React.Component<Props, State> {
private updateNode: (node?: q.TreeNode | undefined) => void
constructor(props: any) {
super(props)
this.state = {}
this.updateNode = (node) => {
if (!node) {
this.setState(this.state)
} else {
this.setState({ node })
}
}
}
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)
}
private open() {
return this.props.node !== undefined
}
public render() {
return <Drawer open={this.open()} variant="permanent" anchor="right">
{this.renderNode()}
</Drawer>
}
private renderNode() {
const style: React.CSSProperties = { display: 'block', width: '40vw' }
const topicStyle: React.CSSProperties = { width: '100%' }
if (!this.state.node) {
return null
}
return <div style={style}>
<TextField style={topicStyle}
label="Topic"
value={this.state.node.path()}
margin="normal"
variant="outlined"
/>
<Paper>
<ValueRenderer node={this.state.node} />
</Paper>
</div>
}
}

View File

@@ -0,0 +1,41 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
// import Drawer from '@material-ui/core/Drawer'
import { Typography } from '@material-ui/core'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
interface Props {
node: q.TreeNode,
classes: any,
theme: Theme
}
interface State {
node?: q.TreeNode | undefined
}
class NodeStats extends React.Component<Props, State> {
constructor(props: any) {
super(props)
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
}
}
public render() {
const leafes = this.props.node.leafes()
const leafMessages = leafes
.map(leaf => leaf.messages)
.reduce((a, b) => a + b)
return <Typography>
<p>Messages: #{this.props.node.messages}</p>
<p>Subtopics: {leafes.length}</p>
<p>Messages Subtopics: #{leafMessages}</p>
</Typography>
}
}
export default withStyles(NodeStats.styles, { withTheme: true })(NodeStats)

View File

@@ -0,0 +1,105 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
// import Drawer from '@material-ui/core/Drawer'
import ExpansionPanel from '@material-ui/core/ExpansionPanel'
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary'
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'
import ExpandMore from '@material-ui/icons/ExpandMore'
import ValueRenderer from './ValueRenderer'
import NodeStats from './NodeStats'
import Topic from './Topic'
import { Typography } from '@material-ui/core'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
interface Props {
node?: q.TreeNode | undefined,
classes: any,
theme: Theme
}
interface State {
node?: q.TreeNode | undefined
}
class Sidebar extends React.Component<Props, State> {
private updateNode: (node?: q.TreeNode | undefined) => void
constructor(props: any) {
super(props)
this.state = {}
this.updateNode = (node) => {
if (!node) {
this.setState(this.state)
} else {
this.setState({ node })
}
}
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
drawer: {
display: 'block',
height: '100%',
},
valuePaper: {
margin: `${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px`,
},
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
}
}
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)
}
private open(): boolean {
return true
}
public render() {
return <div className={this.props.classes.drawer}>
{this.renderNode()}
</div>
}
private renderNode() {
const { classes } = this.props
if (!this.state.node) {
return null
}
return <div>
<ExpansionPanel key="topic" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Topic</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Topic node={this.state.node} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Value</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<ValueRenderer node={this.state.node} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="stats" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<NodeStats node={this.state.node} />
</ExpansionPanelDetails>
</ExpansionPanel>
</div>
}
}
export default withStyles(Sidebar.styles, { withTheme: true })(Sidebar)

View File

@@ -0,0 +1,52 @@
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'
interface Props {
classes: any
theme: Theme
node: q.TreeNode
selected?: q.TreeNode
}
class Topic extends React.Component<Props, {}> {
public static styles: StyleRulesCallback<string> = (theme: Theme) => ({
button: {
textTransform: 'none',
padding: '3px 5px 3px 5px',
minWidth: '30px',
},
})
public render() {
const { node } = this.props
let i = 0
const breadCrumps = node.branch()
.map(node => node.sourceEdge)
.filter(edge => Boolean(edge))
.map(edge =>
[<Button
onClick={() => this.setState({ node: edge!.target })}
size="small"
color="secondary"
className={this.props.classes.button}
key={edge!.hash()}
>
{edge!.name}
</Button>],
)
if (breadCrumps.length === 0) {
return null
}
const joinedBreadCrumps = breadCrumps.reduce((prev, current) =>
prev.concat([<span key={i += 1}>/</span>]).concat(current),
)
return <span style={{ lineHeight: '2.2em' }}>{joinedBreadCrumps}</span>
}
}
export default withStyles(Topic.styles, { withTheme: true })(Topic)

View File

@@ -1,16 +1,18 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import * as q from '../../../../backend/src/Model'
import { default as ReactJson } from 'react-json-view'
import { withTheme, Theme } from '@material-ui/core/styles'
interface Props {
node?: q.TreeNode | undefined
theme: Theme
}
interface State {
node?: q.TreeNode | undefined
}
export class ValueRenderer extends React.Component<Props, State> {
class ValueRenderer extends React.Component<Props, State> {
private updateNode: (node?: q.TreeNode | undefined) => void
constructor(props: any) {
super(props)
@@ -30,6 +32,10 @@ export class ValueRenderer extends React.Component<Props, State> {
nextProps.node && this.updateNode(nextProps.node)
}
private style = (theme: Theme) => {
}
public render() {
const node = this.props.node
if (!node || !node.message) {
@@ -48,7 +54,14 @@ export class ValueRenderer extends React.Component<Props, State> {
} else if (typeof json === 'number') {
return this.renderRawValue(node.message.value)
} else {
return <ReactJson src={json} />
const theme = this.props.theme.palette.type === 'dark' ? 'monokai' : 'bright:inverted'
return <ReactJson
style={{ width: '100%' }}
src={json}
theme={theme}
onEdit={(val) => {
console.log(val)
}} />
}
}
@@ -62,6 +75,8 @@ export class ValueRenderer extends React.Component<Props, State> {
padding: '12px 5px 12px 5px',
}
return <pre><code style={style}>{value}</code></pre>
return <pre style={style}><code>{value}</code></pre>
}
}
export default withTheme()(ValueRenderer)

View File

@@ -0,0 +1,3 @@
import Sidebar from './Sidebar'
export { Sidebar }

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import * as io from 'socket.io-client'
import * as q from '../../../backend/src/Model'
import { TreeNode } from './TreeNode'
import TreeNode from './TreeNode'
import List from '@material-ui/core/List'
const throttle = require('lodash.throttle')
@@ -18,10 +18,14 @@ class TreeState {
export interface TreeNodeProps {
didSelectNode?: (node: q.TreeNode) => void
}
declare const performance: any
export class Tree extends React.Component<TreeNodeProps, TreeState> {
private socket: SocketIOClient.Socket
private renderDuration: number = 300
private renderDuration: number = 200
private updateTimer?: any
private lastUpdate: number = 0
private perf:number = 0
constructor(props: any) {
super(props)
@@ -30,21 +34,36 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
this.socket = io('http://localhost:3000')
}
public componentDidMount() {
let updateState = throttle((state: any) => {
this.setState(state)
updateState.cancel()
updateState = throttle(() => {
this.setState(state)
}, Math.max(this.renderDuration * 5, 300), { trailing: true })
}, 1000)
public time(): number {
const time = performance.now() - this.perf
this.perf = performance.now()
return time
}
public throttledStateUpdate(state: any) {
if (this.updateTimer) {
return
}
const updateInterval = Math.max(this.renderDuration * 5, 200)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
this.updateTimer = setTimeout(() => {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(state)
}, Math.max(0, timeUntilNextUpdate))
}
public componentDidMount() {
this.socket.on('message', (msg: any) => {
const edges = msg.topic.split('/')
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString())
this.state.tree.updateWithNode(node.firstNode())
updateState({ msg, tree: this.state.tree })
this.throttledStateUpdate({ msg, tree: this.state.tree })
})
}
@@ -56,10 +75,14 @@ export class Tree extends React.Component<TreeNodeProps, TreeState> {
return <div>
<List>
<TreeNode
isRoot={true}
didSelectNode={this.props.didSelectNode}
treeNode={this.state.tree}
name="/" collapsed={false}
performanceCallback={ms => this.renderDuration = ms}
key="rootNode"
performanceCallback={(ms: number) => {
this.renderDuration = ms
}}
/>
</List>
</div>

View File

@@ -1,15 +1,21 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import Collapse from '@material-ui/core/Collapse'
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 {
@@ -19,28 +25,59 @@ interface TreeNodeState {
edgeCount: number
}
const collapseLimit = 0
declare var performance: any
export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
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()
}
}
constructor(props: TreeNodeProps, state: TreeNodeState) {
super(props, state)
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.props.treeNode.on('update', () => {
this.dirty = true
})
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 = true
this.dirty = this.state.collapsed !== state.collapsed
|| this.state.collapsedOverride !== state.collapsedOverride
|| this.state.edgeCount !== state.edgeCount
super.setState(state)
}
@@ -49,7 +86,6 @@ export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
}
public componentDidUpdate() {
this.dirty = false
if (this.props.performanceCallback) {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
@@ -73,18 +109,25 @@ export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
private renderNodes() {
const edges = Object.values(this.props.treeNode.edges)
const listItemStyle = {
padding: '3px 8px 3px 8px',
padding: '3px 8px 0px 8px',
}
const listStyle = {
padding: '3px 8px 3px 16px',
padding: '3px 8px 0px 16px',
}
if (edges.length > 0) {
const listItems = edges
.map(edge => edge.target)
.map(node => <ListItem style={listItemStyle} button key={node.hash()}>
<TreeNode didSelectNode={this.props.didSelectNode} treeNode={node} />
</ListItem>)
.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>
@@ -100,13 +143,10 @@ export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
}
const name = this.state.title || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
return <span style={style} onClick={() => this.toggle()}>{name}</span>
}
private getStyle(): React.CSSProperties {
return {
display: 'block',
}
return <span style={style} onClick={() => {
this.toggle()
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}>{name}</span>
}
public componentWillReceiveProps() {
@@ -128,7 +168,7 @@ export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
? <span
style={style}
onMouseOver={() => this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)}
> = {this.props.treeNode.message.toString()}</span>
> = {this.props.treeNode.message.value.toString()}</span>
: null
}
@@ -137,23 +177,33 @@ export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
}
private renderTitleLine() {
const style = {
const style: React.CSSProperties = {
lineHeight: '1em',
whiteSpace: 'nowrap',
width: '15em',
}
return <div style={style}>{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}</div>
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 style={this.getStyle()}>
{this.renderTitleLine()}
<div style={nodeStyle}>
{this.clear()}
<div style={this.subnodesStyle()}>
{this.collapsed() ? null : this.renderNodes()}
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>
@@ -178,10 +228,8 @@ export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
return null
}
const style = {
color: '#333',
}
return <span style={style}>({this.props.treeNode.leafes().length} nodes)</span>
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 {
@@ -190,3 +238,5 @@ export class TreeNode extends React.Component<TreeNodeProps, TreeNodeState> {
}
}
}
export default withTheme()(TreeNode)

View File

@@ -0,0 +1,12 @@
declare const window: any
declare const document: any
export function isElementInViewport(el: any) {
const rect = el.getBoundingClientRect()
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
)
}