cmd, dashboard: dashboard using React, Material-UI, Recharts (#15393)
* cmd, dashboard: dashboard using React, Material-UI, Recharts * cmd, dashboard, metrics: initial proof of concept dashboard * dashboard: delete blobs * dashboard: gofmt -s -w . * dashboard: minor text and code polishes
This commit is contained in:
parent
984c25ac40
commit
ba62215d9e
|
@ -33,3 +33,8 @@ profile.cov
|
||||||
|
|
||||||
# IdeaIDE
|
# IdeaIDE
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# dashboard
|
||||||
|
/dashboard/assets/node_modules
|
||||||
|
/dashboard/assets/stats.json
|
||||||
|
/dashboard/assets/public/bundle.js
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
"github.com/ethereum/go-ethereum/contracts/release"
|
"github.com/ethereum/go-ethereum/contracts/release"
|
||||||
|
"github.com/ethereum/go-ethereum/dashboard"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
"github.com/ethereum/go-ethereum/node"
|
"github.com/ethereum/go-ethereum/node"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
@ -76,10 +77,11 @@ type ethstatsConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type gethConfig struct {
|
type gethConfig struct {
|
||||||
Eth eth.Config
|
Eth eth.Config
|
||||||
Shh whisper.Config
|
Shh whisper.Config
|
||||||
Node node.Config
|
Node node.Config
|
||||||
Ethstats ethstatsConfig
|
Ethstats ethstatsConfig
|
||||||
|
Dashboard dashboard.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(file string, cfg *gethConfig) error {
|
func loadConfig(file string, cfg *gethConfig) error {
|
||||||
|
@ -110,9 +112,10 @@ func defaultNodeConfig() node.Config {
|
||||||
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
||||||
// Load defaults.
|
// Load defaults.
|
||||||
cfg := gethConfig{
|
cfg := gethConfig{
|
||||||
Eth: eth.DefaultConfig,
|
Eth: eth.DefaultConfig,
|
||||||
Shh: whisper.DefaultConfig,
|
Shh: whisper.DefaultConfig,
|
||||||
Node: defaultNodeConfig(),
|
Node: defaultNodeConfig(),
|
||||||
|
Dashboard: dashboard.DefaultConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config file.
|
// Load config file.
|
||||||
|
@ -134,6 +137,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SetShhConfig(ctx, stack, &cfg.Shh)
|
utils.SetShhConfig(ctx, stack, &cfg.Shh)
|
||||||
|
utils.SetDashboardConfig(ctx, &cfg.Dashboard)
|
||||||
|
|
||||||
return stack, cfg
|
return stack, cfg
|
||||||
}
|
}
|
||||||
|
@ -153,6 +157,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
|
||||||
|
|
||||||
utils.RegisterEthService(stack, &cfg.Eth)
|
utils.RegisterEthService(stack, &cfg.Eth)
|
||||||
|
|
||||||
|
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
|
||||||
|
utils.RegisterDashboardService(stack, &cfg.Dashboard)
|
||||||
|
}
|
||||||
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
|
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
|
||||||
shhEnabled := enableWhisper(ctx)
|
shhEnabled := enableWhisper(ctx)
|
||||||
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)
|
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)
|
||||||
|
|
|
@ -61,6 +61,11 @@ var (
|
||||||
utils.DataDirFlag,
|
utils.DataDirFlag,
|
||||||
utils.KeyStoreDirFlag,
|
utils.KeyStoreDirFlag,
|
||||||
utils.NoUSBFlag,
|
utils.NoUSBFlag,
|
||||||
|
utils.DashboardEnabledFlag,
|
||||||
|
utils.DashboardAddrFlag,
|
||||||
|
utils.DashboardPortFlag,
|
||||||
|
utils.DashboardRefreshFlag,
|
||||||
|
utils.DashboardAssetsFlag,
|
||||||
utils.EthashCacheDirFlag,
|
utils.EthashCacheDirFlag,
|
||||||
utils.EthashCachesInMemoryFlag,
|
utils.EthashCachesInMemoryFlag,
|
||||||
utils.EthashCachesOnDiskFlag,
|
utils.EthashCachesOnDiskFlag,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
"github.com/ethereum/go-ethereum/internal/debug"
|
"github.com/ethereum/go-ethereum/internal/debug"
|
||||||
"gopkg.in/urfave/cli.v1"
|
"gopkg.in/urfave/cli.v1"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppHelpTemplate is the test template for the default, global app help topic.
|
// AppHelpTemplate is the test template for the default, global app help topic.
|
||||||
|
@ -97,6 +98,16 @@ var AppHelpFlagGroups = []flagGroup{
|
||||||
utils.EthashDatasetsOnDiskFlag,
|
utils.EthashDatasetsOnDiskFlag,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
//{
|
||||||
|
// Name: "DASHBOARD",
|
||||||
|
// Flags: []cli.Flag{
|
||||||
|
// utils.DashboardEnabledFlag,
|
||||||
|
// utils.DashboardAddrFlag,
|
||||||
|
// utils.DashboardPortFlag,
|
||||||
|
// utils.DashboardRefreshFlag,
|
||||||
|
// utils.DashboardAssetsFlag,
|
||||||
|
// },
|
||||||
|
//},
|
||||||
{
|
{
|
||||||
Name: "TRANSACTION POOL",
|
Name: "TRANSACTION POOL",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
@ -268,6 +279,9 @@ func init() {
|
||||||
uncategorized := []cli.Flag{}
|
uncategorized := []cli.Flag{}
|
||||||
for _, flag := range data.(*cli.App).Flags {
|
for _, flag := range data.(*cli.App).Flags {
|
||||||
if _, ok := categorized[flag.String()]; !ok {
|
if _, ok := categorized[flag.String()]; !ok {
|
||||||
|
if strings.HasPrefix(flag.GetName(), "dashboard") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
uncategorized = append(uncategorized, flag)
|
uncategorized = append(uncategorized, flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
"github.com/ethereum/go-ethereum/core/state"
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/dashboard"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||||
"github.com/ethereum/go-ethereum/eth/gasprice"
|
"github.com/ethereum/go-ethereum/eth/gasprice"
|
||||||
|
@ -183,6 +184,31 @@ var (
|
||||||
Name: "lightkdf",
|
Name: "lightkdf",
|
||||||
Usage: "Reduce key-derivation RAM & CPU usage at some expense of KDF strength",
|
Usage: "Reduce key-derivation RAM & CPU usage at some expense of KDF strength",
|
||||||
}
|
}
|
||||||
|
// Dashboard settings
|
||||||
|
DashboardEnabledFlag = cli.BoolFlag{
|
||||||
|
Name: "dashboard",
|
||||||
|
Usage: "Enable the dashboard",
|
||||||
|
}
|
||||||
|
DashboardAddrFlag = cli.StringFlag{
|
||||||
|
Name: "dashboard.addr",
|
||||||
|
Usage: "Dashboard listening interface",
|
||||||
|
Value: dashboard.DefaultConfig.Host,
|
||||||
|
}
|
||||||
|
DashboardPortFlag = cli.IntFlag{
|
||||||
|
Name: "dashboard.host",
|
||||||
|
Usage: "Dashboard listening port",
|
||||||
|
Value: dashboard.DefaultConfig.Port,
|
||||||
|
}
|
||||||
|
DashboardRefreshFlag = cli.DurationFlag{
|
||||||
|
Name: "dashboard.refresh",
|
||||||
|
Usage: "Dashboard metrics collection refresh rate",
|
||||||
|
Value: dashboard.DefaultConfig.Refresh,
|
||||||
|
}
|
||||||
|
DashboardAssetsFlag = cli.StringFlag{
|
||||||
|
Name: "dashboard.assets",
|
||||||
|
Usage: "Developer flag to serve the dashboard from the local file system",
|
||||||
|
Value: dashboard.DefaultConfig.Assets,
|
||||||
|
}
|
||||||
// Ethash settings
|
// Ethash settings
|
||||||
EthashCacheDirFlag = DirectoryFlag{
|
EthashCacheDirFlag = DirectoryFlag{
|
||||||
Name: "ethash.cachedir",
|
Name: "ethash.cachedir",
|
||||||
|
@ -1019,6 +1045,14 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDashboardConfig applies dashboard related command line flags to the config.
|
||||||
|
func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) {
|
||||||
|
cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name)
|
||||||
|
cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name)
|
||||||
|
cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name)
|
||||||
|
cfg.Assets = ctx.GlobalString(DashboardAssetsFlag.Name)
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterEthService adds an Ethereum client to the stack.
|
// RegisterEthService adds an Ethereum client to the stack.
|
||||||
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
||||||
var err error
|
var err error
|
||||||
|
@ -1041,6 +1075,13 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterDashboardService adds a dashboard to the stack.
|
||||||
|
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config) {
|
||||||
|
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||||
|
return dashboard.New(cfg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterShhService configures Whisper and adds it to the given node.
|
// RegisterShhService configures Whisper and adds it to the given node.
|
||||||
func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
|
func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
|
||||||
if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {
|
if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
## Go Ethereum Dashboard
|
||||||
|
|
||||||
|
The dashboard is a data visualizer integrated into geth, intended to collect and visualize useful information of an Ethereum node. It consists of two parts:
|
||||||
|
|
||||||
|
* The client visualizes the collected data.
|
||||||
|
* The server collects the data, and updates the clients.
|
||||||
|
|
||||||
|
The client's UI uses [React][React] with JSX syntax, which is validated by the [ESLint][ESLint] linter mostly according to the [Airbnb React/JSX Style Guide][Airbnb]. The style is defined in the `.eslintrc` configuration file. The resources are bundled into a single `bundle.js` file using [Webpack][Webpack], which relies on the `webpack.config.js`. The bundled file is referenced from `dashboard.html` and takes part in the `assets.go` too. The necessary dependencies for the module bundler are gathered by [Node.js][Node.js].
|
||||||
|
|
||||||
|
### Development and bundling
|
||||||
|
|
||||||
|
As the dashboard depends on certain NPM packages (which are not included in the go-ethereum repo), these need to be installed first:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ (cd dashboard/assets && npm install)
|
||||||
|
```
|
||||||
|
|
||||||
|
Normally the dashboard assets are bundled into Geth via `go-bindata` to avoid external dependencies. Rebuilding Geth after each UI modification however is not feasible from a developer perspective. Instead, we can run `webpack` in watch mode to automatically rebundle the UI, and ask `geth` to use external assets to not rely on compiled resources:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ (cd dashboard/assets && ./node_modules/.bin/webpack --watch)
|
||||||
|
$ geth --dashboard --dashboard.assets=dashboard/assets/public --vmodule=dashboard=5
|
||||||
|
```
|
||||||
|
|
||||||
|
To bundle up the final UI into Geth, run `webpack` and `go generate`:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ (cd dashboard/assets && ./node_modules/.bin/webpack)
|
||||||
|
$ go generate ./dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Have fun
|
||||||
|
|
||||||
|
[Webpack][Webpack] offers handy tools for visualizing the bundle's dependency tree and space usage.
|
||||||
|
|
||||||
|
* Generate the bundle's profile running `webpack --profile --json > stats.json`
|
||||||
|
* For the _dependency tree_ go to [Webpack Analyze][WA], and import `stats.json`
|
||||||
|
* For the _space usage_ go to [Webpack Visualizer][WV], and import `stats.json`
|
||||||
|
|
||||||
|
[React]: https://reactjs.org/
|
||||||
|
[ESLint]: https://eslint.org/
|
||||||
|
[Airbnb]: https://github.com/airbnb/javascript/tree/master/react
|
||||||
|
[Webpack]: https://webpack.github.io/
|
||||||
|
[WA]: http://webpack.github.io/analyse/
|
||||||
|
[WV]: http://chrisbateman.github.io/webpack-visualizer/
|
||||||
|
[Node.js]: https://nodejs.org/en/
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright 2017 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/>.
|
||||||
|
|
||||||
|
// React syntax style mostly according to https://github.com/airbnb/javascript/tree/master/react
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true,
|
||||||
|
"modules": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/prefer-es6-class": 2,
|
||||||
|
"react/prefer-stateless-function": 2,
|
||||||
|
"react/jsx-pascal-case": 2,
|
||||||
|
"react/jsx-closing-bracket-location": [1, {"selfClosing": "tag-aligned", "nonEmpty": "tag-aligned"}],
|
||||||
|
"react/jsx-closing-tag-location": 1,
|
||||||
|
"jsx-quotes": ["error", "prefer-double"],
|
||||||
|
"no-multi-spaces": "error",
|
||||||
|
"react/jsx-tag-spacing": 2,
|
||||||
|
"react/jsx-curly-spacing": [2, {"when": "never", "children": true}],
|
||||||
|
"react/jsx-boolean-value": 2,
|
||||||
|
"react/no-string-refs": 2,
|
||||||
|
"react/jsx-wrap-multilines": 2,
|
||||||
|
"react/self-closing-comp": 2,
|
||||||
|
"react/jsx-no-bind": 2,
|
||||||
|
"react/require-render-return": 2,
|
||||||
|
"react/no-is-mounted": 2,
|
||||||
|
"key-spacing": ["error", {"align": {
|
||||||
|
"beforeColon": false,
|
||||||
|
"afterColon": true,
|
||||||
|
"on": "value"
|
||||||
|
}}]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright 2017 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/>.
|
||||||
|
|
||||||
|
// isNullOrUndefined returns true if the given variable is null or undefined.
|
||||||
|
export const isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
|
||||||
|
|
||||||
|
export const LIMIT = {
|
||||||
|
memory: 200, // Maximum number of memory data samples.
|
||||||
|
traffic: 200, // Maximum number of traffic data samples.
|
||||||
|
log: 200, // Maximum number of logs.
|
||||||
|
};
|
||||||
|
// The sidebar menu and the main content are rendered based on these elements.
|
||||||
|
export const TAGS = (() => {
|
||||||
|
const T = {
|
||||||
|
home: { title: "Home", },
|
||||||
|
chain: { title: "Chain", },
|
||||||
|
transactions: { title: "Transactions", },
|
||||||
|
network: { title: "Network", },
|
||||||
|
system: { title: "System", },
|
||||||
|
logs: { title: "Logs", },
|
||||||
|
};
|
||||||
|
// Using the key is circumstantial in some cases, so it is better to insert it also as a value.
|
||||||
|
// This way the mistyping is prevented.
|
||||||
|
for(let key in T) {
|
||||||
|
T[key]['id'] = key;
|
||||||
|
}
|
||||||
|
return T;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const DATA_KEYS = (() => {
|
||||||
|
const DK = {};
|
||||||
|
["memory", "traffic", "logs"].map(key => {
|
||||||
|
DK[key] = key;
|
||||||
|
});
|
||||||
|
return DK;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Temporary - taken from Material-UI
|
||||||
|
export const DRAWER_WIDTH = 240;
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright 2017 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 PropTypes from 'prop-types';
|
||||||
|
import {withStyles} from 'material-ui/styles';
|
||||||
|
|
||||||
|
import SideBar from './SideBar.jsx';
|
||||||
|
import Header from './Header.jsx';
|
||||||
|
import Main from "./Main.jsx";
|
||||||
|
import {isNullOrUndefined, LIMIT, TAGS, DATA_KEYS,} from "./Common.jsx";
|
||||||
|
|
||||||
|
// Styles for the Dashboard component.
|
||||||
|
const styles = theme => ({
|
||||||
|
appFrame: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dashboard is the main component, which renders the whole page, makes connection with the server and listens for messages.
|
||||||
|
// When there is an incoming message, updates the page's content correspondingly.
|
||||||
|
class Dashboard extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
active: TAGS.home.id, // active menu
|
||||||
|
sideBar: true, // true if the sidebar is opened
|
||||||
|
memory: [],
|
||||||
|
traffic: [],
|
||||||
|
logs: [],
|
||||||
|
shouldUpdate: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
|
||||||
|
componentDidMount() {
|
||||||
|
this.reconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconnect establishes a websocket connection with the server, listens for incoming messages
|
||||||
|
// and tries to reconnect on connection loss.
|
||||||
|
reconnect = () => {
|
||||||
|
const server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api");
|
||||||
|
|
||||||
|
server.onmessage = event => {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (isNullOrUndefined(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.update(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
server.onclose = () => {
|
||||||
|
setTimeout(this.reconnect, 3000);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// update analyzes the incoming message, and updates the charts' content correspondingly.
|
||||||
|
update = msg => {
|
||||||
|
console.log(msg);
|
||||||
|
this.setState(prevState => {
|
||||||
|
let newState = [];
|
||||||
|
newState.shouldUpdate = {};
|
||||||
|
const insert = (key, values, limit) => {
|
||||||
|
newState[key] = [...prevState[key], ...values];
|
||||||
|
while (newState[key].length > limit) {
|
||||||
|
newState[key].shift();
|
||||||
|
}
|
||||||
|
newState.shouldUpdate[key] = true;
|
||||||
|
};
|
||||||
|
// (Re)initialize the state with the past data.
|
||||||
|
if (!isNullOrUndefined(msg.history)) {
|
||||||
|
const memory = DATA_KEYS.memory;
|
||||||
|
const traffic = DATA_KEYS.traffic;
|
||||||
|
newState[memory] = [];
|
||||||
|
newState[traffic] = [];
|
||||||
|
if (!isNullOrUndefined(msg.history.memorySamples)) {
|
||||||
|
newState[memory] = msg.history.memorySamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
|
||||||
|
while (newState[memory].length > LIMIT.memory) {
|
||||||
|
newState[memory].shift();
|
||||||
|
}
|
||||||
|
newState.shouldUpdate[memory] = true;
|
||||||
|
}
|
||||||
|
if (!isNullOrUndefined(msg.history.trafficSamples)) {
|
||||||
|
newState[traffic] = msg.history.trafficSamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
|
||||||
|
while (newState[traffic].length > LIMIT.traffic) {
|
||||||
|
newState[traffic].shift();
|
||||||
|
}
|
||||||
|
newState.shouldUpdate[traffic] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Insert the new data samples.
|
||||||
|
if (!isNullOrUndefined(msg.memory)) {
|
||||||
|
insert(DATA_KEYS.memory, [isNullOrUndefined(msg.memory.value) ? 0 : msg.memory.value], LIMIT.memory);
|
||||||
|
}
|
||||||
|
if (!isNullOrUndefined(msg.traffic)) {
|
||||||
|
insert(DATA_KEYS.traffic, [isNullOrUndefined(msg.traffic.value) ? 0 : msg.traffic.value], LIMIT.traffic);
|
||||||
|
}
|
||||||
|
if (!isNullOrUndefined(msg.log)) {
|
||||||
|
insert(DATA_KEYS.logs, [msg.log], LIMIT.log);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// The change of the active label on the SideBar component will trigger a new render in the Main component.
|
||||||
|
changeContent = active => {
|
||||||
|
this.setState(prevState => prevState.active !== active ? {active: active} : {});
|
||||||
|
};
|
||||||
|
|
||||||
|
openSideBar = () => {
|
||||||
|
this.setState({sideBar: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
closeSideBar = () => {
|
||||||
|
this.setState({sideBar: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// The classes property is injected by withStyles().
|
||||||
|
const {classes} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.appFrame}>
|
||||||
|
<Header
|
||||||
|
opened={this.state.sideBar}
|
||||||
|
open={this.openSideBar}
|
||||||
|
/>
|
||||||
|
<SideBar
|
||||||
|
opened={this.state.sideBar}
|
||||||
|
close={this.closeSideBar}
|
||||||
|
changeContent={this.changeContent}
|
||||||
|
/>
|
||||||
|
<Main
|
||||||
|
opened={this.state.sideBar}
|
||||||
|
active={this.state.active}
|
||||||
|
memory={this.state.memory}
|
||||||
|
traffic={this.state.traffic}
|
||||||
|
logs={this.state.logs}
|
||||||
|
shouldUpdate={this.state.shouldUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dashboard.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(Dashboard);
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright 2017 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 PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {withStyles} from 'material-ui/styles';
|
||||||
|
import AppBar from 'material-ui/AppBar';
|
||||||
|
import Toolbar from 'material-ui/Toolbar';
|
||||||
|
import Typography from 'material-ui/Typography';
|
||||||
|
import IconButton from 'material-ui/IconButton';
|
||||||
|
import MenuIcon from 'material-ui-icons/Menu';
|
||||||
|
|
||||||
|
import {DRAWER_WIDTH} from './Common.jsx';
|
||||||
|
|
||||||
|
// Styles for the Header component.
|
||||||
|
const styles = theme => ({
|
||||||
|
appBar: {
|
||||||
|
position: 'absolute',
|
||||||
|
transition: theme.transitions.create(['margin', 'width'], {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
appBarShift: {
|
||||||
|
marginLeft: DRAWER_WIDTH,
|
||||||
|
width: `calc(100% - ${DRAWER_WIDTH}px)`,
|
||||||
|
transition: theme.transitions.create(['margin', 'width'], {
|
||||||
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
marginLeft: 12,
|
||||||
|
marginRight: 20,
|
||||||
|
},
|
||||||
|
hide: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header renders a header, which contains a sidebar opener icon when that is closed.
|
||||||
|
class Header extends Component {
|
||||||
|
render() {
|
||||||
|
// The classes property is injected by withStyles().
|
||||||
|
const {classes} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar className={classNames(classes.appBar, this.props.opened && classes.appBarShift)}>
|
||||||
|
<Toolbar disableGutters={!this.props.opened}>
|
||||||
|
<IconButton
|
||||||
|
color="contrast"
|
||||||
|
aria-label="open drawer"
|
||||||
|
onClick={this.props.open}
|
||||||
|
className={classNames(classes.menuButton, this.props.opened && classes.hide)}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography type="title" color="inherit" noWrap>
|
||||||
|
Go Ethereum Dashboard
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
opened: PropTypes.bool.isRequired,
|
||||||
|
open: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(Header);
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright 2017 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 PropTypes from 'prop-types';
|
||||||
|
import Grid from 'material-ui/Grid';
|
||||||
|
import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line, ResponsiveContainer} from 'recharts';
|
||||||
|
import {withTheme} from 'material-ui/styles';
|
||||||
|
|
||||||
|
import {isNullOrUndefined, DATA_KEYS} from "./Common.jsx";
|
||||||
|
|
||||||
|
// ChartGrid renders a grid container for responsive charts.
|
||||||
|
// The children are Recharts components extended with the Material-UI's xs property.
|
||||||
|
class ChartGrid extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={this.props.spacing}>
|
||||||
|
{
|
||||||
|
React.Children.map(this.props.children, child => (
|
||||||
|
<Grid item xs={child.props.xs}>
|
||||||
|
<ResponsiveContainer width="100%" height={child.props.height}>
|
||||||
|
{React.cloneElement(child, {data: child.props.values.map(value => ({value: value}))})}
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartGrid.propTypes = {
|
||||||
|
spacing: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Home renders the home component.
|
||||||
|
class Home extends Component {
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
return !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.memory]) ||
|
||||||
|
!isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.traffic]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {theme} = this.props;
|
||||||
|
const memoryColor = theme.palette.primary[300];
|
||||||
|
const trafficColor = theme.palette.secondary[300];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartGrid spacing={24}>
|
||||||
|
<AreaChart xs={6} height={300} values={this.props.memory}>
|
||||||
|
<YAxis />
|
||||||
|
<Area type="monotone" dataKey="value" stroke={memoryColor} fill={memoryColor} />
|
||||||
|
</AreaChart>
|
||||||
|
<LineChart xs={6} height={300} values={this.props.traffic}>
|
||||||
|
<Line type="monotone" dataKey="value" stroke={trafficColor} dot={false} />
|
||||||
|
</LineChart>
|
||||||
|
<LineChart xs={6} height={300} values={this.props.memory}>
|
||||||
|
<YAxis />
|
||||||
|
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
|
||||||
|
<Line type="monotone" dataKey="value" stroke={memoryColor} dot={false} />
|
||||||
|
</LineChart>
|
||||||
|
<AreaChart xs={6} height={300} values={this.props.traffic}>
|
||||||
|
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
|
||||||
|
<Area type="monotone" dataKey="value" stroke={trafficColor} fill={trafficColor} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Home.propTypes = {
|
||||||
|
theme: PropTypes.object.isRequired,
|
||||||
|
shouldUpdate: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTheme()(Home);
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2017 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 PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {withStyles} from 'material-ui/styles';
|
||||||
|
|
||||||
|
import {TAGS, DRAWER_WIDTH} from "./Common.jsx";
|
||||||
|
import Home from './Home.jsx';
|
||||||
|
|
||||||
|
// ContentSwitch chooses and renders the proper page content.
|
||||||
|
class ContentSwitch extends Component {
|
||||||
|
render() {
|
||||||
|
switch(this.props.active) {
|
||||||
|
case TAGS.home.id:
|
||||||
|
return <Home memory={this.props.memory} traffic={this.props.traffic} shouldUpdate={this.props.shouldUpdate} />;
|
||||||
|
case TAGS.chain.id:
|
||||||
|
return null;
|
||||||
|
case TAGS.transactions.id:
|
||||||
|
return null;
|
||||||
|
case TAGS.network.id:
|
||||||
|
// Only for testing.
|
||||||
|
return null;
|
||||||
|
case TAGS.system.id:
|
||||||
|
return null;
|
||||||
|
case TAGS.logs.id:
|
||||||
|
return <div>{this.props.logs.map((log, index) => <div key={index}>{log}</div>)}</div>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentSwitch.propTypes = {
|
||||||
|
active: PropTypes.string.isRequired,
|
||||||
|
shouldUpdate: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
// styles contains the styles for the Main component.
|
||||||
|
const styles = theme => ({
|
||||||
|
content: {
|
||||||
|
width: '100%',
|
||||||
|
marginLeft: -DRAWER_WIDTH,
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
padding: theme.spacing.unit * 3,
|
||||||
|
transition: theme.transitions.create('margin', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
marginTop: 56,
|
||||||
|
overflow: 'auto',
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
content: {
|
||||||
|
height: 'calc(100% - 64px)',
|
||||||
|
marginTop: 64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentShift: {
|
||||||
|
marginLeft: 0,
|
||||||
|
transition: theme.transitions.create('margin', {
|
||||||
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main renders a component for the page content.
|
||||||
|
class Main extends Component {
|
||||||
|
render() {
|
||||||
|
// The classes property is injected by withStyles().
|
||||||
|
const {classes} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className={classNames(classes.content, this.props.opened && classes.contentShift)}>
|
||||||
|
<ContentSwitch
|
||||||
|
active={this.props.active}
|
||||||
|
memory={this.props.memory}
|
||||||
|
traffic={this.props.traffic}
|
||||||
|
logs={this.props.logs}
|
||||||
|
shouldUpdate={this.props.shouldUpdate}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Main.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
opened: PropTypes.bool.isRequired,
|
||||||
|
active: PropTypes.string.isRequired,
|
||||||
|
shouldUpdate: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(Main);
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright 2017 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 PropTypes from 'prop-types';
|
||||||
|
import {withStyles} from 'material-ui/styles';
|
||||||
|
import Drawer from 'material-ui/Drawer';
|
||||||
|
import {IconButton} from "material-ui";
|
||||||
|
import List, {ListItem, ListItemText} from 'material-ui/List';
|
||||||
|
import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
|
||||||
|
|
||||||
|
import {TAGS, DRAWER_WIDTH} from './Common.jsx';
|
||||||
|
|
||||||
|
// Styles for the SideBar component.
|
||||||
|
const styles = theme => ({
|
||||||
|
drawerPaper: {
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%',
|
||||||
|
width: DRAWER_WIDTH,
|
||||||
|
},
|
||||||
|
drawerHeader: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: '0 8px',
|
||||||
|
...theme.mixins.toolbar,
|
||||||
|
transitionDuration: {
|
||||||
|
enter: theme.transitions.duration.enteringScreen,
|
||||||
|
exit: theme.transitions.duration.leavingScreen,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// SideBar renders a sidebar component.
|
||||||
|
class SideBar extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// clickOn contains onClick event functions for the menu items.
|
||||||
|
// Instantiate only once, and reuse the existing functions to prevent the creation of
|
||||||
|
// new function instances every time the render method is triggered.
|
||||||
|
this.clickOn = {};
|
||||||
|
for(let key in TAGS) {
|
||||||
|
const id = TAGS[key].id;
|
||||||
|
this.clickOn[id] = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log(event.target.key);
|
||||||
|
this.props.changeContent(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// The classes property is injected by withStyles().
|
||||||
|
const {classes} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
type="persistent"
|
||||||
|
classes={{paper: classes.drawerPaper,}}
|
||||||
|
open={this.props.opened}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className={classes.drawerHeader}>
|
||||||
|
<IconButton onClick={this.props.close}>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<List>
|
||||||
|
{
|
||||||
|
Object.values(TAGS).map(tag => {
|
||||||
|
return (
|
||||||
|
<ListItem button key={tag.id} onClick={this.clickOn[tag.id]}>
|
||||||
|
<ListItemText primary={tag.title} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SideBar.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
opened: PropTypes.bool.isRequired,
|
||||||
|
close: PropTypes.func.isRequired,
|
||||||
|
changeContent: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withStyles(styles)(SideBar);
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2017 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 from 'react';
|
||||||
|
import {hydrate} from 'react-dom';
|
||||||
|
import {createMuiTheme, MuiThemeProvider} from 'material-ui/styles';
|
||||||
|
|
||||||
|
import Dashboard from './components/Dashboard.jsx';
|
||||||
|
|
||||||
|
// Theme for the dashboard.
|
||||||
|
const theme = createMuiTheme({
|
||||||
|
palette: {
|
||||||
|
type: 'dark',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renders the whole dashboard.
|
||||||
|
hydrate(
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<Dashboard />
|
||||||
|
</MuiThemeProvider>,
|
||||||
|
document.getElementById('dashboard')
|
||||||
|
);
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-eslint": "^8.0.1",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-preset-env": "^1.6.1",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"eslint": "^4.5.0",
|
||||||
|
"eslint-plugin-react": "^7.4.0",
|
||||||
|
"material-ui": "^1.0.0-beta.18",
|
||||||
|
"material-ui-icons": "^1.0.0-beta.17",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"recharts": "^1.0.0-beta.0",
|
||||||
|
"react": "^16.0.0",
|
||||||
|
"react-dom": "^16.0.0",
|
||||||
|
"url": "^0.11.0",
|
||||||
|
"webpack": "^3.5.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" style="height: 100%">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<title>Go Ethereum Dashboard</title>
|
||||||
|
<link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico"/>
|
||||||
|
|
||||||
|
<!-- TODO (kurkomisi): Return to the external libraries to speed up the bundling during development -->
|
||||||
|
</head>
|
||||||
|
<body style="height: 100%; margin: 0">
|
||||||
|
<div id="dashboard" style="height: 100%"></div>
|
||||||
|
<script src="bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2017 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/>.
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './index.jsx',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'public'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.jsx$/, // regexp for JSX files
|
||||||
|
loader: 'babel-loader', // The babel configuration is in the package.json.
|
||||||
|
query: {
|
||||||
|
presets: ['env', 'react', 'stage-0']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2017 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/>.
|
||||||
|
|
||||||
|
package dashboard
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// DefaultConfig contains default settings for the dashboard.
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 8080,
|
||||||
|
Refresh: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains the configuration parameters of the dashboard.
|
||||||
|
type Config struct {
|
||||||
|
// Host is the host interface on which to start the dashboard server. If this
|
||||||
|
// field is empty, no dashboard will be started.
|
||||||
|
Host string `toml:",omitempty"`
|
||||||
|
|
||||||
|
// Port is the TCP port number on which to start the dashboard server. The
|
||||||
|
// default zero value is/ valid and will pick a port number randomly (useful
|
||||||
|
// for ephemeral nodes).
|
||||||
|
Port int `toml:",omitempty"`
|
||||||
|
|
||||||
|
// Refresh is the refresh rate of the data updates, the chartEntry will be collected this often.
|
||||||
|
Refresh time.Duration `toml:",omitempty"`
|
||||||
|
|
||||||
|
// Assets offers a possibility to manually set the dashboard website's location on the server side.
|
||||||
|
// It is useful for debugging, avoids the repeated generation of the binary.
|
||||||
|
Assets string `toml:",omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,305 @@
|
||||||
|
// Copyright 2017 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/>.
|
||||||
|
|
||||||
|
package dashboard
|
||||||
|
|
||||||
|
//go:generate go-bindata -nometadata -o assets.go -prefix assets -pkg dashboard assets/public/...
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/rcrowley/go-metrics"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
memorySampleLimit = 200 // Maximum number of memory data samples
|
||||||
|
trafficSampleLimit = 200 // Maximum number of traffic data samples
|
||||||
|
)
|
||||||
|
|
||||||
|
var nextId uint32 // Next connection id
|
||||||
|
|
||||||
|
// Dashboard contains the dashboard internals.
|
||||||
|
type Dashboard struct {
|
||||||
|
config *Config
|
||||||
|
|
||||||
|
listener net.Listener
|
||||||
|
conns map[uint32]*client // Currently live websocket connections
|
||||||
|
charts charts // The collected data samples to plot
|
||||||
|
lock sync.RWMutex // Lock protecting the dashboard's internals
|
||||||
|
|
||||||
|
quit chan chan error // Channel used for graceful exit
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// message embraces the data samples of a client message.
|
||||||
|
type message struct {
|
||||||
|
History *charts `json:"history,omitempty"` // Past data samples
|
||||||
|
Memory *chartEntry `json:"memory,omitempty"` // One memory sample
|
||||||
|
Traffic *chartEntry `json:"traffic,omitempty"` // One traffic sample
|
||||||
|
Log string `json:"log,omitempty"` // One log
|
||||||
|
}
|
||||||
|
|
||||||
|
// client represents active websocket connection with a remote browser.
|
||||||
|
type client struct {
|
||||||
|
conn *websocket.Conn // Particular live websocket connection
|
||||||
|
msg chan message // Message queue for the update messages
|
||||||
|
logger log.Logger // Logger for the particular live websocket connection
|
||||||
|
}
|
||||||
|
|
||||||
|
// charts contains the collected data samples.
|
||||||
|
type charts struct {
|
||||||
|
Memory []*chartEntry `json:"memorySamples,omitempty"`
|
||||||
|
Traffic []*chartEntry `json:"trafficSamples,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// chartEntry represents one data sample
|
||||||
|
type chartEntry struct {
|
||||||
|
Time time.Time `json:"time,omitempty"`
|
||||||
|
Value float64 `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new dashboard instance with the given configuration.
|
||||||
|
func New(config *Config) (*Dashboard, error) {
|
||||||
|
return &Dashboard{
|
||||||
|
conns: make(map[uint32]*client),
|
||||||
|
config: config,
|
||||||
|
quit: make(chan chan error),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocols is a meaningless implementation of node.Service.
|
||||||
|
func (db *Dashboard) Protocols() []p2p.Protocol { return nil }
|
||||||
|
|
||||||
|
// APIs is a meaningless implementation of node.Service.
|
||||||
|
func (db *Dashboard) APIs() []rpc.API { return nil }
|
||||||
|
|
||||||
|
// Start implements node.Service, starting the data collection thread and the listening server of the dashboard.
|
||||||
|
func (db *Dashboard) Start(server *p2p.Server) error {
|
||||||
|
db.wg.Add(2)
|
||||||
|
go db.collectData()
|
||||||
|
go db.collectLogs() // In case of removing this line change 2 back to 1 in wg.Add.
|
||||||
|
|
||||||
|
http.HandleFunc("/", db.webHandler)
|
||||||
|
http.Handle("/api", websocket.Handler(db.apiHandler))
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
db.listener = listener
|
||||||
|
|
||||||
|
go http.Serve(listener, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
||||||
|
func (db *Dashboard) Stop() error {
|
||||||
|
// Close the connection listener.
|
||||||
|
var errs []error
|
||||||
|
if err := db.listener.Close(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
// Close the collectors.
|
||||||
|
errc := make(chan error, 1)
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
db.quit <- errc
|
||||||
|
if err := <-errc; err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Close the connections.
|
||||||
|
db.lock.Lock()
|
||||||
|
for _, c := range db.conns {
|
||||||
|
if err := c.conn.Close(); err != nil {
|
||||||
|
c.logger.Warn("Failed to close connection", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.lock.Unlock()
|
||||||
|
|
||||||
|
// Wait until every goroutine terminates.
|
||||||
|
db.wg.Wait()
|
||||||
|
log.Info("Dashboard stopped")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if len(errs) > 0 {
|
||||||
|
err = fmt.Errorf("%v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// webHandler handles all non-api requests, simply flattening and returning the dashboard website.
|
||||||
|
func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Debug("Request", "URL", r.URL)
|
||||||
|
|
||||||
|
path := r.URL.String()
|
||||||
|
if path == "/" {
|
||||||
|
path = "/dashboard.html"
|
||||||
|
}
|
||||||
|
// If the path of the assets is manually set
|
||||||
|
if db.config.Assets != "" {
|
||||||
|
blob, err := ioutil.ReadFile(filepath.Join(db.config.Assets, path))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to read file", "path", path, "err", err)
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(blob)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blob, err := Asset(filepath.Join("public", path))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to load the asset", "path", path, "err", err)
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiHandler handles requests for the dashboard.
|
||||||
|
func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
|
id := atomic.AddUint32(&nextId, 1)
|
||||||
|
client := &client{
|
||||||
|
conn: conn,
|
||||||
|
msg: make(chan message, 128),
|
||||||
|
logger: log.New("id", id),
|
||||||
|
}
|
||||||
|
done := make(chan struct{}) // Buffered channel as sender may exit early
|
||||||
|
|
||||||
|
// Start listening for messages to send.
|
||||||
|
db.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer db.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case msg := <-client.msg:
|
||||||
|
if err := websocket.JSON.Send(client.conn, msg); err != nil {
|
||||||
|
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
|
||||||
|
client.conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// Send the past data.
|
||||||
|
client.msg <- message{
|
||||||
|
History: &db.charts,
|
||||||
|
}
|
||||||
|
// Start tracking the connection and drop at connection loss.
|
||||||
|
db.lock.Lock()
|
||||||
|
db.conns[id] = client
|
||||||
|
db.lock.Unlock()
|
||||||
|
defer func() {
|
||||||
|
db.lock.Lock()
|
||||||
|
delete(db.conns, id)
|
||||||
|
db.lock.Unlock()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
fail := []byte{}
|
||||||
|
if _, err := conn.Read(fail); err != nil {
|
||||||
|
close(done)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ignore all messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectData collects the required data to plot on the dashboard.
|
||||||
|
func (db *Dashboard) collectData() {
|
||||||
|
defer db.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case errc := <-db.quit:
|
||||||
|
errc <- nil
|
||||||
|
return
|
||||||
|
case <-time.After(db.config.Refresh):
|
||||||
|
inboundTraffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
|
||||||
|
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
|
||||||
|
now := time.Now()
|
||||||
|
memory := &chartEntry{
|
||||||
|
Time: now,
|
||||||
|
Value: memoryInUse,
|
||||||
|
}
|
||||||
|
traffic := &chartEntry{
|
||||||
|
Time: now,
|
||||||
|
Value: inboundTraffic,
|
||||||
|
}
|
||||||
|
// Remove the first elements in case the samples' amount exceeds the limit.
|
||||||
|
first := 0
|
||||||
|
if len(db.charts.Memory) == memorySampleLimit {
|
||||||
|
first = 1
|
||||||
|
}
|
||||||
|
db.charts.Memory = append(db.charts.Memory[first:], memory)
|
||||||
|
first = 0
|
||||||
|
if len(db.charts.Traffic) == trafficSampleLimit {
|
||||||
|
first = 1
|
||||||
|
}
|
||||||
|
db.charts.Traffic = append(db.charts.Traffic[first:], traffic)
|
||||||
|
|
||||||
|
db.sendToAll(&message{
|
||||||
|
Memory: memory,
|
||||||
|
Traffic: traffic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectLogs collects and sends the logs to the active dashboards.
|
||||||
|
func (db *Dashboard) collectLogs() {
|
||||||
|
defer db.wg.Done()
|
||||||
|
|
||||||
|
// TODO (kurkomisi): log collection comes here.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case errc := <-db.quit:
|
||||||
|
errc <- nil
|
||||||
|
return
|
||||||
|
case <-time.After(db.config.Refresh / 2):
|
||||||
|
db.sendToAll(&message{
|
||||||
|
Log: "This is a fake log.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendToAll sends the given message to the active dashboards.
|
||||||
|
func (db *Dashboard) sendToAll(msg *message) {
|
||||||
|
db.lock.Lock()
|
||||||
|
for _, c := range db.conns {
|
||||||
|
select {
|
||||||
|
case c.msg <- *msg:
|
||||||
|
default:
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.lock.Unlock()
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import (
|
||||||
|
|
||||||
// MetricsEnabledFlag is the CLI flag name to use to enable metrics collections.
|
// MetricsEnabledFlag is the CLI flag name to use to enable metrics collections.
|
||||||
const MetricsEnabledFlag = "metrics"
|
const MetricsEnabledFlag = "metrics"
|
||||||
|
const DashboardEnabledFlag = "dashboard"
|
||||||
|
|
||||||
// Enabled is the flag specifying if metrics are enable or not.
|
// Enabled is the flag specifying if metrics are enable or not.
|
||||||
var Enabled = false
|
var Enabled = false
|
||||||
|
@ -39,7 +40,7 @@ var Enabled = false
|
||||||
// and peek into the command line args for the metrics flag.
|
// and peek into the command line args for the metrics flag.
|
||||||
func init() {
|
func init() {
|
||||||
for _, arg := range os.Args {
|
for _, arg := range os.Args {
|
||||||
if strings.TrimLeft(arg, "-") == MetricsEnabledFlag {
|
if flag := strings.TrimLeft(arg, "-"); flag == MetricsEnabledFlag || flag == DashboardEnabledFlag {
|
||||||
log.Info("Enabling metrics collection")
|
log.Info("Enabling metrics collection")
|
||||||
Enabled = true
|
Enabled = true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue