infobox/node: render statistic charts based on prometheus backend
history: Frontend generated node charts with graphite backend Resolution for bower conflict Let the user configure the metrics in config.json Fix grunt task for deploying c3 css Fix tooltip layout issue
This commit is contained in:
parent
11a157c58a
commit
2a58bcf5f1
3
app.js
3
app.js
|
@ -14,7 +14,8 @@ require.config({
|
||||||
"virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
|
"virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
|
||||||
"rbush": "../bower_components/rbush/rbush",
|
"rbush": "../bower_components/rbush/rbush",
|
||||||
"helper": "../helper",
|
"helper": "../helper",
|
||||||
"jshashes": "../bower_components/jshashes/hashes"
|
"jshashes": "../bower_components/jshashes/hashes",
|
||||||
|
"c3": "../bower_components/c3/c3.min"
|
||||||
},
|
},
|
||||||
shim: {
|
shim: {
|
||||||
"leaflet.label": ["leaflet"],
|
"leaflet.label": ["leaflet"],
|
||||||
|
|
|
@ -25,12 +25,16 @@
|
||||||
"virtual-dom": "~2.0.1",
|
"virtual-dom": "~2.0.1",
|
||||||
"leaflet-providers": "~1.0.27",
|
"leaflet-providers": "~1.0.27",
|
||||||
"rbush": "https://github.com/mourner/rbush.git#~1.3.5",
|
"rbush": "https://github.com/mourner/rbush.git#~1.3.5",
|
||||||
"jshashes": "~1.0.5"
|
"jshashes": "~1.0.5",
|
||||||
|
"c3": "~0.4.10"
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
"Milan Pässler <me@petabyteboy.de>",
|
"Milan Pässler <me@petabyteboy.de>",
|
||||||
"Nils Schneider <nils@nilsschneider.net>"
|
"Nils Schneider <nils@nilsschneider.net>"
|
||||||
],
|
],
|
||||||
"license": "AGPL3",
|
"license": "AGPL3",
|
||||||
"private": true
|
"private": true,
|
||||||
|
"resolutions": {
|
||||||
|
"d3": "~3.5.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
{
|
{
|
||||||
"dataPath": "https://map.luebeck.freifunk.net/data/",
|
"dataPath": [
|
||||||
"siteName": "Freifunk Lübeck",
|
"http://map.ffgl.eu/data/",
|
||||||
|
"http://karte.ffdus.de/data/",
|
||||||
|
"http://karte.freifunk-iserlohn.de/data/"
|
||||||
|
],
|
||||||
|
"siteName": "Freifunk Fluss",
|
||||||
"mapSigmaScale": 0.5,
|
"mapSigmaScale": 0.5,
|
||||||
"showContact": true,
|
"showContact": true,
|
||||||
"maxAge": 14,
|
"maxAge": 14,
|
||||||
|
@ -19,9 +23,40 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"siteNames": [
|
"siteNames": [
|
||||||
{ "site": "ffhl", "name": "Lübeck" },
|
{ "site": "ffgl-bcd", "name": "Burscheid" },
|
||||||
{ "site": "ffeh", "name": "Entenhausen" },
|
{ "site": "ffgl-bgl", "name": "Bergisch Gladbach" },
|
||||||
{ "site": "ffgt", "name": "Gothamcity" },
|
{ "site": "ffgl-lln", "name": "Leichlingen" },
|
||||||
{ "site": "ffal", "name": "Atlantis" }
|
{ "site": "ffgl-ode", "name": "Odenthal" },
|
||||||
|
{ "site": "ffgl-ovr", "name": "Overath" },
|
||||||
|
{ "site": "ffgl-rrh", "name": "Rösrath" },
|
||||||
|
{ "site": "ffdus", "name": "Flingern" }
|
||||||
|
{ "site": "ffis", "name": "Iserlohn" }
|
||||||
|
],
|
||||||
|
"nodeCharts": [
|
||||||
|
{
|
||||||
|
"name": "Statistik",
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"id": "clients",
|
||||||
|
"color": "#1566A9",
|
||||||
|
"label": "Clients",
|
||||||
|
"query": "sum(statistics_clients_total{nodeid=%22{{NODE_ID}}%22})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "load",
|
||||||
|
"color": "1566A9",
|
||||||
|
"label": "Systemlast",
|
||||||
|
"query": "sum(statistics_loadavg{nodeid=%22{{NODE_ID}}%22})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rx",
|
||||||
|
"color": "#1566A9",
|
||||||
|
"label": "Traffic (RX, kbps)",
|
||||||
|
"query": "sum(rate(statistics_traffic_rx_bytes{nodeid=%22{{NODE_ID}}%22}[30m])/125)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultMetric": "clients",
|
||||||
|
"url": "https://prometheus.map.eulenfunk.de/api/v1/query_range"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<link rel="stylesheet" href="css/ionicons.min.css">
|
<link rel="stylesheet" href="css/ionicons.min.css">
|
||||||
<link rel="stylesheet" href="roboto-slab-fontface.css">
|
<link rel="stylesheet" href="roboto-slab-fontface.css">
|
||||||
<link rel="stylesheet" href="roboto-fontface.css">
|
<link rel="stylesheet" href="roboto-fontface.css">
|
||||||
|
<link rel="stylesheet" href="c3.min.css">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="vendor/es6-shim/es6-shim.min.js"></script>
|
<script src="vendor/es6-shim/es6-shim.min.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
|
<link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
|
||||||
<link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
|
<link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
|
||||||
<link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
|
<link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
|
||||||
|
<link rel="stylesheet" href="bower_components/c3/c3.min.css">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="bower_components/es6-shim/es6-shim.min.js"></script>
|
<script src="bower_components/es6-shim/es6-shim.min.js"></script>
|
||||||
<script src="bower_components/requirejs/require.js" data-main="app"></script>
|
<script src="bower_components/requirejs/require.js" data-main="app"></script>
|
||||||
|
|
255
lib/infobox/charts.js
Normal file
255
lib/infobox/charts.js
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
define(["c3", "d3"], function (c3, d3) {
|
||||||
|
|
||||||
|
var charts = function (node, config) {
|
||||||
|
this.node = node
|
||||||
|
|
||||||
|
this.chartConfig = config
|
||||||
|
this.chart = null
|
||||||
|
|
||||||
|
this.zoomConfig = {
|
||||||
|
"levels": [
|
||||||
|
{"label": "8h", "from": 8 * 3600, "interval": 15 * 60}
|
||||||
|
/*{"label": "24h", "from": "26h", "interval": "1h"},
|
||||||
|
{"label": "1m", "from": "1mon", "interval": "1d"},
|
||||||
|
{"label": "1y", "from": 31536000, "interval": 2628000}*/
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.zoomLevel = 0
|
||||||
|
|
||||||
|
this.c3Config = {
|
||||||
|
"size": {
|
||||||
|
"height": 240
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
bottom: 30
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"item": {
|
||||||
|
"onclick": function (id) {
|
||||||
|
this.api.hide()
|
||||||
|
this.api.show(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"format": {
|
||||||
|
"value": this.c3FormatToolTip.bind(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"axis": {
|
||||||
|
"x": {
|
||||||
|
"type": "timeseries",
|
||||||
|
"tick": {
|
||||||
|
"format": this.c3FormatXAxis.bind(this),
|
||||||
|
"rotate": -45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"min": 0,
|
||||||
|
"padding": {
|
||||||
|
"bottom": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cache = []
|
||||||
|
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
charts.prototype = {
|
||||||
|
|
||||||
|
init: function () {
|
||||||
|
// Workaround for endless loop bug
|
||||||
|
if (this.c3Config.axis.x.tick && this.c3Config.axis.x.tick.format && typeof this.c3Config.axis.x.tick.format === "function") {
|
||||||
|
if (this.c3Config.axis.x.tick.format && !this.c3Config.axis.x.tick._format)
|
||||||
|
this.c3Config.axis.x.tick._format = this.c3Config.axis.x.tick.format
|
||||||
|
this.c3Config.axis.x.tick.format = function (val) {
|
||||||
|
return this.c3Config.axis.x.tick._format(val)
|
||||||
|
}.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure metrics
|
||||||
|
this.c3Config.data = {
|
||||||
|
"keys": {
|
||||||
|
"x": "time",
|
||||||
|
"value": this.chartConfig.metrics.map(function (metric) {
|
||||||
|
return metric.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"colors": this.chartConfig.metrics.reduce(function (collector, metric) {
|
||||||
|
collector[metric.id] = metric.color
|
||||||
|
return collector
|
||||||
|
}, {}),
|
||||||
|
"names": this.chartConfig.metrics.reduce(function (collector, metric) {
|
||||||
|
collector[metric.id] = metric.label
|
||||||
|
return collector
|
||||||
|
}, {}),
|
||||||
|
"hide": this.chartConfig.metrics.map(function (metric) {
|
||||||
|
return metric.id
|
||||||
|
}).filter(function (id) {
|
||||||
|
return id !== this.chartConfig.defaultMetric
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
var div = document.createElement("div")
|
||||||
|
div.classList.add("chart")
|
||||||
|
var h4 = document.createElement("h4")
|
||||||
|
h4.textContent = this.chartConfig.name
|
||||||
|
div.appendChild(h4)
|
||||||
|
|
||||||
|
|
||||||
|
// Render chart
|
||||||
|
this.load(function (data) {
|
||||||
|
div.appendChild(this.renderChart(data))
|
||||||
|
|
||||||
|
// Render zoom controls
|
||||||
|
if (this.zoomConfig.levels.length > 0)
|
||||||
|
div.appendChild(this.renderZoomControls())
|
||||||
|
|
||||||
|
}.bind(this))
|
||||||
|
|
||||||
|
return div
|
||||||
|
},
|
||||||
|
|
||||||
|
renderChart: function (data) {
|
||||||
|
this.c3Config.data.json = data
|
||||||
|
this.chart = c3.generate(this.c3Config)
|
||||||
|
return this.chart.element
|
||||||
|
},
|
||||||
|
|
||||||
|
updateChart: function (data) {
|
||||||
|
this.c3Config.data.json = data
|
||||||
|
this.chart.load(this.c3Config.data)
|
||||||
|
},
|
||||||
|
|
||||||
|
renderZoomControls: function () {
|
||||||
|
// Draw zoom controls
|
||||||
|
var zoomDiv = document.createElement("div")
|
||||||
|
zoomDiv.classList.add("zoom-buttons")
|
||||||
|
|
||||||
|
var zoomButtons = []
|
||||||
|
this.zoomConfig.levels.forEach(function (v, level) {
|
||||||
|
var btn = document.createElement("button")
|
||||||
|
btn.classList.add("zoom-button")
|
||||||
|
btn.setAttribute("data-zoom-level", level)
|
||||||
|
|
||||||
|
if (level === this.zoomLevel)
|
||||||
|
btn.classList.add("active")
|
||||||
|
|
||||||
|
btn.onclick = function () {
|
||||||
|
if (level !== this.zoomLevel) {
|
||||||
|
zoomButtons.forEach(function (v, k) {
|
||||||
|
if (level !== k)
|
||||||
|
v.classList.remove("active")
|
||||||
|
else
|
||||||
|
v.classList.add("active")
|
||||||
|
})
|
||||||
|
this.setZoomLevel(level)
|
||||||
|
}
|
||||||
|
}.bind(this)
|
||||||
|
btn.textContent = v.label
|
||||||
|
zoomButtons[level] = btn
|
||||||
|
zoomDiv.appendChild(btn)
|
||||||
|
}.bind(this))
|
||||||
|
return zoomDiv
|
||||||
|
},
|
||||||
|
|
||||||
|
setZoomLevel: function (level) {
|
||||||
|
if (level !== this.zoomLevel) {
|
||||||
|
this.zoomLevel = level
|
||||||
|
this.load(this.updateChart.bind(this))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
load: function (callback) {
|
||||||
|
if (this.cache[this.zoomLevel])
|
||||||
|
callback(this.cache[this.zoomLevel])
|
||||||
|
else {
|
||||||
|
var urls = []
|
||||||
|
var id = this.node.nodeinfo.node_id
|
||||||
|
var unixStamp = Math.floor(Date.now() / 1000)
|
||||||
|
var zoomConfig = this.zoomConfig.levels[this.zoomLevel]
|
||||||
|
|
||||||
|
this.chartConfig.metrics.forEach(function(metric) {
|
||||||
|
var parameters = [
|
||||||
|
"start=" + (unixStamp - zoomConfig.from),
|
||||||
|
"end=" + unixStamp,
|
||||||
|
"step=" + zoomConfig.interval,
|
||||||
|
"query=" + metric.query.replace("{{NODE_ID}}", id)
|
||||||
|
]
|
||||||
|
|
||||||
|
var url = this.chartConfig.url + "?" + parameters.join("&")
|
||||||
|
|
||||||
|
urls.push(url)
|
||||||
|
|
||||||
|
}.bind(this))
|
||||||
|
|
||||||
|
Promise.all(urls.map(getJSON)).then(function (data) {
|
||||||
|
this.cache[this.zoomLevel] = this.parse(data)
|
||||||
|
callback(this.cache[this.zoomLevel])
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parse: function (results) {
|
||||||
|
var data = []
|
||||||
|
results[0].data.result[0].values.forEach(function (tp) {
|
||||||
|
var time = {"time": new Date(tp[0] * 1000)}
|
||||||
|
results.forEach(function(result) {
|
||||||
|
var metric = this.chartConfig.metrics[results.indexOf(result)]
|
||||||
|
var index = results[0].data.result[0].values.indexOf(tp)
|
||||||
|
time[metric.id] = this.formatValue(metric.id, result.data.result[0].values[index][1])
|
||||||
|
}.bind(this))
|
||||||
|
data.push(time)
|
||||||
|
}.bind(this))
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
c3FormatToolTip: function (d, ratio, id) {
|
||||||
|
switch (id) {
|
||||||
|
case "uptime":
|
||||||
|
return d.toFixed(1) + " Tage"
|
||||||
|
default:
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
c3FormatXAxis: function (d) {
|
||||||
|
var pad = function (number, pad) {
|
||||||
|
var N = Math.pow(10, pad)
|
||||||
|
return number < N ? ("" + (N + number)).slice(1) : "" + number
|
||||||
|
}
|
||||||
|
switch (this.zoomLevel) {
|
||||||
|
case 0: // 8h
|
||||||
|
case 1: // 24h
|
||||||
|
return pad(d.getHours(), 2) + ":" + pad(d.getMinutes(), 2)
|
||||||
|
case 2: // 1m
|
||||||
|
return pad(d.getDate(), 2) + "." + pad(d.getMonth() + 1, 2)
|
||||||
|
case 3: // 1y
|
||||||
|
return ["Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"][d.getMonth()]
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatValue: function (id, value) {
|
||||||
|
switch (id) {
|
||||||
|
case "loadavg":
|
||||||
|
return (d3.format(".2r")(value))
|
||||||
|
case "clientcount":
|
||||||
|
return (Math.ceil(value))
|
||||||
|
case "uptime":
|
||||||
|
return (value / 86400)
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return charts
|
||||||
|
})
|
|
@ -1,5 +1,5 @@
|
||||||
define(["moment", "numeral", "tablesort", "tablesort.numeric"],
|
define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"],
|
||||||
function (moment, numeral, Tablesort) {
|
function (moment, numeral, Tablesort, Charts) {
|
||||||
function showGeoURI(d) {
|
function showGeoURI(d) {
|
||||||
function showLatitude(d) {
|
function showLatitude(d) {
|
||||||
var suffix = Math.sign(d) > -1 ? "' N" : "' S"
|
var suffix = Math.sign(d) > -1 ? "' N" : "' S"
|
||||||
|
@ -284,6 +284,10 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
|
||||||
|
|
||||||
el.appendChild(attributes)
|
el.appendChild(attributes)
|
||||||
|
|
||||||
|
if (!d.flags.gateway && config.nodeCharts)
|
||||||
|
config.nodeCharts.forEach( function (config) {
|
||||||
|
el.appendChild((new Charts(d, config)).render())
|
||||||
|
})
|
||||||
|
|
||||||
if (config.nodeInfos)
|
if (config.nodeInfos)
|
||||||
config.nodeInfos.forEach( function (nodeInfo) {
|
config.nodeInfos.forEach( function (nodeInfo) {
|
||||||
|
|
28
scss/_chart.scss
Normal file
28
scss/_chart.scss
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
.infobox .chart {
|
||||||
|
position: relative;
|
||||||
|
& > .c3 {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-buttons {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 10pt;
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
border-radius: 1.5em;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c3-tooltip-container {
|
||||||
|
width: 150px;
|
||||||
|
.c3-tooltip {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ $buttondistance: 12pt;
|
||||||
@import '_map';
|
@import '_map';
|
||||||
@import '_forcegraph';
|
@import '_forcegraph';
|
||||||
@import '_legend';
|
@import '_legend';
|
||||||
|
@import '_chart';
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -53,6 +53,12 @@ module.exports = function(grunt) {
|
||||||
expand: true,
|
expand: true,
|
||||||
dest: "build/",
|
dest: "build/",
|
||||||
cwd: "bower_components/leaflet/dist/"
|
cwd: "bower_components/leaflet/dist/"
|
||||||
|
},
|
||||||
|
c3: {
|
||||||
|
src: ["c3.min.css"],
|
||||||
|
expand: true,
|
||||||
|
dest: "build/",
|
||||||
|
cwd: "bower_components/c3/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sass: {
|
sass: {
|
||||||
|
|
Loading…
Reference in a new issue