github.com/mandrigin/go-ethereum@v1.7.4-0.20180116162341-02aeb3d76652/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 Footer from './Footer';
    26  import {MENU} from './Common';
    27  import type {Content} from '../types/content';
    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 = (prev: Object, update: Object, updater: Object) => {
    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(prev, update);
    45  	}
    46  	const updated = {};
    47  	Object.keys(prev).forEach((key) => {
    48  		updated[key] = deepUpdate(prev[key], update[key], updater[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 = (msg: Object, updater: Object) => {
    60  	const su = {};
    61  	Object.keys(msg).forEach((key) => {
    62  		su[key] = typeof updater[key] !== 'function' ? shouldUpdate(msg[key], updater[key]) : true;
    63  	});
    64  
    65  	return su;
    66  };
    67  
    68  // appender is a state update generalization function, which appends the update data
    69  // to the existing data. limit defines the maximum allowed size of the created array.
    70  const appender = <T>(limit: number) => (prev: Array<T>, update: Array<T>) => [...prev, ...update].slice(-limit);
    71  
    72  // replacer is a state update generalization function, which replaces the original data.
    73  const replacer = <T>(prev: T, update: T) => update;
    74  
    75  // defaultContent is the initial value of the state content.
    76  const defaultContent: Content = {
    77  	general: {
    78  		version: null,
    79  		commit:  null,
    80  	},
    81  	home: {
    82  		memory:  [],
    83  		traffic: [],
    84  	},
    85  	chain:   {},
    86  	txpool:  {},
    87  	network: {},
    88  	system:  {},
    89  	logs:    {
    90  		log: [],
    91  	},
    92  };
    93  
    94  // updaters contains the state update generalization functions for each path of the state.
    95  // TODO (kurkomisi): Define a tricky type which embraces the content and the handlers.
    96  const updaters = {
    97  	general: {
    98  		version: replacer,
    99  		commit:  replacer,
   100  	},
   101  	home: {
   102  		memory:  appender(200),
   103  		traffic: appender(200),
   104  	},
   105  	chain:   null,
   106  	txpool:  null,
   107  	network: null,
   108  	system:  null,
   109  	logs:    {
   110  		log: appender(200),
   111  	},
   112  };
   113  
   114  // styles returns the styles for the Dashboard component.
   115  const styles = theme => ({
   116  	dashboard: {
   117  		display:    'flex',
   118  		flexFlow:   'column',
   119  		width:      '100%',
   120  		height:     '100%',
   121  		background: theme.palette.background.default,
   122  		zIndex:     1,
   123  		overflow:   'hidden',
   124  	},
   125  });
   126  
   127  export type Props = {
   128  	classes: Object,
   129  };
   130  
   131  type State = {
   132  	active: string, // active menu
   133  	sideBar: boolean, // true if the sidebar is opened
   134  	content: Content, // the visualized data
   135  	shouldUpdate: Object // labels for the components, which need to rerender based on the incoming message
   136  };
   137  
   138  // Dashboard is the main component, which renders the whole page, makes connection with the server and
   139  // listens for messages. When there is an incoming message, updates the page's content correspondingly.
   140  class Dashboard extends Component<Props, State> {
   141  	constructor(props: Props) {
   142  		super(props);
   143  		this.state = {
   144  			active:       MENU.get('home').id,
   145  			sideBar:      true,
   146  			content:      defaultContent,
   147  			shouldUpdate: {},
   148  		};
   149  	}
   150  
   151  	// componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
   152  	componentDidMount() {
   153  		this.reconnect();
   154  	}
   155  
   156  	// reconnect establishes a websocket connection with the server, listens for incoming messages
   157  	// and tries to reconnect on connection loss.
   158  	reconnect = () => {
   159  		const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`);
   160  		server.onopen = () => {
   161  			this.setState({content: defaultContent, shouldUpdate: {}});
   162  		};
   163  		server.onmessage = (event) => {
   164  			const msg: $Shape<Content> = JSON.parse(event.data);
   165  			if (!msg) {
   166  				console.error(`Incoming message is ${msg}`);
   167  				return;
   168  			}
   169  			this.update(msg);
   170  		};
   171  		server.onclose = () => {
   172  			setTimeout(this.reconnect, 3000);
   173  		};
   174  	};
   175  
   176  	// update updates the content corresponding to the incoming message.
   177  	update = (msg: $Shape<Content>) => {
   178  		this.setState(prevState => ({
   179  			content:      deepUpdate(prevState.content, msg, updaters),
   180  			shouldUpdate: shouldUpdate(msg, updaters),
   181  		}));
   182  	};
   183  
   184  	// changeContent sets the active label, which is used at the content rendering.
   185  	changeContent = (newActive: string) => {
   186  		this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
   187  	};
   188  
   189  	// openSideBar opens the sidebar.
   190  	openSideBar = () => {
   191  		this.setState({sideBar: true});
   192  	};
   193  
   194  	// closeSideBar closes the sidebar.
   195  	closeSideBar = () => {
   196  		this.setState({sideBar: false});
   197  	};
   198  
   199  	render() {
   200  		const {classes} = this.props; // The classes property is injected by withStyles().
   201  
   202  		return (
   203  			<div className={classes.dashboard}>
   204  				<Header
   205  					opened={this.state.sideBar}
   206  					openSideBar={this.openSideBar}
   207  					closeSideBar={this.closeSideBar}
   208  				/>
   209  				<Body
   210  					opened={this.state.sideBar}
   211  					changeContent={this.changeContent}
   212  					active={this.state.active}
   213  					content={this.state.content}
   214  					shouldUpdate={this.state.shouldUpdate}
   215  				/>
   216  				<Footer
   217  					opened={this.state.sideBar}
   218  					openSideBar={this.openSideBar}
   219  					closeSideBar={this.closeSideBar}
   220  					general={this.state.content.general}
   221  					shouldUpdate={this.state.shouldUpdate}
   222  				/>
   223  			</div>
   224  		);
   225  	}
   226  }
   227  
   228  export default withStyles(styles)(Dashboard);