From 70867a40fd6aba719b9cb45d5551b590c4e13d52 Mon Sep 17 00:00:00 2001 From: Stephan de Wit Date: Mon, 8 Apr 2024 11:56:43 +0200 Subject: [PATCH] dashboard: interface statistics widget --- .../OPNsense/Core/Api/DashboardController.php | 12 +- .../www/js/widgets/InterfaceStatistics.js | 177 ++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/opnsense/www/js/widgets/InterfaceStatistics.js diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php index ceaeba3c7..7f8daba3b 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php @@ -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'), + ], ]; } diff --git a/src/opnsense/www/js/widgets/InterfaceStatistics.js b/src/opnsense/www/js/widgets/InterfaceStatistics.js new file mode 100644 index 000000000..7ef0f737d --- /dev/null +++ b/src/opnsense/www/js/widgets/InterfaceStatistics.js @@ -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 $(` +
+
+ +
+
+ `); + } + + _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; + } + +}