Skip to content

Commit f6cb05c

Browse files
authored
Feature: Complex port filters (librenms#19596)
* Initial working * Additional types and model trait WIP * Fixes * Backend filtering working * Working * Dark style closer * Device page too, option to hard reload to apply filter * forgot to filter to device * forgot to filter to device * Fix dropdown incorrect * more fixes, restore urlParam and use Laravel * Fix search bar and bare toggles * Working graphs * Auto-focus input * Ports migrated to Laravel Graphs mostly working, needs tidy * Link cleanups * Handle errors sorting correctly * modern port link * Add date-range-picker to graphs page * Ability to save table filters WIP. Some things hardcoded. * Small cleanup * One time filter migration * Don't let menu go off left side of screen * Cleanup WIP * javascript cleanup * Store sensor state to prevent odd behavior * fixes * port filter refinements * small refactor * Merge multi-select into select * Fix population * stupid state logic. this is probably wrong. * Fix cancel * Add filter to graphs on device ports tabs * Implement port groups * Style fixes * remove stupid import * Fix syntax error * Some types and rector * Update and add device and port group filtering Update links * Some font and icon sizing adjustments * tailwind cleanup, but fonts are messed * Fix initial loading on ajax pages fire filter:apply event (when reload not enabled) fix format for filter:loaded and cache formated filter * Make height match other ui elements * Fix visual jumping around by reserving space for the remove button * First try at vertical layout on small screens * events add name * style fixes * Fix font oddities due to font-size: 12px on html body * Some font size changes and a few more tweaks to the mobile vertical stack * Missing import * Types * Style * fix type * more types * types again, what fun
1 parent d69b805 commit f6cb05c

51 files changed

Lines changed: 2860 additions & 1570 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

LibreNMS/Util/Url.php

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,36 @@ public static function modernDeviceLink(?Device $device, string $text = '', stri
5656
DeviceStatus::Disabled => 'device-link-disabled',
5757
};
5858

59-
return sprintf('<a href="%s" class="%s" x-data="deviceLink()">%s</a>%s',
60-
route('device', $device->device_id),
59+
return sprintf('<a href="%s" class="%s" x-data="deviceLink({device_id: %d})">%s</a>%s',
60+
self::deviceUrl($device),
6161
$class,
62+
$device->device_id,
6263
e($text ?: $device->displayName()),
6364
$extra ? '<br />' . e($extra) : $extra
6465
);
6566
}
6667

68+
/**
69+
* Provisional port link generation
70+
*/
71+
public static function modernPortLink(?Port $port, string $text = '', string $extra = ''): string
72+
{
73+
if ($port === null) {
74+
return e($text);
75+
}
76+
77+
$label = Rewrite::normalizeIfName($port->getLabel());
78+
$text = $text ?: $label;
79+
80+
return sprintf('<a href="%s" class="%s" x-data="portLink({port_id: %d})">%s</a>%s',
81+
self::portUrl($port),
82+
self::portLinkDisplayClass($port),
83+
$port->port_id,
84+
e($text),
85+
$extra ? '<br />' . e($extra) : $extra
86+
);
87+
}
88+
6789
/**
6890
* @param Device|null $device
6991
* @param string|null $text
@@ -154,18 +176,10 @@ public static function deviceLink($device, $text = '', $vars = [], $start = 0, $
154176
return $link;
155177
}
156178

157-
/**
158-
* @param Port $port
159-
* @param string $text
160-
* @param string $type
161-
* @param bool $overlib
162-
* @param bool $single_graph
163-
* @return mixed|string
164-
*/
165-
public static function portLink($port, $text = null, $type = null, $overlib = true, $single_graph = false)
179+
public static function portLink(?Port $port, ?string $text = null, ?string $type = null, bool $overlib = true, bool $single_graph = false, ?string $url = null): string
166180
{
167181
if ($port === null) {
168-
return $text;
182+
return (string) $text;
169183
}
170184

171185
$label = Rewrite::normalizeIfName($port->getLabel());
@@ -205,7 +219,7 @@ public static function portLink($port, $text = null, $type = null, $overlib = tr
205219
if (! $overlib) {
206220
return $content;
207221
} elseif (Gate::allows('view', $port)) {
208-
return self::overlibLink(self::portUrl($port), $text, $content, self::portLinkDisplayClass($port));
222+
return self::overlibLink($url ?? self::portUrl($port), $text, $content, self::portLinkDisplayClass($port));
209223
}
210224

211225
return Rewrite::normalizeIfName($text);
@@ -375,7 +389,7 @@ public static function forExternalGraph($args): string
375389

376390
public static function graphPageUrl(string $type, array $args = []): string
377391
{
378-
return url('graphs', ['type' => $type, ...$args]);
392+
return url()->query('graphs', ['type' => $type, ...$args]);
379393
}
380394

381395
/**
@@ -422,15 +436,15 @@ public static function graphPopup($args, $content = null, $link = null)
422436
return self::overlibLink($args['link'], $graph, $popup, null);
423437
}
424438

425-
public static function lazyGraphTag($args)
439+
public static function lazyGraphTag($args, string $class = 'img-responsive'): string
426440
{
427441
$urlargs = [];
428442

429443
foreach ($args as $key => $arg) {
430444
$urlargs[] = $key . '=' . ($arg === null ? '' : urlencode($arg));
431445
}
432446

433-
$tag = '<img class="graph-image img-responsive" src="' . url('graph.php') . '?' . implode('&amp;', $urlargs) . '" style="border:0;"';
447+
$tag = '<img class="graph-image ' . $class . '" src="' . url('graph.php') . '?' . implode('&amp;', $urlargs) . '" style="border:0;"';
434448

435449
if (LibrenmsConfig::get('enable_lazy_load', true)) {
436450
return $tag . ' loading="lazy" />';

app/Http/Controllers/Device/Tabs/PortsController.php

Lines changed: 96 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Illuminate\Database\Eloquent\Builder;
3737
use Illuminate\Http\Request;
3838
use Illuminate\Pagination\LengthAwarePaginator;
39+
use Illuminate\Support\Arr;
3940
use Illuminate\Support\Collection;
4041
use Illuminate\Support\Facades\Auth;
4142
use Illuminate\Support\Facades\Validator;
@@ -49,10 +50,6 @@ class PortsController implements DeviceTab
4950
'perPage' => 32,
5051
'sort' => 'ifIndex',
5152
'order' => 'asc',
52-
'disabled' => false,
53-
'ignored' => false,
54-
'admin' => 'up',
55-
'status' => 'any',
5653
];
5754

5855
public function visible(Device $device): bool
@@ -82,14 +79,11 @@ public function data(Device $device, Request $request): array
8279
'perPage' => ['regex:/^(\d+|all)$/'],
8380
'sort' => 'in:media,mac,port,traffic,speed,index',
8481
'order' => 'in:asc,desc',
85-
'disabled' => 'in:0,1',
86-
'ignored' => 'in:0,1',
87-
'admin' => 'in:up,down,testing,any',
88-
'status' => 'in:up,down,testing,unknown,dormant,notPresent,lowerLayerDown,any',
89-
'type' => 'in:bits,upkts,nupkts,errors,etherlike',
9082
'from' => ['regex:/^(int|[+-]\d+[hdmy])$/'],
9183
'to' => ['regex:/^(int|[+-]\d+[hdmy])$/'],
92-
'searchPort' => 'nullable|string|max:255',
84+
'filter' => ['nullable', 'array'],
85+
'filter.*' => ['array'],
86+
'filter.*.*' => ['nullable', 'max:255'],
9387
]);
9488

9589
$this->loadSettings($request);
@@ -107,11 +101,13 @@ public function data(Device $device, Request $request): array
107101
return array_merge([
108102
'tab' => $tab,
109103
'details' => $this->detail,
104+
'filterFields' => $this->filterFields($device->device_id),
110105
'submenu' => [
111106
$this->getTabs($device),
112107
__('Graphs') => $this->getGraphLinks(),
113108
],
114-
'dropdownLinks' => $this->pageLinks($request),
109+
'dropdownLinks' => [],
110+
'filter' => UserPref::getPref($request->user(), 'filters.device.ports') ?: [],
115111
'perPage' => $this->settings['perPage'],
116112
'sort' => $this->settings['sort'],
117113
'next_order' => $this->settings['order'] == 'asc' ? 'desc' : 'asc',
@@ -139,8 +135,7 @@ private function portData(Device $device, Request $request): array
139135
/** @var Collection<int, Port>|LengthAwarePaginator<Port> $ports */
140136
$ports = $this->getFilteredPortsQuery($device, $relationships, $request)
141137
->paginate(fn ($total) => $this->settings['perPage'] == 'all' ? $total : (int) $this->settings['perPage']) // @phpstan-ignore-line missing closure type
142-
->appends('perPage', $this->settings['perPage'])
143-
->appends('searchPort', $request->input('searchPort'));
138+
->appends('perPage', $this->settings['perPage']);
144139

145140
$data = [
146141
'ports' => $ports,
@@ -296,8 +291,8 @@ private function portSecurityData(Device $device): array
296291
private function getTabs(Device $device): array
297292
{
298293
$tabs = [
299-
['name' => __('Basic'), 'url' => 'basic'],
300-
['name' => __('Detail'), 'url' => 'detail'],
294+
['name' => __('Basic'), 'url' => 'basic', 'class' => 'sync-filter-url'],
295+
['name' => __('Detail'), 'url' => 'detail', 'class' => 'sync-filter-url'],
301296
];
302297

303298
if ($device->macs()->exists()) {
@@ -340,24 +335,28 @@ private function getGraphLinks(): array
340335
[
341336
'name' => __('port.graphs.bits'),
342337
'url' => 'graphs?type=bits',
338+
'class' => 'sync-filter-url',
343339
'sub_name' => __('Mini'),
344340
'sub_url' => 'mini_graphs?type=bits',
345341
],
346342
[
347343
'name' => __('port.graphs.upkts'),
348344
'url' => 'graphs?type=upkts',
345+
'class' => 'sync-filter-url',
349346
'sub_name' => __('Mini'),
350347
'sub_url' => 'mini_graphs?type=upkts',
351348
],
352349
[
353350
'name' => __('port.graphs.nupkts'),
354351
'url' => 'graphs?type=nupkts',
352+
'class' => 'sync-filter-url',
355353
'sub_name' => __('Mini'),
356354
'sub_url' => 'mini_graphs?type=nupkts',
357355
],
358356
[
359357
'name' => __('port.graphs.errors'),
360358
'url' => 'graphs?type=errors',
359+
'class' => 'sync-filter-url',
361360
'sub_name' => __('Mini'),
362361
'sub_url' => 'mini_graphs?type=errors',
363362
],
@@ -377,8 +376,14 @@ private function getGraphLinks(): array
377376

378377
private function loadSettings(Request $request): void
379378
{
380-
$input = $request->only(['perPage', 'sort', 'order', 'disabled', 'ignored', 'admin', 'status']);
381-
$saved = UserPref::getPref($request->user(), 'ports_ui_settings') ?? [];
379+
$input = $request->only(['perPage', 'sort', 'order']);
380+
$saved = UserPref::getPref($request->user(), 'ports_ui_settings');
381+
382+
if ($saved === null) {
383+
$saved = [];
384+
} elseif (array_key_exists('admin', $saved)) {
385+
$saved = $this->migrateFilterSettings($saved);
386+
}
382387

383388
$this->settings = $input + $saved + $this->defaults;
384389

@@ -402,21 +407,10 @@ private function getFilteredPortsQuery(Device $device, array $relationships = []
402407
default => 'ifIndex',
403408
};
404409

405-
// Get search parameter
406-
$searchPort = $request?->input('searchPort');
407-
408410
return Port::where('device_id', $device->device_id)
409411
->isNotDeleted()
410412
->hasAccess(Auth::user())->with($relationships)
411-
->when(! $this->settings['disabled'], fn (Builder $q, $disabled) => $q->where('disabled', 0))
412-
->when(! $this->settings['ignored'], fn (Builder $q, $disabled) => $q->where('ignore', 0))
413-
->when($this->settings['admin'] != 'any', fn (Builder $q, $admin) => $q->where('ifAdminStatus', $this->settings['admin']))
414-
->when($this->settings['status'] != 'any', fn (Builder $q, $admin) => $q->where('ifOperStatus', $this->settings['status']))
415-
->when($searchPort, fn (Builder $q) => $q->where(function (Builder $q) use ($searchPort): void {
416-
$q->where('ifName', 'LIKE', '%' . $searchPort . '%')
417-
->orWhere('ifDescr', 'LIKE', '%' . $searchPort . '%')
418-
->orWhere('ifAlias', 'LIKE', '%' . $searchPort . '%');
419-
}))
413+
->when($request->array('filter'), fn (Builder $q, $filters) => $q->applyFilters($filters))
420414
->when($this->settings['sort'] == 'port', fn (Builder $q, $sort) => $q
421415
->orderByRaw('SOUNDEX(ifName) ' . $this->settings['order'])
422416
->orderByRaw('CHAR_LENGTH(ifName) ' . $this->settings['order'])
@@ -440,37 +434,89 @@ private function parseTab(Request $request): string
440434
return $request->route('vars', LibrenmsConfig::get('ports_page_default')); // fourth segment is called vars to handle legacy urls
441435
}
442436

443-
private function pageLinks(Request $request): array
437+
private function migrateFilterSettings(array $saved): array
444438
{
445-
$disabled = $this->settings['disabled'];
446-
$ignored = $this->settings['ignored'];
447-
$admin = $this->settings['admin'] == 'any';
448-
$status = $this->settings['status'] == 'up';
439+
$filter = [];
449440

441+
if (! $saved['disabled']) { // 0: disabled hidden 1: not filtered
442+
$filter['disabled'] = ['eq' => 0];
443+
}
444+
445+
if (! $saved['ignored']) { // 0: ignored hidden 1: not filtered
446+
$filter['ignore'] = ['eq' => 0];
447+
}
448+
449+
if ($saved['status'] == 'up') { // up: only status up, any: not filtered
450+
$filter['state'] = ['eq' => 'up'];
451+
} elseif ($saved['admin'] == 'up') { // up: only != shutdown, any: not filtered
452+
$filter['state'] = ['neq' => 'shutdown'];
453+
}
454+
455+
Arr::forget($saved, ['admin', 'status', 'disabled', 'ignored']);
456+
UserPref::setPref(request()->user(), 'ports_ui_settings', $saved);
457+
UserPref::setPref(request()->user(), 'filters.device.ports', $filter);
458+
459+
return $saved;
460+
}
461+
462+
private function filterFields(int $device_id): array
463+
{
450464
return [
451465
[
452-
'icon' => $status ? 'fa-regular fa-square-check' : 'fa-regular fa-square',
453-
'url' => $request->fullUrlWithQuery(['status' => $status ? $this->defaults['status'] : 'up']),
454-
'title' => __('port.filters.status_up'),
455-
'external' => false,
466+
'key' => 'search',
467+
'label' => 'Description',
468+
'type' => 'text',
469+
],
470+
[
471+
'key' => 'state',
472+
'label' => 'Oper Status',
473+
'type' => 'select',
474+
'options' => ['up', 'down', 'shutdown'],
475+
],
476+
[
477+
'key' => 'ifSpeed',
478+
'label' => 'Speed',
479+
'type' => 'select',
480+
'endpoint' => route('ajax.select.port-field'),
481+
'params' => [
482+
'field' => 'ifSpeed',
483+
'device' => $device_id,
484+
],
485+
],
486+
[
487+
'key' => 'ifType',
488+
'label' => 'Media',
489+
'type' => 'select',
490+
'endpoint' => route('ajax.select.port-field'),
491+
'params' => [
492+
'field' => 'ifType',
493+
'device' => $device_id,
494+
],
495+
],
496+
[
497+
'key' => 'port_type',
498+
'label' => 'Port Type',
499+
'type' => 'select',
500+
'endpoint' => route('ajax.select.port-field'),
501+
'params' => [
502+
'field' => 'port_descr_type',
503+
'device' => $device_id,
504+
],
456505
],
457506
[
458-
'icon' => $admin ? 'fa-regular fa-square-check' : 'fa-regular fa-square',
459-
'url' => $request->fullUrlWithQuery(['admin' => $admin ? $this->defaults['admin'] : 'any']),
460-
'title' => __('port.filters.admin_down'),
461-
'external' => false,
507+
'key' => 'ignore',
508+
'label' => 'Ignored',
509+
'type' => 'boolean',
462510
],
463511
[
464-
'icon' => $disabled ? 'fa-regular fa-square-check' : 'fa-regular fa-square',
465-
'url' => $request->fullUrlWithQuery(['disabled' => ! $disabled]),
466-
'title' => __('port.filters.disabled'),
467-
'external' => false,
512+
'key' => 'disabled',
513+
'label' => 'Disabled',
514+
'type' => 'boolean',
468515
],
469516
[
470-
'icon' => $ignored ? 'fa-regular fa-square-check' : 'fa-regular fa-square',
471-
'url' => $request->fullUrlWithQuery(['ignored' => ! $ignored]),
472-
'title' => __('port.filters.ignored'),
473-
'external' => false,
517+
'key' => 'deleted',
518+
'label' => 'Deleted',
519+
'type' => 'boolean',
474520
],
475521
];
476522
}

0 commit comments

Comments
 (0)