Forráskód Böngészése

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
Kurkó Mihály 8 éve
szülő
commit
ba62215d9e

+ 5 - 0
.gitignore

@@ -33,3 +33,8 @@ profile.cov
 
 # IdeaIDE
 .idea
+
+# dashboard
+/dashboard/assets/node_modules
+/dashboard/assets/stats.json
+/dashboard/assets/public/bundle.js

+ 14 - 7
cmd/geth/config.go

@@ -30,6 +30,7 @@ import (
 
 	"github.com/ethereum/go-ethereum/cmd/utils"
 	"github.com/ethereum/go-ethereum/contracts/release"
+	"github.com/ethereum/go-ethereum/dashboard"
 	"github.com/ethereum/go-ethereum/eth"
 	"github.com/ethereum/go-ethereum/node"
 	"github.com/ethereum/go-ethereum/params"
@@ -76,10 +77,11 @@ type ethstatsConfig struct {
 }
 
 type gethConfig struct {
-	Eth      eth.Config
-	Shh      whisper.Config
-	Node     node.Config
-	Ethstats ethstatsConfig
+	Eth       eth.Config
+	Shh       whisper.Config
+	Node      node.Config
+	Ethstats  ethstatsConfig
+	Dashboard dashboard.Config
 }
 
 func loadConfig(file string, cfg *gethConfig) error {
@@ -110,9 +112,10 @@ func defaultNodeConfig() node.Config {
 func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
 	// Load defaults.
 	cfg := gethConfig{
-		Eth:  eth.DefaultConfig,
-		Shh:  whisper.DefaultConfig,
-		Node: defaultNodeConfig(),
+		Eth:       eth.DefaultConfig,
+		Shh:       whisper.DefaultConfig,
+		Node:      defaultNodeConfig(),
+		Dashboard: dashboard.DefaultConfig,
 	}
 
 	// Load config file.
@@ -134,6 +137,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
 	}
 
 	utils.SetShhConfig(ctx, stack, &cfg.Shh)
+	utils.SetDashboardConfig(ctx, &cfg.Dashboard)
 
 	return stack, cfg
 }
@@ -153,6 +157,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
 
 	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
 	shhEnabled := enableWhisper(ctx)
 	shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)

+ 5 - 0
cmd/geth/main.go

@@ -61,6 +61,11 @@ var (
 		utils.DataDirFlag,
 		utils.KeyStoreDirFlag,
 		utils.NoUSBFlag,
+		utils.DashboardEnabledFlag,
+		utils.DashboardAddrFlag,
+		utils.DashboardPortFlag,
+		utils.DashboardRefreshFlag,
+		utils.DashboardAssetsFlag,
 		utils.EthashCacheDirFlag,
 		utils.EthashCachesInMemoryFlag,
 		utils.EthashCachesOnDiskFlag,

+ 14 - 0
cmd/geth/usage.go

@@ -25,6 +25,7 @@ import (
 	"github.com/ethereum/go-ethereum/cmd/utils"
 	"github.com/ethereum/go-ethereum/internal/debug"
 	"gopkg.in/urfave/cli.v1"
+	"strings"
 )
 
 // AppHelpTemplate is the test template for the default, global app help topic.
@@ -97,6 +98,16 @@ var AppHelpFlagGroups = []flagGroup{
 			utils.EthashDatasetsOnDiskFlag,
 		},
 	},
+	//{
+	//	Name: "DASHBOARD",
+	//	Flags: []cli.Flag{
+	//		utils.DashboardEnabledFlag,
+	//		utils.DashboardAddrFlag,
+	//		utils.DashboardPortFlag,
+	//		utils.DashboardRefreshFlag,
+	//		utils.DashboardAssetsFlag,
+	//	},
+	//},
 	{
 		Name: "TRANSACTION POOL",
 		Flags: []cli.Flag{
@@ -268,6 +279,9 @@ func init() {
 			uncategorized := []cli.Flag{}
 			for _, flag := range data.(*cli.App).Flags {
 				if _, ok := categorized[flag.String()]; !ok {
+					if strings.HasPrefix(flag.GetName(), "dashboard") {
+						continue
+					}
 					uncategorized = append(uncategorized, flag)
 				}
 			}

+ 41 - 0
cmd/utils/flags.go

@@ -38,6 +38,7 @@ import (
 	"github.com/ethereum/go-ethereum/core/state"
 	"github.com/ethereum/go-ethereum/core/vm"
 	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/dashboard"
 	"github.com/ethereum/go-ethereum/eth"
 	"github.com/ethereum/go-ethereum/eth/downloader"
 	"github.com/ethereum/go-ethereum/eth/gasprice"
@@ -183,6 +184,31 @@ var (
 		Name:  "lightkdf",
 		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
 	EthashCacheDirFlag = DirectoryFlag{
 		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.
 func RegisterEthService(stack *node.Node, cfg *eth.Config) {
 	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.
 func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
 	if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {

+ 46 - 0
dashboard/README.md

@@ -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/

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 71 - 0
dashboard/assets.go


+ 52 - 0
dashboard/assets/.eslintrc

@@ -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"
+    }}]
+  }
+}

+ 52 - 0
dashboard/assets/components/Common.jsx

@@ -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;

+ 169 - 0
dashboard/assets/components/Dashboard.jsx

@@ -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);

+ 87 - 0
dashboard/assets/components/Header.jsx

@@ -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);

+ 89 - 0
dashboard/assets/components/Home.jsx

@@ -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);

+ 109 - 0
dashboard/assets/components/Main.jsx

@@ -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);

+ 106 - 0
dashboard/assets/components/SideBar.jsx

@@ -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);

+ 36 - 0
dashboard/assets/index.jsx

@@ -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')
+);

+ 22 - 0
dashboard/assets/package.json

@@ -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"
+  }
+}

+ 17 - 0
dashboard/assets/public/dashboard.html

@@ -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>

+ 36 - 0
dashboard/assets/webpack.config.js

@@ -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']
+                }
+            },
+        ],
+    },
+};

+ 45 - 0
dashboard/config.go

@@ -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"`
+}

+ 305 - 0
dashboard/dashboard.go

@@ -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()
+}

+ 2 - 1
metrics/metrics.go

@@ -30,6 +30,7 @@ import (
 
 // MetricsEnabledFlag is the CLI flag name to use to enable metrics collections.
 const MetricsEnabledFlag = "metrics"
+const DashboardEnabledFlag = "dashboard"
 
 // Enabled is the flag specifying if metrics are enable or not.
 var Enabled = false
@@ -39,7 +40,7 @@ var Enabled = false
 // and peek into the command line args for the metrics flag.
 func init() {
 	for _, arg := range os.Args {
-		if strings.TrimLeft(arg, "-") == MetricsEnabledFlag {
+		if flag := strings.TrimLeft(arg, "-"); flag == MetricsEnabledFlag || flag == DashboardEnabledFlag {
 			log.Info("Enabling metrics collection")
 			Enabled = true
 		}

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott