{
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 (
@@ -202,6 +222,9 @@ class Footer extends Component {
)}
+
+ Network {network}
+
);
diff --git a/dashboard/assets/components/Header.jsx b/dashboard/assets/components/Header.jsx
index 1e325dd8bd..7db5a990ef 100644
--- a/dashboard/assets/components/Header.jsx
+++ b/dashboard/assets/components/Header.jsx
@@ -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()
switchSideBar: () => void,
+ content: Content,
+ networkID: number,
};
+type State = {
+ since: string,
+}
// Header renders the header of the dashboard.
-class Header extends Component
{
+class Header extends Component {
+ 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 {
Go Ethereum Dashboard
+
+ {this.props.content.chain.currentBlock.number}
+
+
+ {this.state.since}
+
+
+ {this.props.content.network.activePeerCount}
+
);
diff --git a/dashboard/assets/components/Main.jsx b/dashboard/assets/components/Main.jsx
index 5bb13abd49..10529ae770 100644
--- a/dashboard/assets/components/Main.jsx
+++ b/dashboard/assets/components/Main.jsx
@@ -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 {
children = Work in progress.
;
break;
case MENU.get('chain').id:
- children = Work in progress.
;
+ children = ;
break;
case MENU.get('txpool').id:
children = Work in progress.
;
diff --git a/dashboard/assets/components/Network.jsx b/dashboard/assets/components/Network.jsx
index 053d92ba1d..bea3d61e83 100644
--- a/dashboard/assets/components/Network.jsx
+++ b/dashboard/assets/components/Network.jsx
@@ -379,6 +379,17 @@ 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;
};
diff --git a/dashboard/assets/types/content.jsx b/dashboard/assets/types/content.jsx
index 53560b7228..39c3d2ad6b 100644
--- a/dashboard/assets/types/content.jsx
+++ b/dashboard/assets/types/content.jsx
@@ -33,8 +33,9 @@ export type ChartEntry = {
};
export type General = {
- version: ?string,
- commit: ?string,
+ version: ?string,
+ commit: ?string,
+ genesis: ?string,
};
export type Home = {
@@ -42,16 +43,22 @@ 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
+ peers: Peers,
+ diff: Array,
+ activePeerCount: number,
};
export type PeerEvent = {
diff --git a/dashboard/assets/yarn.lock b/dashboard/assets/yarn.lock
index f15c35b3ba..80ca0796e4 100644
--- a/dashboard/assets/yarn.lock
+++ b/dashboard/assets/yarn.lock
@@ -2219,9 +2219,9 @@ d3-collection@1:
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
d3-color@1:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.3.tgz#6c67bb2af6df3cc8d79efcc4d3a3e83e28c8048f"
- integrity sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw==
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.5.tgz#5810ea1808f2f993d04508cb2fad764f48134788"
+ integrity sha512-u4CaFaqQKRofuhr9uo/xLdaGvvzdsMX7MgP42XgQJHLBRWnn0C0T+48rvj80cN9KXAauHEMEfe7ehacIoxmP/g==
d3-format@1:
version "1.3.2"
@@ -2559,9 +2559,9 @@ ee-first@1.1.1:
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.164:
- version "1.3.174"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.174.tgz#011c68d4130e995cbb69c533e147d7f4856e9f39"
- integrity sha512-OEh3EARo2B07ZRtxB0u9GqWyWmTeNS+diMp5bjw4kqMjgpzqM0w1zUOyErDsyWxTdArbvZ79T/w5n3WsBVHLfA==
+ version "1.3.175"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.175.tgz#a269716af5e549f9f3989ae38ba484881dcb3702"
+ integrity sha512-cQ0o7phcPA0EYkN4juZy/rq4XVxl/1ywQnytDT9hZTC0cHfaOBqWK2XlWp9Mk8xRGntgnmxlHTOux4HLb2ZNnA==
elliptic@^6.0.0:
version "6.5.0"
@@ -3498,9 +3498,9 @@ got@^8.3.2:
url-to-options "^1.0.1"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
- version "4.1.15"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
- integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
+ integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==
handle-thing@^2.0.0:
version "2.0.0"
diff --git a/dashboard/chain.go b/dashboard/chain.go
new file mode 100644
index 0000000000..e172ebda03
--- /dev/null
+++ b/dashboard/chain.go
@@ -0,0 +1,77 @@
+package dashboard
+
+import (
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/event"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+type block struct {
+ Number int64 `json:"number,omitempty"`
+ Time uint64 `json:"timestamp,omitempty"`
+}
+
+func (db *Dashboard) collectChainData() {
+ defer db.wg.Done()
+
+ var (
+ currentBlock *block
+ chainCh chan core.ChainHeadEvent
+ chainSub event.Subscription
+ )
+ switch {
+ case db.ethServ != nil:
+ chain := db.ethServ.BlockChain()
+ currentBlock = &block{
+ Number: chain.CurrentHeader().Number.Int64(),
+ Time: chain.CurrentHeader().Time,
+ }
+ chainCh = make(chan core.ChainHeadEvent)
+ chainSub = chain.SubscribeChainHeadEvent(chainCh)
+ case db.lesServ != nil:
+ chain := db.lesServ.BlockChain()
+ currentBlock = &block{
+ Number: chain.CurrentHeader().Number.Int64(),
+ Time: chain.CurrentHeader().Time,
+ }
+ chainCh = make(chan core.ChainHeadEvent)
+ chainSub = chain.SubscribeChainHeadEvent(chainCh)
+ default:
+ errc := <-db.quit
+ errc <- nil
+ return
+ }
+ defer chainSub.Unsubscribe()
+
+ db.chainLock.Lock()
+ db.history.Chain = &ChainMessage{
+ CurrentBlock: currentBlock,
+ }
+ db.chainLock.Unlock()
+ db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
+
+ for {
+ select {
+ case e := <-chainCh:
+ currentBlock := &block{
+ Number: e.Block.Number().Int64(),
+ Time: e.Block.Time(),
+ }
+ db.chainLock.Lock()
+ db.history.Chain = &ChainMessage{
+ CurrentBlock: currentBlock,
+ }
+ db.chainLock.Unlock()
+
+ db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
+ case err := <-chainSub.Err():
+ log.Warn("Chain subscription error", "err", err)
+ errc := <-db.quit
+ errc <- nil
+ return
+ case errc := <-db.quit:
+ errc <- nil
+ return
+ }
+ }
+}
diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go
index a410de7ff6..52bda1edf7 100644
--- a/dashboard/dashboard.go
+++ b/dashboard/dashboard.go
@@ -27,6 +27,7 @@ package dashboard
import (
"fmt"
+ "github.com/ethereum/go-ethereum/common"
"io"
"net"
"net/http"
@@ -46,6 +47,7 @@ import (
const (
sampleLimit = 200 // Maximum number of data samples
+ dataCollectorCount = 4
)
// Dashboard contains the dashboard internals.
@@ -58,10 +60,11 @@ type Dashboard struct {
history *Message // Stored historical data
- lock sync.Mutex // Lock protecting the dashboard's internals
- sysLock sync.RWMutex // Lock protecting the stored system data
- peerLock sync.RWMutex // Lock protecting the stored peer data
- logLock sync.RWMutex // Lock protecting the stored log data
+ lock sync.Mutex // Lock protecting the dashboard's internals
+ chainLock sync.RWMutex // Lock protecting the stored blockchain data
+ sysLock sync.RWMutex // Lock protecting the stored system data
+ peerLock sync.RWMutex // Lock protecting the stored peer data
+ logLock sync.RWMutex // Lock protecting the stored log data
geodb *geoDB // geoip database instance for IP to geographical information conversions
logdir string // Directory containing the log files
@@ -95,6 +98,12 @@ func New(config *Config, ethServ *eth.Ethereum, lesServ *les.LightEthereum, comm
if len(params.VersionMeta) > 0 {
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
}
+ var genesis common.Hash
+ if ethServ != nil {
+ genesis = ethServ.BlockChain().Genesis().Hash()
+ } else if lesServ != nil {
+ genesis = lesServ.BlockChain().Genesis().Hash()
+ }
return &Dashboard{
conns: make(map[uint32]*client),
config: config,
@@ -103,6 +112,7 @@ func New(config *Config, ethServ *eth.Ethereum, lesServ *les.LightEthereum, comm
General: &GeneralMessage{
Commit: commit,
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
+ Genesis: genesis,
},
System: &SystemMessage{
ActiveMemory: emptyChartEntries(sampleLimit),
@@ -143,7 +153,8 @@ func (db *Dashboard) APIs() []rpc.API { return nil }
func (db *Dashboard) Start(server *p2p.Server) error {
log.Info("Starting dashboard")
- db.wg.Add(3)
+ db.wg.Add(dataCollectorCount)
+ go db.collectChainData()
go db.collectSystemData()
go db.streamLogs()
go db.collectPeerData()
@@ -175,8 +186,8 @@ func (db *Dashboard) Stop() error {
errs = append(errs, err)
}
// Close the collectors.
- errc := make(chan error, 1)
- for i := 0; i < 3; i++ {
+ errc := make(chan error, dataCollectorCount)
+ for i := 0; i < dataCollectorCount; i++ {
db.quit <- errc
if err := <-errc; err != nil {
errs = append(errs, err)
@@ -250,20 +261,21 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
}()
// Send the past data.
+ db.chainLock.RLock()
db.sysLock.RLock()
db.peerLock.RLock()
db.logLock.RLock()
h := deepcopy.Copy(db.history).(*Message)
+ db.chainLock.RUnlock()
db.sysLock.RUnlock()
db.peerLock.RUnlock()
db.logLock.RUnlock()
- client.msg <- h
-
// Start tracking the connection and drop at connection loss.
db.lock.Lock()
+ client.msg <- h
db.conns[id] = client
db.lock.Unlock()
defer func() {
diff --git a/dashboard/message.go b/dashboard/message.go
index 15f4c5df31..01fa33c68b 100644
--- a/dashboard/message.go
+++ b/dashboard/message.go
@@ -18,6 +18,7 @@ package dashboard
import (
"encoding/json"
+ "github.com/ethereum/go-ethereum/common"
)
type Message struct {
@@ -37,8 +38,9 @@ type ChartEntry struct {
}
type GeneralMessage struct {
- Version string `json:"version,omitempty"`
- Commit string `json:"commit,omitempty"`
+ Version string `json:"version,omitempty"`
+ Commit string `json:"commit,omitempty"`
+ Genesis common.Hash `json:"genesis,omitempty"`
}
type HomeMessage struct {
@@ -46,7 +48,7 @@ type HomeMessage struct {
}
type ChainMessage struct {
- /* TODO (kurkomisi) */
+ CurrentBlock *block `json:"currentBlock,omitempty"`
}
type TxPoolMessage struct {
diff --git a/dashboard/peers.go b/dashboard/peers.go
index e9377ec518..1260b54875 100644
--- a/dashboard/peers.go
+++ b/dashboard/peers.go
@@ -339,6 +339,8 @@ func (db *Dashboard) collectPeerData() {
db.geodb, err = openGeoDB()
if err != nil {
log.Warn("Failed to open geodb", "err", err)
+ errc := <-db.quit
+ errc <- nil
return
}
defer db.geodb.close()
@@ -517,6 +519,8 @@ func (db *Dashboard) collectPeerData() {
newPeerEvents = newPeerEvents[:0]
case err := <-db.subPeer.Err():
log.Warn("Peer subscription error", "err", err)
+ errc := <-db.quit
+ errc <- nil
return
case errc := <-db.quit:
errc <- nil