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