From 54b1aa9c562694ca53a10a83c837d9b1bd96d90f Mon Sep 17 00:00:00 2001 From: Gregory Eremin Date: Sun, 8 Feb 2015 20:49:26 +0700 Subject: [PATCH] Dashboard built with React.js --- server/server.go | 1 + server/static/app.css | 20 ++-- server/static/app.js | 191 ---------------------------------- server/static/app.jsx | 193 +++++++++++++++++++++++++++++++++++ server/static/dashboard.tmpl | 28 ++--- 5 files changed, 211 insertions(+), 222 deletions(-) delete mode 100644 server/static/app.js create mode 100644 server/static/app.jsx diff --git a/server/server.go b/server/server.go index 365adfe..3f2130d 100644 --- a/server/server.go +++ b/server/server.go @@ -154,5 +154,6 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) { tmpl.ExecuteTemplate(w, "dashboard", map[string]interface{}{ "version": Version, "hostname": hostname, + "port": s.port, }) } diff --git a/server/static/app.css b/server/static/app.css index 6e2020b..37fc7a5 100644 --- a/server/static/app.css +++ b/server/static/app.css @@ -4,7 +4,7 @@ font-weight: 300; } -.heading, table { +.heading, #dashboard { position: absolute; left: 50%; width: 650px; @@ -15,8 +15,11 @@ font-size: 1.8em; text-align: center; } -table { +#dashboard { top: 100px; +} +table { + width: 100%; border-collapse: collapse; border-spacing: 0; } @@ -31,9 +34,6 @@ th { thead tr { border-bottom: #666 1px solid; } -/*tbody tr:nth-child(even) { - background-color: #f5f5f5; -}*/ .title { position: relative; width: 350px; @@ -69,15 +69,7 @@ thead tr { font-weight: 600; color: #f20; } -#loading { - display: none; - position: absolute; - bottom: 10px; - right: 10px; - font-size: 0.5em; - width: auto; -} -#placeholder td { +.placeholder { text-align: center; } diff --git a/server/static/app.js b/server/static/app.js deleted file mode 100644 index 193bb76..0000000 --- a/server/static/app.js +++ /dev/null @@ -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); -}); diff --git a/server/static/app.jsx b/server/static/app.jsx new file mode 100644 index 0000000..b685e25 --- /dev/null +++ b/server/static/app.jsx @@ -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 ( + + + + + ); + } +}); + +var QueuesList = React.createClass({ + render: function(){ + if (!this.props.isDataRecieved) { + return ( + + + Loading... + + + ) + } + + if (Object.keys(this.props.queues) === 0) { + return ( + + + This server has no queues + + + ) + } + + 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 ( + + +
{name}
+ + + +
{meta.messages}
+ + +
{meta.subscriptions}
+ + + ); + }; + + return ( + + {Object.keys(queues).map(createQueue)} + + ); + } +}); + +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 ( + + + + + + + + + +
QueueMessagesSubscriptions
+ ); + } +}); diff --git a/server/static/dashboard.tmpl b/server/static/dashboard.tmpl index cf01668..d9ed1d0 100644 --- a/server/static/dashboard.tmpl +++ b/server/static/dashboard.tmpl @@ -4,26 +4,20 @@ Queues @ {{.hostname}} - + + +

Burlesque v{{.version}} at {{.hostname}}

- - - - - - - - - - - - - -
Loading...
-
QueueMessagesSubscriptions
Loading queues...
- +
+ + {{end}}