dashboard: send current block to the dashboard client (#19762)

This adds all dashboard changes from the last couple months.
We're about to remove the dashboard, but decided that we should
get all the recent work in first in case anyone wants to pick up this
project later on.

* cmd, dashboard, eth, p2p: send peer info to the dashboard
* dashboard: update npm packages, improve UI, rebase
* dashboard, p2p: remove println, change doc
* cmd, dashboard, eth, p2p: cleanup after review
* dashboard: send current block to the dashboard client
This commit is contained in:
Kurkó Mihály
2019-11-13 12:13:13 +01:00
committed by Felix Lange
parent 6f1a600f6c
commit 4ea9b62b5c
20 changed files with 11929 additions and 10886 deletions
+53
View File
@@ -0,0 +1,53 @@
// @flow
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import type {Chain as ChainType} from '../types/content';
export const inserter = () => (update: ChainType, prev: ChainType) => {
if (!update.currentBlock) {
return;
}
if (!prev.currentBlock) {
prev.currentBlock = {};
}
prev.currentBlock.number = update.currentBlock.number;
prev.currentBlock.timestamp = update.currentBlock.timestamp;
return prev;
};
// styles contains the constant styles of the component.
const styles = {};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({});
export type Props = {
content: Content,
};
type State = {};
// Logs renders the log page.
class Chain extends Component<Props, State> {
render() {
return <></>;
}
}
export default Chain;
+15 -5
View File
@@ -25,6 +25,7 @@ import Header from 'Header';
import Body from 'Body';
import {inserter as logInserter, SAME} from 'Logs';
import {inserter as peerInserter} from 'Network';
import {inserter as chainInserter} from 'Chain';
import {MENU} from '../common';
import type {Content} from '../types/content';
@@ -83,17 +84,24 @@ const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, pre
// the execution of unnecessary operations (e.g. copy of the log array).
const defaultContent: () => Content = () => ({
general: {
version: null,
commit: null,
version: null,
genesis: '',
},
home: {},
chain: {
currentBlock: {
number: 0,
timestamp: 0,
},
},
home: {},
chain: {},
txpool: {},
network: {
peers: {
bundles: {},
},
diff: [],
diff: [],
activePeerCount: 0,
},
system: {
activeMemory: [],
@@ -121,9 +129,10 @@ const updaters = {
general: {
version: replacer,
commit: replacer,
genesis: replacer,
},
home: null,
chain: null,
chain: chainInserter(),
txpool: null,
network: peerInserter(200),
system: {
@@ -241,6 +250,7 @@ class Dashboard extends Component<Props, State> {
<div className={this.props.classes.dashboard} style={styles.dashboard}>
<Header
switchSideBar={this.switchSideBar}
content={this.state.content}
/>
<Body
opened={this.state.sideBar}
+23
View File
@@ -32,6 +32,9 @@ import ChartRow from 'ChartRow';
import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from 'CustomTooltip';
import {chartStrokeWidth, styles as commonStyles} from '../common';
import type {General, System} from '../types/content';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faNetworkWired} from "@fortawesome/free-solid-svg-icons";
import Toolbar from "@material-ui/core/Toolbar";
const FOOTER_SYNC_ID = 'footerSyncId';
@@ -154,6 +157,23 @@ class Footer extends Component<Props, State> {
render() {
const {general, system} = this.props;
let network = '';
switch (general.genesis) {
case '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3':
network = 'main';
break;
case '0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d':
network = 'ropsten';
break;
case '0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177':
network = 'rinkeby';
break;
case '0xbf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a':
network = 'görli';
break;
default:
network = `unknown (${general.genesis.substring(0, 8)})`;
}
return (
<Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}>
@@ -202,6 +222,9 @@ class Footer extends Component<Props, State> {
</a>
</Typography>
)}
<Typography style={styles.headerText}>
<span style={commonStyles.light}>Network</span> {network}
</Typography>
</Grid>
</Grid>
);
+58 -4
View File
@@ -23,16 +23,25 @@ import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBars} from '@fortawesome/free-solid-svg-icons';
import {faBars, faSortAmountUp, faClock, faUsers, faSync} from '@fortawesome/free-solid-svg-icons';
import Typography from '@material-ui/core/Typography';
import type {Content} from '../types/content';
const magnitude = [31536000, 604800, 86400, 3600, 60, 1];
const label = ['y', 'w', 'd', 'h', 'm', 's'];
// styles contains the constant styles of the component.
const styles = {
header: {
height: '8%',
},
headerText: {
marginRight: 15,
},
toolbar: {
height: '100%',
height: '100%',
minHeight: 'unset',
},
};
@@ -50,16 +59,52 @@ const themeStyles = (theme: Object) => ({
title: {
paddingLeft: theme.spacing.unit,
fontSize: 3 * theme.spacing.unit,
flex: 1,
},
});
export type Props = {
classes: Object, // injected by withStyles()
classes: Object, // injected by withStyles()
switchSideBar: () => void,
content: Content,
networkID: number,
};
type State = {
since: string,
}
// Header renders the header of the dashboard.
class Header extends Component<Props> {
class Header extends Component<Props, State> {
constructor(props) {
super(props);
this.state = {since: ''};
}
componentDidMount() {
this.interval = setInterval(() => this.setState(() => {
// time (seconds) since last block.
let timeDiff = Math.floor((Date.now() - this.props.content.chain.currentBlock.timestamp * 1000) / 1000);
let since = '';
let i = 0;
for (; i < magnitude.length && timeDiff < magnitude[i]; i++);
for (let j = 2; i < magnitude.length && j > 0; j--, i++) {
const t = Math.floor(timeDiff / magnitude[i]);
if (t > 0) {
since += `${t}${label[i]} `;
timeDiff %= magnitude[i];
}
}
if (since === '') {
since = 'now';
}
this.setState({since: since});
}), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const {classes} = this.props;
@@ -72,6 +117,15 @@ class Header extends Component<Props> {
<Typography type='title' color='inherit' noWrap className={classes.title}>
Go Ethereum Dashboard
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faSortAmountUp} /> {this.props.content.chain.currentBlock.number}
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faClock} /> {this.state.since}
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faUsers} /> {this.props.content.network.activePeerCount}
</Typography>
</Toolbar>
</AppBar>
);
+4 -1
View File
@@ -20,6 +20,7 @@ import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Chain from 'Chain';
import Network from 'Network';
import Logs from 'Logs';
import Footer from 'Footer';
@@ -95,7 +96,9 @@ class Main extends Component<Props, State> {
children = <div>Work in progress.</div>;
break;
case MENU.get('chain').id:
children = <div>Work in progress.</div>;
children = <Chain
content={this.props.content.chain}
/>;
break;
case MENU.get('txpool').id:
children = <div>Work in progress.</div>;
+518 -189
View File
@@ -18,6 +18,7 @@
import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableBody from '@material-ui/core/TableBody';
@@ -27,17 +28,23 @@ import Grid from '@material-ui/core/Grid/Grid';
import Typography from '@material-ui/core/Typography';
import {AreaChart, Area, Tooltip, YAxis} from 'recharts';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons';
import {faCircle as farCircle} from '@fortawesome/free-regular-svg-icons';
import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons'; // More icons at fontawesome.com/icons
import {faCircle as farCircle, faClipboard as farClipboard} from '@fortawesome/free-regular-svg-icons';
import convert from 'color-convert';
import {Scrollbars} from 'react-custom-scrollbars';
import CustomTooltip, {bytePlotter, multiplier} from 'CustomTooltip';
import type {Network as NetworkType, PeerEvent} from '../types/content';
import {styles as commonStyles, chartStrokeWidth, hues, hueScale} from '../common';
import {chartStrokeWidth, hues, hueScale} from '../common';
// Peer chart dimensions.
const trafficChartHeight = 18;
const trafficChartWidth = 400;
const trafficChartHeight = 15;
const trafficChartWidth = 200;
// attemptSeparator separates the peer connection attempts
// such as the peers from the addresses with more attempts
// go to the beginning of the table, and the rest go to the end.
const attemptSeparator = 9;
// setMaxIngress adjusts the peer chart's gradient values based on the given value.
const setMaxIngress = (peer, value) => {
@@ -120,6 +127,58 @@ const setEgressChartAttributes = (peer) => {
setMaxEgress(peer, max);
};
// shortName adds some heuristics to the node name in order to make it look meaningful.
const shortName = (name: string) => {
const parts = name.split('/');
if (parts[0].substring(0, 'parity'.length).toLowerCase() === 'parity') {
// Merge Parity and Parity-Ethereum under the same name.
parts[0] = 'Parity';
}
if (parts.length < 2) {
console.error('Incorrect node name', name);
return parts[0];
}
const versionRE = RegExp(/^v?\d+\.\d+\.\d+.*/);
// Drop optional custom identifier.
if (!versionRE.test(parts[1])) {
if (parts.length < 3 || !versionRE.test(parts[2])) {
console.error('Incorrect node name', name);
return parts[0];
}
parts[1] = parts[2];
}
// Cutting anything from the version after the first - or +.
parts[1] = parts[1].split('-')[0].split('+')[0];
return `${parts[0]}/${parts[1]}`;
};
// shortLocation returns a shortened version of the given location object.
const shortLocation = (location: Object) => {
if (!location) {
return '';
}
return `${location.city ? `${location.city}/` : ''}${location.country ? location.country : ''}`;
};
// protocol returns a shortened version of the eth protocol values.
const protocol = (p: Object) => {
if (!p) {
return '';
}
if (typeof p === 'string') {
return p;
}
if (!(p instanceof Object)) {
console.error('Wrong protocol type', p, typeof p);
return '';
}
if (!p.hasOwnProperty('version') || !p.hasOwnProperty('difficulty') || !p.hasOwnProperty('head')) {
console.error('Missing protocol attributes', p);
return '';
}
return `h=${p.head.substring(0, 10)} v=${p.version} td=${p.difficulty}`;
};
// inserter is a state updater function for the main component, which handles the peers.
export const inserter = (sampleLimit: number) => (update: NetworkType, prev: NetworkType) => {
// The first message contains the metered peer history.
@@ -134,84 +193,104 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
if (!peer.maxEgress) {
setEgressChartAttributes(peer);
}
if (!peer.name) {
peer.name = '';
peer.shortName = '';
} else if (!peer.shortName) {
peer.shortName = shortName(peer.name);
}
if (!peer.enode) {
peer.enode = '';
}
if (!peer.protocols) {
peer.protocols = {};
}
peer.eth = protocol(peer.protocols.eth);
peer.les = protocol(peer.protocols.les);
});
}
bundle.shortLocation = shortLocation(bundle.location);
});
}
if (Array.isArray(update.diff)) {
update.diff.forEach((event: PeerEvent) => {
if (!event.ip) {
console.error('Peer event without IP', event);
if (!event.addr) {
console.error('Peer event without TCP address', event);
return;
}
switch (event.remove) {
case 'bundle': {
delete prev.peers.bundles[event.ip];
delete prev.peers.bundles[event.addr];
return;
}
case 'known': {
if (!event.id) {
console.error('Remove known peer event without ID', event.ip);
if (!event.enode) {
console.error('Remove known peer event without node URL', event.addr);
return;
}
const bundle = prev.peers.bundles[event.ip];
if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.id]) {
console.error('No known peer to remove', event.ip, event.id);
const bundle = prev.peers.bundles[event.addr];
if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.enode]) {
console.error('No known peer to remove', event.addr, event.enode);
return;
}
delete bundle.knownPeers[event.id];
return;
}
case 'attempt': {
const bundle = prev.peers.bundles[event.ip];
if (!bundle || !Array.isArray(bundle.attempts) || bundle.attempts.length < 1) {
console.error('No unknown peer to remove', event.ip);
return;
}
bundle.attempts.splice(0, 1);
delete bundle.knownPeers[event.enode];
return;
}
}
if (!prev.peers.bundles[event.ip]) {
prev.peers.bundles[event.ip] = {
if (!prev.peers.bundles[event.addr]) {
prev.peers.bundles[event.addr] = {
location: {
country: '',
city: '',
latitude: 0,
longitude: 0,
},
shortLocation: '',
knownPeers: {},
attempts: [],
attempts: 0,
};
}
const bundle = prev.peers.bundles[event.ip];
const bundle = prev.peers.bundles[event.addr];
if (event.location) {
bundle.location = event.location;
bundle.shortLocation = shortLocation(bundle.location);
return;
}
if (!event.id) {
if (!bundle.attempts) {
bundle.attempts = [];
}
bundle.attempts.push({
connected: event.connected,
disconnected: event.disconnected,
});
if (!event.enode) {
bundle.attempts++;
return;
}
if (!bundle.knownPeers) {
bundle.knownPeers = {};
}
if (!bundle.knownPeers[event.id]) {
bundle.knownPeers[event.id] = {
if (!bundle.knownPeers[event.enode]) {
bundle.knownPeers[event.enode] = {
connected: [],
disconnected: [],
ingress: [],
egress: [],
active: false,
name: '',
shortName: '',
enode: '',
protocols: {},
eth: '',
les: '',
};
}
const peer = bundle.knownPeers[event.id];
const peer = bundle.knownPeers[event.enode];
if (event.name) {
peer.name = event.name;
peer.shortName = shortName(event.name);
}
if (event.enode) {
peer.enode = event.enode;
}
if (event.protocols) {
peer.protocols = event.protocols;
peer.eth = protocol(peer.protocols.eth);
peer.les = protocol(peer.protocols.les);
}
if (!peer.maxIngress) {
setIngressChartAttributes(peer);
}
@@ -300,11 +379,29 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
}
});
}
prev.activePeerCount = 0;
Object.entries(prev.peers.bundles).forEach(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return;
}
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
if (peer.active === true) {
prev.activePeerCount++;
}
});
});
return prev;
};
// styles contains the constant styles of the component.
const styles = {
title: {
marginLeft: 5,
},
table: {
borderCollapse: 'unset',
padding: 5,
},
tableHead: {
height: 'auto',
},
@@ -317,13 +414,39 @@ const styles = {
paddingBottom: 0,
paddingLeft: 5,
border: 'none',
fontFamily: 'monospace',
fontSize: 10,
},
content: {
height: '800px',
},
};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({
title: {
color: theme.palette.common.white,
},
table: {
background: theme.palette.grey[900],
},
});
// limitedWidthStyle returns a style object which cuts the long text with three dots.
const limitedWidthStyle = (width) => {
return {
textOverflow: 'ellipsis',
maxWidth: width,
overflow: 'hidden',
whiteSpace: 'nowrap',
};
};
export type Props = {
container: Object,
content: NetworkType,
shouldUpdate: Object,
classes: Object, // injected by withStyles()
container: Object,
content: NetworkType,
shouldUpdate: Object,
};
type State = {};
@@ -351,179 +474,385 @@ class Network extends Component<Props, State> {
return `${month}/${date}/${hours}:${minutes}:${seconds}`;
};
copyToClipboard = (id) => (event) => {
copyToClipboard = (text: string) => (event) => {
event.preventDefault();
navigator.clipboard.writeText(id).then(() => {}, () => {
console.error("Failed to copy node id", id);
navigator.clipboard.writeText(text).then(() => {}, () => {
console.error("Failed to copy", text);
});
};
peerTableRow = (ip, id, bundle, peer) => {
lesList = () => {
const list = [];
Object.values(this.props.content.peers.bundles).forEach((bundle) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return;
}
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
if (peer.les === '' || peer.eth !== '') {
return;
}
list.push({enode, name: peer.name, location: bundle.location, protocols: peer.protocols});
});
});
return list;
};
ethList = () => {
const list = [];
Object.values(this.props.content.peers.bundles).forEach((bundle) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return;
}
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
if (peer.eth === '' && peer.les !== '') {
return;
}
list.push({enode, name: peer.name, location: bundle.location, protocols: peer.protocols});
});
});
return list;
};
attemptList = () => {
const list = [];
Object.entries(this.props.content.peers.bundles).forEach(([addr, bundle]) => {
if (!bundle.attempts) {
return;
}
list.push({addr, location: bundle.location, attempts: bundle.attempts});
});
return list;
};
knownPeerTableRow = (addr, enode, bundle, peer, showTraffic, proto) => {
const ingressValues = peer.ingress.map(({value}) => ({ingress: value || 0.001}));
const egressValues = peer.egress.map(({value}) => ({egress: -value || -0.001}));
return (
<TableRow key={`known_${ip}_${id}`} style={styles.tableRow}>
<TableRow key={`known_${addr}_${enode}`} style={styles.tableRow}>
<TableCell style={styles.tableCell}>
{peer.active
? <FontAwesomeIcon icon={fasCircle} color='green' />
: <FontAwesomeIcon icon={farCircle} style={commonStyles.light} />
: <FontAwesomeIcon icon={farCircle} />
}
</TableCell>
<TableCell style={{fontFamily: 'monospace', cursor: 'copy', ...styles.tableCell, ...commonStyles.light}} onClick={this.copyToClipboard(id)}>
{id.substring(0, 10)}
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(80),
}}
onClick={this.copyToClipboard(enode)}
>
{enode.substring(8)}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(80),
}}
onClick={this.copyToClipboard(peer.name)}
>
{peer.shortName}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(100),
}}
onClick={this.copyToClipboard(JSON.stringify(bundle.location))}
>
{bundle.shortLocation}
</TableCell>
<TableCell style={styles.tableCell}>
{bundle.location ? (() => {
const l = bundle.location;
return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
})() : ''}
</TableCell>
<TableCell style={styles.tableCell}>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={ingressValues}
margin={{top: 5, right: 5, bottom: 0, left: 5}}
syncId={`peerIngress_${ip}_${id}`}
>
<defs>
<linearGradient id={`ingressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
{peer.ingressGradient
&& peer.ingressGradient.map(({offset, color}, i) => (
<stop
key={`ingressStop_${ip}_${id}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
<Area
dataKey='ingress'
isAnimationActive={false}
type='monotone'
fill={`url(#ingressGradient_${ip}_${id})`}
stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={egressValues}
margin={{top: 0, right: 5, bottom: 5, left: 5}}
syncId={`peerIngress_${ip}_${id}`}
>
<defs>
<linearGradient id={`egressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
{peer.egressGradient
&& peer.egressGradient.map(({offset, color}, i) => (
<stop
key={`egressStop_${ip}_${id}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
<Area
dataKey='egress'
isAnimationActive={false}
type='monotone'
fill={`url(#egressGradient_${ip}_${id})`}
stroke={peer.egressGradient[0].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
{showTraffic ? (
<>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={ingressValues}
margin={{top: 5, right: 5, bottom: 0, left: 5}}
syncId={`peerIngress_${addr}_${enode}`}
>
<defs>
<linearGradient id={`ingressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
{peer.ingressGradient
&& peer.ingressGradient.map(({offset, color}, i) => (
<stop
key={`ingressStop_${addr}_${enode}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
<Area
dataKey='ingress'
isAnimationActive={false}
type='monotone'
fill={`url(#ingressGradient_${addr}_${enode})`}
stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={egressValues}
margin={{top: 0, right: 5, bottom: 5, left: 5}}
syncId={`peerIngress_${addr}_${enode}`}
>
<defs>
<linearGradient id={`egressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
{peer.egressGradient
&& peer.egressGradient.map(({offset, color}, i) => (
<stop
key={`egressStop_${addr}_${enode}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
<Area
dataKey='egress'
isAnimationActive={false}
type='monotone'
fill={`url(#egressGradient_${addr}_${enode})`}
stroke={peer.egressGradient[0].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
</>
) : null}
</TableCell>
{typeof proto === 'object' ? (
<>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(80),
}}
onClick={this.copyToClipboard(JSON.stringify(proto.head))}
>
{proto.head}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
}}
onClick={this.copyToClipboard(JSON.stringify(proto.difficulty))}
>
{proto.difficulty}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
}}
onClick={this.copyToClipboard(JSON.stringify(proto.version))}
>
{proto.version}
</TableCell>
</>
) : null }
</TableRow>
);
};
connectionAttemptTableRow = (addr, bundle) => (
<TableRow key={`attempt_${addr}`} style={styles.tableRow}>
<TableCell
style={{cursor: 'copy', ...styles.tableCell}}
onClick={this.copyToClipboard(addr)}
>
{addr}
</TableCell>
<TableCell
style={{cursor: 'copy', ...limitedWidthStyle(100), ...styles.tableCell}}
onClick={this.copyToClipboard(JSON.stringify(bundle.location))}
>
{bundle.shortLocation}
</TableCell>
<TableCell style={styles.tableCell}>
{bundle.attempts}
</TableCell>
</TableRow>
);
render() {
const {classes} = this.props;
return (
<Grid container direction='row' justify='space-between'>
<Grid item>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell} />
<TableCell style={styles.tableCell}>Node ID</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Traffic</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([id, peer]) => {
if (peer.active === false) {
return null;
}
return this.peerTableRow(ip, id, bundle, peer);
});
})}
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([id, peer]) => {
if (peer.active === true) {
return null;
}
return this.peerTableRow(ip, id, bundle, peer);
});
})}
</TableBody>
</Table>
</Grid>
<Grid item>
<Typography variant='subtitle1' gutterBottom>
Connection attempts
</Typography>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell}>IP</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Nr</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
if (!bundle.attempts || bundle.attempts.length < 1) {
return null;
}
return (
<TableRow key={`attempt_${ip}`} style={styles.tableRow}>
<TableCell style={styles.tableCell}>{ip}</TableCell>
<TableCell style={styles.tableCell}>
{bundle.location ? (() => {
const l = bundle.location;
return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
})() : ''}
</TableCell>
<TableCell style={styles.tableCell}>
{Object.values(bundle.attempts).length}
</TableCell>
<Grid container direction='row' spacing={3}>
<Grid item style={{width: '40%'}}>
<div className={classes.table} style={styles.table}>
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
Full peers
<FontAwesomeIcon
icon={farClipboard}
onClick={this.copyToClipboard(JSON.stringify(this.ethList()))}
style={{float: 'right'}}
/>
</Typography>
<Scrollbars style={styles.content}>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell} />
<TableCell style={styles.tableCell}>Node URL</TableCell>
<TableCell style={styles.tableCell}>Name</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Traffic</TableCell>
<TableCell style={styles.tableCell}>Head</TableCell>
<TableCell style={styles.tableCell}>TD</TableCell>
<TableCell style={styles.tableCell}>V</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === false) {
return null;
}
if (peer.eth === '' && peer.les !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, true, peer.protocols.eth);
});
})}
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === true) {
return null;
}
if (peer.eth === '' && peer.les !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, false, peer.protocols.eth);
});
})}
</TableBody>
</Table>
</Scrollbars>
</div>
</Grid>
<Grid item style={{width: '40%'}}>
<div className={classes.table} style={styles.table}>
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
Light peers
<FontAwesomeIcon
icon={farClipboard}
onClick={this.copyToClipboard(JSON.stringify(this.lesList()))}
style={{float: 'right'}}
/>
</Typography>
<Scrollbars style={styles.content}>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell} />
<TableCell style={styles.tableCell}>Node URL</TableCell>
<TableCell style={styles.tableCell}>Name</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Traffic</TableCell>
<TableCell style={styles.tableCell}>Head</TableCell>
<TableCell style={styles.tableCell}>TD</TableCell>
<TableCell style={styles.tableCell}>V</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === false) {
return null;
}
if (peer.les === '' || peer.eth !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, true, peer.protocols.les);
});
})}
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === true) {
return null;
}
if (peer.les === '' || peer.eth !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, false, peer.protocols.les);
});
})}
</TableBody>
</Table>
</Scrollbars>
</div>
</Grid>
<Grid item xs>
<div className={classes.table} style={styles.table}>
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
Connection attempts
<FontAwesomeIcon
icon={farClipboard}
onClick={this.copyToClipboard(JSON.stringify(this.attemptList()))}
style={{float: 'right'}}
/>
</Typography>
<Scrollbars style={styles.content}>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell}>TCP address</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Nr</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.attempts || bundle.attempts <= attemptSeparator) {
return null;
}
return this.connectionAttemptTableRow(addr, bundle);
})}
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.attempts || bundle.attempts < 1 || bundle.attempts > attemptSeparator) {
return null;
}
return this.connectionAttemptTableRow(addr, bundle);
})}
</TableBody>
</Table>
</Scrollbars>
</div>
</Grid>
</Grid>
);
}
}
export default Network;
export default withStyles(themeStyles)(Network);
+3 -2
View File
@@ -47,10 +47,11 @@ const themeStyles = theme => ({
background: theme.palette.grey[900],
},
listItem: {
minWidth: theme.spacing.unit * 7,
minWidth: theme.spacing(7),
color: theme.palette.common.white,
},
icon: {
fontSize: theme.spacing.unit * 3,
fontSize: theme.spacing(3),
overflow: 'unset',
},
});
+31 -29
View File
@@ -1,54 +1,56 @@
{
"private": true,
"dependencies": {
"@babel/core": "7.3.4",
"@babel/plugin-proposal-class-properties": "7.3.4",
"@babel/core": "7.4.5",
"@babel/plugin-proposal-class-properties": "7.4.4",
"@babel/plugin-proposal-function-bind": "7.2.0",
"@babel/plugin-transform-flow-strip-types": "7.3.4",
"@babel/preset-env": "7.3.4",
"@babel/plugin-transform-flow-strip-types": "7.4.4",
"@babel/preset-env": "7.4.5",
"@babel/preset-react": "^7.0.0",
"@babel/preset-stage-0": "^7.0.0",
"@fortawesome/fontawesome-free-regular": "^5.0.13",
"@fortawesome/fontawesome-svg-core": "^1.2.15",
"@fortawesome/free-regular-svg-icons": "^5.7.2",
"@fortawesome/free-solid-svg-icons": "^5.7.2",
"@fortawesome/fontawesome-svg-core": "1.2.18",
"@fortawesome/free-regular-svg-icons": "5.8.2",
"@fortawesome/free-solid-svg-icons": "5.8.2",
"@fortawesome/react-fontawesome": "^0.1.4",
"@material-ui/core": "3.9.2",
"@material-ui/icons": "3.0.2",
"@material-ui/core": "4.0.1",
"@material-ui/icons": "4.0.1",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.5",
"babel-loader": "8.0.6",
"classnames": "^2.2.6",
"color-convert": "^2.0.0",
"css-loader": "2.1.1",
"escape-html": "^1.0.3",
"eslint": "5.15.1",
"eslint": "5.16.0",
"eslint-config-airbnb": "^17.0.0",
"eslint-loader": "2.1.2",
"eslint-plugin-flowtype": "3.4.2",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-flowtype": "3.9.1",
"eslint-plugin-import": "2.17.3",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-node": "8.0.1",
"eslint-plugin-promise": "4.0.1",
"eslint-plugin-react": "7.12.4",
"eslint-plugin-node": "9.1.0",
"eslint-plugin-promise": "4.1.1",
"eslint-plugin-react": "7.13.0",
"file-loader": "3.0.1",
"flow-bin": "0.94.0",
"flow-bin": "0.98.1",
"flow-bin-loader": "^1.0.3",
"flow-typed": "^2.5.1",
"js-beautify": "1.9.0",
"flow-typed": "2.5.2",
"js-beautify": "1.10.0",
"path": "^0.12.7",
"react": "16.8.4",
"react-dom": "16.8.4",
"react-hot-loader": "4.8.0",
"react-transition-group": "2.6.1",
"recharts": "1.5.0",
"react": "16.8.6",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "16.8.6",
"react-hot-loader": "4.8.8",
"react-scrollbar": "0.5.6",
"react-transition-group": "4.0.1",
"recharts": "1.6.2",
"style-loader": "0.23.1",
"terser-webpack-plugin": "^1.2.3",
"terser-webpack-plugin": "1.3.0",
"url": "^0.11.0",
"url-loader": "1.1.2",
"webpack": "4.29.6",
"webpack-cli": "3.2.3",
"webpack-dashboard": "3.0.0",
"webpack-dev-server": "3.2.1",
"webpack": "4.32.2",
"webpack-cli": "3.3.2",
"webpack-dashboard": "3.0.7",
"webpack-dev-server": "3.4.1",
"webpack-merge": "4.2.1"
},
"scripts": {
+20 -13
View File
@@ -35,6 +35,7 @@ export type ChartEntry = {
export type General = {
version: ?string,
commit: ?string,
genesis: ?string,
};
export type Home = {
@@ -42,21 +43,29 @@ export type Home = {
};
export type Chain = {
/* TODO (kurkomisi) */
currentBlock: Block,
};
export type Block = {
number: number,
timestamp: number,
}
export type TxPool = {
/* TODO (kurkomisi) */
};
export type Network = {
peers: Peers,
diff: Array<PeerEvent>
peers: Peers,
diff: Array<PeerEvent>,
activePeerCount: number,
};
export type PeerEvent = {
ip: string,
id: string,
name: string,
addr: string,
enode: string,
protocols: {[string]: Object},
remove: string,
location: GeoLocation,
connected: Date,
@@ -71,9 +80,9 @@ export type Peers = {
};
export type PeerBundle = {
location: GeoLocation,
knownPeers: {[string]: KnownPeer},
attempts: Array<UnknownPeer>,
location: GeoLocation,
knownPeers: {[string]: KnownPeer},
attempts: number,
};
export type KnownPeer = {
@@ -81,14 +90,12 @@ export type KnownPeer = {
disconnected: Array<Date>,
ingress: Array<ChartEntries>,
egress: Array<ChartEntries>,
name: string,
enode: string,
protocols: {[string]: Object},
active: boolean,
};
export type UnknownPeer = {
connected: Date,
disconnected: Date,
};
export type GeoLocation = {
country: string,
city: string,
+739 -664
View File
File diff suppressed because it is too large Load Diff