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);