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