github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/dashboard/assets/components/Dashboard.jsx (about) 1 // @flow 2 3 // Copyright 2017 The Spectrum Authors 4 // This file is part of the Spectrum library. 5 // 6 // The Spectrum 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 Spectrum 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 Spectrum 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 import {lensPath, view, set} from 'ramda'; 23 24 import Header from './Header'; 25 import Body from './Body'; 26 import {MENU, SAMPLE} from './Common'; 27 import type {Message, HomeMessage, LogsMessage, Chart} from '../types/message'; 28 import type {Content} from '../types/content'; 29 30 // appender appends an array (A) to the end of another array (B) in the state. 31 // lens is the path of B in the state, samples is A, and limit is the maximum size of the changed array. 32 // 33 // appender retrieves a function, which overrides the state's value at lens, and returns with the overridden state. 34 const appender = (lens, samples, limit) => (state) => { 35 const newSamples = [ 36 ...view(lens, state), // retrieves a specific value of the state at the given path (lens). 37 ...samples, 38 ]; 39 // set is a function of ramda.js, which needs the path, the new value, the original state, and retrieves 40 // the altered state. 41 return set( 42 lens, 43 newSamples.slice(newSamples.length > limit ? newSamples.length - limit : 0), 44 state 45 ); 46 }; 47 // Lenses for specific data fields in the state, used for a clearer deep update. 48 // NOTE: This solution will be changed very likely. 49 const memoryLens = lensPath(['content', 'home', 'memory']); 50 const trafficLens = lensPath(['content', 'home', 'traffic']); 51 const logLens = lensPath(['content', 'logs', 'log']); 52 // styles retrieves the styles for the Dashboard component. 53 const styles = theme => ({ 54 dashboard: { 55 display: 'flex', 56 flexFlow: 'column', 57 width: '100%', 58 height: '100%', 59 background: theme.palette.background.default, 60 zIndex: 1, 61 overflow: 'hidden', 62 }, 63 }); 64 export type Props = { 65 classes: Object, 66 }; 67 type State = { 68 active: string, // active menu 69 sideBar: boolean, // true if the sidebar is opened 70 content: $Shape<Content>, // the visualized data 71 shouldUpdate: Set<string> // labels for the components, which need to rerender based on the incoming message 72 }; 73 // Dashboard is the main component, which renders the whole page, makes connection with the server and 74 // listens for messages. When there is an incoming message, updates the page's content correspondingly. 75 class Dashboard extends Component<Props, State> { 76 constructor(props: Props) { 77 super(props); 78 this.state = { 79 active: MENU.get('home').id, 80 sideBar: true, 81 content: {home: {memory: [], traffic: []}, logs: {log: []}}, 82 shouldUpdate: new Set(), 83 }; 84 } 85 86 // componentDidMount initiates the establishment of the first websocket connection after the component is rendered. 87 componentDidMount() { 88 this.reconnect(); 89 } 90 91 // reconnect establishes a websocket connection with the server, listens for incoming messages 92 // and tries to reconnect on connection loss. 93 reconnect = () => { 94 this.setState({ 95 content: {home: {memory: [], traffic: []}, logs: {log: []}}, 96 }); 97 const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`); 98 server.onmessage = (event) => { 99 const msg: Message = JSON.parse(event.data); 100 if (!msg) { 101 return; 102 } 103 this.update(msg); 104 }; 105 server.onclose = () => { 106 setTimeout(this.reconnect, 3000); 107 }; 108 }; 109 110 // samples retrieves the raw data of a chart field from the incoming message. 111 samples = (chart: Chart) => { 112 let s = []; 113 if (chart.history) { 114 s = chart.history.map(({value}) => (value || 0)); // traffic comes without value at the beginning 115 } 116 if (chart.new) { 117 s = [...s, chart.new.value || 0]; 118 } 119 return s; 120 }; 121 122 // handleHome changes the home-menu related part of the state. 123 handleHome = (home: HomeMessage) => { 124 this.setState((prevState) => { 125 let newState = prevState; 126 newState.shouldUpdate = new Set(); 127 if (home.memory) { 128 newState = appender(memoryLens, this.samples(home.memory), SAMPLE.get('memory').limit)(newState); 129 newState.shouldUpdate.add('memory'); 130 } 131 if (home.traffic) { 132 newState = appender(trafficLens, this.samples(home.traffic), SAMPLE.get('traffic').limit)(newState); 133 newState.shouldUpdate.add('traffic'); 134 } 135 return newState; 136 }); 137 }; 138 139 // handleLogs changes the logs-menu related part of the state. 140 handleLogs = (logs: LogsMessage) => { 141 this.setState((prevState) => { 142 let newState = prevState; 143 newState.shouldUpdate = new Set(); 144 if (logs.log) { 145 newState = appender(logLens, [logs.log], SAMPLE.get('logs').limit)(newState); 146 newState.shouldUpdate.add('logs'); 147 } 148 return newState; 149 }); 150 }; 151 152 // update analyzes the incoming message, and updates the charts' content correspondingly. 153 update = (msg: Message) => { 154 if (msg.home) { 155 this.handleHome(msg.home); 156 } 157 if (msg.logs) { 158 this.handleLogs(msg.logs); 159 } 160 }; 161 162 // changeContent sets the active label, which is used at the content rendering. 163 changeContent = (newActive: string) => { 164 this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {})); 165 }; 166 167 // openSideBar opens the sidebar. 168 openSideBar = () => { 169 this.setState({sideBar: true}); 170 }; 171 172 // closeSideBar closes the sidebar. 173 closeSideBar = () => { 174 this.setState({sideBar: false}); 175 }; 176 177 render() { 178 const {classes} = this.props; // The classes property is injected by withStyles(). 179 180 return ( 181 <div className={classes.dashboard}> 182 <Header 183 opened={this.state.sideBar} 184 openSideBar={this.openSideBar} 185 closeSideBar={this.closeSideBar} 186 /> 187 <Body 188 opened={this.state.sideBar} 189 changeContent={this.changeContent} 190 active={this.state.active} 191 content={this.state.content} 192 shouldUpdate={this.state.shouldUpdate} 193 /> 194 </div> 195 ); 196 } 197 } 198 199 export default withStyles(styles)(Dashboard);