diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index b0abc2f545..21258925d5 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -27,15 +27,20 @@ interface SettingsProps { setThemePreference: (pref: 'light' | 'dark' | 'system') => void; } +const SettingsPanelLoadingFallback = () => ( +
+ {getSettingsLoadingState().text} +
+); + const Settings: Component = (props) => { const { state, connected: _connected } = useWebSocket(); const navigate = useNavigate(); const location = useLocation(); - const { activeTab, selectedAgent, setActiveTab, handleSelectAgent } = - useSettingsNavigation({ - navigate, - location, - }); + const { activeTab, selectedAgent, setActiveTab, handleSelectAgent } = useSettingsNavigation({ + navigate, + location, + }); const { headerMeta, sidebarCollapsed, @@ -166,22 +171,10 @@ const Settings: Component = (props) => { > - {getSettingsLoadingState().text} - - ) : null - } + fallback={securityStatusLoading() ? : null} > {(entry) => ( - - {getSettingsLoadingState().text} - - } - > + }> )} diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 656a8ce22c..8910f3f388 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -83,6 +83,8 @@ describe('settings architecture guardrails', () => { expect(settingsSource).toContain('useSettingsNavigation({'); expect(settingsSource).toContain(' { it('keeps platform inventory tables on the shared dense table styling contract', () => { expect(sharedPlatformPageSource).toContain('PLATFORM_TABLE_CARD_CLASS'); @@ -74,6 +78,22 @@ describe('platform overview layout guardrails', () => { } }); + it('keeps Proxmox detail tables on the shared platform table primitives', () => { + for (const source of proxmoxDetailTableSources) { + expect(source).toContain('TableCard'); + expect(source).toContain('PlatformTableToolbar'); + expect(source).toContain('PLATFORM_TABLE_CARD_CLASS'); + expect(source).toContain('PLATFORM_TABLE_HEADER_ROW_CLASS'); + expect(source).toContain('PLATFORM_TABLE_BODY_CLASS'); + expect(source).toContain('getPlatformTableHeadClassForKind'); + expect(source).toContain('getPlatformTableCellClassForKind'); + expect(source).not.toContain("from '@/components/shared/SearchInput'"); + expect(source).not.toContain("from '@/components/shared/FilterButtonGroup'"); + expect(source).not.toContain('border-collapse text-xs'); + expect(source).not.toContain('bg-surface-alt text-muted border-b border-border'); + } + }); + it('keeps provider overview pages in the parent-table plus child-inventory stack', () => { for (const source of overviewSurfaceSources) { expect(source).toContain('
'); diff --git a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx index f76d67ba1c..a91f62937d 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx @@ -1,9 +1,8 @@ -import { For, Show, createMemo, createSignal, type Component, type JSX } from 'solid-js'; +import { For, Show, createSignal, type Component, type JSX } from 'solid-js'; import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; -import { FilterButtonGroup, type FilterOption } from '@/components/shared/FilterButtonGroup'; -import { SearchInput } from '@/components/shared/SearchInput'; import { StatusDot } from '@/components/shared/StatusDot'; +import { TableCard } from '@/components/shared/TableCard'; import { Table, TableBody, @@ -15,7 +14,15 @@ import { import { getSimpleStatusIndicator } from '@/utils/status'; import { asTrimmedString } from '@/utils/stringUtils'; import { + PLATFORM_TABLE_BODY_CLASS, + PLATFORM_TABLE_CARD_CLASS, + PLATFORM_TABLE_HEADER_ROW_CLASS, + PLATFORM_HEALTH_FILTER_OPTIONS, + PlatformTableToolbar, + createPlatformTableFilterState, filterPlatformResources, + getPlatformTableCellClassForKind, + getPlatformTableHeadClassForKind, type PlatformResourceStatusFilter, } from '@/features/platformPage/sharedPlatformPage'; import type { Resource } from '@/types/resource'; @@ -28,13 +35,6 @@ import { ProxmoxMailGatewayDrawer } from './ProxmoxMailGatewayDrawer'; // operator columns. This bespoke table reuses canonical shared // primitives and surfaces those PMG-native columns. -const STATUS_FILTER_OPTIONS: FilterOption[] = [ - { value: 'all', label: 'All' }, - { value: 'online', label: 'Healthy' }, - { value: 'degraded', label: 'Degraded' }, - { value: 'offline', label: 'Offline' }, -]; - const formatUptime = (seconds: number | undefined): string => { if (!seconds || seconds <= 0) return '—'; const days = Math.floor(seconds / 86_400); @@ -54,15 +54,13 @@ export const ProxmoxMailGatewayTable: Component<{ emptyTitle: string; emptyDescription: string; }> = (props) => { - const [search, setSearch] = createSignal(''); - const [status, setStatus] = createSignal('all'); + const tableState = createPlatformTableFilterState({ + resources: () => props.resources, + initialStatus: 'all' as PlatformResourceStatusFilter, + filter: filterPlatformResources, + }); const [selectedId, setSelectedId] = createSignal(null); - const toggleSelected = (id: string) => - setSelectedId((current) => (current === id ? null : id)); - - const filtered = createMemo(() => filterPlatformResources(props.resources, search(), status())); - const visible = createMemo(() => filtered().length); - const total = createMemo(() => props.resources.length); + const toggleSelected = (id: string) => setSelectedId((current) => (current === id ? null : id)); return (
-
-
- -
- - - {total()} instances}> - {visible()} of {total()} instances - - -
+ 0} + when={tableState.filtered().length > 0} fallback={ } > - - - - - Instance - Version - Nodes - Uptime - Mail in - Spam - Virus - Quarantine - Queue - Deferred + +
+ + + Instance + Version + + Nodes + + + Uptime + + + Mail in + + + Spam + + + Virus + + + Quarantine + + + Queue + + + Deferred + - - + + {(instance) => { const pmg = () => instance.pmg; const name = () => asTrimmedString(instance.name) || instance.id; @@ -138,7 +144,7 @@ export const ProxmoxMailGatewayTable: Component<{ onClick={() => toggleSelected(instance.id)} aria-expanded={isOpen()} > - +
- + {version()} - + {countCell(pmg()?.nodeCount)} - + {formatUptime(instance.uptime ?? pmg()?.uptimeSeconds)} - + {countCell(pmg()?.mailCountTotal)} - + {countCell(pmg()?.spamIn)} - + {countCell(pmg()?.virusIn)} - + {countCell(pmg()?.quarantine)} - + {countCell(pmg()?.queueTotal ?? pmg()?.queueActive)} - + {countCell(pmg()?.queueDeferred)} @@ -185,10 +209,7 @@ export const ProxmoxMailGatewayTable: Component<{ colspan={10} class="p-0 border-b border-border bg-surface-alt" > -
event.stopPropagation()} - > +
event.stopPropagation()}> setSelectedId(null)} @@ -203,7 +224,7 @@ export const ProxmoxMailGatewayTable: Component<{
-
+
diff --git a/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx index dece208db2..153be3c42f 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx @@ -10,9 +10,8 @@ import { import ArrowRightIcon from 'lucide-solid/icons/arrow-right'; import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; -import { FilterButtonGroup, type FilterOption } from '@/components/shared/FilterButtonGroup'; -import { SearchInput } from '@/components/shared/SearchInput'; import { StatusDot } from '@/components/shared/StatusDot'; +import { TableCard } from '@/components/shared/TableCard'; import type { StatusIndicatorVariant } from '@/utils/status'; import { Table, @@ -24,6 +23,14 @@ import { } from '@/components/shared/Table'; import { apiFetch } from '@/utils/apiClient'; import { formatRelativeTime } from '@/utils/format'; +import { + PLATFORM_TABLE_BODY_CLASS, + PLATFORM_TABLE_CARD_CLASS, + PLATFORM_TABLE_HEADER_ROW_CLASS, + PlatformTableToolbar, + getPlatformTableCellClassForKind, + getPlatformTableHeadClassForKind, +} from '@/features/platformPage/sharedPlatformPage'; import type { ReplicationJob, ReplicationJobsResponse } from '@/types/api'; // Replication is a Proxmox-specific concept (zfs send/receive scheduled @@ -35,13 +42,13 @@ import type { ReplicationJob, ReplicationJobsResponse } from '@/types/api'; type ReplicationStatusFilter = 'all' | 'healthy' | 'failed' | 'pending' | 'disabled'; -const STATUS_FILTER_OPTIONS: FilterOption[] = [ +const STATUS_FILTER_OPTIONS = [ { value: 'all', label: 'All' }, { value: 'healthy', label: 'Healthy' }, { value: 'failed', label: 'Failed' }, { value: 'pending', label: 'Pending' }, { value: 'disabled', label: 'Disabled' }, -]; +] satisfies Array<{ value: ReplicationStatusFilter; label: string }>; interface ReplicationStatusIndicator { variant: StatusIndicatorVariant; @@ -61,7 +68,11 @@ function classifyJob(job: ReplicationJob): ReplicationStatusFilter { function indicatorFor(classification: ReplicationStatusFilter): ReplicationStatusIndicator { switch (classification) { case 'healthy': - return { variant: 'success', label: 'Healthy', tone: 'text-emerald-600 dark:text-emerald-300' }; + return { + variant: 'success', + label: 'Healthy', + tone: 'text-emerald-600 dark:text-emerald-300', + }; case 'failed': return { variant: 'danger', label: 'Failed', tone: 'text-red-600 dark:text-red-300' }; case 'pending': @@ -194,25 +205,17 @@ export const ProxmoxReplicationTable: Component<{ } >
-
-
- -
- - - {total()} jobs}> - {visible()} of {total()} jobs - - -
+ 0} @@ -226,22 +229,32 @@ export const ProxmoxReplicationTable: Component<{ } > - - - - - Status - Job - Guest - Source → Target - Schedule - Last sync - Duration - Fail count - Error + +
+ + + Status + Job + Guest + + Source → Target + + + Schedule + + + Last sync + + + Duration + + + Fail count + + Error - + {(job) => { const classification = classifyJob(job); @@ -250,7 +263,7 @@ export const ProxmoxReplicationTable: Component<{ const targetNode = (job.targetNode ?? '').trim() || '—'; return ( - +
- {ind.label} + + {ind.label} +
- + {job.jobId || job.id} - {formatGuestLabel(job)} - + + {formatGuestLabel(job)} + + {sourceNode} - + {job.schedule || '—'} - {formatSyncTime(job)} - - {formatDuration(job.lastSyncDurationSeconds, job.lastSyncDurationHuman)} + + {formatSyncTime(job)} - + + {formatDuration( + job.lastSyncDurationSeconds, + job.lastSyncDurationHuman, + )} + + 0} fallback={0} > - {job.failCount} + + {job.failCount} + - + —} @@ -306,7 +346,7 @@ export const ProxmoxReplicationTable: Component<{
-
+