Dashboard built with React.js
This commit is contained in:
parent
4db8e84cac
commit
54b1aa9c56
|
@ -154,5 +154,6 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
tmpl.ExecuteTemplate(w, "dashboard", map[string]interface{}{
|
tmpl.ExecuteTemplate(w, "dashboard", map[string]interface{}{
|
||||||
"version": Version,
|
"version": Version,
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
|
"port": s.port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading, table {
|
.heading, #dashboard {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 650px;
|
width: 650px;
|
||||||
|
@ -15,8 +15,11 @@
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
table {
|
#dashboard {
|
||||||
top: 100px;
|
top: 100px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
}
|
}
|
||||||
|
@ -31,9 +34,6 @@ th {
|
||||||
thead tr {
|
thead tr {
|
||||||
border-bottom: #666 1px solid;
|
border-bottom: #666 1px solid;
|
||||||
}
|
}
|
||||||
/*tbody tr:nth-child(even) {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}*/
|
|
||||||
.title {
|
.title {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
|
@ -69,15 +69,7 @@ thead tr {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #f20;
|
color: #f20;
|
||||||
}
|
}
|
||||||
#loading {
|
.placeholder {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
font-size: 0.5em;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
#placeholder td {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,191 +0,0 @@
|
||||||
/*
|
|
||||||
* Dashboard
|
|
||||||
*/
|
|
||||||
|
|
||||||
function loadStatus(callback) {
|
|
||||||
var xhr = new XMLHttpRequest(),
|
|
||||||
loading = document.getElementById('loading');
|
|
||||||
|
|
||||||
loading.setAttribute('style', 'display: block;');
|
|
||||||
xhr.open('GET', '/status?rates=please', true);
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
var queues = JSON.parse(xhr.responseText);
|
|
||||||
loading.setAttribute('style', 'display: none;');
|
|
||||||
callback(queues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDashboard(queues) {
|
|
||||||
var queuesList = document.getElementById('queues'),
|
|
||||||
placeholder = document.getElementById('placeholder'),
|
|
||||||
fatThreshold = 100,
|
|
||||||
hotThreshold = 1000;
|
|
||||||
|
|
||||||
if (Object.keys(queues).length === 0) {
|
|
||||||
var td = placeholder.getElementsByTagName('td')[0];
|
|
||||||
td.innerHTML = 'Empty';
|
|
||||||
} else if (placeholder) {
|
|
||||||
queuesList.removeChild(placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (queue in queues) {
|
|
||||||
var meta = queues[queue],
|
|
||||||
id = 'queue_' + queue,
|
|
||||||
tr = document.getElementById(id);
|
|
||||||
|
|
||||||
if (!tr) {
|
|
||||||
tr = document.createElement('tr');
|
|
||||||
tr.setAttribute('id', id);
|
|
||||||
|
|
||||||
var titleCol = document.createElement('td'),
|
|
||||||
messagesCol = document.createElement('td'),
|
|
||||||
subscriptionsCol = document.createElement('td'),
|
|
||||||
nameDiv = document.createElement('div'),
|
|
||||||
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
|
|
||||||
pathIn = document.createElementNS('http://www.w3.org/2000/svg', 'path'),
|
|
||||||
pathOut = document.createElementNS('http://www.w3.org/2000/svg', 'path'),
|
|
||||||
messagesDiv = document.createElement('div'),
|
|
||||||
subscriptionsDiv = document.createElement('div');
|
|
||||||
|
|
||||||
pathIn.setAttribute('class', 'in');
|
|
||||||
pathOut.setAttribute('class', 'out');
|
|
||||||
svg.setAttributeNS(null, 'viewbox', '0 0 300 40');
|
|
||||||
svg.appendChild(pathIn);
|
|
||||||
svg.appendChild(pathOut);
|
|
||||||
|
|
||||||
nameDiv.setAttribute('class', 'name');
|
|
||||||
svg.setAttribute('class', 'chart');
|
|
||||||
titleCol.setAttribute('class', 'title');
|
|
||||||
messagesCol.setAttribute('class', 'messages');
|
|
||||||
subscriptionsCol.setAttribute('class', 'subscriptions');
|
|
||||||
|
|
||||||
nameDiv.appendChild(document.createTextNode(queue));
|
|
||||||
titleCol.appendChild(svg);
|
|
||||||
titleCol.appendChild(nameDiv);
|
|
||||||
messagesCol.appendChild(messagesDiv);
|
|
||||||
subscriptionsCol.appendChild(subscriptionsDiv);
|
|
||||||
tr.appendChild(titleCol);
|
|
||||||
tr.appendChild(messagesCol);
|
|
||||||
tr.appendChild(subscriptionsCol);
|
|
||||||
queuesList.appendChild(tr);
|
|
||||||
}
|
|
||||||
|
|
||||||
var titleCol = tr.getElementsByClassName('title')[0],
|
|
||||||
nameDiv = titleCol.getElementsByClassName('name')[0],
|
|
||||||
svg = titleCol.getElementsByClassName('chart')[0],
|
|
||||||
messagesCol = tr.getElementsByClassName('messages')[0],
|
|
||||||
subscriptionsCol = tr.getElementsByClassName('subscriptions')[0],
|
|
||||||
messagesDiv = messagesCol.getElementsByTagName('div')[0],
|
|
||||||
subscriptionsDiv = subscriptionsCol.getElementsByTagName('div')[0],
|
|
||||||
messages = Number(meta.messages).toLocaleString(),
|
|
||||||
subscriptions = Number(meta.subscriptions).toLocaleString();
|
|
||||||
|
|
||||||
messagesDiv.innerHTML = messages;
|
|
||||||
subscriptionsDiv.innerHTML = subscriptions;
|
|
||||||
|
|
||||||
if (meta.messages > hotThreshold) {
|
|
||||||
nameDiv.setAttribute('class', 'name hot');
|
|
||||||
messagesDiv.setAttribute('class', 'num messages hot');
|
|
||||||
} else if (meta.messages > fatThreshold) {
|
|
||||||
nameDiv.setAttribute('class', 'name fat');
|
|
||||||
messagesDiv.setAttribute('class', 'num messages fat');
|
|
||||||
} else if (meta.messages === 0) {
|
|
||||||
messagesDiv.setAttribute('class', 'num messages zero');
|
|
||||||
} else {
|
|
||||||
nameDiv.setAttribute('class', 'name');
|
|
||||||
messagesDiv.setAttribute('class', 'num messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.subscriptions === 0) {
|
|
||||||
subscriptionsDiv.setAttribute('class', 'num subscriptions zero');
|
|
||||||
} else {
|
|
||||||
subscriptionsDiv.setAttribute('class', 'num subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
svg.setAttributeNS(null, 'viewbox', '0 0 300 '+ titleCol.offsetTop);
|
|
||||||
drawChart(svg, titleCol.offsetTop, meta.in_rate_history, meta.out_rate_history);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Charts
|
|
||||||
*/
|
|
||||||
|
|
||||||
function drawChart(svg, maxHeight, valuesIn, valuesOut) {
|
|
||||||
var pathIn = svg.getElementsByClassName('in')[0],
|
|
||||||
pathOut = svg.getElementsByClassName('out')[0],
|
|
||||||
// valuesIn = generateValues(300),
|
|
||||||
// valuesOut = generateValues(300),
|
|
||||||
maxDouble = calcMaxDouble(valuesIn, valuesOut),
|
|
||||||
pointsIn = [],
|
|
||||||
pointsOut = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < valuesIn.length; i++) {
|
|
||||||
var normIn = Math.ceil(valuesIn[i] / maxDouble * maxHeight),
|
|
||||||
normOut = Math.ceil(valuesOut[i] / maxDouble * maxHeight),
|
|
||||||
pointIn = maxHeight/2 - normIn,
|
|
||||||
pointOut = maxHeight/2 + normOut;
|
|
||||||
|
|
||||||
pointsIn.push(pointIn);
|
|
||||||
pointsOut.push(pointOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
pathIn.setAttributeNS(null, 'd', buildPathD(pointsIn, maxHeight));
|
|
||||||
pathIn.setAttributeNS(null, 'class', 'in');
|
|
||||||
pathOut.setAttributeNS(null, 'd', buildPathD(pointsOut, maxHeight));
|
|
||||||
pathOut.setAttributeNS(null, 'class', 'out');
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateValues(num) {
|
|
||||||
var values = [];
|
|
||||||
for (var i = 0; i < num; i++) {
|
|
||||||
var value = Math.ceil(Math.random() * 60) + 30;
|
|
||||||
values.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcMaxDouble(a, b) {
|
|
||||||
var doubleValue = 0;
|
|
||||||
for (var i = 0; i < a.length; i++) {
|
|
||||||
if (a[i] * 2 > doubleValue) {
|
|
||||||
doubleValue = a[i] * 2;
|
|
||||||
}
|
|
||||||
if (b[i] * 2 > doubleValue) {
|
|
||||||
doubleValue = b[i] * 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return doubleValue * 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPathD(points, maxHeight) {
|
|
||||||
var d = ['M0,'+ maxHeight/2];
|
|
||||||
for (var i = 0; i < points.length; i++) {
|
|
||||||
d.push('L'+ i +','+ points[i]);
|
|
||||||
}
|
|
||||||
d.push('L300,'+ maxHeight/2, 'Z');
|
|
||||||
|
|
||||||
return d.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Starting up
|
|
||||||
*/
|
|
||||||
|
|
||||||
function loop(timeout, func) {
|
|
||||||
func();
|
|
||||||
window.setTimeout(function(){
|
|
||||||
loop(timeout, func);
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
loop(1000, function(){
|
|
||||||
loadStatus(updateDashboard);
|
|
||||||
});
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
var Chart = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {pointsIn: [], pointsOut: []};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.buildPoints(this.props);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this.buildPoints(nextProps);
|
||||||
|
},
|
||||||
|
|
||||||
|
buildPoints: function(props) {
|
||||||
|
var maxDouble = this.calcMaxDouble(props.valuesIn, props.valuesOut) || 1,
|
||||||
|
pointsIn = [],
|
||||||
|
pointsOut = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < props.valuesIn.length; i++) {
|
||||||
|
var normIn = Math.ceil(props.valuesIn[i] / maxDouble * props.height),
|
||||||
|
normOut = Math.ceil(props.valuesOut[i] / maxDouble * props.height),
|
||||||
|
pointIn = props.height/2 - normIn,
|
||||||
|
pointOut = props.height/2 + normOut;
|
||||||
|
|
||||||
|
pointsIn.push(pointIn);
|
||||||
|
pointsOut.push(pointOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({pointsIn: pointsIn, pointsOut: pointsOut});
|
||||||
|
},
|
||||||
|
|
||||||
|
calcMaxDouble: function(a, b) {
|
||||||
|
var doubleValue = 0;
|
||||||
|
for (var i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] * 2 > doubleValue) {
|
||||||
|
doubleValue = a[i] * 2;
|
||||||
|
}
|
||||||
|
if (b[i] * 2 > doubleValue) {
|
||||||
|
doubleValue = b[i] * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doubleValue * 1.2;
|
||||||
|
},
|
||||||
|
|
||||||
|
buildPathD: function(points) {
|
||||||
|
var d = ['M0,'+ this.props.height/2],
|
||||||
|
missing = this.props.width - points.length;
|
||||||
|
|
||||||
|
for (var i = 0; i < missing; i++) {
|
||||||
|
d.push('L'+ i +','+ this.props.height/2);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < points.length; i++) {
|
||||||
|
d.push('L'+ missing+i +','+ points[i]);
|
||||||
|
}
|
||||||
|
d.push('L'+ this.props.width +','+ this.props.height/2, 'Z');
|
||||||
|
|
||||||
|
return d.join(' ');
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var viewBox = [0, 0, this.props.width, this.props.height].join(' ');
|
||||||
|
return (
|
||||||
|
<svg className="chart" viewBox={viewBox}>
|
||||||
|
<path className="in" d={this.buildPathD(this.state.pointsIn)} />
|
||||||
|
<path className="out" d={this.buildPathD(this.state.pointsOut)} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var QueuesList = React.createClass({
|
||||||
|
render: function(){
|
||||||
|
if (!this.props.isDataRecieved) {
|
||||||
|
return (
|
||||||
|
<tbody id="queues">
|
||||||
|
<tr>
|
||||||
|
<td colSpan="3" className="placeholder">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(this.props.queues) === 0) {
|
||||||
|
return (
|
||||||
|
<tbody id="queues">
|
||||||
|
<tr>
|
||||||
|
<td colSpan="3" className="placeholder">This server has no queues</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var queues = this.props.queues;
|
||||||
|
var createQueue = function(name) {
|
||||||
|
var meta = queues[name],
|
||||||
|
titleClasses = ['title'],
|
||||||
|
messagesClasses = ['messages'],
|
||||||
|
subscriptionsClasses = ['subscriptions'];
|
||||||
|
|
||||||
|
if (meta.messages > 1000) {
|
||||||
|
titleClasses.push('hot');
|
||||||
|
messagesClasses.push('hot');
|
||||||
|
} else if (meta.messages > 100) {
|
||||||
|
titleClasses.push('fat');
|
||||||
|
messagesClasses.push('fat');
|
||||||
|
} else if (meta.messages === 0) {
|
||||||
|
messagesClasses.push('zero');
|
||||||
|
}
|
||||||
|
if (meta.subscriptions === 0) {
|
||||||
|
subscriptionsClasses.push('zero');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr key={name}>
|
||||||
|
<td className="title">
|
||||||
|
<div className={titleClasses.join(' ')}>{name}</div>
|
||||||
|
<Chart
|
||||||
|
valuesIn={meta.in_rate_history}
|
||||||
|
valuesOut={meta.out_rate_history}
|
||||||
|
width={300}
|
||||||
|
height={40} />
|
||||||
|
</td>
|
||||||
|
<td className={messagesClasses.join(' ')}>
|
||||||
|
<div className="num">{meta.messages}</div>
|
||||||
|
</td>
|
||||||
|
<td className={subscriptionsClasses.join(' ')}>
|
||||||
|
<div className="num">{meta.subscriptions}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tbody id="queues">
|
||||||
|
{Object.keys(queues).map(createQueue)}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var Dashboard = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {queues: {}, isDataRecieved: false};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.loop(this.props.interval, this.refresh);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
},
|
||||||
|
|
||||||
|
loop: function (timeout, func) {
|
||||||
|
var loop = this.loop;
|
||||||
|
func();
|
||||||
|
this.timeout = setTimeout(function(){
|
||||||
|
loop(timeout, func);
|
||||||
|
}, timeout);
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function () {
|
||||||
|
var xhr = new XMLHttpRequest()
|
||||||
|
self = this;
|
||||||
|
xhr.open('GET', '/status?rates=please', true);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
self.setState({
|
||||||
|
queues: JSON.parse(xhr.responseText),
|
||||||
|
isDataRecieved: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(null);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<table className="stats">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="name">Queue</th>
|
||||||
|
<th className="messages">Messages</th>
|
||||||
|
<th className="subscriptions">Subscriptions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<QueuesList queues={this.state.queues} isDataRecieved={this.state.isDataRecieved} />
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -4,26 +4,20 @@
|
||||||
<head>
|
<head>
|
||||||
<title>Queues @ {{.hostname}}</title>
|
<title>Queues @ {{.hostname}}</title>
|
||||||
<meta charset="utf8">
|
<meta charset="utf8">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/app.css">
|
<link rel="stylesheet" href="/static/app.css">
|
||||||
|
<script src="http://fb.me/react-0.12.2.js"></script>
|
||||||
|
<script src="http://fb.me/JSXTransformer-0.12.2.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 class="heading">Burlesque v{{.version}} at {{.hostname}}</h1>
|
<h1 class="heading">Burlesque v{{.version}} at {{.hostname}}</h1>
|
||||||
<table class="stats">
|
<div id="dashboard"></div>
|
||||||
<thead>
|
<script type="text/jsx" src="/static/app.jsx"></script>
|
||||||
<tr>
|
<script type="text/jsx">
|
||||||
<th class="name">Queue</th>
|
React.render(
|
||||||
<th class="messages">Messages</th>
|
<Dashboard api="http://127.0.0.1:{{.port}}/status" interval={1000} />,
|
||||||
<th class="subscriptions">Subscriptions</th>
|
document.getElementById('dashboard')
|
||||||
</tr>
|
);
|
||||||
</thead>
|
</script>
|
||||||
<tbody id="queues">
|
|
||||||
<tr id="placeholder">
|
|
||||||
<td colspan="3">Loading queues...</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<div id="loading">Loading...</div>
|
|
||||||
</table>
|
|
||||||
<script type="text/javascript" src="/static/app.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in New Issue