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 4ebe67919..4122b668b 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php @@ -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'), + ], ]; } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php index d86bf5c96..3db6b86b6 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php @@ -322,4 +322,9 @@ class SystemController extends ApiControllerBase return $result; } + + public function systemMbufAction() + { + return json_decode((new Backend())->configdRun('system show mbuf'), true); + } } diff --git a/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt b/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt index dc89d0bb5..6c21bfb1f 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt @@ -44,7 +44,7 @@ $( document ).ready(function() { let widgetManager = new WidgetManager({ float: false, - column: 5, + column: 6, margin: 10, alwaysShowResizeHandle: false, sizeToContent: true, diff --git a/src/opnsense/service/conf/actions.d/actions_system.conf b/src/opnsense/service/conf/actions.d/actions_system.conf index 3b874210a..4977726f6 100644 --- a/src/opnsense/service/conf/actions.d/actions_system.conf +++ b/src/opnsense/service/conf/actions.d/actions_system.conf @@ -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 diff --git a/src/opnsense/www/js/widgets/BaseGaugeWidget.js b/src/opnsense/www/js/widgets/BaseGaugeWidget.js new file mode 100644 index 000000000..af4d01c1d --- /dev/null +++ b/src/opnsense/www/js/widgets/BaseGaugeWidget.js @@ -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 $(` +
+
+ +
+
+ `); + } + + 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(); + } + } +} \ No newline at end of file diff --git a/src/opnsense/www/js/widgets/Disk.js b/src/opnsense/www/js/widgets/Disk.js index 09f347d77..bfe1e8839 100644 --- a/src/opnsense/www/js/widgets/Disk.js +++ b/src/opnsense/www/js/widgets/Disk.js @@ -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 $(` -
+
- +
- +
@@ -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); diff --git a/src/opnsense/www/js/widgets/FirewallStates.js b/src/opnsense/www/js/widgets/FirewallStates.js index fa24b0cfd..53dce9813 100644 --- a/src/opnsense/www/js/widgets/FirewallStates.js +++ b/src/opnsense/www/js/widgets/FirewallStates.js @@ -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 $(` -
-
- -
-
- `); } 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(); - } - } } diff --git a/src/opnsense/www/js/widgets/Mbuf.js b/src/opnsense/www/js/widgets/Mbuf.js new file mode 100644 index 000000000..39f7e88ab --- /dev/null +++ b/src/opnsense/www/js/widgets/Mbuf.js @@ -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)]); + }); + } + +} \ No newline at end of file diff --git a/src/opnsense/www/js/widgets/Memory.js b/src/opnsense/www/js/widgets/Memory.js index f56155542..8ac69c29b 100644 --- a/src/opnsense/www/js/widgets/Memory.js +++ b/src/opnsense/www/js/widgets/Memory.js @@ -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 $(` -
-
- -
-
- `); } 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(); - } - } }