diff options
Diffstat (limited to 'dashboard/assets/components/Dashboard.jsx')
-rw-r--r-- | dashboard/assets/components/Dashboard.jsx | 360 |
1 files changed, 213 insertions, 147 deletions
diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx index 740acf959..90b1a785c 100644 --- a/dashboard/assets/components/Dashboard.jsx +++ b/dashboard/assets/components/Dashboard.jsx @@ -1,3 +1,5 @@ +// @flow + // Copyright 2017 The go-ethereum Authors // This file is part of the go-ethereum library. // @@ -15,155 +17,219 @@ // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import {withStyles} from 'material-ui/styles'; - -import SideBar from './SideBar.jsx'; -import Header from './Header.jsx'; -import Main from "./Main.jsx"; -import {isNullOrUndefined, LIMIT, TAGS, DATA_KEYS,} from "./Common.jsx"; - -// Styles for the Dashboard component. -const styles = theme => ({ - appFrame: { - position: 'relative', - display: 'flex', - width: '100%', - height: '100%', - background: theme.palette.background.default, - }, + +import withStyles from 'material-ui/styles/withStyles'; + +import Header from './Header'; +import Body from './Body'; +import {MENU} from '../common'; +import type {Content} from '../types/content'; + +// deepUpdate updates an object corresponding to the given update data, which has +// the shape of the same structure as the original object. updater also has the same +// structure, except that it contains functions where the original data needs to be +// updated. These functions are used to handle the update. +// +// Since the messages have the same shape as the state content, this approach allows +// the generalization of the message handling. The only necessary thing is to set a +// handler function for every path of the state in order to maximize the flexibility +// of the update. +const deepUpdate = (updater: Object, update: Object, prev: Object): $Shape<Content> => { + if (typeof update === 'undefined') { + // TODO (kurkomisi): originally this was deep copy, investigate it. + return prev; + } + if (typeof updater === 'function') { + return updater(update, prev); + } + const updated = {}; + Object.keys(prev).forEach((key) => { + updated[key] = deepUpdate(updater[key], update[key], prev[key]); + }); + + return updated; +}; + +// shouldUpdate returns the structure of a message. It is used to prevent unnecessary render +// method triggerings. In the affected component's shouldComponentUpdate method it can be checked +// whether the involved data was changed or not by checking the message structure. +// +// We could return the message itself too, but it's safer not to give access to it. +const shouldUpdate = (updater: Object, msg: Object) => { + const su = {}; + Object.keys(msg).forEach((key) => { + su[key] = typeof updater[key] !== 'function' ? shouldUpdate(updater[key], msg[key]) : true; + }); + + return su; +}; + +// replacer is a state updater function, which replaces the original data. +const replacer = <T>(update: T) => update; + +// appender is a state updater function, which appends the update data to the +// existing data. limit defines the maximum allowed size of the created array, +// mapper maps the update data. +const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, prev: Array<T>) => [ + ...prev, + ...update.map(sample => mapper(sample)), +].slice(-limit); + +// defaultContent is the initial value of the state content. +const defaultContent: Content = { + general: { + version: null, + commit: null, + }, + home: { + activeMemory: [], + virtualMemory: [], + networkIngress: [], + networkEgress: [], + processCPU: [], + systemCPU: [], + diskRead: [], + diskWrite: [], + }, + chain: {}, + txpool: {}, + network: {}, + system: {}, + logs: { + log: [], + }, +}; + +// updaters contains the state updater functions for each path of the state. +// +// TODO (kurkomisi): Define a tricky type which embraces the content and the updaters. +const updaters = { + general: { + version: replacer, + commit: replacer, + }, + home: { + activeMemory: appender(200), + virtualMemory: appender(200), + networkIngress: appender(200), + networkEgress: appender(200), + processCPU: appender(200), + systemCPU: appender(200), + diskRead: appender(200), + diskWrite: appender(200), + }, + chain: null, + txpool: null, + network: null, + system: null, + logs: { + log: appender(200), + }, +}; + +// styles contains the constant styles of the component. +const styles = { + dashboard: { + display: 'flex', + flexFlow: 'column', + width: '100%', + height: '100%', + zIndex: 1, + overflow: 'hidden', + } +}; + +// themeStyles returns the styles generated from the theme for the component. +const themeStyles: Object = (theme: Object) => ({ + dashboard: { + background: theme.palette.background.default, + }, }); -// Dashboard is the main component, which renders the whole page, makes connection with the server and listens for messages. -// When there is an incoming message, updates the page's content correspondingly. -class Dashboard extends Component { - constructor(props) { - super(props); - this.state = { - active: TAGS.home.id, // active menu - sideBar: true, // true if the sidebar is opened - memory: [], - traffic: [], - logs: [], - shouldUpdate: {}, - }; - } - - // componentDidMount initiates the establishment of the first websocket connection after the component is rendered. - componentDidMount() { - this.reconnect(); - } - - // reconnect establishes a websocket connection with the server, listens for incoming messages - // and tries to reconnect on connection loss. - reconnect = () => { - const server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api"); - - server.onmessage = event => { - const msg = JSON.parse(event.data); - if (isNullOrUndefined(msg)) { - return; - } - this.update(msg); - }; - - server.onclose = () => { - setTimeout(this.reconnect, 3000); - }; - }; - - // update analyzes the incoming message, and updates the charts' content correspondingly. - update = msg => { - console.log(msg); - this.setState(prevState => { - let newState = []; - newState.shouldUpdate = {}; - const insert = (key, values, limit) => { - newState[key] = [...prevState[key], ...values]; - while (newState[key].length > limit) { - newState[key].shift(); - } - newState.shouldUpdate[key] = true; - }; - // (Re)initialize the state with the past data. - if (!isNullOrUndefined(msg.history)) { - const memory = DATA_KEYS.memory; - const traffic = DATA_KEYS.traffic; - newState[memory] = []; - newState[traffic] = []; - if (!isNullOrUndefined(msg.history.memorySamples)) { - newState[memory] = msg.history.memorySamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value); - while (newState[memory].length > LIMIT.memory) { - newState[memory].shift(); - } - newState.shouldUpdate[memory] = true; - } - if (!isNullOrUndefined(msg.history.trafficSamples)) { - newState[traffic] = msg.history.trafficSamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value); - while (newState[traffic].length > LIMIT.traffic) { - newState[traffic].shift(); - } - newState.shouldUpdate[traffic] = true; - } - } - // Insert the new data samples. - if (!isNullOrUndefined(msg.memory)) { - insert(DATA_KEYS.memory, [isNullOrUndefined(msg.memory.value) ? 0 : msg.memory.value], LIMIT.memory); - } - if (!isNullOrUndefined(msg.traffic)) { - insert(DATA_KEYS.traffic, [isNullOrUndefined(msg.traffic.value) ? 0 : msg.traffic.value], LIMIT.traffic); - } - if (!isNullOrUndefined(msg.log)) { - insert(DATA_KEYS.logs, [msg.log], LIMIT.log); - } - - return newState; - }); - }; - - // The change of the active label on the SideBar component will trigger a new render in the Main component. - changeContent = active => { - this.setState(prevState => prevState.active !== active ? {active: active} : {}); - }; - - openSideBar = () => { - this.setState({sideBar: true}); - }; - - closeSideBar = () => { - this.setState({sideBar: false}); - }; - - render() { - // The classes property is injected by withStyles(). - const {classes} = this.props; - - return ( - <div className={classes.appFrame}> - <Header - opened={this.state.sideBar} - open={this.openSideBar} - /> - <SideBar - opened={this.state.sideBar} - close={this.closeSideBar} - changeContent={this.changeContent} - /> - <Main - opened={this.state.sideBar} - active={this.state.active} - memory={this.state.memory} - traffic={this.state.traffic} - logs={this.state.logs} - shouldUpdate={this.state.shouldUpdate} - /> - </div> - ); - } -} +export type Props = { + classes: Object, // injected by withStyles() +}; -Dashboard.propTypes = { - classes: PropTypes.object.isRequired, +type State = { + active: string, // active menu + sideBar: boolean, // true if the sidebar is opened + content: Content, // the visualized data + shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message }; -export default withStyles(styles)(Dashboard); +// Dashboard is the main component, which renders the whole page, makes connection with the server and +// listens for messages. When there is an incoming message, updates the page's content correspondingly. +class Dashboard extends Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + active: MENU.get('home').id, + sideBar: true, + content: defaultContent, + shouldUpdate: {}, + }; + } + + // componentDidMount initiates the establishment of the first websocket connection after the component is rendered. + componentDidMount() { + this.reconnect(); + } + + // reconnect establishes a websocket connection with the server, listens for incoming messages + // and tries to reconnect on connection loss. + reconnect = () => { + const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`); + server.onopen = () => { + this.setState({content: defaultContent, shouldUpdate: {}}); + }; + server.onmessage = (event) => { + const msg: $Shape<Content> = JSON.parse(event.data); + if (!msg) { + console.error(`Incoming message is ${msg}`); + return; + } + this.update(msg); + }; + server.onclose = () => { + setTimeout(this.reconnect, 3000); + }; + }; + + // update updates the content corresponding to the incoming message. + update = (msg: $Shape<Content>) => { + this.setState(prevState => ({ + content: deepUpdate(updaters, msg, prevState.content), + shouldUpdate: shouldUpdate(updaters, msg), + })); + }; + + // changeContent sets the active label, which is used at the content rendering. + changeContent = (newActive: string) => { + this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {})); + }; + + // switchSideBar opens or closes the sidebar's state. + switchSideBar = () => { + this.setState(prevState => ({sideBar: !prevState.sideBar})); + }; + + render() { + return ( + <div className={this.props.classes.dashboard} style={styles.dashboard}> + <Header + opened={this.state.sideBar} + switchSideBar={this.switchSideBar} + /> + <Body + opened={this.state.sideBar} + changeContent={this.changeContent} + active={this.state.active} + content={this.state.content} + shouldUpdate={this.state.shouldUpdate} + /> + </div> + ); + } +} + +export default withStyles(themeStyles)(Dashboard); |