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",
|
||||
"rbush": "../bower_components/rbush/rbush",
|
||||
"helper": "../helper",
|
||||
"jshashes": "../bower_components/jshashes/hashes"
|
||||
"jshashes": "../bower_components/jshashes/hashes",
|
||||
"c3": "../bower_components/c3/c3.min"
|
||||
},
|
||||
shim: {
|
||||
"leaflet.label": ["leaflet"],
|
||||
|
|
|
@ -25,12 +25,16 @@
|
|||
"virtual-dom": "~2.0.1",
|
||||
"leaflet-providers": "~1.0.27",
|
||||
"rbush": "https://github.com/mourner/rbush.git#~1.3.5",
|
||||
"jshashes": "~1.0.5"
|
||||
"jshashes": "~1.0.5",
|
||||
"c3": "~0.4.10"
|
||||
},
|
||||
"authors": [
|
||||
"Milan Pässler <me@petabyteboy.de>",
|
||||
"Nils Schneider <nils@nilsschneider.net>"
|
||||
],
|
||||
"license": "AGPL3",
|
||||
"private": true
|
||||
"private": true,
|
||||
"resolutions": {
|
||||
"d3": "~3.5.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"dataPath": "https://map.luebeck.freifunk.net/data/",
|
||||
"siteName": "Freifunk Lübeck",
|
||||
"dataPath": [
|
||||
"http://map.ffgl.eu/data/",
|
||||
"http://karte.ffdus.de/data/",
|
||||
"http://karte.freifunk-iserlohn.de/data/"
|
||||
],
|
||||
"siteName": "Freifunk Fluss",
|
||||
"mapSigmaScale": 0.5,
|
||||
"showContact": true,
|
||||
"maxAge": 14,
|
||||
|
@ -19,9 +23,40 @@
|
|||
}
|
||||
],
|
||||
"siteNames": [
|
||||
{ "site": "ffhl", "name": "Lübeck" },
|
||||
{ "site": "ffeh", "name": "Entenhausen" },
|
||||
{ "site": "ffgt", "name": "Gothamcity" },
|
||||
{ "site": "ffal", "name": "Atlantis" }
|
||||
{ "site": "ffgl-bcd", "name": "Burscheid" },
|
||||
{ "site": "ffgl-bgl", "name": "Bergisch Gladbach" },
|
||||
{ "site": "ffgl-lln", "name": "Leichlingen" },
|
||||
{ "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="roboto-slab-fontface.css">
|
||||
<link rel="stylesheet" href="roboto-fontface.css">
|
||||
<link rel="stylesheet" href="c3.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="vendor/es6-shim/es6-shim.min.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.label/dist/leaflet.label.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">
|
||||
<script src="bower_components/es6-shim/es6-shim.min.js"></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"],
|
||||
function (moment, numeral, Tablesort) {
|
||||
define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"],
|
||||
function (moment, numeral, Tablesort, Charts) {
|
||||
function showGeoURI(d) {
|
||||
function showLatitude(d) {
|
||||
var suffix = Math.sign(d) > -1 ? "' N" : "' S"
|
||||
|
@ -284,6 +284,10 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
|
|||
|
||||
el.appendChild(attributes)
|
||||
|
||||
if (!d.flags.gateway && config.nodeCharts)
|
||||
config.nodeCharts.forEach( function (config) {
|
||||
el.appendChild((new Charts(d, config)).render())
|
||||
})
|
||||
|
||||
if (config.nodeInfos)
|
||||
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 '_forcegraph';
|
||||
@import '_legend';
|
||||
@import '_chart';
|
||||
|
||||
.content {
|
||||
position: fixed;
|
||||
|
|
|
@ -53,6 +53,12 @@ module.exports = function(grunt) {
|
|||
expand: true,
|
||||
dest: "build/",
|
||||
cwd: "bower_components/leaflet/dist/"
|
||||
},
|
||||
c3: {
|
||||
src: ["c3.min.css"],
|
||||
expand: true,
|
||||
dest: "build/",
|
||||
cwd: "bower_components/c3/"
|
||||
}
|
||||
},
|
||||
sass: {
|
||||
|
|
Loading…
Reference in a new issue