Network.jsx 17 KB

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