diff --git a/frontend/src/components/ResourcesView.tsx b/frontend/src/components/ResourcesView.tsx index d6afe0b0..5b353d5e 100644 --- a/frontend/src/components/ResourcesView.tsx +++ b/frontend/src/components/ResourcesView.tsx @@ -14,7 +14,8 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { Trash2, HardDrive, Network, PackageMinus, MonitorX, MoreVertical, AlertTriangle, ShieldCheck, Plus, Eye, Loader2, History, FolderOpen } from 'lucide-react'; +import { Trash2, HardDrive, Network, PackageMinus, MonitorX, MoreVertical, AlertTriangle, ShieldCheck, Plus, Eye, Loader2, History, FolderOpen, Search } from 'lucide-react'; +import { Input } from '@/components/ui/input'; import { SeverityBadge } from '@/components/ui/SeverityBadge'; import { useTrivyStatus } from '@/hooks/useTrivyStatus'; import { VulnerabilityScanSheet } from './VulnerabilityScanSheet'; @@ -144,29 +145,19 @@ function FilterToggle({ value, onChange, counts }: FilterToggleProps) { ]; return ( -
-
- {options.map(({ key, label, count }) => ( - - ))} -
+
+ {options.map(({ key, label, count }) => ( + + ))}
); } @@ -352,6 +343,11 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} const [volumeFilter, setVolumeFilter] = useState('all'); const [networkFilter, setNetworkFilter] = useState('all'); + // Search state + const [imageSearch, setImageSearch] = useState(''); + const [volumeSearch, setVolumeSearch] = useState(''); + const [networkSearch, setNetworkSearch] = useState(''); + // Modal states const [confirmPrune, setConfirmPrune] = useState<{ target: PruneTarget; scope: PruneScope } | null>(null); const [confirmDelete, setConfirmDelete] = useState<{ type: 'images' | 'volumes' | 'networks'; id: string; name?: string } | null>(null); @@ -629,16 +625,19 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} // Derived filtered lists const filteredImages = images.filter(img => - imageFilter === 'managed' ? img.managedStatus === 'managed' : - imageFilter === 'unmanaged' ? img.managedStatus !== 'managed' : true + (imageFilter === 'managed' ? img.managedStatus === 'managed' : + imageFilter === 'unmanaged' ? img.managedStatus !== 'managed' : true) && + (imageSearch === '' || (img.RepoTags?.[0] || '').toLowerCase().includes(imageSearch.toLowerCase())) ); const filteredVolumes = volumes.filter(vol => - volumeFilter === 'managed' ? vol.managedStatus === 'managed' : - volumeFilter === 'unmanaged' ? vol.managedStatus !== 'managed' : true + (volumeFilter === 'managed' ? vol.managedStatus === 'managed' : + volumeFilter === 'unmanaged' ? vol.managedStatus !== 'managed' : true) && + (volumeSearch === '' || vol.Name.toLowerCase().includes(volumeSearch.toLowerCase())) ); const filteredNetworks = networks.filter(net => - networkFilter === 'managed' ? net.managedStatus === 'managed' : - networkFilter === 'unmanaged' ? net.managedStatus !== 'managed' : true + (networkFilter === 'managed' ? net.managedStatus === 'managed' : + networkFilter === 'unmanaged' ? net.managedStatus !== 'managed' : true) && + (networkSearch === '' || net.Name.toLowerCase().includes(networkSearch.toLowerCase())) ); // Sortable tables (standard sort behavior across the resource tables). @@ -817,7 +816,7 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} setResourceTab(v as typeof resourceTab)} - className="flex-1 flex flex-col w-full rounded-lg border bg-card shadow-card-bevel overflow-hidden min-h-[400px] animate-in fade-in-0 slide-in-from-bottom-2 duration-300 delay-150" + className="flex-1 flex flex-col w-full min-h-[400px]" > {isMobile ? ( ) : ( -
- +
+ {(['images', 'volumes', 'networks'] as const).map(tab => ( - - {tab} + + {tab.charAt(0).toUpperCase() + tab.slice(1)} + + {tab === 'images' ? images.length : tab === 'volumes' ? volumes.length : networks.length} + ))} Unmanaged + {totalOrphansCount} {totalOrphansCount > 0 && ( {totalOrphansCount} @@ -857,11 +860,20 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
)} - + {/* Images */} -
+
+
+ + setImageSearch(e.target.value)} + className="pl-9 h-9" + /> +
i.managedStatus !== 'managed').length, }} /> +
{trivy.available && ( - - - - - - View completed vulnerability scans and compare them - - + )}
+
@@ -1015,19 +1022,32 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} )}
+
{/* Volumes */} - v.managedStatus === 'managed').length, - unmanaged: volumes.filter(v => v.managedStatus !== 'managed').length, - }} - /> +
+
+ + setVolumeSearch(e.target.value)} + className="pl-9 h-9" + /> +
+ v.managedStatus === 'managed').length, + unmanaged: volumes.filter(v => v.managedStatus !== 'managed').length, + }} + /> +
+
@@ -1104,27 +1124,38 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {} )}
+
{/* Networks */} -
+
{networkViewMode === 'list' ? ( - n.managedStatus === 'managed').length, - unmanaged: networks.filter(n => n.managedStatus !== 'managed').length, - }} - /> + <> +
+ + setNetworkSearch(e.target.value)} + className="pl-9 h-9" + /> +
+ n.managedStatus === 'managed').length, + unmanaged: networks.filter(n => n.managedStatus !== 'managed').length, + }} + /> + ) : ( - // Keep a left spacer so justify-between holds the toggle + - // Create Network group anchored on the right in topology mode.