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