github.com/anthdm/go-ethereum@v1.8.4-0.20180412101906-60516c83b011/dashboard/assets/components/Dashboard.jsx (about)

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