');
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) => {
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
+
+
+
+
-
+
{(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}
{targetNode}
-
+
{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<{
-
+