mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 01:13:45 +00:00
dashboard: React client
This commit is contained in:
parent
1d298a8338
commit
163aa99033
5 changed files with 370 additions and 372 deletions
File diff suppressed because one or more lines are too long
282
dashboard/assets/components/dashboard.js
Normal file
282
dashboard/assets/components/dashboard.js
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
Chart.defaults.global.legend = { enabled: false };
|
||||||
|
|
||||||
|
const Component = React.Component;
|
||||||
|
// const Component = Inferno.Component;
|
||||||
|
|
||||||
|
// isNullOrUndefined returns true if a variable is null or undefined.
|
||||||
|
let isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
|
||||||
|
|
||||||
|
// MenuItem renders an item for a Menu component and the belonging submenu items, if there is any.
|
||||||
|
class MenuItem extends Component {
|
||||||
|
render = () => <li>
|
||||||
|
<a>
|
||||||
|
<i className={`fa ${this.props.className}`}/>
|
||||||
|
{this.props.text}
|
||||||
|
<div className="fa fa-chevron-down"/>
|
||||||
|
</a>
|
||||||
|
{
|
||||||
|
// Render dropdown menu only if there are children.
|
||||||
|
isNullOrUndefined(this.props.children) || <ul className="nav child_menu">
|
||||||
|
{React.Children.map(this.props.children, child => <li>{child}</li>)}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu renders a menu component.
|
||||||
|
let Menu = () => <ul className="nav side-menu">
|
||||||
|
<MenuItem className="fa-home" text="Home">
|
||||||
|
<a href="dashboard1.html">Dashboard1</a>
|
||||||
|
<a href="dashboard2.html">Dashboard2</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-edit" text="Networking">
|
||||||
|
<a href="networking.html">Networking</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-desktop" text="Txpool">
|
||||||
|
<a href="txpool.html">Txpool</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-table" text="Logs">
|
||||||
|
<a href="logs.html">Logs</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-clone" text="Blockchain">
|
||||||
|
<a href="blockchain1.html">Blockchain1</a>
|
||||||
|
<a href="blockchain2.html">Blockchain2</a>
|
||||||
|
<a href="blockchain3.html">Blockchain3</a>
|
||||||
|
<a href="blockchain4.html">Blockchain4</a>
|
||||||
|
<a href="blockchain5.html">Blockchain5</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-bar-chart-o" text="System"/>
|
||||||
|
</ul>;
|
||||||
|
|
||||||
|
let Clearfix = () => <div className="clearfix"/>;
|
||||||
|
|
||||||
|
// SideBar renders a sidebar component.
|
||||||
|
let SideBar = () => <div className="col-md-3 left_col">
|
||||||
|
<div className="left_col scroll-view">
|
||||||
|
<div className="navbar nav_title" style={{border: 0}}>
|
||||||
|
<a href="dashboard.html" className="site_title"><i className="fa fa-paw"/> <span>Go Ethereum Dashboard</span></a>
|
||||||
|
</div>
|
||||||
|
<Clearfix/>
|
||||||
|
<div id="sidebar-menu" className="main_menu_side hidden-print main_menu">
|
||||||
|
<div className="menu_section">
|
||||||
|
<Menu/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
// TopNavigation renders a top navigation component.
|
||||||
|
let TopNavigation = () => <div className="top_nav">
|
||||||
|
<div className="nav_menu">
|
||||||
|
<nav className="" role="navigation">
|
||||||
|
<div className="nav toggle">
|
||||||
|
<a id="_______menu_toggle"> {/* TODO (kurkomisi): Resize the main container on toggle */}
|
||||||
|
<i className="fa fa-bars"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
// Chart name is already in use.
|
||||||
|
// ChartComponent renders a chart component and updates it, when the related data changes.
|
||||||
|
class ChartComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => this.state.chart = new Chart(this.data, {
|
||||||
|
type: this.props.type,
|
||||||
|
data: this.props.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
render = () => {
|
||||||
|
if (!isNullOrUndefined(this.state.chart)) {
|
||||||
|
this.state.chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={this.props.className}>
|
||||||
|
<div className="x_panel">
|
||||||
|
<div className="x_title">
|
||||||
|
<h2>{this.props.text}</h2>
|
||||||
|
<ul className="nav navbar-right panel_toolbox">
|
||||||
|
<li><a className="collapse-link"><i className="fa fa-chevron-up"/></a></li>
|
||||||
|
{
|
||||||
|
// Render dropdown menu only if there are children.
|
||||||
|
isNullOrUndefined(this.props.children) || <li className="dropdown">
|
||||||
|
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||||
|
aria-expanded="false"><i className="fa fa-wrench"/></a>
|
||||||
|
<ul className="dropdown-menu" role="menu">
|
||||||
|
{React.Children.map(this.props.children, child => <li>{child}</li>)}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
<li><a className="close-link"><i className="fa fa-close"/></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Clearfix/>
|
||||||
|
</div>
|
||||||
|
<div className="x_content">
|
||||||
|
{/* The chart will be generated here after the component is mounted (this.componentDidMount). */}
|
||||||
|
<canvas ref={data => {
|
||||||
|
this.data = data
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row renders a row component of charts only if there is any chart.
|
||||||
|
class Row extends Component {
|
||||||
|
render = () => isNullOrUndefined(this.props.children) || <div className="row"> {this.props.children} </div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageContent renders a component for the page content.
|
||||||
|
class PageContent extends Component {
|
||||||
|
render = () => <div className="right_col" role="main">
|
||||||
|
<div className="">
|
||||||
|
<div className="page-title">
|
||||||
|
<div className="title_left">
|
||||||
|
<h3>Go Ethereum Dashboard
|
||||||
|
<small>Statistics</small>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Clearfix/>
|
||||||
|
<Row>
|
||||||
|
<ChartComponent className="col-md-6 col-sm-6 col-xs-12" text="Memory usage system/memory/inuse" type="line" data={this.props.charts.memory}>
|
||||||
|
<a href="#">Settings 1</a>
|
||||||
|
<a href="#">Settings 2</a>
|
||||||
|
</ChartComponent>
|
||||||
|
<ChartComponent className="col-md-6 col-sm-6 col-xs-12" text="Inbound traffic p2p/InboundTraffic" type="line" data={this.props.charts.traffic}/>
|
||||||
|
</Row>
|
||||||
|
<Clearfix/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer renders a footer.
|
||||||
|
let Footer = () => <footer>
|
||||||
|
<div className="pull-right">
|
||||||
|
Gentelella - Bootstrap Admin Template by <a href="https://colorlib.com">Colorlib</a>
|
||||||
|
</div>
|
||||||
|
<Clearfix/>
|
||||||
|
</footer>;
|
||||||
|
|
||||||
|
const MEMORY_SAMPLE_LIMIT = 200; // Maximum number of memory data samples.
|
||||||
|
const TRAFFIC_SAMPLE_LIMIT = 200; // Maximum number of traffic data samples.
|
||||||
|
|
||||||
|
// Dashboard renders a full dashboard component, which makes connection with the server and waits for messages.
|
||||||
|
// When there is a message, correspondingly updates the page's content.
|
||||||
|
class Dashboard extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
charts: {
|
||||||
|
memory: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: "system/memory/inuse",
|
||||||
|
backgroundColor: "rgba(38, 185, 154, 0.31)",
|
||||||
|
borderColor: "rgba(38, 185, 154, 0.7)",
|
||||||
|
pointBorderColor: "rgba(38, 185, 154, 0.7)",
|
||||||
|
pointBackgroundColor: "rgba(38, 185, 154, 0.7)",
|
||||||
|
pointHoverBackgroundColor: "#fff",
|
||||||
|
pointHoverBorderColor: "rgba(220,220,220,1)",
|
||||||
|
pointBorderWidth: 1,
|
||||||
|
data: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
traffic: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: "p2p/InboundTraffic",
|
||||||
|
backgroundColor: "rgba(3, 88, 106, 0.3)",
|
||||||
|
borderColor: "rgba(3, 88, 106, 0.70)",
|
||||||
|
pointBorderColor: "rgba(3, 88, 106, 0.70)",
|
||||||
|
pointBackgroundColor: "rgba(3, 88, 106, 0.70)",
|
||||||
|
pointHoverBackgroundColor: "#fff",
|
||||||
|
pointHoverBorderColor: "rgba(151,187,205,1)",
|
||||||
|
pointBorderWidth: 1,
|
||||||
|
data: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCharts = msg => {
|
||||||
|
let memory = this.state.charts.memory;
|
||||||
|
let traffic = this.state.charts.traffic;
|
||||||
|
|
||||||
|
// Fill the dashboard with the past data. metrics is set only in the first msg, after the connection is established.
|
||||||
|
if (msg.metrics !== undefined) {
|
||||||
|
// Clear the arrays to prevent data confusion with the previous connection.
|
||||||
|
memory.labels = [];
|
||||||
|
traffic.labels = [];
|
||||||
|
memory.datasets[0].data = [];
|
||||||
|
traffic.datasets[0].data = [];
|
||||||
|
|
||||||
|
let mem = msg.metrics.memory;
|
||||||
|
let traff = msg.metrics.processor; // TODO (kurkomisi): !!!
|
||||||
|
|
||||||
|
// Put the past data to the beginning of the arrays. This prevents confusion with the next msg data, which goes to the end.
|
||||||
|
for (let i = mem.length - 1; i >= 0 && MEMORY_SAMPLE_LIMIT > memory.labels.length; --i) {
|
||||||
|
memory.labels.unshift(mem[i].time.substring(mem[i].time.length - 5));
|
||||||
|
traffic.labels.unshift(mem[i].time.substring(mem[i].time.length - 5));
|
||||||
|
memory.datasets[0].data.unshift(mem[i].value);
|
||||||
|
traffic.datasets[0].data.unshift(traff[i].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({charts: {memory: memory, traffic: traffic,}}); // Update the components.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the new data to the end of the arrays.
|
||||||
|
if (msg.memory !== undefined) {
|
||||||
|
// Remove the first elements in case the samples' amount exceeds the limit.
|
||||||
|
if (memory.labels.length === MEMORY_SAMPLE_LIMIT) {
|
||||||
|
memory.labels.shift();
|
||||||
|
traffic.labels.shift();
|
||||||
|
memory.datasets[0].data.shift();
|
||||||
|
traffic.datasets[0].data.shift();
|
||||||
|
}
|
||||||
|
memory.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
||||||
|
traffic.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
||||||
|
memory.datasets[0].data.push(msg.memory.value);
|
||||||
|
traffic.datasets[0].data.push(msg.processor.value);
|
||||||
|
|
||||||
|
this.setState({charts: {memory: memory, traffic: traffic,}}); // Update the components.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reconnect = () => {
|
||||||
|
let server = new WebSocket("ws://" + location.host + "/api");
|
||||||
|
let that = this;
|
||||||
|
|
||||||
|
server.onmessage = event => {
|
||||||
|
let msg = JSON.parse(event.data);
|
||||||
|
if (isNullOrUndefined(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
that.updateCharts(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
server.onclose = () => setTimeout(that.reconnect, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount = () => this.reconnect();
|
||||||
|
|
||||||
|
render = () => <div className="container body">
|
||||||
|
<div className="main_container">
|
||||||
|
<SideBar/>
|
||||||
|
<TopNavigation/>
|
||||||
|
<PageContent charts={this.state.charts}/>
|
||||||
|
<Footer/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -1,179 +1,48 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title>Go Ethereum Dashboard</title>
|
<title>Go Ethereum Dashboard</title>
|
||||||
|
|
||||||
<link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico"/>
|
<link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico"/>
|
||||||
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/css/custom.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/css/custom.min.css" rel="stylesheet">
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="nav-md">
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
|
||||||
<div class="container body">
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
|
||||||
<div class="main_container">
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script>
|
||||||
<div class="col-md-3 left_col">
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router/4.1.2/react-router.min.js"></script>
|
||||||
<div class="left_col scroll-view">
|
|
||||||
<div class="navbar nav_title" style="border: 0;">
|
|
||||||
<a href="dashboard.html" class="site_title"><i class="fa fa-paw"></i> <span>Go Ethereum Dashboard</span></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
|
|
||||||
<!-- sidebar menu -->
|
|
||||||
<div id="sidebar-menu" class="main_menu_side hidden-print main_menu">
|
|
||||||
<div class="menu_section">
|
|
||||||
<ul class="nav side-menu">
|
|
||||||
<li><a><i class="fa fa-home"></i> Home <span class="fa fa-chevron-down"></span></a>
|
|
||||||
<ul class="nav child_menu">
|
|
||||||
<li><a href="dashboard.html">Dashboard1</a></li>
|
|
||||||
<li><a href="dashboard.html">Dashboard2</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a><i class="fa fa-edit"></i> Networking <span class="fa fa-chevron-down"></span></a>
|
|
||||||
<ul class="nav child_menu">
|
|
||||||
<li><a href="networking.html"></a>Networking1</li>
|
|
||||||
<li><a href="networking.html"></a>Networking2</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a><i class="fa fa-desktop"></i> Txpool <span class="fa fa-chevron-down"></span></a>
|
|
||||||
<ul class="nav child_menu">
|
|
||||||
<li><a href="txpool.html">Txpool1</a></li>
|
|
||||||
<li><a href="txpool.html">Txpool2</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a><i class="fa fa-table"></i> Logs <span class="fa fa-chevron-down"></span></a>
|
|
||||||
<ul class="nav child_menu">
|
|
||||||
<li><a href="logs.html">Logs1</a></li>
|
|
||||||
<li><a href="logs.html">Logs2</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a><i class="fa fa-bar-chart-o"></i> System <span class="fa fa-chevron-down"></span></a>
|
|
||||||
<ul class="nav child_menu">
|
|
||||||
<li><a href="system.html">System1</a></li>
|
|
||||||
<li><a href="system.html">System2</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a><i class="fa fa-clone"></i>Blockchain <span class="fa fa-chevron-down"></span></a>
|
|
||||||
<ul class="nav child_menu">
|
|
||||||
<li><a href="blockchain.html">Blockchain1</a></li>
|
|
||||||
<li><a href="blockchain.html">Blockchain2</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /sidebar menu -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- top navigation -->
|
|
||||||
<div class="top_nav">
|
|
||||||
<div class="nav_menu">
|
|
||||||
<nav class="" role="navigation">
|
|
||||||
<div class="nav toggle">
|
|
||||||
<a id="menu_toggle"><i class="fa fa-bars"></i></a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /top navigation -->
|
|
||||||
|
|
||||||
<!-- page content -->
|
|
||||||
<div class="right_col" role="main">
|
|
||||||
<div class="">
|
|
||||||
<div class="page-title">
|
|
||||||
<div class="title_left">
|
|
||||||
<h3>Go Ethereum Dashboard <small>Statistics</small></h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
|
||||||
<div class="x_panel">
|
|
||||||
<div class="x_title">
|
|
||||||
<h2>Memory usage <small>system/memory/inuse</small></h2>
|
|
||||||
<ul class="nav navbar-right panel_toolbox">
|
|
||||||
<li><a class="collapse-link"><i class="fa fa-chevron-up"></i></a>
|
|
||||||
</li>
|
|
||||||
<li class="dropdown">
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-wrench"></i></a>
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
|
||||||
<li><a href="#">Settings 1</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#">Settings 2</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a class="close-link"><i class="fa fa-close"></i></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
<div class="x_content">
|
|
||||||
<canvas id="memoryLineChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
|
||||||
<div class="x_panel">
|
|
||||||
<div class="x_title">
|
|
||||||
<h2>Inbound traffic <small>p2p/InboundTraffic</small></h2>
|
|
||||||
<ul class="nav navbar-right panel_toolbox">
|
|
||||||
<li><a class="collapse-link"><i class="fa fa-chevron-up"></i></a>
|
|
||||||
</li>
|
|
||||||
<li class="dropdown">
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-wrench"></i></a>
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
|
||||||
<li><a href="#">Settings 1</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#">Settings 2</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a class="close-link"><i class="fa fa-close"></i></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
<div class="x_content">
|
|
||||||
<canvas id="trafficLineChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /page content -->
|
|
||||||
|
|
||||||
<!-- footer content -->
|
|
||||||
<footer>
|
|
||||||
<div class="pull-right">
|
|
||||||
Gentelella - Bootstrap Admin Template by <a href="https://colorlib.com">Colorlib</a>
|
|
||||||
</div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</footer>
|
|
||||||
<!-- /footer content -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-noty/2.4.1/packaged/jquery.noty.packaged.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-noty/2.4.1/packaged/jquery.noty.packaged.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.0.0/Chart.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.0.0/Chart.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
|
|
||||||
|
|
||||||
<script src="js/handlers.js"></script>
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!--<script src="Something replacing babel plugin"></script>-->
|
||||||
|
<!--<script src="https://unpkg.com/inferno@3.7.0/dist/inferno.js"></script>-->
|
||||||
|
<!--<script src="https://unpkg.com/inferno-component@3.7.0/dist/inferno-component.js"></script>-->
|
||||||
|
<!--<script src="https://unpkg.com/inferno-router@3.7.0/dist/inferno-router.min.js"></script>-->
|
||||||
|
|
||||||
|
<div id="dashboard" class="nav-md"></div>
|
||||||
|
|
||||||
|
<script type="text/babel" src="components/dashboard.js"></script>
|
||||||
|
<script type="text/babel">
|
||||||
|
ReactDOM.render(<Dashboard/>, document.getElementById('dashboard'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--<script type="text/babel">-->
|
||||||
|
<!--Inferno.render(<Dashboard/>, document.getElementById('dashboard'));-->
|
||||||
|
<!--</script>-->
|
||||||
|
|
||||||
|
<script type="text/babel" src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
Chart.defaults.global.legend = {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Line chart for memory
|
|
||||||
var ctx = document.getElementById("memoryLineChart");
|
|
||||||
var memoryLineChart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: "system/memory/inuse",
|
|
||||||
backgroundColor: "rgba(38, 185, 154, 0.31)",
|
|
||||||
borderColor: "rgba(38, 185, 154, 0.7)",
|
|
||||||
pointBorderColor: "rgba(38, 185, 154, 0.7)",
|
|
||||||
pointBackgroundColor: "rgba(38, 185, 154, 0.7)",
|
|
||||||
pointHoverBackgroundColor: "#fff",
|
|
||||||
pointHoverBorderColor: "rgba(220,220,220,1)",
|
|
||||||
pointBorderWidth: 1,
|
|
||||||
data: []
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Line chart for traffic
|
|
||||||
var trafficCtx = document.getElementById("trafficLineChart");
|
|
||||||
var trafficLineChart = new Chart(trafficCtx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: "p2p/InboundTraffic",
|
|
||||||
backgroundColor: "rgba(3, 88, 106, 0.3)",
|
|
||||||
borderColor: "rgba(3, 88, 106, 0.70)",
|
|
||||||
pointBorderColor: "rgba(3, 88, 106, 0.70)",
|
|
||||||
pointBackgroundColor: "rgba(3, 88, 106, 0.70)",
|
|
||||||
pointHoverBackgroundColor: "#fff",
|
|
||||||
pointHoverBorderColor: "rgba(151,187,205,1)",
|
|
||||||
pointBorderWidth: 1,
|
|
||||||
data: []
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//TODO (kurkomisi): remove static values after debugging
|
|
||||||
const MEMORY_SAMPLE_LIMIT = 200;//{{.memorySampleLimit}}; // Maximum number of memory data samples
|
|
||||||
const TRAFFIC_SAMPLE_LIMIT = 200;//{{.trafficSampleLimit}}; // Maximum number of traffic data samples
|
|
||||||
const PROCESSOR_SAMPLE_LIMIT = 200;//{{.processorSampleLimit}}; // Maximum number of processor data samples
|
|
||||||
|
|
||||||
function updateCharts(msg) {
|
|
||||||
|
|
||||||
// Fill the dashboard with past data
|
|
||||||
if(msg.metrics !== undefined) {
|
|
||||||
// Clear in case the dashboard was opened before
|
|
||||||
memoryLineChart.data.labels = [];
|
|
||||||
trafficLineChart.data.labels = [];
|
|
||||||
memoryLineChart.data.datasets[0].data = [];
|
|
||||||
trafficLineChart.data.datasets[0].data = [];
|
|
||||||
|
|
||||||
var memory = msg.metrics.memory;
|
|
||||||
var processor = msg.metrics.processor;
|
|
||||||
// It is possible to get another message while filling the arrays by push(), so instead
|
|
||||||
// put the history to the beginning of the arrays
|
|
||||||
for (var i = memory.length - 1; i >= 0 && MEMORY_SAMPLE_LIMIT > memoryLineChart.data.labels.length; --i) {
|
|
||||||
memoryLineChart.data.labels.unshift(memory[i].time.substring(memory[i].time.length - 5));
|
|
||||||
trafficLineChart.data.labels.unshift(memory[i].time.substring(memory[i].time.length - 5));
|
|
||||||
memoryLineChart.data.datasets[0].data.unshift(memory[i].value);
|
|
||||||
trafficLineChart.data.datasets[0].data.unshift(processor[i].value);
|
|
||||||
}
|
|
||||||
memoryLineChart.update();
|
|
||||||
trafficLineChart.update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update
|
|
||||||
if(msg.memory !== undefined) {
|
|
||||||
if(memoryLineChart.data.labels.length === MEMORY_SAMPLE_LIMIT) {
|
|
||||||
memoryLineChart.data.labels.shift();
|
|
||||||
trafficLineChart.data.labels.shift();
|
|
||||||
memoryLineChart.data.datasets[0].data.shift();
|
|
||||||
trafficLineChart.data.datasets[0].data.shift();
|
|
||||||
}
|
|
||||||
memoryLineChart.data.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
|
||||||
trafficLineChart.data.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
|
||||||
memoryLineChart.data.datasets[0].data.push(msg.memory.value);
|
|
||||||
trafficLineChart.data.datasets[0].data.push(msg.processor.value);
|
|
||||||
memoryLineChart.update();
|
|
||||||
trafficLineChart.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global variables to hold the current status of the dashboard
|
|
||||||
var server;
|
|
||||||
|
|
||||||
// Define a method to reconnect upon server loss
|
|
||||||
var reconnect = function() {
|
|
||||||
server = new WebSocket("ws://" + location.host + "/api");
|
|
||||||
|
|
||||||
server.onmessage = function(event) {
|
|
||||||
var msg = JSON.parse(event.data);
|
|
||||||
if (msg === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCharts(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
server.onclose = function() { setTimeout(reconnect, 3000); };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establish a websocket connection to the API server
|
|
||||||
reconnect();
|
|
||||||
|
|
@ -48,13 +48,13 @@ type dashboard struct {
|
||||||
config *Config
|
config *Config
|
||||||
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
conns []*client // Currently live websocket connections
|
conns map[uint32]*client // Currently live websocket connections
|
||||||
Metrics *metricSamples `json:"metrics,omitempty"`
|
Metrics *metricSamples `json:"metrics,omitempty"`
|
||||||
Stats *status `json:"stats,omitempty"`
|
Stats *status `json:"stats,omitempty"`
|
||||||
lock sync.RWMutex // Lock protecting the dashboard's internals
|
lock sync.RWMutex // Lock protecting the dashboard's internals
|
||||||
|
|
||||||
closing map[*chan chan error]bool // Channels used for graceful exit
|
closing chan chan error // Channel used for graceful exit
|
||||||
mapLock sync.RWMutex // Lock protecting the closing map's internals
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
|
|
@ -81,9 +81,10 @@ type status struct {
|
||||||
// New creates a new dashboard instance with the given configuration.
|
// New creates a new dashboard instance with the given configuration.
|
||||||
func New(config *Config) (*dashboard, error) {
|
func New(config *Config) (*dashboard, error) {
|
||||||
return &dashboard{
|
return &dashboard{
|
||||||
|
conns: make(map[uint32]*client),
|
||||||
config: config,
|
config: config,
|
||||||
Metrics: &metricSamples{},
|
Metrics: &metricSamples{},
|
||||||
closing: make(map[*chan chan error]bool),
|
closing: make(chan chan error),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +96,7 @@ func (db *dashboard) APIs() []rpc.API { return nil }
|
||||||
|
|
||||||
// Start implements node.Service, starting the data collection thread and the listening server of the dashboard.
|
// Start implements node.Service, starting the data collection thread and the listening server of the dashboard.
|
||||||
func (db *dashboard) Start(server *p2p.Server) error {
|
func (db *dashboard) Start(server *p2p.Server) error {
|
||||||
|
db.wg.Add(1)
|
||||||
go db.collectData()
|
go db.collectData()
|
||||||
|
|
||||||
http.HandleFunc("/", db.webHandler)
|
http.HandleFunc("/", db.webHandler)
|
||||||
|
|
@ -106,20 +108,13 @@ func (db *dashboard) Start(server *p2p.Server) error {
|
||||||
}
|
}
|
||||||
db.listener = listener
|
db.listener = listener
|
||||||
|
|
||||||
go func() {
|
go http.Serve(listener, nil)
|
||||||
if err := http.Serve(listener, nil); err != nil {
|
|
||||||
log.Warn("Server failed", "err", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
||||||
func (db *dashboard) Stop() error {
|
func (db *dashboard) Stop() error {
|
||||||
db.lock.Lock()
|
|
||||||
defer db.lock.Unlock()
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
// Close the connection listener
|
// Close the connection listener
|
||||||
if err = db.listener.Close(); err != nil {
|
if err = db.listener.Close(); err != nil {
|
||||||
|
|
@ -127,18 +122,19 @@ func (db *dashboard) Stop() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
errc := make(chan error)
|
errc := make(chan error)
|
||||||
for closing := range db.closing {
|
db.closing <- errc
|
||||||
*closing <- errc
|
|
||||||
<-errc
|
<-errc
|
||||||
close(*closing)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
db.lock.Lock()
|
||||||
for _, c := range db.conns {
|
for _, c := range db.conns {
|
||||||
if err := c.conn.Close(); err != nil {
|
if err := c.conn.Close(); err != nil {
|
||||||
c.logger.Warn("Failed to close connection", "err", err)
|
c.logger.Warn("Failed to close connection", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db.conns = db.conns[:0]
|
db.lock.Unlock()
|
||||||
|
|
||||||
|
db.wg.Wait()
|
||||||
|
log.Info("Dashboard stopped")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -179,31 +175,28 @@ func (db *dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// apiHandler handles requests for the dashboard.
|
// apiHandler handles requests for the dashboard.
|
||||||
func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
|
id := atomic.AddUint32(&nextId, 1)
|
||||||
client := &client{
|
client := &client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
msg: make(chan *map[string]interface{}, 128),
|
msg: make(chan *map[string]interface{}, 128),
|
||||||
logger: log.New("id", atomic.AddUint32(&nextId, 1)),
|
logger: log.New("id", id),
|
||||||
}
|
}
|
||||||
|
|
||||||
loss := make(chan int)
|
loss := make(chan bool, 1) // Buffered channel as sender may exit early
|
||||||
|
|
||||||
// Start listening for messages to send.
|
// Start listening for messages to send.
|
||||||
|
db.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
closing := db.addClosing()
|
defer db.wg.Done()
|
||||||
defer db.removeClosing(closing)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case errc := <-*closing:
|
case <-loss:
|
||||||
errc <- nil
|
|
||||||
return
|
|
||||||
case val := <-loss:
|
|
||||||
loss <- val - 1
|
|
||||||
return
|
return
|
||||||
case msg := <-client.msg:
|
case msg := <-client.msg:
|
||||||
if err := websocket.JSON.Send(client.conn, msg); err != nil {
|
if err := websocket.JSON.Send(client.conn, msg); err != nil {
|
||||||
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
|
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
|
||||||
// TODO (kurkomisi): Handle message loss
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,36 +209,18 @@ func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
|
|
||||||
// Start tracking the connection and drop at connection loss.
|
// Start tracking the connection and drop at connection loss.
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
db.conns = append(db.conns, client)
|
db.conns[id] = client
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
|
defer func() {
|
||||||
go func() {
|
|
||||||
closing := db.addClosing()
|
|
||||||
defer db.removeClosing(closing)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case errc := <-*closing:
|
|
||||||
errc <- nil
|
|
||||||
case val := <-loss:
|
|
||||||
loss <- val - 1
|
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
for i, c := range db.conns {
|
delete(db.conns, id)
|
||||||
if c.conn == client.conn {
|
|
||||||
db.conns = append(db.conns[:i], db.conns[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fail := []byte{}
|
fail := []byte{}
|
||||||
if _, err := conn.Read(fail); err != nil {
|
if _, err := conn.Read(fail); err != nil {
|
||||||
loss <- 2
|
loss <- true
|
||||||
if val := <-loss; val > 0 {
|
|
||||||
loss <- val
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Ignore all messages
|
// Ignore all messages
|
||||||
|
|
@ -254,12 +229,11 @@ func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
|
|
||||||
// collectData collects the required data to plot on the dashboard.
|
// collectData collects the required data to plot on the dashboard.
|
||||||
func (db *dashboard) collectData() {
|
func (db *dashboard) collectData() {
|
||||||
closing := db.addClosing()
|
defer db.wg.Done()
|
||||||
defer db.removeClosing(closing)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case errc := <-*closing:
|
case errc := <-db.closing:
|
||||||
errc <- nil
|
errc <- nil
|
||||||
return
|
return
|
||||||
case <-time.After(db.config.Refresh):
|
case <-time.After(db.config.Refresh):
|
||||||
|
|
@ -283,10 +257,7 @@ func (db *dashboard) collectData() {
|
||||||
|
|
||||||
// update updates the dashboards through the live websocket connections.
|
// update updates the dashboards through the live websocket connections.
|
||||||
func (db *dashboard) update(processor *data, memory *data) {
|
func (db *dashboard) update(processor *data, memory *data) {
|
||||||
db.lock.Lock()
|
// Remove the first elements in case the samples' amount exceeds the limit.
|
||||||
defer db.lock.Unlock()
|
|
||||||
|
|
||||||
// if the samples' # exceeds the limit, just remove the first element
|
|
||||||
first := 0
|
first := 0
|
||||||
if len(db.Metrics.Processor) == processorSampleLimit {
|
if len(db.Metrics.Processor) == processorSampleLimit {
|
||||||
first = 1
|
first = 1
|
||||||
|
|
@ -303,6 +274,7 @@ func (db *dashboard) update(processor *data, memory *data) {
|
||||||
"memory": memory,
|
"memory": memory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.lock.Lock()
|
||||||
for _, c := range db.conns {
|
for _, c := range db.conns {
|
||||||
select {
|
select {
|
||||||
case c.msg <- msg:
|
case c.msg <- msg:
|
||||||
|
|
@ -310,18 +282,5 @@ func (db *dashboard) update(processor *data, memory *data) {
|
||||||
c.logger.Warn("Client message queue is full")
|
c.logger.Warn("Client message queue is full")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
db.lock.Unlock()
|
||||||
|
|
||||||
func (db *dashboard) addClosing() *chan chan error {
|
|
||||||
closing := make(chan chan error)
|
|
||||||
db.mapLock.Lock()
|
|
||||||
db.closing[&closing] = true
|
|
||||||
db.mapLock.Unlock()
|
|
||||||
return &closing
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *dashboard) removeClosing(closing *chan chan error) {
|
|
||||||
db.mapLock.Lock()
|
|
||||||
delete(db.closing, closing)
|
|
||||||
db.mapLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue