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