dashboard: add CPU Usage widget

Also allow for each widget to override the resize handles in case
they should be more restrictive in terms of dimensions.
This commit is contained in:
Stephan de Wit 2024-03-22 15:57:52 +01:00
parent af459fff31
commit 1d593fe984
8 changed files with 162 additions and 3 deletions

1
plist
View File

@ -1913,6 +1913,7 @@
/usr/local/opnsense/www/js/tree.jquery.min.js
/usr/local/opnsense/www/js/widgets/BaseTableWidget.js
/usr/local/opnsense/www/js/widgets/BaseWidget.js
/usr/local/opnsense/www/js/widgets/Cpu.js
/usr/local/opnsense/www/js/widgets/Interfaces.js
/usr/local/opnsense/www/themes/opnsense/LICENSE
/usr/local/opnsense/www/themes/opnsense/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot

View File

@ -37,6 +37,7 @@
<script src="{{ cache_safe('/ui/js/chart.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/chartjs-plugin-colorschemes.js') }}"></script>
<script src="{{ cache_safe('/ui/js/smoothie.js') }}"></script>
<script>
$( document ).ready(function() {
@ -47,7 +48,7 @@ $( document ).ready(function() {
alwaysShowResizeHandle: false,
sizeToContent: true,
resizable: {
handles: 'e, w'
handles: 'all'
}
}, {
'save': "{{ lang._('Save') }}",

View File

@ -32,6 +32,8 @@
import subprocess
import select
import argparse
import ujson
import re
if __name__ == '__main__':
parser = argparse.ArgumentParser()
@ -53,8 +55,20 @@ if __name__ == '__main__':
if data:
output = data.decode().strip()
if not (output.startswith("tty") or output.startswith("tin")):
print(f"event: message\ndata: {output}\n\n", flush=True)
if output.startswith("tty") or output.startswith("tin"):
continue
formatted = re.sub(r'\s+', ' ', output).split(" ")[2:]
formatted = [int(x) for x in formatted]
result = {
'total': sum(formatted) - formatted[4],
'user': formatted[0],
'nice': formatted[1],
'sys': formatted[2],
'intr': formatted[3],
'idle': formatted[4]
}
print(f"event: message\ndata: {ujson.dumps(result)}\n\n", flush=True)
else:
read_fds.remove(fd)

View File

@ -74,6 +74,11 @@ td {
}
.canvas-container {
display: flex;
flex-direction: column;
}
.smoothie-container {
width: 100%;
}

View File

@ -316,6 +316,12 @@ class WidgetManager {
await onMarkupRendered();
$(`.spinner-${widget.id}`).remove();
// XXX this code enforces per-widget resize handle definitions, which isn't natively
// supported by GridStack.
$(this.widgetHTMLElements[widget.id]).attr('gs-resize-handles', widget.getResizeHandles());
this.widgetHTMLElements[widget.id].gridstackNode._initDD = false;
this.grid.resizable(this.widgetHTMLElements[widget.id], true);
// second: start the widget-specific tick routine
let onWidgetTick = widget.onWidgetTick.bind(widget);
await onWidgetTick();

View File

@ -4,6 +4,11 @@ export default class BaseWidget {
this.title = "";
this.id = null;
this.tickTimeout = 5000; // Default tick timeout
this.resizeHandles = "all"
}
getResizeHandles() {
return this.resizeHandles;
}
setId(id) {

View File

@ -0,0 +1,126 @@
// endpoint:/api/diagnostics/cpu_usage/*
/**
* Copyright (C) 2024 Deciso B.V.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import BaseWidget from "./BaseWidget.js";
export default class Cpu extends BaseWidget {
constructor() {
super();
this.title = 'CPU Usage';
this.resizeHandles = "e, w";
}
_createChart(selector, timeSeries) {
let smoothie = new SmoothieChart({
responsive: true,
millisPerPixel:50,
tooltip: true,
labels: {
fillStyle: '#000000',
precision: 0,
fontSize: 11
},
grid: {
strokeStyle:'rgba(119,119,119,0.12)',
verticalSections:4,
millisPerLine:1000,
fillStyle: 'transparent'
}
});
smoothie.streamTo(document.getElementById(selector), 1000);
smoothie.addTimeSeries(timeSeries, {
lineWidth: 3,
strokeStyle: '#d94f00'
});
}
getMarkup() {
let $container = $(`
<div class="canvas-container">
<div class="smoothie-container">
<b>Total</b>
<div><canvas id="cpu-usage" style="width: 80%; height: 50px;"></canvas></div>
</div>
<div class="smoothie-container">
<b>Interrupt</b>
<div><canvas id="cpu-usage-intr" style="width: 80%; height: 50px;"></canvas></div>
</div>
<div class="smoothie-container">
<b>User</b>
<div><canvas id="cpu-usage-user" style="width: 80%; height: 50px;"></canvas></div>
</div>
<div class="smoothie-container">
<b>System</b>
<div><canvas id="cpu-usage-sys" style="width: 80%; height: 50px;"></canvas></div>
</div>
</div>`);
return $container;
}
async onMarkupRendered() {
let total_ts = new TimeSeries();
let intr_ts = new TimeSeries();
let user_ts = new TimeSeries();
let sys_ts = new TimeSeries();
this._createChart('cpu-usage', total_ts);
this._createChart('cpu-usage-intr', intr_ts);
this._createChart('cpu-usage-user', user_ts);
this._createChart('cpu-usage-sys', sys_ts);
const eventSource = new EventSource('/api/diagnostics/cpu_usage/stream');
eventSource.onmessage = function(event) {
if (!event) {
eventSource.close();
}
const data = JSON.parse(event.data);
let date = Date.now();
total_ts.append(date, data.total);
intr_ts.append(date, data.intr);
user_ts.append(date, data.user);
sys_ts.append(date, data.sys);
}
eventSource.onerror = function(event) {
eventSource.close();
};
}
onWidgetResize(elem, width, height) {
let curWidth = document.getElementById('cpu-usage').getBoundingClientRect().width;
let viewPort = document.getElementsByClassName('page-content-main')[0].getBoundingClientRect().width;
if (width > (viewPort / 2)) {
$('.canvas-container').css('flex-direction', 'row');
} else {
$('.canvas-container').css('flex-direction', 'column');
}
}
}

View File

@ -33,6 +33,7 @@ export default class Interfaces extends BaseTableWidget {
constructor() {
super();
this.title = "Interfaces";
this.resizeHandles = "e, w";
}
getGridOptions() {