dashboard: interface statistics widget

This commit is contained in:
Stephan de Wit 2024-04-08 11:56:43 +02:00
parent ceb51410ba
commit 70867a40fd
2 changed files with 188 additions and 1 deletions

View File

@ -55,7 +55,17 @@ class DashboardController extends ApiControllerBase
'datetime' => gettext('Current date/time'),
'uptime' => gettext('Uptime'),
'config' => gettext('Last configuration change')
]
],
'interfacestatistics' => [
'title' => gettext('Interface Statistics'),
'bytesin' => gettext('Bytes In'),
'bytesout' => gettext('Bytes Out'),
'packetsin' => gettext('Packets In'),
'packetsout' => gettext('Packets Out'),
'errorsin' => gettext('Errors In'),
'errorsout' => gettext('Errors Out'),
'collisions' => gettext('Collisions'),
],
];
}

View File

@ -0,0 +1,177 @@
// endpoint:/api/diagnostics/traffic/interface
/**
* 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 InterfaceStatistics extends BaseWidget {
constructor() {
super();
this.chart = null;
this.labels = [];
this.rawData = {};
this.dataset = {};
}
getMarkup() {
return $(`
<div class="interface-statistics-chart-container">
<div class="canvas-container">
<canvas id="intf-stats"></canvas>
</div>
</div>
`);
}
_setAlpha(color, opacity) {
const op = Math.round(Math.min(Math.max(opacity || 1, 0), 1) * 255);
return color + op.toString(16).toUpperCase();
}
_getIndexedData(data) {
let indexedData = Array(this.labels.length).fill(null);
let indexedColors = Array(this.labels.length).fill(null);
for (const item in data) {
let obj = data[item];
let idx = this.labels.indexOf(obj.name);
indexedData[idx] = obj.data;
indexedColors[idx] = obj.color;
}
return {
data: indexedData,
colors: indexedColors
};
}
async onWidgetTick() {
ajaxGet('/api/diagnostics/traffic/interface', {}, (data, status) => {
let i = 0;
let colors = Chart.colorschemes.tableau.Classic10;
for (const intf in data.interfaces) {
const obj = data.interfaces[intf];
this.labels.indexOf(obj.name) === -1 && this.labels.push(obj.name);
obj.data = parseInt(obj["packets received"]) + parseInt(obj["packets transmitted"]);
obj.color = colors[i % colors.length]
this.rawData[obj.name] = obj;
i++;
}
let formattedData = this._getIndexedData(data.interfaces);
this.dataset = {
label: 'statistics',
data: formattedData.data,
backgroundColor: formattedData.colors,
hoverBackgroundColor: formattedData.colors.map((color) => this._setAlpha(color, 0.5)),
fill: true,
borderWidth: 2,
hoverOffset: 10,
}
if (this.chart.config.data.datasets.length > 0) {
this.chart.config.data.datasets[0].data = this.dataset.data;
} else {
this.chart.config.data.labels = this.labels;
this.chart.config.data.datasets.push(this.dataset);
}
this.chart.update();
});
}
async onMarkupRendered() {
let context = $(`#intf-stats`)[0].getContext('2d');
let config = {
type: 'doughnut',
data: {
labels: [],
datasets: []
},
options: {
cutout: '40%',
maintainAspectRatio: true,
responsive: true,
aspectRatio: 2,
layout: {
padding: 10
},
parsing: false,
plugins: {
legend: {
display: false,
position: 'left',
title: 'Traffic',
onHover: (event, legendItem) => {
const activeElement = {
datasetIndex: 0,
index: legendItem.index
};
this.chart.setActiveElements([activeElement]);
this.chart.tooltip.setActiveElements([activeElement]);
this.chart.update();
}
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
const idx = this.labels.indexOf(tooltipItem.label);
let obj = this.rawData[tooltipItem.label];
let result = [
`${tooltipItem.label}`,
`${this.translations.bytesin}: ${obj["bytes received"]}`,
`${this.translations.bytesout}: ${obj["bytes transmitted"]}`,
`${this.translations.packetsin}: ${obj["packets received"]}`,
`${this.translations.packetsout}: ${obj["packets transmitted"]}`,
`${this.translations.errorsin}: ${obj["input errors"]}`,
`${this.translations.errorsout}: ${obj["output errors"]}`,
`${this.translations.collisions}: ${obj["collisions"]}`,
];
return result;
}
}
},
}
}
}
this.chart = new Chart(context, config);
}
onWidgetResize(elem, width, height) {
if (this.chart !== null) {
if (width > 450) {
this.chart.options.plugins.legend.display = true;
} else {
this.chart.options.plugins.legend.display = false;
}
}
return true;
}
}