github.com/mandrigin/go-ethereum@v1.7.4-0.20180116162341-02aeb3d76652/dashboard/assets/components/Dashboard.jsx (about) 1 // @flow 2 3 // Copyright 2017 The go-ethereum Authors 4 // This file is part of the go-ethereum library. 5 // 6 // The go-ethereum library is free software: you can redistribute it and/or modify 7 // it under the terms of the GNU Lesser General Public License as published by 8 // the Free Software Foundation, either version 3 of the License, or 9 // (at your option) any later version. 10 // 11 // The go-ethereum library is distributed in the hope that it will be useful, 12 // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 // GNU Lesser General Public License for more details. 15 // 16 // You should have received a copy of the GNU Lesser General Public License 17 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 18 19 import React, {Component} from 'react'; 20 21 import withStyles from 'material-ui/styles/withStyles'; 22 23 import Header from './Header'; 24 import Body from './Body'; 25 import Footer from './Footer'; 26 import {MENU} from './Common'; 27 import type {Content} from '../types/content'; 28 29 // deepUpdate updates an object corresponding to the given update data, which has 30 // the shape of the same structure as the original object. updater also has the same 31 // structure, except that it contains functions where the original data needs to be 32 // updated. These functions are used to handle the update. 33 // 34 // Since the messages have the same shape as the state content, this approach allows 35 // the generalization of the message handling. The only necessary thing is to set a 36 // handler function for every path of the state in order to maximize the flexibility 37 // of the update. 38 const deepUpdate = (prev: Object, update: Object, updater: Object) => { 39 if (typeof update === 'undefined') { 40 // TODO (kurkomisi): originally this was deep copy, investigate it. 41 return prev; 42 } 43 if (typeof updater === 'function') { 44 return updater(prev, update); 45 } 46 const updated = {}; 47 Object.keys(prev).forEach((key) => { 48 updated[key] = deepUpdate(prev[key], update[key], updater[key]); 49 }); 50 51 return updated; 52 }; 53 54 // shouldUpdate returns the structure of a message. It is used to prevent unnecessary render 55 // method triggerings. In the affected component's shouldComponentUpdate method it can be checked 56 // whether the involved data was changed or not by checking the message structure. 57 // 58 // We could return the message itself too, but it's safer not to give access to it. 59 const shouldUpdate = (msg: Object, updater: Object) => { 60 const su = {}; 61 Object.keys(msg).forEach((key) => { 62 su[key] = typeof updater[key] !== 'function' ? shouldUpdate(msg[key], updater[key]) : true; 63 }); 64 65 return su; 66 }; 67 68 // appender is a state update generalization function, which appends the update data 69 // to the existing data. limit defines the maximum allowed size of the created array. 70 const appender = <T>(limit: number) => (prev: Array<T>, update: Array<T>) => [...prev, ...update].slice(-limit); 71 72 // replacer is a state update generalization function, which replaces the original data. 73 const replacer = <T>(prev: T, update: T) => update; 74 75 // defaultContent is the initial value of the state content. 76 const defaultContent: Content = { 77 general: { 78 version: null, 79 commit: null, 80 }, 81 home: { 82 memory: [], 83 traffic: [], 84 }, 85 chain: {}, 86 txpool: {}, 87 network: {}, 88 system: {}, 89 logs: { 90 log: [], 91 }, 92 }; 93 94 // updaters contains the state update generalization functions for each path of the state. 95 // TODO (kurkomisi): Define a tricky type which embraces the content and the handlers. 96 const updaters = { 97 general: { 98 version: replacer, 99 commit: replacer, 100 }, 101 home: { 102 memory: appender(200), 103 traffic: appender(200), 104 }, 105 chain: null, 106 txpool: null, 107 network: null, 108 system: null, 109 logs: { 110 log: appender(200), 111 }, 112 }; 113 114 // styles returns the styles for the Dashboard component. 115 const styles = theme => ({ 116 dashboard: { 117 display: 'flex', 118 flexFlow: 'column', 119 width: '100%', 120 height: '100%', 121 background: theme.palette.background.default, 122 zIndex: 1, 123 overflow: 'hidden', 124 }, 125 }); 126 127 export type Props = { 128 classes: Object, 129 }; 130 131 type State = { 132 active: string, // active menu 133 sideBar: boolean, // true if the sidebar is opened 134 content: Content, // the visualized data 135 shouldUpdate: Object // labels for the components, which need to rerender based on the incoming message 136 }; 137 138 // Dashboard is the main component, which renders the whole page, makes connection with the server and 139 // listens for messages. When there is an incoming message, updates the page's content correspondingly. 140 class Dashboard extends Component<Props, State> { 141 constructor(props: Props) { 142 super(props); 143 this.state = { 144 active: MENU.get('home').id, 145 sideBar: true, 146 content: defaultContent, 147 shouldUpdate: {}, 148 }; 149 } 150 151 // componentDidMount initiates the establishment of the first websocket connection after the component is rendered. 152 componentDidMount() { 153 this.reconnect(); 154 } 155 156 // reconnect establishes a websocket connection with the server, listens for incoming messages 157 // and tries to reconnect on connection loss. 158 reconnect = () => { 159 const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`); 160 server.onopen = () => { 161 this.setState({content: defaultContent, shouldUpdate: {}}); 162 }; 163 server.onmessage = (event) => { 164 const msg: $Shape<Content> = JSON.parse(event.data); 165 if (!msg) { 166 console.error(`Incoming message is ${msg}`); 167 return; 168 } 169 this.update(msg); 170 }; 171 server.onclose = () => { 172 setTimeout(this.reconnect, 3000); 173 }; 174 }; 175 176 // update updates the content corresponding to the incoming message. 177 update = (msg: $Shape<Content>) => { 178 this.setState(prevState => ({ 179 content: deepUpdate(prevState.content, msg, updaters), 180 shouldUpdate: shouldUpdate(msg, updaters), 181 })); 182 }; 183 184 // changeContent sets the active label, which is used at the content rendering. 185 changeContent = (newActive: string) => { 186 this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {})); 187 }; 188 189 // openSideBar opens the sidebar. 190 openSideBar = () => { 191 this.setState({sideBar: true}); 192 }; 193 194 // closeSideBar closes the sidebar. 195 closeSideBar = () => { 196 this.setState({sideBar: false}); 197 }; 198 199 render() { 200 const {classes} = this.props; // The classes property is injected by withStyles(). 201 202 return ( 203 <div className={classes.dashboard}> 204 <Header 205 opened={this.state.sideBar} 206 openSideBar={this.openSideBar} 207 closeSideBar={this.closeSideBar} 208 /> 209 <Body 210 opened={this.state.sideBar} 211 changeContent={this.changeContent} 212 active={this.state.active} 213 content={this.state.content} 214 shouldUpdate={this.state.shouldUpdate} 215 /> 216 <Footer 217 opened={this.state.sideBar} 218 openSideBar={this.openSideBar} 219 closeSideBar={this.closeSideBar} 220 general={this.state.content.general} 221 shouldUpdate={this.state.shouldUpdate} 222 /> 223 </div> 224 ); 225 } 226 } 227 228 export default withStyles(styles)(Dashboard);