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();
- }
- }
}