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