dashboard: refactor gauges into base class, add mbuf gauge as well

This commit is contained in:
Stephan de Wit 2024-05-23 09:34:01 +02:00
parent f643d964c3
commit ff3bb2f731
9 changed files with 264 additions and 258 deletions

View File

@ -107,9 +107,14 @@ class DashboardController extends ApiControllerBase
],
'firewallstates' => [
'title' => gettext('Firewall States'),
'current' => gettext('Current'),
'limit' => gettext('Limit'),
]
'used' => gettext('Used'),
'free' => gettext('Free'),
],
'mbuf' => [
'title' => gettext('MBUF Usage'),
'used' => gettext('Used'),
'free' => gettext('Free'),
],
];
}

View File

@ -322,4 +322,9 @@ class SystemController extends ApiControllerBase
return $result;
}
public function systemMbufAction()
{
return json_decode((new Backend())->configdRun('system show mbuf'), true);
}
}

View File

@ -44,7 +44,7 @@
$( document ).ready(function() {
let widgetManager = new WidgetManager({
float: false,
column: 5,
column: 6,
margin: 10,
alwaysShowResizeHandle: false,
sizeToContent: true,

View File

@ -126,3 +126,9 @@ command:/usr/local/bin/openssl version | cut -f -2 -d ' '
parameters:
type:script_output
message:Show OpenSSL version
[show.mbuf]
command:/usr/bin/netstat -m --libxo json
parameters:
type:script_output
message:Show mbuf stats

View File

@ -0,0 +1,150 @@
/*
* 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 BaseGaugeWidget extends BaseWidget {
constructor() {
super();
this.chart = null;
}
getMarkup() {
return $(`
<div class="${this.id}-chart-container">
<div class="canvas-container">
<canvas id="${this.id}-chart"></canvas>
</div>
</div>
`);
}
createGaugeChart(options) {
let _options = {
colorMap: ['#D94F00', '#E5E5E5'],
labels: [],
tooltipLabelCallback: (tooltipItem) => {
return `${tooltipItem.label}: ${tooltipItem.parsed}`;
},
primaryText: (data) => {
return `${(data[0] / (data[0] + data[1]) * 100).toFixed(2)}%`;
},
secondaryText: (data) => false,
...options
}
let context = document.getElementById(`${this.id}-chart`).getContext("2d");
let config = {
type: 'doughnut',
data: {
labels: _options.labels,
datasets: [
{
data: [],
backgroundColor: _options.colorMap,
hoverBackgroundColor: _options.colorMap.map((color) => this._setAlpha(color, 0.5)),
hoverOffset: 10,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2,
layout: {
padding: 10
},
cutout: '64%',
rotation: 270,
circumference: 180,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: _options.tooltipLabelCallback
}
}
}
},
plugins: [{
id: 'custom_positioned_text',
beforeDatasetsDraw: (chart, _, __) => {
let data = chart.config.data.datasets[0].data;
if (data.length !== 0) {
let width = chart.width;
let height = chart.height;
let ctx = chart.ctx;
ctx.restore();
let divisor = 114;
let primaryText = _options.primaryText(data, chart);
let secondaryText = _options.secondaryText(data, chart);
if (secondaryText) {
divisor = 135;
}
let fontSize = (height / divisor).toFixed(2);
ctx.font = fontSize + "em SourceSansProSemiBold";
ctx.textBaseline = "middle";
let textX = Math.round((width - ctx.measureText(primaryText).width) / 2);
let textY = (height * 0.66);
ctx.fillText(primaryText, textX, textY);
if (secondaryText) {
let textBX = Math.round((width - ctx.measureText(secondaryText).width) / 2);
let textBY = height * 0.83;
ctx.fillText(secondaryText, textBX, textBY);
}
ctx.save();
}
}
}]
}
this.chart = new Chart(context, config);
}
updateChart(data) {
if (this.chart) {
this.chart.data.datasets[0].data = data;
this.chart.update();
}
}
onWidgetClose() {
if (this.chart) {
this.chart.destroy();
}
}
}

View File

@ -26,13 +26,12 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
import BaseWidget from "./BaseWidget.js";
import BaseGaugeWidget from "./BaseGaugeWidget.js";
export default class Disk extends BaseWidget {
export default class Disk extends BaseGaugeWidget {
constructor() {
super();
this.simple_chart = null;
this.detailed_chart = null;
}
@ -63,12 +62,12 @@ export default class Disk extends BaseWidget {
getMarkup() {
return $(`
<div class="disk-chart-container">
<div class="${this.id}-chart-container">
<div class="canvas-container">
<canvas id="disk-chart"></canvas>
<canvas id="${this.id}-chart"></canvas>
</div>
<div class="canvas-container">
<canvas id="disk-detailed-chart"></canvas>
<canvas id="${this.id}-detailed-chart"></canvas>
</div>
</div>
</div>
@ -76,69 +75,18 @@ export default class Disk extends BaseWidget {
}
async onMarkupRendered() {
let context_simple = document.getElementById("disk-chart").getContext("2d");
let colorMap = ['#D94F00', '#E5E5E5'];
let config_simple = {
type: 'doughnut',
data: {
labels: [this.translations.used, this.translations.free],
datasets: [
{
data: [],
backgroundColor: colorMap,
hoverBackgroundColor: colorMap.map((color) => this._setAlpha(color, 0.5)),
hoveroffset: 50,
fill: true
},
]
super.createGaugeChart({
colorMap: ['#D94F00', '#E5E5E5'],
labels: [this.translations.used, this.translations.free],
tooltipLabelCallback: (tooltipItem) => {
let pct = tooltipItem.dataset.pct[tooltipItem.dataIndex];
return `${tooltipItem.label}: ${pct}%`;
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2,
layout: {
padding: 10
},
cutout: '64%',
rotation: 270,
circumference: 180,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
let pct = tooltipItem.dataset.pct[tooltipItem.dataIndex];
return `${tooltipItem.label}: ${pct}%`;
}
}
}
}
primaryText: (data, chart) => {
return chart.config.data.datasets[0].pct[0] + '%';
},
plugins: [{
id: 'custom_positioned_text',
beforeDatasetsDraw: (chart, args, options) => {
// custom plugin: draw text at 2/3 y position of chart
if (chart.config.data.datasets[0].data.length !== 0) {
let width = chart.width;
let height = chart.height;
let ctx = chart.ctx;
ctx.restore();
let fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em SourceSansProSemibold";
ctx.textBaseline = "middle";
let text = this.simple_chart.config.data.datasets[0].pct[0] + '%';
let textX = Math.round((width - ctx.measureText(text).width) / 2);
let textY = (height / 3) * 2;
ctx.fillText(text, textX, textY);
ctx.save();
}
}
}]
}
})
this.simple_chart = new Chart(context_simple, config_simple);
let context_detailed = document.getElementById("disk-detailed-chart").getContext("2d");
let config = {
type: 'bar',
@ -218,9 +166,8 @@ export default class Disk extends BaseWidget {
let total = this._convertToBytes(device.blocks);
let free = total - used;
if (device.mountpoint === '/') {
this.simple_chart.config.data.datasets[0].data = [used, free];
this.simple_chart.config.data.datasets[0].pct = [device.used_pct, (100 - device.used_pct)];
this.simple_chart.update();
this.chart.config.data.datasets[0].pct = [device.used_pct, (100 - device.used_pct)];
super.updateChart([used, free]);
}
totals.push(total);

View File

@ -26,113 +26,27 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
import BaseWidget from "./BaseWidget.js";
import BaseGaugeWidget from "./BaseGaugeWidget.js";
export default class FirewallStates extends BaseWidget {
export default class FirewallStates extends BaseGaugeWidget {
constructor() {
super();
this.chart = null;
this.current = null;
this.limit = null;
}
getMarkup() {
return $(`
<div class="fw-states-chart-container">
<div class="canvas-container">
<canvas id="fw-states-chart"></canvas>
</div>
</div>
`);
}
async onMarkupRendered() {
let context = document.getElementById("fw-states-chart").getContext("2d");
let colorMap = ['#D94F00', '#E5E5E5'];
let config = {
type: 'doughnut',
data: {
labels: [this.translations.current, this.translations.limit],
datasets: [
{
data: [],
backgroundColor: colorMap,
hoverBackgroundColor: colorMap.map((color) => this._setAlpha(color, 0.5)),
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2,
layout: {
padding: 10
},
cutout: '64%',
rotation: 270,
circumference: 180,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.label}: ${tooltipItem.parsed}`;
}
}
}
}
},
plugins: [{
id: 'custom_positioned_text',
beforeDatasetsDraw: (chart, args, options) => {
if (chart.config.data.datasets[0].data.length !== 0) {
let width = chart.width;
let height = chart.height;
let ctx = chart.ctx;
ctx.restore();
let percentage = (this.current / this.limit * 100).toFixed(2);
let fontSize = (height / (percentage < 1 ? 135 : 114)).toFixed(2);
ctx.font = fontSize + "em SourceSansProSemiBold";
ctx.textBaseline = "middle";
let text = `${percentage} % `;
let textX = Math.round((width - ctx.measureText(text).width) / 2);
let textY = (height * 0.66);
ctx.fillText(text, textX, textY);
if (percentage < 1) {
let textB = `(${this.current} / ${this.limit})`;
let textBX = Math.round((width - ctx.measureText(textB).width) / 2);
let textBY = height * 0.85;
ctx.fillText(textB, textBX, textBY);
}
ctx.save();
}
}
}]
}
this.chart = new Chart(context, config);
super.createGaugeChart({
labels: [this.translations.used, this.translations.free],
secondaryText: (data) => {
return `(${data[0]} / ${data[0] + data[1]})`;
}
});
}
async onWidgetTick() {
ajaxGet('/api/diagnostics/firewall/pf_states', {}, (data, status) => {
this.current = parseInt(data.current);
this.limit = parseInt(data.limit);
this.chart.config.data.datasets[0].data = [this.current, (this.limit - this.current)];
this.chart.update();
let current = parseInt(data.current);
let limit = parseInt(data.limit);
super.updateChart([current, (limit - current)]);
});
}
onWidgetClose() {
if (this.chart !== null) {
this.chart.destroy();
}
}
}

View File

@ -0,0 +1,53 @@
// endpoint:/api/core/system/system_mbuf
/*
* 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 BaseGaugeWidget from "./BaseGaugeWidget.js";
export default class Mbuf extends BaseGaugeWidget {
constructor() {
super();
}
async onMarkupRendered() {
super.createGaugeChart({
labels: [this.translations.used, this.translations.free],
secondaryText: (data) => {
return `(${data[0]} / ${data[0] + data[1]})`;
}
});
}
async onWidgetTick() {
ajaxGet('/api/core/system/system_mbuf', {}, (data, status) => {
let current = parseInt(data['mbuf-statistics']['cluster-total']);
let limit = parseInt(data['mbuf-statistics']['cluster-max']);
super.updateChart([current, (limit - current)]);
});
}
}

View File

@ -26,92 +26,29 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
import BaseWidget from "./BaseWidget.js";
import BaseGaugeWidget from "./BaseGaugeWidget.js";
export default class Memory extends BaseWidget {
export default class Memory extends BaseGaugeWidget {
constructor() {
super();
this.tickTimeout = 15000;
this.chart = null;
this.curMemUsed = null;
this.curMemTotal = null;
}
getMarkup() {
return $(`
<div class="memory-chart-container">
<div class="canvas-container">
<canvas id="memory-chart"></canvas>
</div>
</div>
`);
}
async onMarkupRendered() {
let context = document.getElementById("memory-chart").getContext("2d");
let colorMap = ['#D94F00', '#A8C49B', '#E5E5E5'];
let config = {
type: 'doughnut',
data: {
labels: [this.translations.used, this.translations.arc, this.translations.free],
datasets: [
{
data: [],
backgroundColor: colorMap,
hoverBackgroundColor: colorMap.map((color) => this._setAlpha(color, 0.5)),
hoveroffset: 50,
fill: true
},
]
super.createGaugeChart({
colorMap: colorMap,
labels: [this.translations.used, this.translations.arc, this.translations.free],
tooltipLabelCallback: (tooltipItem) => {
return `${tooltipItem.label}: ${tooltipItem.parsed} MB`;
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2,
layout: {
padding: 10
},
cutout: '64%',
rotation: 270,
circumference: 180,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.label}: ${tooltipItem.parsed} MB`;
}
}
}
}
primaryText: (data) => {
return `${(data[0] / (data[0] + data[1] + data[2]) * 100).toFixed(2)}%`;
},
plugins: [{
id: 'custom_positioned_text',
beforeDatasetsDraw: (chart, args, options) => {
// custom plugin: draw text at 2/3 y position of chart
if (chart.config.data.datasets[0].data.length !== 0) {
let width = chart.width;
let height = chart.height;
let ctx = chart.ctx;
ctx.restore();
let fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em SourceSansProSemibold";
ctx.textBaseline = "middle";
let text = (this.curMemUsed / this.curMemTotal * 100).toFixed(2) + "%";
let textX = Math.round((width - ctx.measureText(text).width) / 2);
let textY = (height / 3) * 2;
ctx.fillText(text, textX, textY);
ctx.save();
}
}
}]
}
this.chart = new Chart(context, config);
secondaryText: (data) => {
return `${data[0]} / ${data[0] + data[1] + data[2]} MB`;
}
});
}
async onWidgetTick() {
@ -120,19 +57,8 @@ export default class Memory extends BaseWidget {
let used = parseInt(data.memory.used_frmt);
let arc = data.memory.hasOwnProperty('arc') ? parseInt(data.memory.arc_frmt) : 0;
let total = parseInt(data.memory.total_frmt);
let result = [(used - arc), arc, total - used];
this.chart.config.data.datasets[0].data = result
this.curMemUsed = used - arc;
this.curMemTotal = total;
this.chart.update();
super.updateChart([(used - arc), arc, total - used]);
}
});
}
onWidgetClose() {
if (this.chart !== null) {
this.chart.destroy();
}
}
}