github.com/arieschain/arieschain@v0.0.0-20191023063405-37c074544356/dashboard/assets/components/Dashboard.jsx (about)

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