Add dashboard with gauges
This commit is contained in:
parent
2f400fccee
commit
42ba485794
11
app.py
11
app.py
|
@ -8,6 +8,7 @@ import os
|
||||||
from flask import (Flask, request, jsonify, abort, render_template)
|
from flask import (Flask, request, jsonify, abort, render_template)
|
||||||
|
|
||||||
from ltsdb_json import LTS
|
from ltsdb_json import LTS
|
||||||
|
from dashboard import Dashboard
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
@ -125,3 +126,13 @@ def visualize():
|
||||||
ts = LTS(id=id)
|
ts = LTS(id=id)
|
||||||
timeseries_data.append(ts)
|
timeseries_data.append(ts)
|
||||||
return render_template("visualize.html", ts=timeseries_data)
|
return render_template("visualize.html", ts=timeseries_data)
|
||||||
|
|
||||||
|
@app.get("/dashboard/")
|
||||||
|
def dashboard_index():
|
||||||
|
d = Dashboard("dashboards/" + "index" + ".json")
|
||||||
|
return d.as_html()
|
||||||
|
|
||||||
|
@app.get("/dashboard/<dashboard>")
|
||||||
|
def dashboard_file(dashboard):
|
||||||
|
d = Dashboard("dashboards/" + dashboard + ".json")
|
||||||
|
return d.as_html()
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import (Flask, request, jsonify, abort, render_template_string, render_template)
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from ltsdb_json import LTS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Dashboard:
|
||||||
|
def __init__(self, filename="dashboard.json"):
|
||||||
|
with open (filename) as fh:
|
||||||
|
d = json.load(fh)
|
||||||
|
self.title = d["title"]
|
||||||
|
log.debug("title = “%s”", self.title)
|
||||||
|
self.description = d["description"]
|
||||||
|
self.widgets = []
|
||||||
|
for w in d["widgets"]:
|
||||||
|
if w["type"] == "timeseries":
|
||||||
|
self.widgets.append(TimeSeries(w))
|
||||||
|
elif w["type"] == "gauge":
|
||||||
|
if w.get("multi"):
|
||||||
|
ts_list = LTS.find(w["data"][0])
|
||||||
|
for ts in ts_list:
|
||||||
|
w1 = {**w, "data": [ts]}
|
||||||
|
self.widgets.append(Gauge(w1))
|
||||||
|
else:
|
||||||
|
self.widgets.append(Gauge(w))
|
||||||
|
else:
|
||||||
|
self.widgets.append(Widget(w))
|
||||||
|
|
||||||
|
def as_html(self):
|
||||||
|
return render_template("dashboard.html", dashboard=self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Widget:
|
||||||
|
def __init__(self, d):
|
||||||
|
self.type = d["type"]
|
||||||
|
self.stops = d["stops"]
|
||||||
|
log.debug("data = %s", d["data"])
|
||||||
|
self.lts = LTS(id=d["data"][0]) # by default we handle only one data source
|
||||||
|
pass
|
||||||
|
|
||||||
|
def as_html(self):
|
||||||
|
log.debug("")
|
||||||
|
self.lastvalue = self.lts.data[-1][1]
|
||||||
|
return Markup(render_template("widget.html", widget=self))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def criticalcolor(self):
|
||||||
|
log.debug("stops = %s", self.stops)
|
||||||
|
brightness = 30
|
||||||
|
if self.stops[0] < self.stops[2]:
|
||||||
|
if self.lastvalue < self.stops[0]:
|
||||||
|
log.debug("definitely ok")
|
||||||
|
return f"hsl(120, 100%, {brightness}%)"
|
||||||
|
elif self.lastvalue < self.stops[1]:
|
||||||
|
log.debug("mostly ok")
|
||||||
|
hue = 120 - round(
|
||||||
|
(self.lastvalue - self.stops[0])
|
||||||
|
/ (self.stops[1] - self.stops[0])
|
||||||
|
* 60
|
||||||
|
)
|
||||||
|
return f"hsl({hue}, 100%, {brightness}%)"
|
||||||
|
elif self.lastvalue < self.stops[2]:
|
||||||
|
log.debug("maybe fail")
|
||||||
|
hue = 60 - round(
|
||||||
|
(self.lastvalue - self.stops[1])
|
||||||
|
/ (self.stops[2] - self.stops[1])
|
||||||
|
* 60
|
||||||
|
)
|
||||||
|
return f"hsl({hue}, 100%, {brightness}%)"
|
||||||
|
else:
|
||||||
|
log.debug("definitely fail")
|
||||||
|
return f"hsl(0, 100%, {brightness}%)"
|
||||||
|
else:
|
||||||
|
log.debug("the other side")
|
||||||
|
return "#CCC"
|
||||||
|
|
||||||
|
class TimeSeries(Widget):
|
||||||
|
def __init__(self, d):
|
||||||
|
super().__init__(d)
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Gauge(Widget):
|
||||||
|
def __init__(self, d):
|
||||||
|
super().__init__(d)
|
||||||
|
pass
|
||||||
|
|
||||||
|
gaugesize = 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gaugepos(self):
|
||||||
|
max_value = max([d[1] for d in self.lts.data])
|
||||||
|
log.debug("max_value = %s", max_value)
|
||||||
|
return self.lastvalue / max_value * self.gaugesize
|
||||||
|
|
||||||
|
def as_html(self):
|
||||||
|
log.debug("")
|
||||||
|
self.lastvalue = self.lts.data[-1][1]
|
||||||
|
value = self.lastvalue
|
||||||
|
unit = self.lts.description["unit"]
|
||||||
|
log.debug("unit = %s", unit)
|
||||||
|
if unit == "s":
|
||||||
|
if value >= 86400:
|
||||||
|
value /= 86400
|
||||||
|
unit = "days"
|
||||||
|
elif value >= 3600:
|
||||||
|
value /= 3600
|
||||||
|
unit = "h"
|
||||||
|
elif value >= 60:
|
||||||
|
value /= 60
|
||||||
|
unit = "m"
|
||||||
|
elif value >= 1:
|
||||||
|
pass
|
||||||
|
elif value >= 0.001:
|
||||||
|
value *= 1000
|
||||||
|
unit = "ms"
|
||||||
|
self.lastvalue_formatted = Markup(f"<span class='value'>{value:.2f}</span><span class='unit'>{unit}</unit>")
|
||||||
|
return Markup(render_template("gauge.html", widget=self))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description_formatted(self):
|
||||||
|
s = "<table class='description'>"
|
||||||
|
for d, v in self.lts.description.items():
|
||||||
|
if v:
|
||||||
|
s += render_template_string(
|
||||||
|
"<tr><th>{{d}}:</th><td>{{v}}</td></tr>",
|
||||||
|
d=d, v=v)
|
||||||
|
s += "</table>"
|
||||||
|
return Markup(s)
|
|
@ -0,0 +1,63 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width; initial-scale=1">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.widget {
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
.unit {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.gauge {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.gauge-container {
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #CCC;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.gauge-indicator {
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ dashboard.title }}</h1>
|
||||||
|
<p class="description">
|
||||||
|
{{ dashboard.description }}
|
||||||
|
</p>
|
||||||
|
<main>
|
||||||
|
{% for widget in dashboard.widgets %}
|
||||||
|
{{ widget.as_html() }}
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="gauge">
|
||||||
|
<div class="gauge-container"
|
||||||
|
style="height: {{widget.gaugesize}}px">
|
||||||
|
<div class="gauge-indicator"
|
||||||
|
style="height: {{widget.gaugepos}}px;
|
||||||
|
background-color: {{widget.criticalcolor}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style="color: {{widget.criticalcolor}}">
|
||||||
|
{{ widget.lastvalue_formatted}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{ widget.description_formatted }}
|
||||||
|
</div>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="widget">
|
||||||
|
<p>Unknown widget type {{ widget.type }}.</p>
|
||||||
|
<p>
|
||||||
|
Last value:
|
||||||
|
<span class="value" style="color: {{widget.criticalcolor}}">{{ widget.lastvalue}}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
Loading…
Reference in New Issue