refactor(proxmox): extract PBS artifacts table into ProxmoxPbsTable

Fifth decomposition step. Moves the "Source details > PBS artifacts" view out
of ProxmoxBackupsTable into a presentational ProxmoxPbsTable component that
owns its error / loading / empty / table states and takes the filtered
backups, sort accessors/handler, size scale, and resource-derived
error/loading/refresh as props. The parent keeps the filtered+sorted memo and
the shared search/status filters.

Bakes in the table-fixed + colgroup layout fix so the Files column no longer
balloons on wide viewports. Behavior-preserving; verified live at 1800px
(balanced columns, sorting and verification/protection badges intact, zero
console errors). The orchestrator drops from 2571 to 2366 lines.
This commit is contained in:
rcourtman
2026-05-29 10:03:01 +01:00
parent 6537912891
commit 25d4eff2be
2 changed files with 278 additions and 220 deletions
@@ -123,6 +123,7 @@ import {
artifactStateLabel,
} from './proxmoxBackupsTableShared';
import { ProxmoxBackupsCoverageStrip } from './ProxmoxBackupsCoverageStrip';
import { ProxmoxPbsTable } from './ProxmoxPbsTable';
import { ProxmoxRecoverableTable } from './ProxmoxRecoverableTable';
import { ProxmoxTasksTable } from './ProxmoxTasksTable';
@@ -1854,226 +1855,20 @@ export const ProxmoxBackupsTable: Component<{
</Show>
<Show when={tab() === 'sources' && sourceDetailTab() === 'pbs'}>
<Show
when={!pbsBackups.error}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title="Could not load PBS artifacts"
description={
(pbsBackups.error as Error | undefined)?.message ?? 'Refresh to retry.'
}
actions={
<button
type="button"
onClick={() => void refetchPBS()}
class="inline-flex min-h-10 items-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-surface-hover"
>
Refresh
</button>
}
/>
</Card>
}
>
<Show
when={pbsBackups() !== undefined}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title="Loading PBS artifacts"
description="Reading deduplicated backup snapshots from Proxmox Backup Server."
/>
</Card>
}
>
<Show
when={filteredPBSBackups().length > 0}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title={
pbsArtifacts().length === 0
? sourceDetailSpecFor('pbs').emptyTitle
: 'No PBS artifacts match current filters'
}
description={
pbsArtifacts().length === 0
? sourceDetailSpecFor('pbs').emptyDescription
: 'Adjust the search or status filter to see more PBS artifacts.'
}
/>
</Card>
}
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<Table class="min-w-[1050px] text-xs">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<SortableHead
label="Workload"
sortKey="workload"
currentSort={pbsSortKey}
direction={pbsSortDirection}
onSort={handlePBSSort}
align="left"
headClass={getPlatformTableHeadClassForKind('name')}
/>
<SortableHead
label="Repository"
sortKey="repository"
currentSort={pbsSortKey}
direction={pbsSortDirection}
onSort={handlePBSSort}
align="left"
headClass={getPlatformTableHeadClassForKind('text')}
/>
<SortableHead
label="Created"
sortKey="created"
currentSort={pbsSortKey}
direction={pbsSortDirection}
onSort={handlePBSSort}
align="right"
headClass={getPlatformTableHeadClassForKind('numeric-value')}
/>
<SortableHead
label="Size"
sortKey="size"
currentSort={pbsSortKey}
direction={pbsSortDirection}
onSort={handlePBSSort}
align="center"
headClass={getPlatformTableHeadClassForKind('metric-bar')}
/>
<SortableHead
label="Verified"
sortKey="verified"
currentSort={pbsSortKey}
direction={pbsSortDirection}
onSort={handlePBSSort}
align="left"
headClass={getPlatformTableHeadClassForKind('text')}
/>
<SortableHead
label="Protection"
sortKey="protected"
currentSort={pbsSortKey}
direction={pbsSortDirection}
onSort={handlePBSSort}
align="left"
headClass={getPlatformTableHeadClassForKind('text')}
/>
<TableHead class={getPlatformTableHeadClassForKind('text')}>
Files
</TableHead>
</TableRow>
</TableHeader>
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
<For each={filteredPBSBackups()}>
{(backup) => (
<TableRow class="hover:bg-surface-hover">
<TableCell
class={`${getPlatformTableCellClassForKind('name')} text-base-content`}
>
<div class="min-w-0">
<div class="font-semibold">{pbsWorkloadLabel(backup)}</div>
<div class="font-mono text-[10px] uppercase text-muted">
{backup.backupType || 'backup'}
</div>
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<div class="min-w-0">
<div class="font-mono text-[11px]">
{pbsRepositoryLabel(backup)}
</div>
<div
class="truncate text-[10px] text-muted"
title={backup.instance}
>
{backup.instance || '—'}
</div>
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
<Show
when={pbsTimestampMs(backup) !== undefined}
fallback={<span class="text-muted"></span>}
>
{formatRelativeTime(backup.backupTime, { compact: true })}
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('metric-bar')} text-base-content`}
>
<RowMetricBar
valuePct={
pbsSizeMaxBytes() > 0
? (backup.size / pbsSizeMaxBytes()) * 100
: 0
}
fillClass="bg-blue-500/40 dark:bg-blue-500/40"
label={formatBytes(backup.size)}
tooltip={`${formatBytes(backup.size)} (relative to largest PBS artifact in view)`}
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<Show
when={backup.verified}
fallback={
<span class="text-amber-600 dark:text-amber-300">
Unverified
</span>
}
>
<span class="inline-flex items-center rounded-sm bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200">
Verified
</span>
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<Show
when={backup.protected}
fallback={<span class="text-muted">Unprotected</span>}
>
<span class="inline-flex items-center rounded-sm bg-blue-100 px-1.5 py-0.5 text-[10px] font-semibold text-blue-700 dark:bg-blue-900/40 dark:text-blue-200">
Protected
</span>
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<span
class="inline-block max-w-[16rem] truncate"
title={(backup.files ?? []).join(', ')}
>
{(backup.files ?? []).length > 0
? `${backup.files.length} files`
: '—'}
</span>
</TableCell>
</TableRow>
)}
</For>
</TableBody>
</Table>
</TableCard>
</Show>
</Show>
</Show>
<ProxmoxPbsTable
backups={filteredPBSBackups()}
hasAnyArtifacts={pbsArtifacts().length > 0}
errorMessage={(pbsBackups.error as Error | undefined)?.message}
isLoading={pbsBackups() === undefined}
onRefresh={() => void refetchPBS()}
emptyIcon={props.emptyIcon}
emptyTitle={sourceDetailSpecFor('pbs').emptyTitle}
emptyDescription={sourceDetailSpecFor('pbs').emptyDescription}
sortKey={pbsSortKey}
sortDirection={pbsSortDirection}
onSort={handlePBSSort}
sizeMaxBytes={pbsSizeMaxBytes()}
/>
</Show>
<Show when={tab() === 'sources' && sourceDetailTab() === 'snapshots'}>
@@ -0,0 +1,263 @@
import { For, Show, type Accessor, type JSX } from 'solid-js';
import { Card } from '@/components/shared/Card';
import { EmptyState } from '@/components/shared/EmptyState';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/shared/Table';
import { TableCard } from '@/components/shared/TableCard';
import { formatBytes, formatRelativeTime } from '@/utils/format';
import {
PLATFORM_TABLE_BODY_CLASS,
PLATFORM_TABLE_CARD_CLASS,
PLATFORM_TABLE_HEADER_ROW_CLASS,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import type { PBSBackup } from '@/types/api';
import { pbsRepositoryLabel, pbsWorkloadLabel, type PBSSortKey } from './proxmoxBackupsTableModel';
import { RowMetricBar, SortableHead } from './proxmoxBackupsTableShared';
const hasValidBackupTime = (backup: PBSBackup): boolean =>
!!backup.backupTime && Number.isFinite(Date.parse(backup.backupTime));
// "Source details > PBS artifacts" table: every Proxmox Backup Server snapshot
// with repository, age, size, verification, and protection. Presentational —
// the parent owns the filtered + sorted memo, shared filters, and the PBS
// resource; this component just renders its error / loading / empty / table
// states. table-fixed + colgroup keeps columns from ballooning on wide views.
export function ProxmoxPbsTable(props: {
backups: PBSBackup[];
hasAnyArtifacts: boolean;
errorMessage?: string;
isLoading: boolean;
onRefresh: () => void;
emptyIcon: JSX.Element;
emptyTitle: string;
emptyDescription: string;
sortKey: Accessor<PBSSortKey>;
sortDirection: Accessor<'asc' | 'desc'>;
onSort: (key: PBSSortKey) => void;
sizeMaxBytes: number;
}) {
return (
<Show
when={!props.errorMessage}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title="Could not load PBS artifacts"
description={props.errorMessage ?? 'Refresh to retry.'}
actions={
<button
type="button"
onClick={() => props.onRefresh()}
class="inline-flex min-h-10 items-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-surface-hover"
>
Refresh
</button>
}
/>
</Card>
}
>
<Show
when={!props.isLoading}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title="Loading PBS artifacts"
description="Reading deduplicated backup snapshots from Proxmox Backup Server."
/>
</Card>
}
>
<Show
when={props.backups.length > 0}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title={
!props.hasAnyArtifacts ? props.emptyTitle : 'No PBS artifacts match current filters'
}
description={
!props.hasAnyArtifacts
? props.emptyDescription
: 'Adjust the search or status filter to see more PBS artifacts.'
}
/>
</Card>
}
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<Table class="min-w-[1050px] table-fixed text-xs">
<colgroup>
<col style={{ width: '16%' }} />
<col style={{ width: '20%' }} />
<col style={{ width: '12%' }} />
<col style={{ width: '16%' }} />
<col style={{ width: '11%' }} />
<col style={{ width: '12%' }} />
<col style={{ width: '13%' }} />
</colgroup>
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<SortableHead
label="Workload"
sortKey="workload"
currentSort={props.sortKey}
direction={props.sortDirection}
onSort={props.onSort}
align="left"
headClass={getPlatformTableHeadClassForKind('name')}
/>
<SortableHead
label="Repository"
sortKey="repository"
currentSort={props.sortKey}
direction={props.sortDirection}
onSort={props.onSort}
align="left"
headClass={getPlatformTableHeadClassForKind('text')}
/>
<SortableHead
label="Created"
sortKey="created"
currentSort={props.sortKey}
direction={props.sortDirection}
onSort={props.onSort}
align="right"
headClass={getPlatformTableHeadClassForKind('numeric-value')}
/>
<SortableHead
label="Size"
sortKey="size"
currentSort={props.sortKey}
direction={props.sortDirection}
onSort={props.onSort}
align="center"
headClass={getPlatformTableHeadClassForKind('metric-bar')}
/>
<SortableHead
label="Verified"
sortKey="verified"
currentSort={props.sortKey}
direction={props.sortDirection}
onSort={props.onSort}
align="left"
headClass={getPlatformTableHeadClassForKind('text')}
/>
<SortableHead
label="Protection"
sortKey="protected"
currentSort={props.sortKey}
direction={props.sortDirection}
onSort={props.onSort}
align="left"
headClass={getPlatformTableHeadClassForKind('text')}
/>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Files</TableHead>
</TableRow>
</TableHeader>
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
<For each={props.backups}>
{(backup) => (
<TableRow class="hover:bg-surface-hover">
<TableCell
class={`${getPlatformTableCellClassForKind('name')} text-base-content`}
>
<div class="min-w-0">
<div class="font-semibold">{pbsWorkloadLabel(backup)}</div>
<div class="font-mono text-[10px] uppercase text-muted">
{backup.backupType || 'backup'}
</div>
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<div class="min-w-0">
<div class="font-mono text-[11px]">{pbsRepositoryLabel(backup)}</div>
<div class="truncate text-[10px] text-muted" title={backup.instance}>
{backup.instance || '—'}
</div>
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
<Show
when={hasValidBackupTime(backup)}
fallback={<span class="text-muted"></span>}
>
{formatRelativeTime(backup.backupTime, { compact: true })}
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('metric-bar')} text-base-content`}
>
<RowMetricBar
valuePct={
props.sizeMaxBytes > 0 ? (backup.size / props.sizeMaxBytes) * 100 : 0
}
fillClass="bg-blue-500/40 dark:bg-blue-500/40"
label={formatBytes(backup.size)}
tooltip={`${formatBytes(backup.size)} (relative to largest PBS artifact in view)`}
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<Show
when={backup.verified}
fallback={
<span class="text-amber-600 dark:text-amber-300">Unverified</span>
}
>
<span class="inline-flex items-center rounded-sm bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200">
Verified
</span>
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<Show
when={backup.protected}
fallback={<span class="text-muted">Unprotected</span>}
>
<span class="inline-flex items-center rounded-sm bg-blue-100 px-1.5 py-0.5 text-[10px] font-semibold text-blue-700 dark:bg-blue-900/40 dark:text-blue-200">
Protected
</span>
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<span
class="inline-block max-w-[16rem] truncate"
title={(backup.files ?? []).join(', ')}
>
{(backup.files ?? []).length > 0 ? `${backup.files.length} files` : '—'}
</span>
</TableCell>
</TableRow>
)}
</For>
</TableBody>
</Table>
</TableCard>
</Show>
</Show>
</Show>
);
}