// @flow // Copyright 2017 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // The go-ethereum library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . import React, {Component} from 'react'; import withStyles from 'material-ui/styles/withStyles'; import Header from './Header'; import Body from './Body'; import Footer from './Footer'; 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 = (prev: Object, update: Object, updater: Object) => { if (typeof update === 'undefined') { // TODO (kurkomisi): originally this was deep copy, investigate it. return prev; } if (typeof updater === 'function') { return updater(prev, update); } const updated = {}; Object.keys(prev).forEach((key) => { updated[key] = deepUpdate(prev[key], update[key], updater[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 = (msg: Object, updater: Object) => { const su = {}; Object.keys(msg).forEach((key) => { su[key] = typeof updater[key] !== 'function' ? shouldUpdate(msg[key], updater[key]) : true; }); return su; }; // appender is a state update generalization function, which appends the update data // to the existing data. limit defines the maximum allowed size of the created array. const appender = (limit: number) => (prev: Array, update: Array) => [...prev, ...update].slice(-limit); // replacer is a state update generalization function, which replaces the original data. const replacer = (prev: T, update: T) => update; // defaultContent is the initial value of the state content. const defaultContent: Content = { general: { version: null, commit: null, }, home: { memory: [], traffic: [], }, chain: {}, txpool: {}, network: {}, system: {}, logs: { log: [], }, }; // updaters contains the state update generalization functions for each path of the state. // TODO (kurkomisi): Define a tricky type which embraces the content and the handlers. const updaters = { general: { version: replacer, commit: replacer, }, home: { memory: appender(200), traffic: appender(200), }, chain: null, txpool: null, network: null, system: null, logs: { log: appender(200), }, }; // styles returns the styles for the Dashboard component. const styles = theme => ({ dashboard: { display: 'flex', flexFlow: 'column', width: '100%', height: '100%', background: theme.palette.background.default, zIndex: 1, overflow: 'hidden', }, }); export type Props = { classes: Object, }; 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 rerender based on the incoming message }; // 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: 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 = 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) => { this.setState(prevState => ({ content: deepUpdate(prevState.content, msg, updaters), shouldUpdate: shouldUpdate(msg, updaters), })); }; // changeContent sets the active label, which is used at the content rendering. changeContent = (newActive: string) => { this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {})); }; // openSideBar opens the sidebar. openSideBar = () => { this.setState({sideBar: true}); }; // closeSideBar closes the sidebar. closeSideBar = () => { this.setState({sideBar: false}); }; render() { const {classes} = this.props; // The classes property is injected by withStyles(). return (
); } } export default withStyles(styles)(Dashboard);