From fcfcdea07cb4a97247eb6aa8a7eb6e66da405d33 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Mon, 18 Aug 2025 17:54:54 +0000 Subject: [PATCH] feat: enhance backup search with advanced filtering like Dashboard - Add parseFilterStack support to Backups tab for advanced filtering - Support field-specific filters: vmid:, node:, type:, storage:, datastore:, namespace: - Support comparison operators: size>1GB for backup size filtering - Support boolean fields: verified:true, protected:true for PBS backups - Add search help tooltip with backup-specific filter examples - Update placeholder text to show relevant backup search examples - Make searchQuery utils generic to work with any data type, not just VM/Container - Backups tab now has same powerful search capabilities as Dashboard --- .../src/components/Backups/UnifiedBackups.tsx | 96 +++++++++++++++---- frontend-modern/src/utils/searchQuery.ts | 44 ++++++--- 2 files changed, 107 insertions(+), 33 deletions(-) diff --git a/frontend-modern/src/components/Backups/UnifiedBackups.tsx b/frontend-modern/src/components/Backups/UnifiedBackups.tsx index 7ad0c0f96..0f70d9155 100644 --- a/frontend-modern/src/components/Backups/UnifiedBackups.tsx +++ b/frontend-modern/src/components/Backups/UnifiedBackups.tsx @@ -3,6 +3,7 @@ import { useWebSocket } from '@/App'; import { formatBytes, formatAbsoluteTime, formatRelativeTime } from '@/utils/format'; import { createLocalStorageBooleanSignal, STORAGE_KEYS } from '@/utils/localStorage'; import PBSCard from '@/components/Dashboard/PBSCard'; +import { parseFilterStack, evaluateFilterStack } from '@/utils/searchQuery'; type BackupType = 'snapshot' | 'local' | 'remote'; type GuestType = 'VM' | 'LXC' | 'Template' | 'ISO'; @@ -312,25 +313,52 @@ const UnifiedBackups: Component = () => { ); } - // Search filter + // Search filter - with advanced filtering support like Dashboard if (search) { - const searchTerms = search.split(',').map(term => term.trim()).filter(term => term.length > 0); - data = data.filter(item => - searchTerms.some(term => { - const searchFields = [ - item.vmid?.toString(), - item.name, - item.node, - item.backupName, - item.description, - item.storage, - item.datastore, - item.namespace - ].filter(Boolean).map(field => field!.toString().toLowerCase()); - - return searchFields.some(field => field.includes(term)); - }) - ); + // Split by commas first + const searchParts = search.split(',').map(t => t.trim()).filter(t => t); + + // Separate filters from text searches + const filters: string[] = []; + const textSearches: string[] = []; + + searchParts.forEach(part => { + if (part.includes('>') || part.includes('<') || part.includes(':')) { + filters.push(part); + } else { + textSearches.push(part.toLowerCase()); + } + }); + + // Apply filters if any + if (filters.length > 0) { + // Join filters with AND operator + const filterString = filters.join(' AND '); + const stack = parseFilterStack(filterString); + if (stack.filters.length > 0) { + data = data.filter(item => evaluateFilterStack(item, stack)); + } + } + + // Apply text search if any + if (textSearches.length > 0) { + data = data.filter(item => + textSearches.some(term => { + const searchFields = [ + item.vmid?.toString(), + item.name, + item.node, + item.backupName, + item.description, + item.storage, + item.datastore, + item.namespace + ].filter(Boolean).map(field => field!.toString().toLowerCase()); + + return searchFields.some(field => field.includes(term)); + }) + ); + } } // Type filter @@ -1163,7 +1191,7 @@ const UnifiedBackups: Component = () => { { if (!isSearchLocked()) { @@ -1179,6 +1207,36 @@ const UnifiedBackups: Component = () => { + + {/* Help tooltip button */} +