From 33e525a1a2cde4cd8ef6df242284b9febffb480d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 5 Feb 2026 14:08:19 +0000 Subject: [PATCH] Add infrastructure source/status filters --- frontend-modern/src/pages/Infrastructure.tsx | 249 ++++++++++++++++++- 1 file changed, 247 insertions(+), 2 deletions(-) diff --git a/frontend-modern/src/pages/Infrastructure.tsx b/frontend-modern/src/pages/Infrastructure.tsx index 35162df1c..b230dd001 100644 --- a/frontend-modern/src/pages/Infrastructure.tsx +++ b/frontend-modern/src/pages/Infrastructure.tsx @@ -1,4 +1,4 @@ -import { Show, createMemo } from 'solid-js'; +import { For, Show, createMemo, createSignal } from 'solid-js'; import { SectionHeader } from '@/components/shared/SectionHeader'; import { EmptyState } from '@/components/shared/EmptyState'; import { Card } from '@/components/shared/Card'; @@ -6,10 +6,155 @@ import { useUnifiedResources } from '@/hooks/useUnifiedResources'; import { UnifiedResourceTable } from '@/components/Infrastructure/UnifiedResourceTable'; import ServerIcon from 'lucide-solid/icons/server'; import RefreshCwIcon from 'lucide-solid/icons/refresh-cw'; +import type { Resource } from '@/types/resource'; export function Infrastructure() { const { resources, loading, error, refetch } = useUnifiedResources(); const hasResources = createMemo(() => resources().length > 0); + const [selectedSources, setSelectedSources] = createSignal>(new Set()); + const [selectedStatuses, setSelectedStatuses] = createSignal>(new Set()); + + const sourceOptions = [ + { key: 'proxmox', label: 'PVE' }, + { key: 'agent', label: 'Agent' }, + { key: 'docker', label: 'Docker' }, + { key: 'pbs', label: 'PBS' }, + { key: 'pmg', label: 'PMG' }, + { key: 'kubernetes', label: 'K8s' }, + ]; + + const statusLabels: Record = { + online: 'Online', + offline: 'Offline', + degraded: 'Degraded', + paused: 'Paused', + unknown: 'Unknown', + running: 'Running', + stopped: 'Stopped', + }; + + const statusOrder = ['online', 'degraded', 'paused', 'offline', 'stopped', 'unknown', 'running']; + + const normalizeSource = (value: string): string | null => { + const normalized = value.toLowerCase(); + switch (normalized) { + case 'pve': + case 'proxmox': + case 'proxmox-pve': + return 'proxmox'; + case 'agent': + case 'host-agent': + return 'agent'; + case 'docker': + return 'docker'; + case 'pbs': + case 'proxmox-pbs': + return 'pbs'; + case 'pmg': + case 'proxmox-pmg': + return 'pmg'; + case 'k8s': + case 'kubernetes': + return 'kubernetes'; + default: + return null; + } + }; + + const getResourceSources = (resource: Resource): string[] => { + const platformData = resource.platformData as { sources?: string[] } | undefined; + const normalized = (platformData?.sources ?? []) + .map((source) => normalizeSource(source)) + .filter((source): source is string => Boolean(source)); + return Array.from(new Set(normalized)); + }; + + const availableSources = createMemo(() => { + const set = new Set(); + resources().forEach((resource) => { + getResourceSources(resource).forEach((source) => set.add(source)); + }); + return set; + }); + + const availableStatuses = createMemo(() => { + const set = new Set(); + resources().forEach((resource) => { + const status = (resource.status || 'unknown').toLowerCase(); + if (status) set.add(status); + }); + return set; + }); + + const statusOptions = createMemo(() => { + const statuses = Array.from(availableStatuses()); + statuses.sort((a, b) => { + const indexA = statusOrder.indexOf(a); + const indexB = statusOrder.indexOf(b); + if (indexA === -1 && indexB === -1) return a.localeCompare(b); + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + return statuses.map((status) => ({ + key: status, + label: statusLabels[status] ?? status, + })); + }); + + const hasActiveFilters = createMemo( + () => selectedSources().size > 0 || selectedStatuses().size > 0, + ); + + const toggleSource = (source: string) => { + const next = new Set(selectedSources()); + if (next.has(source)) { + next.delete(source); + } else { + next.add(source); + } + setSelectedSources(next); + }; + + const toggleStatus = (status: string) => { + const next = new Set(selectedStatuses()); + if (next.has(status)) { + next.delete(status); + } else { + next.add(status); + } + setSelectedStatuses(next); + }; + + const clearFilters = () => { + setSelectedSources(new Set()); + setSelectedStatuses(new Set()); + }; + + const filteredResources = createMemo(() => { + let filtered = resources(); + const sources = selectedSources(); + const statuses = selectedStatuses(); + + if (sources.size > 0) { + filtered = filtered.filter((resource) => { + const resourceSources = getResourceSources(resource); + if (resourceSources.length === 0) return false; + return resourceSources.some((source) => sources.has(source)); + }); + } + + if (statuses.size > 0) { + filtered = filtered.filter((resource) => { + const status = (resource.status || 'unknown').toLowerCase(); + return statuses.has(status); + }); + } + + return filtered; + }); + + const hasFilteredResources = createMemo(() => filteredResources().length > 0); return (
@@ -58,7 +203,107 @@ export function Infrastructure() { } > - +
+
+
+ Source +
+ + {(source) => { + const isSelected = () => selectedSources().has(source.key); + const isDisabled = () => + !availableSources().has(source.key) && !selectedSources().has(source.key); + return ( + + ); + }} + +
+
+ +
+ +
+ Status +
+ + {(status) => { + const isSelected = () => selectedStatuses().has(status.key); + const isDisabled = () => + !availableStatuses().has(status.key) && !selectedStatuses().has(status.key); + return ( + + ); + }} + +
+
+ + + + +
+ + + } + title="No resources match filters" + description="Try adjusting the source or status filters." + actions={ + + + + } + /> + + } + > + + +