github.com/openethereum/go-ethereum@v1.9.7/dashboard/assets/components/Network.jsx (about)

     1  // @flow
     2  
     3  // Copyright 2018 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 Table from '@material-ui/core/Table';
    22  import TableHead from '@material-ui/core/TableHead';
    23  import TableBody from '@material-ui/core/TableBody';
    24  import TableRow from '@material-ui/core/TableRow';
    25  import TableCell from '@material-ui/core/TableCell';
    26  import Grid from '@material-ui/core/Grid/Grid';
    27  import Typography from '@material-ui/core/Typography';
    28  import {AreaChart, Area, Tooltip, YAxis} from 'recharts';
    29  import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
    30  import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons';
    31  import {faCircle as farCircle} from '@fortawesome/free-regular-svg-icons';
    32  import convert from 'color-convert';
    33  
    34  import CustomTooltip, {bytePlotter, multiplier} from 'CustomTooltip';
    35  import type {Network as NetworkType, PeerEvent} from '../types/content';
    36  import {styles as commonStyles, chartStrokeWidth, hues, hueScale} from '../common';
    37  
    38  // Peer chart dimensions.
    39  const trafficChartHeight = 18;
    40  const trafficChartWidth  = 400;
    41  
    42  // setMaxIngress adjusts the peer chart's gradient values based on the given value.
    43  const setMaxIngress = (peer, value) => {
    44  	peer.maxIngress = value;
    45  	peer.ingressGradient = [];
    46  	peer.ingressGradient.push({offset: hueScale[0], color: hues[0]});
    47  	let i = 1;
    48  	for (; i < hues.length && value > hueScale[i]; i++) {
    49  		peer.ingressGradient.push({offset: Math.floor(hueScale[i] * 100 / value), color: hues[i]});
    50  	}
    51  	i--;
    52  	if (i < hues.length - 1) {
    53  		// Usually the maximum value gets between two points on the predefined
    54  		// color scale (e.g. 123KB is somewhere between 100KB (#FFFF00) and
    55  		// 1MB (#FF0000)), and the charts need to be comparable by the colors,
    56  		// so we have to calculate the last hue using the maximum value and the
    57  		// surrounding hues in order to avoid the uniformity of the top colors
    58  		// on the charts. For this reason the two hues are translated into the
    59  		// CIELAB color space, and the top color will be their weighted average
    60  		// (CIELAB is perceptually uniform, meaning that any point on the line
    61  		// between two pure color points is also a pure color, so the weighted
    62  		// average will not lose from the saturation).
    63  		//
    64  		// In case the maximum value is greater than the biggest predefined
    65  		// scale value, the top of the chart will have uniform color.
    66  		const lastHue = convert.hex.lab(hues[i]);
    67  		const proportion = (value - hueScale[i]) * 100 / (hueScale[i + 1] - hueScale[i]);
    68  		convert.hex.lab(hues[i + 1]).forEach((val, j) => {
    69  			lastHue[j] = (lastHue[j] * proportion + val * (100 - proportion)) / 100;
    70  		});
    71  		peer.ingressGradient.push({offset: 100, color: `#${convert.lab.hex(lastHue)}`});
    72  	}
    73  };
    74  
    75  // setMaxEgress adjusts the peer chart's gradient values based on the given value.
    76  // In case of the egress the chart is upside down, so the gradients need to be
    77  // calculated inversely compared to the ingress.
    78  const setMaxEgress = (peer, value) => {
    79  	peer.maxEgress = value;
    80  	peer.egressGradient = [];
    81  	peer.egressGradient.push({offset: 100 - hueScale[0], color: hues[0]});
    82  	let i = 1;
    83  	for (; i < hues.length && value > hueScale[i]; i++) {
    84  		peer.egressGradient.unshift({offset: 100 - Math.floor(hueScale[i] * 100 / value), color: hues[i]});
    85  	}
    86  	i--;
    87  	if (i < hues.length - 1) {
    88  		// Calculate the last hue.
    89  		const lastHue = convert.hex.lab(hues[i]);
    90  		const proportion = (value - hueScale[i]) * 100 / (hueScale[i + 1] - hueScale[i]);
    91  		convert.hex.lab(hues[i + 1]).forEach((val, j) => {
    92  			lastHue[j] = (lastHue[j] * proportion + val * (100 - proportion)) / 100;
    93  		});
    94  		peer.egressGradient.unshift({offset: 0, color: `#${convert.lab.hex(lastHue)}`});
    95  	}
    96  };
    97  
    98  
    99  // setIngressChartAttributes searches for the maximum value of the ingress
   100  // samples, and adjusts the peer chart's gradient values accordingly.
   101  const setIngressChartAttributes = (peer) => {
   102  	let max = 0;
   103  	peer.ingress.forEach(({value}) => {
   104  		if (value > max) {
   105  			max = value;
   106  		}
   107  	});
   108  	setMaxIngress(peer, max);
   109  };
   110  
   111  // setEgressChartAttributes searches for the maximum value of the egress
   112  // samples, and adjusts the peer chart's gradient values accordingly.
   113  const setEgressChartAttributes = (peer) => {
   114  	let max = 0;
   115  	peer.egress.forEach(({value}) => {
   116  		if (value > max) {
   117  			max = value;
   118  		}
   119  	});
   120  	setMaxEgress(peer, max);
   121  };
   122  
   123  // inserter is a state updater function for the main component, which handles the peers.
   124  export const inserter = (sampleLimit: number) => (update: NetworkType, prev: NetworkType) => {
   125  	// The first message contains the metered peer history.
   126  	if (update.peers && update.peers.bundles) {
   127  		prev.peers = update.peers;
   128  		Object.values(prev.peers.bundles).forEach((bundle) => {
   129  			if (bundle.knownPeers) {
   130  				Object.values(bundle.knownPeers).forEach((peer) => {
   131  					if (!peer.maxIngress) {
   132  						setIngressChartAttributes(peer);
   133  					}
   134  					if (!peer.maxEgress) {
   135  						setEgressChartAttributes(peer);
   136  					}
   137  				});
   138  			}
   139  		});
   140  	}
   141  	if (Array.isArray(update.diff)) {
   142  		update.diff.forEach((event: PeerEvent) => {
   143  			if (!event.ip) {
   144  				console.error('Peer event without IP', event);
   145  				return;
   146  			}
   147  			switch (event.remove) {
   148  			case 'bundle': {
   149  				delete prev.peers.bundles[event.ip];
   150  				return;
   151  			}
   152  			case 'known': {
   153  				if (!event.id) {
   154  					console.error('Remove known peer event without ID', event.ip);
   155  					return;
   156  				}
   157  				const bundle = prev.peers.bundles[event.ip];
   158  				if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.id]) {
   159  					console.error('No known peer to remove', event.ip, event.id);
   160  					return;
   161  				}
   162  				delete bundle.knownPeers[event.id];
   163  				return;
   164  			}
   165  			case 'attempt': {
   166  				const bundle = prev.peers.bundles[event.ip];
   167  				if (!bundle || !Array.isArray(bundle.attempts) || bundle.attempts.length < 1) {
   168  					console.error('No unknown peer to remove', event.ip);
   169  					return;
   170  				}
   171  				bundle.attempts.splice(0, 1);
   172  				return;
   173  			}
   174  			}
   175  			if (!prev.peers.bundles[event.ip]) {
   176  				prev.peers.bundles[event.ip] = {
   177  					location: {
   178  						country:   '',
   179  						city:      '',
   180  						latitude:  0,
   181  						longitude: 0,
   182  					},
   183  					knownPeers: {},
   184  					attempts:   [],
   185  				};
   186  			}
   187  			const bundle = prev.peers.bundles[event.ip];
   188  			if (event.location) {
   189  				bundle.location = event.location;
   190  				return;
   191  			}
   192  			if (!event.id) {
   193  				if (!bundle.attempts) {
   194  					bundle.attempts = [];
   195  				}
   196  				bundle.attempts.push({
   197  					connected:    event.connected,
   198  					disconnected: event.disconnected,
   199  				});
   200  				return;
   201  			}
   202  			if (!bundle.knownPeers) {
   203  				bundle.knownPeers = {};
   204  			}
   205  			if (!bundle.knownPeers[event.id]) {
   206  				bundle.knownPeers[event.id] = {
   207  					connected:    [],
   208  					disconnected: [],
   209  					ingress:      [],
   210  					egress:       [],
   211  					active:       false,
   212  				};
   213  			}
   214  			const peer = bundle.knownPeers[event.id];
   215  			if (!peer.maxIngress) {
   216  				setIngressChartAttributes(peer);
   217  			}
   218  			if (!peer.maxEgress) {
   219  				setEgressChartAttributes(peer);
   220  			}
   221  			if (event.connected) {
   222  				if (!peer.connected) {
   223  					console.warn('peer.connected should exist');
   224  					peer.connected = [];
   225  				}
   226  				peer.connected.push(event.connected);
   227  			}
   228  			if (event.disconnected) {
   229  				if (!peer.disconnected) {
   230  					console.warn('peer.disconnected should exist');
   231  					peer.disconnected = [];
   232  				}
   233  				peer.disconnected.push(event.disconnected);
   234  			}
   235  			switch (event.activity) {
   236  			case 'active':
   237  				peer.active = true;
   238  				break;
   239  			case 'inactive':
   240  				peer.active = false;
   241  				break;
   242  			}
   243  			if (Array.isArray(event.ingress) && Array.isArray(event.egress)) {
   244  				if (event.ingress.length !== event.egress.length) {
   245  					console.error('Different traffic sample length', event);
   246  					return;
   247  				}
   248  				// Check if there is a new maximum value, and reset the colors in case.
   249  				let maxIngress = peer.maxIngress;
   250  				event.ingress.forEach(({value}) => {
   251  					if (value > maxIngress) {
   252  						maxIngress = value;
   253  					}
   254  				});
   255  				if (maxIngress > peer.maxIngress) {
   256  					setMaxIngress(peer, maxIngress);
   257  				}
   258  				// Push the new values.
   259  				peer.ingress.splice(peer.ingress.length, 0, ...event.ingress);
   260  				const ingressDiff = peer.ingress.length - sampleLimit;
   261  				if (ingressDiff > 0) {
   262  					// Check if the maximum value is in the beginning.
   263  					let i = 0;
   264  					while (i < ingressDiff && peer.ingress[i].value < peer.maxIngress) {
   265  						i++;
   266  					}
   267  					// Remove the old values from the beginning.
   268  					peer.ingress.splice(0, ingressDiff);
   269  					if (i < ingressDiff) {
   270  						// Reset the colors if the maximum value leaves the chart.
   271  						setIngressChartAttributes(peer);
   272  					}
   273  				}
   274  				// Check if there is a new maximum value, and reset the colors in case.
   275  				let maxEgress = peer.maxEgress;
   276  				event.egress.forEach(({value}) => {
   277  					if (value > maxEgress) {
   278  						maxEgress = value;
   279  					}
   280  				});
   281  				if (maxEgress > peer.maxEgress) {
   282  					setMaxEgress(peer, maxEgress);
   283  				}
   284  				// Push the new values.
   285  				peer.egress.splice(peer.egress.length, 0, ...event.egress);
   286  				const egressDiff = peer.egress.length - sampleLimit;
   287  				if (egressDiff > 0) {
   288  					// Check if the maximum value is in the beginning.
   289  					let i = 0;
   290  					while (i < egressDiff && peer.egress[i].value < peer.maxEgress) {
   291  						i++;
   292  					}
   293  					// Remove the old values from the beginning.
   294  					peer.egress.splice(0, egressDiff);
   295  					if (i < egressDiff) {
   296  						// Reset the colors if the maximum value leaves the chart.
   297  						setEgressChartAttributes(peer);
   298  					}
   299  				}
   300  			}
   301  		});
   302  	}
   303  	return prev;
   304  };
   305  
   306  // styles contains the constant styles of the component.
   307  const styles = {
   308  	tableHead: {
   309  		height: 'auto',
   310  	},
   311  	tableRow: {
   312  		height: 'auto',
   313  	},
   314  	tableCell: {
   315  		paddingTop:    0,
   316  		paddingRight:  5,
   317  		paddingBottom: 0,
   318  		paddingLeft:   5,
   319  		border:        'none',
   320  	},
   321  };
   322  
   323  export type Props = {
   324      container:    Object,
   325      content:      NetworkType,
   326      shouldUpdate: Object,
   327  };
   328  
   329  type State = {};
   330  
   331  // Network renders the network page.
   332  class Network extends Component<Props, State> {
   333  	componentDidMount() {
   334  		const {container} = this.props;
   335  		if (typeof container === 'undefined') {
   336  			return;
   337  		}
   338  		container.scrollTop = 0;
   339  	}
   340  
   341  	formatTime = (t: string) => {
   342  		const time = new Date(t);
   343  		if (isNaN(time)) {
   344  			return '';
   345  		}
   346  		const month = `0${time.getMonth() + 1}`.slice(-2);
   347  		const date = `0${time.getDate()}`.slice(-2);
   348  		const hours = `0${time.getHours()}`.slice(-2);
   349  		const minutes = `0${time.getMinutes()}`.slice(-2);
   350  		const seconds = `0${time.getSeconds()}`.slice(-2);
   351  		return `${month}/${date}/${hours}:${minutes}:${seconds}`;
   352  	};
   353  
   354  	copyToClipboard = (id) => (event) => {
   355  		event.preventDefault();
   356  		navigator.clipboard.writeText(id).then(() => {}, () => {
   357  			console.error("Failed to copy node id", id);
   358  		});
   359  	};
   360  
   361  	peerTableRow = (ip, id, bundle, peer) => {
   362  		const ingressValues = peer.ingress.map(({value}) => ({ingress: value || 0.001}));
   363  		const egressValues = peer.egress.map(({value}) => ({egress: -value || -0.001}));
   364  
   365  		return (
   366  			<TableRow key={`known_${ip}_${id}`} style={styles.tableRow}>
   367  				<TableCell style={styles.tableCell}>
   368  					{peer.active
   369  						? <FontAwesomeIcon icon={fasCircle} color='green' />
   370  						: <FontAwesomeIcon icon={farCircle} style={commonStyles.light} />
   371  					}
   372  				</TableCell>
   373  				<TableCell style={{fontFamily: 'monospace', cursor: 'copy', ...styles.tableCell, ...commonStyles.light}} onClick={this.copyToClipboard(id)}>
   374  					{id.substring(0, 10)}
   375  				</TableCell>
   376  				<TableCell style={styles.tableCell}>
   377  					{bundle.location ? (() => {
   378  						const l = bundle.location;
   379  						return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
   380  					})() : ''}
   381  				</TableCell>
   382  				<TableCell style={styles.tableCell}>
   383  					<AreaChart
   384  						width={trafficChartWidth}
   385  						height={trafficChartHeight}
   386  						data={ingressValues}
   387  						margin={{top: 5, right: 5, bottom: 0, left: 5}}
   388  						syncId={`peerIngress_${ip}_${id}`}
   389  					>
   390  						<defs>
   391  							<linearGradient id={`ingressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
   392  								{peer.ingressGradient
   393  								&& peer.ingressGradient.map(({offset, color}, i) => (
   394  									<stop
   395  										key={`ingressStop_${ip}_${id}_${i}`}
   396  										offset={`${offset}%`}
   397  										stopColor={color}
   398  									/>
   399  								))}
   400  							</linearGradient>
   401  						</defs>
   402  						<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
   403  						<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
   404  						<Area
   405  							dataKey='ingress'
   406  							isAnimationActive={false}
   407  							type='monotone'
   408  							fill={`url(#ingressGradient_${ip}_${id})`}
   409  							stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
   410  							strokeWidth={chartStrokeWidth}
   411  						/>
   412  					</AreaChart>
   413  					<AreaChart
   414  						width={trafficChartWidth}
   415  						height={trafficChartHeight}
   416  						data={egressValues}
   417  						margin={{top: 0, right: 5, bottom: 5, left: 5}}
   418  						syncId={`peerIngress_${ip}_${id}`}
   419  					>
   420  						<defs>
   421  							<linearGradient id={`egressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
   422  								{peer.egressGradient
   423  								&& peer.egressGradient.map(({offset, color}, i) => (
   424  									<stop
   425  										key={`egressStop_${ip}_${id}_${i}`}
   426  										offset={`${offset}%`}
   427  										stopColor={color}
   428  									/>
   429  								))}
   430  							</linearGradient>
   431  						</defs>
   432  						<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
   433  						<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
   434  						<Area
   435  							dataKey='egress'
   436  							isAnimationActive={false}
   437  							type='monotone'
   438  							fill={`url(#egressGradient_${ip}_${id})`}
   439  							stroke={peer.egressGradient[0].color}
   440  							strokeWidth={chartStrokeWidth}
   441  						/>
   442  					</AreaChart>
   443  				</TableCell>
   444  			</TableRow>
   445  		);
   446  	};
   447  
   448  	render() {
   449  		return (
   450  			<Grid container direction='row' justify='space-between'>
   451  				<Grid item>
   452  					<Table>
   453  						<TableHead style={styles.tableHead}>
   454  							<TableRow style={styles.tableRow}>
   455  								<TableCell style={styles.tableCell} />
   456  								<TableCell style={styles.tableCell}>Node ID</TableCell>
   457  								<TableCell style={styles.tableCell}>Location</TableCell>
   458  								<TableCell style={styles.tableCell}>Traffic</TableCell>
   459  							</TableRow>
   460  						</TableHead>
   461  						<TableBody>
   462  							{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
   463  								if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
   464  									return null;
   465  								}
   466  								return Object.entries(bundle.knownPeers).map(([id, peer]) => {
   467  									if (peer.active === false) {
   468  										return null;
   469  									}
   470  									return this.peerTableRow(ip, id, bundle, peer);
   471  								});
   472  							})}
   473  						</TableBody>
   474  						<TableBody>
   475  							{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
   476  								if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
   477  									return null;
   478  								}
   479  								return Object.entries(bundle.knownPeers).map(([id, peer]) => {
   480  									if (peer.active === true) {
   481  										return null;
   482  									}
   483  									return this.peerTableRow(ip, id, bundle, peer);
   484  								});
   485  							})}
   486  						</TableBody>
   487  					</Table>
   488  				</Grid>
   489  				<Grid item>
   490  					<Typography variant='subtitle1' gutterBottom>
   491  						Connection attempts
   492  					</Typography>
   493  					<Table>
   494  						<TableHead style={styles.tableHead}>
   495  							<TableRow style={styles.tableRow}>
   496  								<TableCell style={styles.tableCell}>IP</TableCell>
   497  								<TableCell style={styles.tableCell}>Location</TableCell>
   498  								<TableCell style={styles.tableCell}>Nr</TableCell>
   499  							</TableRow>
   500  						</TableHead>
   501  						<TableBody>
   502  							{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
   503  								if (!bundle.attempts || bundle.attempts.length < 1) {
   504  									return null;
   505  								}
   506  								return (
   507  									<TableRow key={`attempt_${ip}`} style={styles.tableRow}>
   508  										<TableCell style={styles.tableCell}>{ip}</TableCell>
   509  										<TableCell style={styles.tableCell}>
   510  											{bundle.location ? (() => {
   511  												const l = bundle.location;
   512  												return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
   513  											})() : ''}
   514  										</TableCell>
   515  										<TableCell style={styles.tableCell}>
   516  											{Object.values(bundle.attempts).length}
   517  										</TableCell>
   518  									</TableRow>
   519  								);
   520  							})}
   521  						</TableBody>
   522  					</Table>
   523  				</Grid>
   524  			</Grid>
   525  		);
   526  	}
   527  }
   528  
   529  export default Network;