Add basic dashboard page for TempMon
only the older jQuery theme is supported for now...
This commit is contained in:
parent
0fbc8c9247
commit
d2c4791611
|
@ -27,4 +27,12 @@
|
||||||
</style>
|
</style>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
${parent.context_menu_items()}
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
${parent.body()}
|
||||||
|
|
12
tailbone/templates/tempmon/appliances/view.mako
Normal file
12
tailbone/templates/tempmon/appliances/view.mako
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
${parent.context_menu_items()}
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
12
tailbone/templates/tempmon/clients/index.mako
Normal file
12
tailbone/templates/tempmon/clients/index.mako
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/index.mako" />
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
${parent.context_menu_items()}
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -15,6 +15,13 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
${parent.context_menu_items()}
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="object_helpers()">
|
<%def name="object_helpers()">
|
||||||
% if instance.enabled and master.restartable_client(instance) and request.has_perm('{}.restart'.format(route_prefix)):
|
% if instance.enabled and master.restartable_client(instance) and request.has_perm('{}.restart'.format(route_prefix)):
|
||||||
<div class="object-helper">
|
<div class="object-helper">
|
||||||
|
|
135
tailbone/templates/tempmon/dashboard.mako
Normal file
135
tailbone/templates/tempmon/dashboard.mako
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/page.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">TempMon Appliances » Dashboard</%def>
|
||||||
|
|
||||||
|
<%def name="content_title()">Dashboard</%def>
|
||||||
|
|
||||||
|
<%def name="extra_javascript()">
|
||||||
|
${parent.extra_javascript()}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.bundle.min.js"></script>
|
||||||
|
% if not use_buefy:
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
var contexts = {};
|
||||||
|
var charts = {};
|
||||||
|
|
||||||
|
function fetchReadings(appliance_uuid) {
|
||||||
|
if (appliance_uuid === undefined) {
|
||||||
|
appliance_uuid = $('#appliance_uuid').val();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.form-wrapper').mask("Fetching data");
|
||||||
|
|
||||||
|
if (Object.keys(charts).length) {
|
||||||
|
Object.keys(charts).forEach(function(key) {
|
||||||
|
charts[key].destroy();
|
||||||
|
delete charts[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = '${url("tempmon.dashboard.readings")}';
|
||||||
|
var params = {'appliance_uuid': appliance_uuid};
|
||||||
|
$.get(url, params, function(data) {
|
||||||
|
|
||||||
|
if (data.probes) {
|
||||||
|
data.probes.forEach(function(probe) {
|
||||||
|
charts[probe.uuid] = new Chart(contexts[probe.uuid], {
|
||||||
|
type: 'scatter',
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
label: probe.description,
|
||||||
|
data: probe.readings
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
type: 'time',
|
||||||
|
time: {unit: 'minute'},
|
||||||
|
position: 'bottom'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// TODO: should improve this
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.form-wrapper').unmask();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
% for probe in appliance.probes:
|
||||||
|
contexts['${probe.uuid}'] = $('#tempchart-${probe.uuid}');
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
$('#appliance_uuid').selectmenu({
|
||||||
|
change: function(event, ui) {
|
||||||
|
$('.form-wrapper').mask("Fetching data");
|
||||||
|
$(this).parents('form').submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchReadings();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_this_page()">
|
||||||
|
<div style="display: flex;">
|
||||||
|
|
||||||
|
<div class="form-wrapper">
|
||||||
|
<div class="form">
|
||||||
|
${h.form(request.current_route_url())}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
% if use_buefy:
|
||||||
|
<b-field horizontal label="Appliance">
|
||||||
|
${appliance_select}
|
||||||
|
</b-field>
|
||||||
|
% else:
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label>Appliance</label>
|
||||||
|
<div class="field">
|
||||||
|
${appliance_select}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="${url('tempmon.appliances.view', uuid=appliance.uuid)}">
|
||||||
|
${h.image(url('tempmon.appliances.thumbnail', uuid=appliance.uuid), "")}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
% if appliance.probes:
|
||||||
|
% for probe in appliance.probes:
|
||||||
|
<h3>
|
||||||
|
Probe: ${h.link_to(probe.description, url('tempmon.probes.graph', uuid=probe.uuid))}
|
||||||
|
(status: ${enum.TEMPMON_PROBE_STATUS[probe.status]})
|
||||||
|
</h3>
|
||||||
|
% if probe.enabled:
|
||||||
|
% if use_buefy:
|
||||||
|
<canvas ref="tempchart" width="400" height="150"></canvas>
|
||||||
|
% else:
|
||||||
|
<canvas id="tempchart-${probe.uuid}" width="400" height="60"></canvas>
|
||||||
|
% endif
|
||||||
|
% else:
|
||||||
|
<p>This probe is not enabled.</p>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
% else:
|
||||||
|
<h3>This appliance has no probes configured!</h3>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -73,6 +73,16 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
${parent.context_menu_items()}
|
||||||
|
% if master.has_perm('view'):
|
||||||
|
<li>${h.link_to("View this {}".format(model_title), master.get_action_url('view', probe))}</li>
|
||||||
|
% endif
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="render_this_page()">
|
<%def name="render_this_page()">
|
||||||
<div style="display: flex; justify-content: space-between;">
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
|
||||||
|
|
12
tailbone/templates/tempmon/probes/index.mako
Normal file
12
tailbone/templates/tempmon/probes/index.mako
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/index.mako" />
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
${parent.context_menu_items()}
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -92,6 +92,9 @@
|
||||||
<%def name="context_menu_items()">
|
<%def name="context_menu_items()">
|
||||||
${parent.context_menu_items()}
|
${parent.context_menu_items()}
|
||||||
<li>${h.link_to("View Readings as Graph", action_url('graph', instance))}</li>
|
<li>${h.link_to("View Readings as Graph", action_url('graph', instance))}</li>
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_main_fields(form)">
|
<%def name="render_main_fields(form)">
|
||||||
|
|
12
tailbone/templates/tempmon/readings/index.mako
Normal file
12
tailbone/templates/tempmon/readings/index.mako
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/index.mako" />
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
${parent.context_menu_items()}
|
||||||
|
% if request.has_perm('tempmon.appliances.dashboard'):
|
||||||
|
<li>${h.link_to("Go to the Dashboard", url('tempmon.dashboard'))}</li>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2018 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -34,3 +34,4 @@ def includeme(config):
|
||||||
config.include('tailbone.views.tempmon.clients')
|
config.include('tailbone.views.tempmon.clients')
|
||||||
config.include('tailbone.views.tempmon.probes')
|
config.include('tailbone.views.tempmon.probes')
|
||||||
config.include('tailbone.views.tempmon.readings')
|
config.include('tailbone.views.tempmon.readings')
|
||||||
|
config.include('tailbone.views.tempmon.dashboard')
|
||||||
|
|
162
tailbone/views/tempmon/dashboard.py
Normal file
162
tailbone/views/tempmon/dashboard.py
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Tempmon "Dashboard" View
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from rattail.time import localtime, make_utc
|
||||||
|
from rattail_tempmon.db import model as tempmon
|
||||||
|
|
||||||
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
|
from tailbone.views import View
|
||||||
|
from tailbone.db import TempmonSession
|
||||||
|
|
||||||
|
|
||||||
|
class TempmonDashboardView(View):
|
||||||
|
"""
|
||||||
|
Dashboard view for tempmon
|
||||||
|
"""
|
||||||
|
session_key = 'tempmon.dashboard.appliance_uuid'
|
||||||
|
|
||||||
|
def dashboard(self):
|
||||||
|
use_buefy = self.get_use_buefy()
|
||||||
|
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
appliance = None
|
||||||
|
uuid = self.request.POST.get('appliance_uuid')
|
||||||
|
if uuid:
|
||||||
|
appliance = TempmonSession.query(tempmon.Appliance).get(uuid)
|
||||||
|
if appliance:
|
||||||
|
self.request.session[self.session_key] = appliance.uuid
|
||||||
|
if not appliance:
|
||||||
|
self.request.session.flash("Appliance could not be found: {}".format(uuid), 'error')
|
||||||
|
raise self.redirect(self.request.current_route_url())
|
||||||
|
|
||||||
|
selected_uuid = self.request.params.get('appliance_uuid')
|
||||||
|
selected_appliance = None
|
||||||
|
if not selected_uuid:
|
||||||
|
selected_uuid = self.request.session.get(self.session_key)
|
||||||
|
if not selected_uuid:
|
||||||
|
# must declare the "first" appliance selected
|
||||||
|
selected_appliance = TempmonSession.query(tempmon.Appliance)\
|
||||||
|
.order_by(tempmon.Appliance.name)\
|
||||||
|
.first()
|
||||||
|
if selected_appliance:
|
||||||
|
selected_uuid = selected_appliance.uuid
|
||||||
|
self.request.session[self.session_key] = selected_uuid
|
||||||
|
|
||||||
|
if not selected_appliance and selected_uuid:
|
||||||
|
selected_appliance = TempmonSession.query(tempmon.Appliance)\
|
||||||
|
.get(selected_uuid)
|
||||||
|
|
||||||
|
appliances = TempmonSession.query(tempmon.Appliance)\
|
||||||
|
.order_by(tempmon.Appliance.name)\
|
||||||
|
.all()
|
||||||
|
appliance_options = tags.Options([
|
||||||
|
tags.Option(appliance.name, appliance.uuid)
|
||||||
|
for appliance in appliances])
|
||||||
|
|
||||||
|
if use_buefy:
|
||||||
|
appliance_select = None
|
||||||
|
raise NotImplementedError
|
||||||
|
else:
|
||||||
|
appliance_select = tags.select('appliance_uuid', selected_uuid, appliance_options)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'index_url': self.request.route_url('tempmon.appliances'),
|
||||||
|
'index_title': "TempMon Appliances",
|
||||||
|
'use_buefy': use_buefy,
|
||||||
|
'appliance_select': appliance_select,
|
||||||
|
'appliance': selected_appliance,
|
||||||
|
}
|
||||||
|
|
||||||
|
def readings(self):
|
||||||
|
|
||||||
|
# track down the requested appliance
|
||||||
|
uuid = self.request.params.get('appliance_uuid')
|
||||||
|
if not uuid:
|
||||||
|
return {'error': "Must specify valid appliance_uuid"}
|
||||||
|
appliance = TempmonSession.query(tempmon.Appliance).get(uuid)
|
||||||
|
if not appliance:
|
||||||
|
return {'error': "Must specify valid appliance_uuid"}
|
||||||
|
|
||||||
|
# remember which appliance was shown last
|
||||||
|
self.request.session[self.session_key] = appliance.uuid
|
||||||
|
|
||||||
|
# fetch all "current" (recent) readings for all connected probes
|
||||||
|
probes = []
|
||||||
|
cutoff = make_utc() - datetime.timedelta(seconds=7200) # 2 hours ago
|
||||||
|
for probe in appliance.probes:
|
||||||
|
probes.append({
|
||||||
|
'uuid': probe.uuid,
|
||||||
|
'description': probe.description,
|
||||||
|
'location': probe.location,
|
||||||
|
'enabled': str(probe.enabled) if probe.enabled else None,
|
||||||
|
'readings': self.get_probe_readings(probe, cutoff),
|
||||||
|
})
|
||||||
|
return {'probes': probes}
|
||||||
|
|
||||||
|
def get_probe_readings(self, probe, cutoff):
|
||||||
|
|
||||||
|
# figure out which readings we need to graph
|
||||||
|
readings = TempmonSession.query(tempmon.Reading)\
|
||||||
|
.filter(tempmon.Reading.probe == probe)\
|
||||||
|
.filter(tempmon.Reading.taken >= cutoff)\
|
||||||
|
.order_by(tempmon.Reading.taken)\
|
||||||
|
.all()
|
||||||
|
|
||||||
|
# convert readings to data for scatter plot
|
||||||
|
return [{
|
||||||
|
'x': localtime(self.rattail_config, reading.taken, from_utc=True).isoformat(),
|
||||||
|
'y': float(reading.degrees_f),
|
||||||
|
} for reading in readings]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _defaults(cls, config):
|
||||||
|
|
||||||
|
# dashboard
|
||||||
|
config.add_tailbone_permission('tempmon.appliances', 'tempmon.appliances.dashboard',
|
||||||
|
"View the Tempmon Appliance \"Dashboard\" page")
|
||||||
|
config.add_route('tempmon.dashboard', '/tempmon/dashboard',
|
||||||
|
request_method=('GET', 'POST'))
|
||||||
|
config.add_view(cls, attr='dashboard', route_name='tempmon.dashboard',
|
||||||
|
renderer='/tempmon/dashboard.mako')
|
||||||
|
|
||||||
|
# readings
|
||||||
|
config.add_route('tempmon.dashboard.readings', '/tempmon/dashboard/readings',
|
||||||
|
request_method='GET')
|
||||||
|
config.add_view(cls, attr='readings', route_name='tempmon.dashboard.readings',
|
||||||
|
renderer='json')
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
TempmonDashboardView.defaults(config)
|
Loading…
Reference in a new issue