Firewall: Rules [new]: Add a command button to open the live log with prefilled rule ID (#9770)

fw_log.volt:
Uses a url hash inside fw_log.volt to set a filter when opening it through a link from filter_rule.volt
The url hash can set any type of a single filter, so it can be reused in other pages as well.
Combine entry point of addCombinedFilter and addFilter, decide via array in field what type it is.
Change init entrypoint to always go through the filterChange() pipeline, but have a fast path in there that initializes without a filter. This adds the filter immediately when loading with the page with the URL hash
Make sure we want for tableBuilt to prevent replaceData errors

filter_rule.volt
Only show the log search button when row.log is 1 or true
Change fa icon in the lookup rule reference button in dnat as well for consistency
Use URLSearchParams()

---------

Co-authored-by: Stephan de Wit <stephan.de.wit@deciso.com>
This commit is contained in:
Monviech 2026-02-12 14:31:04 +01:00 committed by GitHub
parent c6db10f564
commit 63e0b92278
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 44 deletions

View file

@ -273,8 +273,11 @@
}
_init() {
// pushes this.bufferSize entries to the models' buffer
this.bucket.copyTo(this.viewBuffer, this.bufferSize);
/**
* Rebuild the view buffer from the raw bucket using the current filter state.
* Acts as the single pipeline for init, filter updates, and bucket resets.
*/
this._filterChange();
}
_onBucketEvent(event) {
@ -437,10 +440,11 @@
* search string modified). This function is idempotent.
*/
_filterChange() {
const { fn, reset } = this._buildFilterFn();
const { fn, isNoop } = this._buildFilterFn();
if (reset) {
this._init();
if (isNoop) {
// pure init without any filters
this.bucket.copyTo(this.viewBuffer, this.bufferSize);
return;
}
@ -490,21 +494,15 @@
/**
* Example: { field: 'src', operator: '=', value: '192.168.1.1', format:'RFC1918' (optional) }
* The optional 'format' parameter replaces the value for display purposes only
* Combined filters are inferred when `field` is an array (e.g. ['src','dst'])
*/
addFilter(filter) {
const id = this._hashFilter(filter);
this.filterStore.filters[id] = filter;
this._filterChange();
}
/**
* Example: { field: ['src', 'dst'], operator: '!=', value: '192.168.1.1' }
* The meaning of a combined filter is dependent on the operator. In the case of a
* positive operator, "OR" is used, otherwise "AND".
*/
addCombinedFilter(filter) {
const id = this._hashFilter(filter);
this.filterStore.combinedFilters[id] = filter;
if (Array.isArray(filter.field)) {
this.filterStore.combinedFilters[id] = filter;
} else {
this.filterStore.filters[id] = filter;
}
this._filterChange();
}
@ -888,6 +886,18 @@
location.reload();
});
table.on('tableBuilt', () => {
const params = new URLSearchParams(window.location.hash.slice(1));
const field = params.get('field');
const operator = params.get('operator');
const value = params.get('value');
if (field && operator && value) {
filterVM.addFilter({ field, operator, value });
history.replaceState(null, '', location.pathname);
}
});
$apply.on('click', function () {
const field = $filterField.val();
const operator = $filterOperator.val();
@ -896,10 +906,10 @@
switch (field) {
case '__addr__':
filterVM.addCombinedFilter({field: ['src', 'dst'], operator: operator, value: searchString});
filterVM.addFilter({field: ['src', 'dst'], operator: operator, value: searchString});
break;
case '__port__':
filterVM.addCombinedFilter({field: ['srcport', 'dstport'], operator: operator, value: searchString});
filterVM.addFilter({field: ['srcport', 'dstport'], operator: operator, value: searchString});
break;
case 'interface':
filterVM.addFilter({field: field, operator: operator, value: $interfaceSelect.val(), format: $interfaceSelect.find('option:selected').text()});
@ -1207,11 +1217,7 @@
};
const {filters, mode} = parseTemplate(tmpl);
for (const filter of filters) {
if (Array.isArray(filter.field)) {
filterVM.addCombinedFilter(filter);
} else {
filterVM.addFilter(filter);
}
filterVM.addFilter(filter);
}
filterVM.setFilterMode(mode);
if (mode === 'OR') {

View file

@ -139,10 +139,10 @@
if (!rowId.includes('-')) {
return `
<a href="/system_advanced_firewall.php"
<a href="/system_advanced_firewall.php" target="_blank" rel="noopener noreferrer"
class="btn btn-xs btn-default bootgrid-tooltip"
title="{{ lang._('Lookup Rule') }}">
<span class="fa fa-fw fa-search"></span>
title="{{ lang._('Lookup rule reference') }}">
<span class="fa fa-fw fa-link"></span>
</a>
`;
}

View file

@ -235,29 +235,46 @@
},
},
formatters:{
// Only show command buttons for rules that have a uuid, internal rules will not have one
commands: function (column, row) {
// All formatters except category must skip processing bucket rows in tree view
if (row.isGroup) {
return "";
}
let rowId = row.uuid;
const rowId = row.uuid || "";
const hasUuid = rowId.includes("-");
const logSearchCommand = (rid, log) => {
const loggingEnabled = log === '1' || log === true;
if (!loggingEnabled) return '';
return `
<a href="/ui/diagnostics/firewall/log#${new URLSearchParams({field:'rid',operator:'=',value:rid})}"
target="_blank"
rel="noopener noreferrer"
class="btn btn-xs btn-default bootgrid-tooltip"
title="{{ lang._('View log entries for this rule') }}">
<i class="fa fa-fw fa-search"></i>
</a>
`;
};
// If UUID is invalid, its an internal rule, use the #ref field to show a lookup button.
if (!rowId || !rowId.includes('-')) {
let ref = row["ref"] || "";
if (ref.trim().length > 0) {
let url = `/${ref}`;
return `
<a href="${url}"
class="btn btn-xs btn-default bootgrid-tooltip"
title="{{ lang._('Lookup Rule') }}">
<span class="fa fa-fw fa-search"></span>
</a>
`;
}
// If ref is empty
return "";
if (!hasUuid) {
const ref = (row["ref"] || "");
// optional lookup button if ref exists
const lookupRefCommand = ref ? `
<a href="/${ref}" target="_blank" rel="noopener noreferrer"
class="btn btn-xs btn-default bootgrid-tooltip"
title="{{ lang._('Lookup rule reference') }}">
<i class="fa fa-fw fa-link"></i>
</a>
` : '';
return `
${logSearchCommand(rowId, row.log)}
${lookupRefCommand}
`;
}
return `
@ -292,6 +309,8 @@
title="{{ lang._('Delete') }}">
<span class="fa fa-fw fa-trash-o"></span>
</button>
${logSearchCommand(rowId, row.log)}
`;
},
// Disable rowtoggle for internal rules
@ -1129,7 +1148,7 @@
</div>
</div>
<!-- grid -->
{{ partial('layout_partials/base_bootgrid_table', formGridFilterRule + {'command_width': '150'}+ {
{{ partial('layout_partials/base_bootgrid_table', formGridFilterRule + {'command_width': '180'}+ {
'grid_commands': {
'upload_rules': {
'title': lang._('Import csv'),