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.
)}
-
+
+
) : (
+
@@ -1256,11 +1288,13 @@ export default function ResourcesView({ headerActions }: ResourcesViewProps = {}
)}
+
)}
{/* Unmanaged Containers */}
+