mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-13 08:09:41 +00:00
dnsmasq: Add filter function for interfaces and tags with multiselect (#8465)
* dnsmasq: Add filter function for interfaces and tags with multiselect * dnsmasq: Small cleanup in filter selectpicker previous * Refactor search actions and tag filtering - Use single helper function for building filter - Use tag UUIDs instead of names for filtering - Avoid building filter functions when filters are empty - Pass null to searchBase() when no filtering is required - Use UUID-based filtering for dhcp_tags via record attributes * dnsmasq: Make tags and interfaces dropdown just a tad nicer * Services: Dnsmasq DNS & DHCP - cleanups for https://github.com/opnsense/core/pull/8465 simplify recurring pattern for tag search and move select options generation into common jquery function. --------- Co-authored-by: Ad Schellevis <ad@opnsense.org>
This commit is contained in:
parent
b163c68bf9
commit
8db4e28614
@ -37,6 +37,39 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
protected static $internalModelClass = '\OPNsense\Dnsmasq\Dnsmasq';
|
||||
protected static $internalModelUseSafeDelete = true;
|
||||
|
||||
/**
|
||||
* Tags and interface filter function.
|
||||
* Interfaces are tags too, in the sense of dnsmasq.
|
||||
*
|
||||
* @return callable|null
|
||||
*/
|
||||
private function buildFilterFunction(): ?callable
|
||||
{
|
||||
$filterValues = $this->request->get('tags') ?? [];
|
||||
$fieldNames = ['interface', 'set_tag', 'tag'];
|
||||
if (empty($filterValues)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return function ($record) use ($filterValues, $fieldNames) {
|
||||
foreach ($fieldNames as $fieldName) {
|
||||
// Skip this field if not present in current record
|
||||
if (!isset($record->{$fieldName})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match field values against filter list
|
||||
foreach (array_map('trim', explode(',', (string)$record->{$fieldName})) as $value) {
|
||||
if (in_array($value, $filterValues, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
@ -51,7 +84,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* hosts */
|
||||
public function searchHostAction()
|
||||
{
|
||||
return $this->searchBase('hosts');
|
||||
return $this->searchBase('hosts', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getHostAction($uuid = null)
|
||||
@ -137,7 +170,18 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp tags */
|
||||
public function searchTagAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_tags');
|
||||
$filters = $this->request->get('tags') ?? [];
|
||||
|
||||
$filter_funct = null;
|
||||
if (!empty($filters)) {
|
||||
$filter_funct = function ($record) use ($filters) {
|
||||
$attributes = $record->getAttributes();
|
||||
$uuid = $attributes['uuid'] ?? null;
|
||||
return in_array($uuid, $filters, true);
|
||||
};
|
||||
}
|
||||
|
||||
return $this->searchBase('dhcp_tags', null, null, $filter_funct);
|
||||
}
|
||||
|
||||
public function getTagAction($uuid = null)
|
||||
@ -163,7 +207,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp ranges */
|
||||
public function searchRangeAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_ranges');
|
||||
return $this->searchBase('dhcp_ranges', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getRangeAction($uuid = null)
|
||||
@ -189,7 +233,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp options */
|
||||
public function searchOptionAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_options');
|
||||
return $this->searchBase('dhcp_options', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getOptionAction($uuid = null)
|
||||
@ -215,7 +259,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp match options */
|
||||
public function searchMatchAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_options_match');
|
||||
return $this->searchBase('dhcp_options_match', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getMatchAction($uuid = null)
|
||||
@ -241,7 +285,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
/* dhcp boot options */
|
||||
public function searchBootAction()
|
||||
{
|
||||
return $this->searchBase('dhcp_boot');
|
||||
return $this->searchBase('dhcp_boot', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function getBootAction($uuid = null)
|
||||
@ -263,4 +307,51 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
{
|
||||
return $this->delBase('dhcp_boot', $uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return selectpicker options for interfaces and tags
|
||||
*/
|
||||
public function getTagListAction()
|
||||
{
|
||||
$result = [
|
||||
'tags' => [
|
||||
'label' => gettext('Tags'),
|
||||
'icon' => 'fa fa-tag text-primary',
|
||||
'items' => []
|
||||
],
|
||||
'interfaces' => [
|
||||
'label' => gettext('Interfaces'),
|
||||
'icon' => 'fa fa-ethernet text-info',
|
||||
'items' => []
|
||||
]
|
||||
];
|
||||
|
||||
// Interfaces
|
||||
foreach (Config::getInstance()->object()->interfaces->children() as $key => $intf) {
|
||||
if ((string)$intf->type === 'group') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result['interfaces']['items'][] = [
|
||||
'value' => $key,
|
||||
'label' => empty($intf->descr) ? strtoupper($key) : (string)$intf->descr
|
||||
];
|
||||
}
|
||||
|
||||
// Tags
|
||||
foreach ($this->getModel()->dhcp_tags->iterateItems() as $uuid => $tag) {
|
||||
$result['tags']['items'][] = [
|
||||
'value' => $uuid,
|
||||
'label' => (string)$tag->tag
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_keys($result) as $key) {
|
||||
usort($result[$key]['items'], fn($a, $b) => strcasecmp($a['label'], $b['label']));
|
||||
}
|
||||
|
||||
// Assemble result
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -73,7 +73,18 @@
|
||||
'get':'/api/dnsmasq/settings/get_' + grid_id + '/',
|
||||
'set':'/api/dnsmasq/settings/set_' + grid_id + '/',
|
||||
'add':'/api/dnsmasq/settings/add_' + grid_id + '/',
|
||||
'del':'/api/dnsmasq/settings/del_' + grid_id + '/'
|
||||
'del':'/api/dnsmasq/settings/del_' + grid_id + '/',
|
||||
options: {
|
||||
triggerEditFor: getUrlHash('edit'),
|
||||
initialSearchPhrase: getUrlHash('search'),
|
||||
requestHandler: function(request) {
|
||||
const selectedTags = $('#tag_select').val();
|
||||
if (selectedTags && selectedTags.length > 0) {
|
||||
request['tags'] = selectedTags;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
}
|
||||
});
|
||||
/* insert headers when multiple grids exist on a single tab */
|
||||
let header = $("#" + grid_id + "-header");
|
||||
@ -90,6 +101,16 @@
|
||||
}
|
||||
} else {
|
||||
all_grids[grid_id].bootgrid('reload');
|
||||
|
||||
}
|
||||
// insert tag selectpicker in all grids that use tags or interfaces, boot excluded cause two grids in same tab
|
||||
if (!['domain', 'boot'].includes(grid_id)) {
|
||||
let header = $("#" + grid_id + "-header");
|
||||
let $actionBar = header.find('.actionBar');
|
||||
if ($actionBar.length) {
|
||||
$('#tag_select_container').detach().insertBefore($actionBar.find('.search'));
|
||||
$('#tag_select_container').show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -150,6 +171,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Populate tag selectpicker
|
||||
$('#tag_select').fetch_options('/api/dnsmasq/settings/get_tag_list');
|
||||
|
||||
$('#tag_select').change(function () {
|
||||
Object.keys(all_grids).forEach(function (grid_id) {
|
||||
// boot is not excluded here, as it reloads in same tab as options
|
||||
if (!['domain'].includes(grid_id)) {
|
||||
all_grids[grid_id].bootgrid('reload');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -157,6 +190,9 @@
|
||||
tbody.collapsible > tr > td:first-child {
|
||||
padding-left: 30px;
|
||||
}
|
||||
#tag_select_container {
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: none;" id="hosts_tfoot_append">
|
||||
@ -172,6 +208,11 @@
|
||||
<button id="download_hosts" type="button" title="{{ lang._('Export as csv') }}" data-toggle="tooltip" class="btn btn-xs"><span class="fa fa-fw fa-table"></span></button>
|
||||
</div>
|
||||
|
||||
<div id="tag_select_container" class="btn-group" style="display: none;">
|
||||
<select id="tag_select" class="selectpicker" multiple data-title="{{ lang._('Tags & Interfaces') }}" data-show-subtext="true" data-live-search="true" data-size="15" data-width="200px" data-container="body">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Navigation bar -->
|
||||
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
|
||||
<li><a data-toggle="tab" href="#general">{{ lang._('General') }}</a></li>
|
||||
|
||||
@ -622,6 +622,74 @@ $.fn.SimpleActionButton = function (params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch option list for remote url, when a list is returned, expects either a list of items formatted like
|
||||
* [
|
||||
* {'label': 'my_label', value: 'my_value'}
|
||||
* ]
|
||||
* or an option group like:
|
||||
* {
|
||||
* group_value: {
|
||||
* label: 'my_group_label',
|
||||
* icon: 'fa fa-tag text-primary',
|
||||
* items: [{'label': 'my_label', value: 'my_value'}]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* When data is formatted differently, the data_callback can be used to re-format.
|
||||
*
|
||||
* @param url
|
||||
* @param params
|
||||
* @param data_callback callout to cleanse data before usage
|
||||
* @param store_data store data in data attribute (in its original form)
|
||||
*/
|
||||
$.fn.fetch_options = function(url, params, data_callback, store_data) {
|
||||
var deferred = $.Deferred();
|
||||
var $obj = $(this);
|
||||
$obj.empty();
|
||||
|
||||
ajaxGet(url, params ?? {}, function(data){
|
||||
if (store_data === true) {
|
||||
$obj.data("store", data);
|
||||
}
|
||||
if (typeof data_callback === "function") {
|
||||
data = data_callback(data);
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
data.map(function (item) {
|
||||
$obj.append($("<option/>").attr({"value": item.value}).text(item.label));
|
||||
});
|
||||
} else {
|
||||
for (const groupKey in data) {
|
||||
const group = data[groupKey];
|
||||
if (group.items.length > 0) {
|
||||
const $optgroup = $('<optgroup>', {
|
||||
label: group.label,
|
||||
'data-icon': group.icon
|
||||
});
|
||||
for (const item of group.items) {
|
||||
$optgroup.append(
|
||||
$('<option>', {
|
||||
value: item.value,
|
||||
text: item.label,
|
||||
'data-subtext': group.label
|
||||
})
|
||||
);
|
||||
}
|
||||
$obj.append($optgroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($obj.hasClass('selectpicker')) {
|
||||
$obj.selectpicker('refresh');
|
||||
}
|
||||
$obj.change();
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* File upload dialog, constructs a modal, asks for a file to upload and sets {'payload': ..,, 'filename': ...}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user