Add dashboard with gauges

This commit is contained in:
Peter J. Holzer 2022-12-27 10:29:49 +01:00
parent 2f400fccee
commit 42ba485794
5 changed files with 230 additions and 0 deletions

11
app.py
View File

@ -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()

133
dashboard.py Normal file
View File

@ -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)

63
templates/dashboard.html Normal file
View File

@ -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>

15
templates/gauge.html Normal file
View File

@ -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>

8
templates/widget.html Normal file
View File

@ -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>