From 31fb5cbd64c6d4b0b25851a1b8340a340094b769 Mon Sep 17 00:00:00 2001 From: Monviech Date: Tue, 26 May 2026 13:19:07 +0200 Subject: [PATCH 01/11] Firewall: Rules [new]: Always show automatic and legacy rules, even without Inspect enabled. --- .../Firewall/Api/FilterController.php | 12 ++- .../views/OPNsense/Firewall/filter_rule.volt | 89 +++++++++++-------- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php index 4bc5e2969c..e06829a0b7 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php @@ -190,13 +190,11 @@ class FilterController extends FilterBaseController return true; }; - /* only fetch internal and legacy rules when 'show_all' is set */ - if ($show_all) { - $allrules = array_merge( - $allrules, - json_decode((new Backend())->configdRun('filter list non_mvc_rules'), true) ?? [] - ); - } + /* always fetch internal and legacy rules, automatic rules have their own category that is always visible */ + $allrules = array_merge( + $allrules, + json_decode((new Backend())->configdRun('filter list non_mvc_rules'), true) ?? [] + ); $search_clauses = []; $backend = new Backend(); diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 1bd072931f..33388e14d2 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -58,6 +58,7 @@ $('#toggle_tree_button').toggleClass('active btn-primary', treeViewEnabled); let inspectEnabled = localStorage.getItem("firewall_rule_inspect") === "1"; + $('#toggle_inspect_button').toggleClass('active btn-primary', inspectEnabled); function updateStatisticColumns() { @@ -68,40 +69,60 @@ let pendingUrlInterface = getUrlHash('interface') || null; // Lives outside the grid, so the logic of the response handler can be changed after grid initialization - function dynamicResponseHandler(resp) { - // convert the flat rows into a tree view (if enabled) + function dynamicResponseHandler(response) { + const makeBucket = function(row, label, uuid) { + const bucket = { + // ensure uuid is as unique as possible for persistence handling + uuid : uuid, + isGroup : true, + _label : label, // internal + children : [] + }; + + // copy the category info from the first child to use as parent + bucket.categories = label; + bucket.category_colors = row.category_colors || []; + + return bucket; + }; + if (!treeViewEnabled) { - return resp; + // automatic rules are always collected in a single bucket + // non-automatic rows are pushed without children, creating a mixed array of bucket and regular rows + const rows = []; + let automatic = null; + response.rows.forEach(row => { + if (row.is_automatic === true) { + if (automatic === null) { + // readable label used for grouping + const label = (row["%categories"] || row.categories || ""); + automatic = makeBucket(row, label, "automaticrules"); + rows.push(automatic); + } + automatic.children.push(row); + } else { + rows.push(row); + } + }); + + return Object.assign({}, response, { rows: rows }); + } else { + // tree view groups all rows into category buckets instead of using mixed top-level rows + const buckets = []; + let current = null; + response.rows.forEach(row => { + // readable label used for grouping + const label = (row["%categories"] || row.categories || ""); + // start a new bucket whenever the label changes + if (!current || current._label !== label) { + current = makeBucket(row, label, `${String(row.uuid).replace(/-/g, '')}`); + buckets.push(current); + } + current.children.push(row); + }); + + return Object.assign({}, response, { rows: buckets }); } - - const buckets = []; - let current = null; - - resp.rows.forEach(r => { - // readable label used for grouping - const label = (r["%categories"] || r.categories || ""); - - // start a new bucket whenever the label changes - if (!current || current._label !== label) { - current = { - // ensure uuid is as unique as possible for persistence handling - uuid : `${String(r.uuid).replace(/-/g, '')}`, - isGroup : true, - _label : label, // internal - children : [] - }; - - // copy the category info from the first child to use as parent - current.categories = label; - current.category_colors = r.category_colors || []; - - buckets.push(current); - } - - current.children.push(r); - }); - - return Object.assign({}, resp, { rows: buckets }); } $('#download_rules').click(function(e){ @@ -1116,8 +1137,7 @@ class="btn btn-default" data-toggle="tooltip" data-placement="bottom" - data-delay='{"show": 1000}' - title="{{ lang._('Show all rules and statistics') }}"> + title="{{ lang._('Show statistics and a detailed view of the current ruleset') }}"> {{ lang._('Inspect') }} @@ -1129,7 +1149,6 @@ class="btn btn-default" data-toggle="tooltip" data-placement="bottom" - data-delay='{"show": 1000}' title="{{ lang._('Show all categories in a tree') }}"> {{ lang._('Tree') }} From 1c643da59e04e7889a2ac722176502b5172b1410 Mon Sep 17 00:00:00 2001 From: Monviech Date: Tue, 26 May 2026 13:22:34 +0200 Subject: [PATCH 02/11] Newline sneaked in --- src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 33388e14d2..5a36330dd3 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -58,7 +58,6 @@ $('#toggle_tree_button').toggleClass('active btn-primary', treeViewEnabled); let inspectEnabled = localStorage.getItem("firewall_rule_inspect") === "1"; - $('#toggle_inspect_button').toggleClass('active btn-primary', inspectEnabled); function updateStatisticColumns() { From 07f90b72bafb2ab30c80aec3bc89aa68aaf4f536 Mon Sep 17 00:00:00 2001 From: Monviech Date: Wed, 27 May 2026 17:00:29 +0200 Subject: [PATCH 03/11] firewall: group filter rules by type and category Always group rules by their priority/type in the filter grid and reuse the same rule type metadata for both bucket labels and icons. When tree view is enabled, categorized non-automatic rules are grouped one level deeper by category, while automatic and uncategorized rules remain directly below their rule type bucket. This keeps the default view structured without relying on a mixed flat/tree array and makes the tree toggle an additive category grouping layer. --- .../views/OPNsense/Firewall/filter_rule.volt | 151 +++++++++++------- 1 file changed, 96 insertions(+), 55 deletions(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 5a36330dd3..99fb63cfc6 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -67,61 +67,113 @@ // read interface from URL hash once, for the first grid load let pendingUrlInterface = getUrlHash('interface') || null; + const ruleTypeMap = { + '0': { label: "{{ lang._('Automatic rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, + '1': { label: "{{ lang._('Automatic rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, + '2': { label: "{{ lang._('Floating rules') }}", icon: "fa-layer-group", tooltip: "{{ lang._('Floating Rule') }}", color: "text-primary" }, + '3': { label: "{{ lang._('Group rules') }}", icon: "fa-sitemap", tooltip: "{{ lang._('Group Rule') }}", color: "text-warning" }, + '4': { label: "{{ lang._('Interface rules') }}", icon: "fa-ethernet", tooltip: "{{ lang._('Interface Rule') }}", color: "text-info" }, + '5': { label: "{{ lang._('Automatic rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, + }; + + const getRuleTypeDigit = function(row) { + const sortOrder = row.sort_order ? row.sort_order.toString() : ""; + return sortOrder.charAt(0); + }; + + const getRuleType = function(row) { + return ruleTypeMap[getRuleTypeDigit(row)] || null; + }; + // Lives outside the grid, so the logic of the response handler can be changed after grid initialization function dynamicResponseHandler(response) { - const makeBucket = function(row, label, uuid) { - const bucket = { + const getCategoryLabel = function(row) { + return row["%categories"] || row.categories || ""; + }; + + const makeBucket = function(label, uuid, categoryColors) { + return { // ensure uuid is as unique as possible for persistence handling uuid : uuid, isGroup : true, _label : label, // internal + categories : label, + /* + * Bucket rows reuse the category formatter. + * For category buckets, this copies the first child's category metadata + * so the bucket can render the same category icon/color as its rules. + * For rule type buckets, a synthetic categoryColors entry is supplied. + */ + category_colors: categoryColors, children : [] }; + }; - // copy the category info from the first child to use as parent - bucket.categories = label; - bucket.category_colors = row.category_colors || []; + const createBucket = function(parent, label, uuid, categoryColors) { + let bucket = parent.children.find(child => child.isGroup && child._label === label); + + if (!bucket) { + bucket = makeBucket(label, uuid, categoryColors); + parent.children.push(bucket); + } return bucket; }; - if (!treeViewEnabled) { - // automatic rules are always collected in a single bucket - // non-automatic rows are pushed without children, creating a mixed array of bucket and regular rows - const rows = []; - let automatic = null; - response.rows.forEach(row => { - if (row.is_automatic === true) { - if (automatic === null) { - // readable label used for grouping - const label = (row["%categories"] || row.categories || ""); - automatic = makeBucket(row, label, "automaticrules"); - rows.push(automatic); - } - automatic.children.push(row); - } else { - rows.push(row); - } - }); + const root = { children: [] }; - return Object.assign({}, response, { rows: rows }); - } else { - // tree view groups all rows into category buckets instead of using mixed top-level rows - const buckets = []; - let current = null; - response.rows.forEach(row => { - // readable label used for grouping - const label = (row["%categories"] || row.categories || ""); - // start a new bucket whenever the label changes - if (!current || current._label !== label) { - current = makeBucket(row, label, `${String(row.uuid).replace(/-/g, '')}`); - buckets.push(current); - } - current.children.push(row); - }); + response.rows.forEach(row => { + const ruleType = getRuleType(row); + const ruleTypeDigit = getRuleTypeDigit(row) || "other"; + const ruleTypeLabel = ruleType.label || "{{ lang._('Other rules') }}"; + const categoryLabel = getCategoryLabel(row); - return Object.assign({}, response, { rows: buckets }); - } + /* + * The first tree level is the default view, and always based on the rule priority/type. + * + * Automatic rules + * rule + * Interface rules + * rule + */ + const ruleTypeBucket = createBucket( + root, + ruleTypeLabel, + `ruletype${ruleTypeDigit}`, + [{ name: ruleTypeLabel }] + ); + + if (treeViewEnabled && row.is_automatic !== true && categoryLabel !== "") { + /* + * When tree view is enabled, categorized non-automatic rules are grouped + * one level deeper by category below their rule priority/type bucket. + * + * Automatic rules and uncategorized rules stay directly below their rule + * priority/type bucket to avoid redundant or low-value nesting. + * + * Automatic rules + * rule + * Interface rules + * rule + * Web (Category) + * rule + * Mail (Category) + * rule + */ + const categoryBucket = createBucket( + ruleTypeBucket, + categoryLabel, + `ruletype${ruleTypeDigit}category${String(categoryLabel).replace(/[^a-z0-9]/gi, '')}`, + row.category_colors || [] + ); + + categoryBucket.children.push(row); + } else { + ruleTypeBucket.children.push(row); + } + }); + + return Object.assign({}, response, { rows: root.children }); } $('#download_rules').click(function(e){ @@ -436,22 +488,11 @@ let result = ""; // Rule Type Icons (Determined by first digit of sort_order) - const ruleTypeIcons = { - '0': { icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, - '1': { icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, - '2': { icon: "fa-layer-group", tooltip: "{{ lang._('Floating Rule') }}", color: "text-primary" }, - '3': { icon: "fa-sitemap", tooltip: "{{ lang._('Group Rule') }}", color: "text-warning" }, - '4': { icon: "fa-ethernet", tooltip: "{{ lang._('Interface Rule') }}", color: "text-info" }, - '5': { icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, - }; + const ruleType = getRuleType(row); - const sortOrder = row.sort_order ? row.sort_order.toString() : ""; - if (sortOrder.length > 0) { - const typeDigit = sortOrder.charAt(0); - if (ruleTypeIcons[typeDigit]) { - result += ` `; - } + if (ruleType) { + result += ` `; } // Action From 3d2e8a5a627f6a2918f12eb0dabf6dca299ddeef Mon Sep 17 00:00:00 2001 From: Monviech Date: Wed, 27 May 2026 17:02:48 +0200 Subject: [PATCH 04/11] The front end doesn't need any fake category for the automatic rules anymore, since it's decided by priority group now in the response handler logic --- .../OPNsense/Firewall/Api/FilterController.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php index e06829a0b7..8dc17ca78c 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php @@ -170,15 +170,8 @@ class FilterController extends FilterBaseController $record = array_merge($record, $rule_stats[$record['uuid']]); } - // Tag legacy rules as "Automatic generated rules" if they have an empty category - if (!empty($record['is_automatic'])) { - $label = gettext('Automatically generated rules'); - $record['categories'] = $label; // Grouping key for tree view - $record['category_colors'] = [['name' => $label]]; // Category formatter metadata - } else { - /* frontend can format categories with colors */ - $record['category_colors'] = $this->getCategoryColors($r_categories); - } + /* frontend can format categories with colors */ + $record['category_colors'] = $this->getCategoryColors($r_categories); /* frontend can format aliases with an alias icon */ foreach (['source_net','source_port','destination_net','destination_port'] as $field) { From e8d316d8c75a2ad37f99e17aaeaa69f3432e6713 Mon Sep 17 00:00:00 2001 From: Monviech Date: Thu, 28 May 2026 08:41:55 +0200 Subject: [PATCH 05/11] Render icons for the top tree level groups, remove counts behind them to reduce visual noise --- .../views/OPNsense/Firewall/filter_rule.volt | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 99fb63cfc6..012c9ee1ec 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -415,26 +415,39 @@ return '*'; } }, - // The category formatter is special as it renders differently for the bucket row + // Bucket rows reuse the category column because Tabulator only renders one + // formatter per cell, so both category buckets and rule type buckets are + // represented here. category: function (column, row) { const isGroup = row.isGroup; const hasCategories = row.categories && Array.isArray(row.category_colors); + // Rows without category metadata render nothing in this column. + // This also avoids creating a fake label for rules that + // are intentionally kept directly below their rule type bucket. if (!hasCategories) { - - return isGroup - ? ` - - {{ lang._('Uncategorized') }} - ${(row.children && row.children.length) || 0} - ` - : ''; + return ''; } const categories = row.category_colors || []; const icons = categories.map(cat => { + /* + * Top-level tree icons, e.g. automatic/floating/interface rules, are + * resolved here as well because each row can only use one formatter for + * this column. Rule type buckets provide a synthetic category entry + * whose name matches ruleTypeMap, while real category buckets continue + * to render normal category tag icons. + */ + const ruleType = Object.values(ruleTypeMap).find(type => type.label === cat.name); + + if (isGroup && ruleType) { + return ` + + + `; + } + const bgColor = cat.color ? ` style="color:${cat.color};"` : ''; return ` @@ -447,8 +460,6 @@ ? ` ${icons} ${categories.map(cat => cat.name).join(', ')} - ${(row.children && row.children.length) || 0} ` : icons; From 74994237514b598f51f68220bd0cb22d3d6aaf12 Mon Sep 17 00:00:00 2001 From: Monviech Date: Thu, 28 May 2026 08:45:58 +0200 Subject: [PATCH 06/11] Make label shorter --- .../mvc/app/views/OPNsense/Firewall/filter_rule.volt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 012c9ee1ec..119bfe21a2 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -68,12 +68,12 @@ let pendingUrlInterface = getUrlHash('interface') || null; const ruleTypeMap = { - '0': { label: "{{ lang._('Automatic rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, - '1': { label: "{{ lang._('Automatic rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, - '2': { label: "{{ lang._('Floating rules') }}", icon: "fa-layer-group", tooltip: "{{ lang._('Floating Rule') }}", color: "text-primary" }, - '3': { label: "{{ lang._('Group rules') }}", icon: "fa-sitemap", tooltip: "{{ lang._('Group Rule') }}", color: "text-warning" }, - '4': { label: "{{ lang._('Interface rules') }}", icon: "fa-ethernet", tooltip: "{{ lang._('Interface Rule') }}", color: "text-info" }, - '5': { label: "{{ lang._('Automatic rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, + '0': { label: "{{ lang._('Automatic') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, + '1': { label: "{{ lang._('Automatic') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, + '2': { label: "{{ lang._('Floating') }}", icon: "fa-layer-group", tooltip: "{{ lang._('Floating Rule') }}", color: "text-primary" }, + '3': { label: "{{ lang._('Group') }}", icon: "fa-sitemap", tooltip: "{{ lang._('Group Rule') }}", color: "text-warning" }, + '4': { label: "{{ lang._('Interface') }}", icon: "fa-ethernet", tooltip: "{{ lang._('Interface Rule') }}", color: "text-info" }, + '5': { label: "{{ lang._('Automatic') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, }; const getRuleTypeDigit = function(row) { From 1871818783f606b4439e63a5d42b77229019729e Mon Sep 17 00:00:00 2001 From: Monviech Date: Thu, 28 May 2026 09:06:51 +0200 Subject: [PATCH 07/11] Remove the count labels from interfaces, as they imply something has to be done as they resemble event badges --- .../Firewall/Api/FilterController.php | 28 ++++--------------- .../views/OPNsense/Firewall/filter_rule.volt | 22 ++++++--------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php index 8dc17ca78c..f5ca40c097 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php @@ -376,31 +376,15 @@ class FilterController extends FilterBaseController ], ]; - // Count rules per interface - $ruleCounts = []; - foreach ((new Filter())->rules->rule->iterateItems() as $rule) { - $interfaces = $rule->interface->getValues(); - - if (!$rule->interfacenot->isEmpty() || count($interfaces) !== 1) { - // floating: empty, multiple, or inverted interface - $ruleCounts['floating'] = ($ruleCounts['floating'] ?? 0) + 1; - } else { - // single interface - $ruleCounts[$interfaces[0]] = ($ruleCounts[$interfaces[0]] ?? 0) + 1; - } - } - $totalRules = array_sum($ruleCounts); - - // Helper to build item with label and count - $makeItem = fn($value, $label, $count, $type) => [ + // Helper to build item + $makeItem = fn($value, $label, $type) => [ 'value' => $value, 'label' => $label, - 'count' => $count, 'type' => $type ]; // Floating - $result['floating']['items'][] = $makeItem('__floating', gettext('Floating'), $ruleCounts['floating'] ?? 0, 'floating'); + $result['floating']['items'][] = $makeItem('__floating', gettext('Floating'), 'floating'); // Groups + Interfaces foreach (Config::getInstance()->object()->interfaces->children() as $key => $intf) { @@ -410,11 +394,11 @@ class FilterController extends FilterBaseController } $descr = !empty($intf->descr) ? (string)$intf->descr : strtoupper($key); $type = (string)$intf->type == 'group' ? 'group' : 'interface'; - $result["{$type}s"]['items'][] = $makeItem($key, $descr, $ruleCounts[$key] ?? 0, $type); + $result["{$type}s"]['items'][] = $makeItem($key, $descr, $type); } - // ALL rules - $result['any']['items'][] = $makeItem('__any', gettext('All rules'), $totalRules, 'any'); + // All rules + $result['any']['items'][] = $makeItem('__any', gettext('All rules'), 'any'); foreach ($result as &$section) { usort($section['items'], fn($a, $b) => strcasecmp($a['label'], $b['label'])); diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 119bfe21a2..a4ee6da3c8 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -805,7 +805,7 @@ label: row.name, id: row.used > 0 ? row.uuid : undefined, 'data-content': row.used > 0 - ? `${row.used} ${optVal}` + ? `${optVal} ${row.used}` : undefined }; }); @@ -824,37 +824,31 @@ // Populate interface selectpicker function populateInterfaceSelectpicker() { const currentSelection = $("#interface_select").val(); + return $('#interface_select').fetch_options( '/api/firewall/filter/get_interface_list', {}, function (data) { for (const groupKey in data) { const group = data[groupKey]; - group.items = group.items.map(item => { - const count = item.count ?? 0; - const label = (item.label || ''); - const subtext = group.label; + const icon = group.icon || ''; - const bgClassMap = { - floating: 'label-primary', - group: 'label-warning', - interface: 'label-info', - any: 'label-primary', - }; - const badgeClass = bgClassMap[item.type] || 'label-info'; + group.items = group.items.map(item => { + const label = item.label || ''; return { value: item.value, label: label, 'data-content': ` - ${count > 0 ? `${count}` : ''} + ${icon ? `` : ''} ${label} `.trim() }; }); } + return data; }, false, @@ -873,7 +867,7 @@ interfaceInitialized = true; pendingUrlInterface = null; // consume the hash so it is not used again }, - true // render_html to show counts as badges + true // render_html to show icons ); } From 588a70a48f4b5151fc3ba6d57bfce8dc241a8e0a Mon Sep 17 00:00:00 2001 From: Monviech Date: Thu, 28 May 2026 09:13:38 +0200 Subject: [PATCH 08/11] Move enabled checkbox before category --- .../controllers/OPNsense/Firewall/forms/dialogFilterRule.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml index 6b657cdca7..06a0676d6d 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml @@ -12,7 +12,7 @@ 50 boolean rowtoggle - 5 + 1 center @@ -47,7 +47,7 @@ For grouping purposes you may select multiple groups here to organize items. category - 1 + 2 80 From 8a064a0a817e84f706d4e8182db09813a9f67e6f Mon Sep 17 00:00:00 2001 From: Monviech Date: Thu, 28 May 2026 09:19:31 +0200 Subject: [PATCH 09/11] Rework the buttons, only use icons with tooltips, always show the expand tree button --- .../mvc/app/views/OPNsense/Firewall/filter_rule.volt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index a4ee6da3c8..8903c521d3 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -906,13 +906,12 @@ localStorage.setItem("firewall_rule_tree", treeViewEnabled ? "1" : "0"); $(this).toggleClass('active btn-primary', treeViewEnabled); $("#{{formGridFilterRule['table_id']}}").toggleClass("tree-enabled", treeViewEnabled); - $("#tree_expand_container").toggle(treeViewEnabled); grid.bootgrid("reload"); }); - // Visible only when tree view is enabled $("#tree_expand_container").detach().insertAfter("#tree_toggle_container"); - $("#tree_expand_container").toggle(treeViewEnabled); + $("#tree_expand_container").show(); + $('#expand_tree_button').on('click', function () { const $table = $('#{{ formGridFilterRule["table_id"] }}'); @@ -1184,7 +1183,6 @@ data-placement="bottom" title="{{ lang._('Show statistics and a detailed view of the current ruleset') }}"> - {{ lang._('Inspect') }} @@ -1194,9 +1192,8 @@ class="btn btn-default" data-toggle="tooltip" data-placement="bottom" - title="{{ lang._('Show all categories in a tree') }}"> - - {{ lang._('Tree') }} + title="{{ lang._('Group rules by categories') }}"> +
@@ -1205,7 +1202,6 @@ class="btn btn-default" data-toggle="tooltip" data-placement="bottom" - data-delay='{"show": 1000}' title="{{ lang._('Expand/Collapse all') }}"> From 72beec14069f4bfe1022df7f8464406e72195268 Mon Sep 17 00:00:00 2001 From: Monviech Date: Thu, 28 May 2026 09:26:59 +0200 Subject: [PATCH 10/11] Match terminology with legacy page --- .../mvc/app/views/OPNsense/Firewall/filter_rule.volt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 8903c521d3..1ff50cf23d 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -68,12 +68,12 @@ let pendingUrlInterface = getUrlHash('interface') || null; const ruleTypeMap = { - '0': { label: "{{ lang._('Automatic') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, - '1': { label: "{{ lang._('Automatic') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, - '2': { label: "{{ lang._('Floating') }}", icon: "fa-layer-group", tooltip: "{{ lang._('Floating Rule') }}", color: "text-primary" }, - '3': { label: "{{ lang._('Group') }}", icon: "fa-sitemap", tooltip: "{{ lang._('Group Rule') }}", color: "text-warning" }, - '4': { label: "{{ lang._('Interface') }}", icon: "fa-ethernet", tooltip: "{{ lang._('Interface Rule') }}", color: "text-info" }, - '5': { label: "{{ lang._('Automatic') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" }, + '0': { label: "{{ lang._('Automatically generated rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatically generated rules') }}", color: "text-secondary" }, + '1': { label: "{{ lang._('Automatically generated rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatically generated rules') }}", color: "text-secondary" }, + '2': { label: "{{ lang._('Floating rules') }}", icon: "fa-layer-group", tooltip: "{{ lang._('Floating rule') }}", color: "text-primary" }, + '3': { label: "{{ lang._('Group rules') }}", icon: "fa-sitemap", tooltip: "{{ lang._('Group rule') }}", color: "text-warning" }, + '4': { label: "{{ lang._('Interface rules') }}", icon: "fa-ethernet", tooltip: "{{ lang._('Interface rule') }}", color: "text-info" }, + '5': { label: "{{ lang._('Automatically generated rules') }}", icon: "fa-magic", tooltip: "{{ lang._('Automatically generated rules') }}", color: "text-secondary" }, }; const getRuleTypeDigit = function(row) { From 9f77d6a6fc2675ddadcca8cb73c4e2707769e3e6 Mon Sep 17 00:00:00 2001 From: Monviech Date: Thu, 28 May 2026 09:38:09 +0200 Subject: [PATCH 11/11] Change it back that category is first again, but it needs some more space now --- .../OPNsense/Firewall/forms/dialogFilterRule.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml index 06a0676d6d..f9d1a8c618 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml @@ -12,7 +12,7 @@ 50 boolean rowtoggle - 1 + 5 center @@ -47,8 +47,8 @@ For grouping purposes you may select multiple groups here to organize items. category - 2 - 80 + 1 + 120