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