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