mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
Clarify Proxmox backup table columns
This commit is contained in:
@@ -32,6 +32,7 @@ import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresent
|
||||
import {
|
||||
ArtifactSourceBadge,
|
||||
ArtifactStateBadge,
|
||||
PROXMOX_BACKUP_COLUMN_LABELS,
|
||||
ProxmoxBackupWorkloadTypeBadge,
|
||||
SortableHead,
|
||||
artifactStateLabel,
|
||||
@@ -149,7 +150,9 @@ export function ProxmoxCoverageTable(props: {
|
||||
headClass={getPlatformTableHeadClassForKind('name')}
|
||||
/>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>Type</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>ID</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>
|
||||
{PROXMOX_BACKUP_COLUMN_LABELS.targetId}
|
||||
</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>Node</TableHead>
|
||||
<SortableHead
|
||||
label="Posture"
|
||||
@@ -404,10 +407,14 @@ export function ProxmoxCoverageTable(props: {
|
||||
<tr class="bg-surface-alt text-muted">
|
||||
<th class="px-2 py-0.5 text-left font-medium">Source</th>
|
||||
<th class="px-2 py-0.5 text-left font-medium">Location</th>
|
||||
<th class="px-2 py-0.5 text-right font-medium">Created</th>
|
||||
<th class="px-2 py-0.5 text-right font-medium">
|
||||
{PROXMOX_BACKUP_COLUMN_LABELS.created}
|
||||
</th>
|
||||
<th class="px-2 py-0.5 text-right font-medium">Size</th>
|
||||
<th class="px-2 py-0.5 text-left font-medium">State</th>
|
||||
<th class="px-2 py-0.5 text-left font-medium">Details</th>
|
||||
<th class="px-2 py-0.5 text-left font-medium">
|
||||
{PROXMOX_BACKUP_COLUMN_LABELS.details}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border-subtle">
|
||||
|
||||
@@ -29,6 +29,7 @@ import type { RecoverableSortKey } from './proxmoxBackupsTableModel';
|
||||
import {
|
||||
ArtifactSourceBadge,
|
||||
ArtifactStateBadge,
|
||||
PROXMOX_BACKUP_COLUMN_LABELS,
|
||||
ProxmoxBackupWorkloadTypeBadge,
|
||||
RowMetricBar,
|
||||
SortableHead,
|
||||
@@ -187,7 +188,9 @@ export function ProxmoxRecoverableTable(props: {
|
||||
headClass={getPlatformTableHeadClassForKind('name')}
|
||||
/>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>Type</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>ID</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>
|
||||
{PROXMOX_BACKUP_COLUMN_LABELS.targetId}
|
||||
</TableHead>
|
||||
<SortableHead
|
||||
label="Source"
|
||||
sortKey="source"
|
||||
@@ -207,7 +210,7 @@ export function ProxmoxRecoverableTable(props: {
|
||||
headClass={getPlatformTableHeadClassForKind('text')}
|
||||
/>
|
||||
<SortableHead
|
||||
label="Created"
|
||||
label={PROXMOX_BACKUP_COLUMN_LABELS.created}
|
||||
sortKey="created"
|
||||
currentSort={props.sortKey}
|
||||
direction={props.sortDirection}
|
||||
@@ -233,7 +236,9 @@ export function ProxmoxRecoverableTable(props: {
|
||||
align="left"
|
||||
headClass={getPlatformTableHeadClassForKind('text')}
|
||||
/>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>Details</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>
|
||||
{PROXMOX_BACKUP_COLUMN_LABELS.details}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
|
||||
@@ -250,8 +255,7 @@ export function ProxmoxRecoverableTable(props: {
|
||||
colspan={COLUMN_COUNT}
|
||||
class="border-t border-border bg-surface-alt px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-base-content"
|
||||
>
|
||||
{group.label}
|
||||
{' '}
|
||||
{group.label}{' '}
|
||||
<span class="ml-2 normal-case tracking-normal text-muted">
|
||||
{group.items.length} {group.items.length === 1 ? 'backup' : 'backups'}
|
||||
</span>
|
||||
|
||||
@@ -143,8 +143,10 @@ describe('ProxmoxBackupsTable', () => {
|
||||
expect(screen.getByRole('columnheader', { name: /location/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: /source/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: /type/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: /target id/i })).toBeInTheDocument();
|
||||
expect(screen.getAllByText('LXC').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('PBS').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('2 PBS files')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /pbs snapshots/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /pve backup files/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /guest snapshots/i })).toBeInTheDocument();
|
||||
|
||||
@@ -55,12 +55,12 @@ describe('ProxmoxCoverageTable column visibility', () => {
|
||||
// Always-on columns.
|
||||
expect(headers).toContain('Workload');
|
||||
expect(headers).toContain('Type');
|
||||
expect(headers).toContain('ID');
|
||||
expect(headers).toContain('Target ID');
|
||||
expect(headers).toContain('Node');
|
||||
// Source columns gate on their flags.
|
||||
expect(headers).toContain('PBS');
|
||||
expect(headers).toContain('Snapshots');
|
||||
expect(headers).not.toContain('PVE files');
|
||||
expect(headers).toContain('Latest PBS');
|
||||
expect(headers).toContain('Latest snapshot');
|
||||
expect(headers).not.toContain('Latest PVE file');
|
||||
expect(headers).not.toContain('Latest task');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,11 +108,28 @@ describe('proxmoxBackupRecoveryModel', () => {
|
||||
'PVE file',
|
||||
'Snapshot',
|
||||
]);
|
||||
expect(model.recoverableArtifacts[0].detail).toBe('2 PBS files');
|
||||
expect(coverageRowMatchesSearch(row, 'pbs-docker')).toBe(true);
|
||||
expect(coverageRowMatchesSearch(row, 'PVE backup file')).toBe(true);
|
||||
expect(recoverableArtifactMatchesSearch(model.recoverableArtifacts[0], 'main')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not describe PBS backups with omitted file manifests as zero-file backups', () => {
|
||||
const model = buildProxmoxBackupRecoveryModel({
|
||||
workloads: [workload({})],
|
||||
pbsBackups: [pbsBackup({ files: [] })],
|
||||
archives: [],
|
||||
snapshots: [],
|
||||
tasks: [],
|
||||
nowMs: Date.parse('2026-05-26T08:00:00Z'),
|
||||
});
|
||||
|
||||
expect(model.recoverableArtifacts[0].detail).toBe('PBS files not listed');
|
||||
expect(recoverableArtifactMatchesSearch(model.recoverableArtifacts[0], 'not listed')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('surfaces a failed latest backup task as workload attention', () => {
|
||||
const model = buildProxmoxBackupRecoveryModel({
|
||||
workloads: [workload({})],
|
||||
|
||||
@@ -12,16 +12,19 @@ describe('proxmoxBackupSourcePresentation', () => {
|
||||
|
||||
expect(getProxmoxBackupSourcePresentation('pbs')).toMatchObject({
|
||||
badgeLabel: 'PBS',
|
||||
coverageColumnLabel: 'Latest PBS',
|
||||
filterLabel: 'PBS snapshots',
|
||||
timelineLabel: 'PBS snapshots',
|
||||
});
|
||||
expect(getProxmoxBackupSourcePresentation('archive')).toMatchObject({
|
||||
badgeLabel: 'PVE file',
|
||||
coverageColumnLabel: 'Latest PVE file',
|
||||
filterLabel: 'PVE backup files',
|
||||
timelineLabel: 'PVE backup files',
|
||||
});
|
||||
expect(getProxmoxBackupSourcePresentation('snapshot')).toMatchObject({
|
||||
badgeLabel: 'Snapshot',
|
||||
coverageColumnLabel: 'Latest snapshot',
|
||||
filterLabel: 'Guest snapshots',
|
||||
timelineLabel: 'Guest snapshots',
|
||||
});
|
||||
|
||||
@@ -116,6 +116,12 @@ function parseTimestampMs(value: string | undefined): number | undefined {
|
||||
return Number.isFinite(ms) ? ms : undefined;
|
||||
}
|
||||
|
||||
function pbsBackupFileDetailLabel(fileCount: number): string {
|
||||
if (fileCount <= 0) return 'PBS files not listed';
|
||||
if (fileCount === 1) return '1 PBS file';
|
||||
return `${fileCount} PBS files`;
|
||||
}
|
||||
|
||||
function normalizeKey(value: string | number | undefined | null): string {
|
||||
return String(value ?? '')
|
||||
.trim()
|
||||
@@ -463,7 +469,7 @@ export function buildProxmoxBackupRecoveryModel(
|
||||
createdMs,
|
||||
size: backup.size,
|
||||
location: `${backup.datastore || '—'} / ${backup.namespace?.trim() || '(root)'}`,
|
||||
detail: backup.files.length === 1 ? '1 file' : `${backup.files.length} files`,
|
||||
detail: pbsBackupFileDetailLabel(backup.files.length),
|
||||
protected: backup.protected,
|
||||
verified: backup.verified,
|
||||
fileCount: backup.files.length,
|
||||
|
||||
@@ -24,7 +24,7 @@ const SOURCE_PRESENTATION: Record<ProxmoxBackupSourceKind, ProxmoxBackupSourcePr
|
||||
badgeClassName: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200',
|
||||
badgeLabel: 'PBS',
|
||||
compactFilterLabel: 'PBS',
|
||||
coverageColumnLabel: 'PBS',
|
||||
coverageColumnLabel: 'Latest PBS',
|
||||
coverageFallbackLabel: 'No PBS snapshot',
|
||||
detailFallbackLabel: 'PBS snapshot',
|
||||
filterAriaLabel: 'PBS snapshots from Proxmox Backup Server',
|
||||
@@ -40,7 +40,7 @@ const SOURCE_PRESENTATION: Record<ProxmoxBackupSourceKind, ProxmoxBackupSourcePr
|
||||
badgeClassName: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200',
|
||||
badgeLabel: 'PVE file',
|
||||
compactFilterLabel: 'PVE files',
|
||||
coverageColumnLabel: 'PVE files',
|
||||
coverageColumnLabel: 'Latest PVE file',
|
||||
coverageFallbackLabel: 'No PVE backup file',
|
||||
detailFallbackLabel: 'PVE backup file',
|
||||
filterAriaLabel: 'PVE backup files found on Proxmox VE storage',
|
||||
@@ -56,7 +56,7 @@ const SOURCE_PRESENTATION: Record<ProxmoxBackupSourceKind, ProxmoxBackupSourcePr
|
||||
badgeClassName: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200',
|
||||
badgeLabel: 'Snapshot',
|
||||
compactFilterLabel: 'Snapshots',
|
||||
coverageColumnLabel: 'Snapshots',
|
||||
coverageColumnLabel: 'Latest snapshot',
|
||||
coverageFallbackLabel: 'No guest snapshot',
|
||||
detailFallbackLabel: 'Guest snapshot',
|
||||
filterAriaLabel: 'Guest snapshots from Proxmox VE',
|
||||
|
||||
@@ -26,18 +26,25 @@ import type {
|
||||
|
||||
const statusDot = (className: string) => <span class={`h-2 w-2 rounded-full ${className}`} />;
|
||||
|
||||
export const ARCHIVE_STATUS_FILTERS: FilterOption<'all' | 'protected' | 'verified' | 'unverified'>[] =
|
||||
[
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'protected', label: 'Protected', tone: 'info', leading: statusDot('bg-blue-500') },
|
||||
{ value: 'verified', label: 'Verified', tone: 'success', leading: statusDot('bg-emerald-500') },
|
||||
{
|
||||
value: 'unverified',
|
||||
label: 'Unverified',
|
||||
tone: 'warning',
|
||||
leading: statusDot('bg-amber-500'),
|
||||
},
|
||||
];
|
||||
export const PROXMOX_BACKUP_COLUMN_LABELS = {
|
||||
targetId: 'Target ID',
|
||||
created: 'Created',
|
||||
details: 'Details',
|
||||
} as const;
|
||||
|
||||
export const ARCHIVE_STATUS_FILTERS: FilterOption<
|
||||
'all' | 'protected' | 'verified' | 'unverified'
|
||||
>[] = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'protected', label: 'Protected', tone: 'info', leading: statusDot('bg-blue-500') },
|
||||
{ value: 'verified', label: 'Verified', tone: 'success', leading: statusDot('bg-emerald-500') },
|
||||
{
|
||||
value: 'unverified',
|
||||
label: 'Unverified',
|
||||
tone: 'warning',
|
||||
leading: statusDot('bg-amber-500'),
|
||||
},
|
||||
];
|
||||
|
||||
export const PBS_STATUS_FILTERS: FilterOption<'all' | 'protected' | 'verified' | 'unverified'>[] = [
|
||||
{ value: 'all', label: 'All' },
|
||||
|
||||
Reference in New Issue
Block a user