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