diff --git a/dashboard/assets/components/Chain.jsx b/dashboard/assets/components/Chain.jsx new file mode 100644 index 0000000000..3b1bd334d2 --- /dev/null +++ b/dashboard/assets/components/Chain.jsx @@ -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 . + +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 { + render() { + return <>; + } +} + +export default Chain; diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx index a36905ead8..58a431c06b 100644 --- a/dashboard/assets/components/Dashboard.jsx +++ b/dashboard/assets/components/Dashboard.jsx @@ -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 = (limit: number, mapper = replacer) => (update: Array, pre // the execution of unnecessary operations (e.g. copy of the log array). const defaultContent: () => Content = () => ({ general: { - version: null, - commit: 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: [], @@ -119,11 +127,12 @@ const defaultContent: () => Content = () => ({ // TODO (kurkomisi): Define a tricky type which embraces the content and the updaters. const updaters = { general: { - version: replacer, - commit: replacer, + 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 {
{ 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