github.com/anthdm/go-ethereum@v1.8.4-0.20180412101906-60516c83b011/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 {MENU} from '../common'; 26 import type {Content} from '../types/content'; 27 28 // deepUpdate updates an object corresponding to the given update data, which has 29 // the shape of the same structure as the original object. updater also has the same 30 // structure, except that it contains functions where the original data needs to be 31 // updated. These functions are used to handle the update. 32 // 33 // Since the messages have the same shape as the state content, this approach allows 34 // the generalization of the message handling. The only necessary thing is to set a 35 // handler function for every path of the state in order to maximize the flexibility 36 // of the update. 37 const deepUpdate = (updater: Object, update: Object, prev: Object): $Shape<Content> => { 38 if (typeof update === 'undefined') { 39 // TODO (kurkomisi): originally this was deep copy, investigate it. 40 return prev; 41 } 42 if (typeof updater === 'function') { 43 return updater(update, prev); 44 } 45 const updated = {}; 46 Object.keys(prev).forEach((key) => { 47 updated[key] = deepUpdate(updater[key], update[key], prev[key]); 48 }); 49 50 return updated; 51 }; 52 53 // shouldUpdate returns the structure of a message. It is used to prevent unnecessary render 54 // method triggerings. In the affected component's shouldComponentUpdate method it can be checked 55 // whether the involved data was changed or not by checking the message structure. 56 // 57 // We could return the message itself too, but it's safer not to give access to it. 58 const shouldUpdate = (updater: Object, msg: Object) => { 59 const su = {}; 60 Object.keys(msg).forEach((key) => { 61 su[key] = typeof updater[key] !== 'function' ? shouldUpdate(updater[key], msg[key]) : true; 62 }); 63 64 return su; 65 }; 66 67 // replacer is a state updater function, which replaces the original data. 68 const replacer = <T>(update: T) => update; 69 70 // appender is a state updater function, which appends the update data to the 71 // existing data. limit defines the maximum allowed size of the created array, 72 // mapper maps the update data. 73 const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, prev: Array<T>) => [ 74 ...prev, 75 ...update.map(sample => mapper(sample)), 76 ].slice(-limit); 77 78 // defaultContent is the initial value of the state content. 79 const defaultContent: Content = { 80 general: { 81 version: null, 82 commit: null, 83 }, 84 home: {}, 85 chain: {}, 86 txpool: {}, 87 network: {}, 88 system: { 89 activeMemory: [], 90 virtualMemory: [], 91 networkIngress: [], 92 networkEgress: [], 93 processCPU: [], 94 systemCPU: [], 95 diskRead: [], 96 diskWrite: [], 97 }, 98 logs: { 99 log: [], 100 }, 101 }; 102 103 // updaters contains the state updater functions for each path of the state. 104 // 105 // TODO (kurkomisi): Define a tricky type which embraces the content and the updaters. 106 const updaters = { 107 general: { 108 version: replacer, 109 commit: replacer, 110 }, 111 home: null, 112 chain: null, 113 txpool: null, 114 network: null, 115 system: { 116 activeMemory: appender(200), 117 virtualMemory: appender(200), 118 networkIngress: appender(200), 119 networkEgress: appender(200), 120 processCPU: appender(200), 121 systemCPU: appender(200), 122 diskRead: appender(200), 123 diskWrite: appender(200), 124 }, 125 logs: { 126 log: appender(200), 127 }, 128 }; 129 130 // styles contains the constant styles of the component. 131 const styles = { 132 dashboard: { 133 display: 'flex', 134 flexFlow: 'column', 135 width: '100%', 136 height: '100%', 137 zIndex: 1, 138 overflow: 'hidden', 139 }, 140 }; 141 142 // themeStyles returns the styles generated from the theme for the component. 143 const themeStyles: Object = (theme: Object) => ({ 144 dashboard: { 145 background: theme.palette.background.default, 146 }, 147 }); 148 149 export type Props = { 150 classes: Object, // injected by withStyles() 151 }; 152 153 type State = { 154 active: string, // active menu 155 sideBar: boolean, // true if the sidebar is opened 156 content: Content, // the visualized data 157 shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message 158 }; 159 160 // Dashboard is the main component, which renders the whole page, makes connection with the server and 161 // listens for messages. When there is an incoming message, updates the page's content correspondingly. 162 class Dashboard extends Component<Props, State> { 163 constructor(props: Props) { 164 super(props); 165 this.state = { 166 active: MENU.get('home').id, 167 sideBar: true, 168 content: defaultContent, 169 shouldUpdate: {}, 170 }; 171 } 172 173 // componentDidMount initiates the establishment of the first websocket connection after the component is rendered. 174 componentDidMount() { 175 this.reconnect(); 176 } 177 178 // reconnect establishes a websocket connection with the server, listens for incoming messages 179 // and tries to reconnect on connection loss. 180 reconnect = () => { 181 // PROD is defined by webpack. 182 const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${PROD ? window.location.host : 'localhost:8080'}/api`); 183 server.onopen = () => { 184 this.setState({content: defaultContent, shouldUpdate: {}}); 185 }; 186 server.onmessage = (event) => { 187 const msg: $Shape<Content> = JSON.parse(event.data); 188 if (!msg) { 189 console.error(`Incoming message is ${msg}`); 190 return; 191 } 192 this.update(msg); 193 }; 194 server.onclose = () => { 195 setTimeout(this.reconnect, 3000); 196 }; 197 }; 198 199 // update updates the content corresponding to the incoming message. 200 update = (msg: $Shape<Content>) => { 201 this.setState(prevState => ({ 202 content: deepUpdate(updaters, msg, prevState.content), 203 shouldUpdate: shouldUpdate(updaters, msg), 204 })); 205 }; 206 207 // changeContent sets the active label, which is used at the content rendering. 208 changeContent = (newActive: string) => { 209 this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {})); 210 }; 211 212 // switchSideBar opens or closes the sidebar's state. 213 switchSideBar = () => { 214 this.setState(prevState => ({sideBar: !prevState.sideBar})); 215 }; 216 217 render() { 218 return ( 219 <div className={this.props.classes.dashboard} style={styles.dashboard}> 220 <Header 221 switchSideBar={this.switchSideBar} 222 /> 223 <Body 224 opened={this.state.sideBar} 225 changeContent={this.changeContent} 226 active={this.state.active} 227 content={this.state.content} 228 shouldUpdate={this.state.shouldUpdate} 229 /> 230 </div> 231 ); 232 } 233 } 234 235 export default withStyles(themeStyles)(Dashboard);