Clarify Proxmox backup source labels

This commit is contained in:
rcourtman
2026-06-01 11:35:57 +01:00
parent 74def6b9ad
commit b4e1957c47
10 changed files with 243 additions and 56 deletions
@@ -52,6 +52,7 @@ import {
RECOVERABLE_FILTERS,
artifactStateLabel,
} from './proxmoxBackupsTableShared';
import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresentation';
import { ProxmoxBackupsCoverageStrip } from './ProxmoxBackupsCoverageStrip';
import { ProxmoxBackupServersTable } from './ProxmoxBackupServersTable';
import { ProxmoxCoverageTable } from './ProxmoxCoverageTable';
@@ -200,7 +201,7 @@ export const ProxmoxBackupsTable: Component<{
);
// Node options for the scope filter: the distinct nodes across the workload
// set, "All nodes" first. PBS artifacts carry no node, so this scopes the
// set, "All nodes" first. PBS snapshots carry no node, so this scopes the
// node-bearing rows only.
const nodeOptions = createMemo<FilterSelectOption[]>(() => {
const nodes = new Set<string>();
@@ -297,7 +298,7 @@ export const ProxmoxBackupsTable: Component<{
};
});
// Recoverable artifact feed: PBS, PVE archive, and guest snapshot rows.
// Recoverable artifact feed: PBS snapshots, PVE backup files, and guest snapshot rows.
const RECOVERABLE_SEGMENT_KINDS: readonly BackupActivitySegmentKind[] = [
'pbs',
'archive',
@@ -462,7 +463,7 @@ export const ProxmoxBackupsTable: Component<{
<EmptyState
icon={props.emptyIcon}
title="Loading Proxmox backup inventory"
description="Reading PBS artifacts, PVE snapshots, archives and recent backup tasks."
description="Reading PBS snapshots, PVE backup files, guest snapshots, and recent backup tasks."
/>
</Card>
}
@@ -499,11 +500,12 @@ export const ProxmoxBackupsTable: Component<{
title="Backup health"
tail={
<span>
{liveTotalCount()} targets · {recoveryModel().coverageSummary.recoverableArtifacts}{' '}
restore points
{liveTotalCount()} targets ·{' '}
{recoveryModel().coverageSummary.recoverableArtifacts} restore points
<Show when={recoveryModel().coverageSummary.withPBS > 0}>
{' · '}
{recoveryModel().coverageSummary.withPBS} with PBS
{recoveryModel().coverageSummary.withPBS} with{' '}
{getProxmoxBackupSourcePresentation('pbs').filterLabel}
</Show>
<Show when={orphanedTotalCount() > 0}>
{' · '}
@@ -566,7 +568,7 @@ export const ProxmoxBackupsTable: Component<{
title={
recoverableMetricMode() === 'volume' ? 'Backup volume per day' : 'Backups per day'
}
noun="artifact"
noun="backup"
segmentKinds={RECOVERABLE_SEGMENT_KINDS}
range={chartRange}
onRangeChange={setChartRange}
@@ -28,6 +28,7 @@ import {
type WorkloadCoverageRow,
} from './proxmoxBackupRecoveryModel';
import type { CoverageSortKey } from './proxmoxBackupsTableModel';
import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresentation';
import {
ArtifactSourceBadge,
ArtifactStateBadge,
@@ -62,10 +63,11 @@ const taskWordVariant = (label: string): StatusIndicatorVariant => {
};
// "Workload coverage" table: one row per workload answering "does this have a
// backup?" across PBS / archive / snapshot, each expanding to its restore
// evidence. Presentational — the parent owns the filtered+sorted memo, shared
// filters, and the expansion set. table-fixed + a colgroup keeps the columns
// from ballooning; the inner evidence table stays content-sized.
// backup?" across PBS snapshots / PVE backup files / guest snapshots, each
// expanding to its restore evidence. Presentational — the parent owns the
// filtered+sorted memo, shared filters, and the expansion set. table-fixed + a
// colgroup keeps the columns from ballooning; the inner evidence table stays
// content-sized.
export function ProxmoxCoverageTable(props: {
rows: WorkloadCoverageRow[];
hasAnyRows: boolean;
@@ -78,7 +80,7 @@ export function ProxmoxCoverageTable(props: {
expandedKeys: ReadonlySet<string>;
onToggleExpand: (key: string) => void;
// Source columns auto-hide when no workload anywhere has that data (e.g. a
// PBS-only fleet drops the Archive and Snapshot columns), matching how the
// PBS-only fleet drops the PVE files and Snapshots columns), matching how the
// source-detail tables already drop their conditional columns.
showPbsColumn: boolean;
showArchiveColumn: boolean;
@@ -101,6 +103,9 @@ export function ProxmoxCoverageTable(props: {
];
const totalColumnWeight = () => visibleColumns().reduce((sum, c) => sum + c.weight, 0);
const columnCount = () => visibleColumns().length;
const pbsSource = getProxmoxBackupSourcePresentation('pbs');
const archiveSource = getProxmoxBackupSourcePresentation('archive');
const snapshotSource = getProxmoxBackupSourcePresentation('snapshot');
return (
<Show
@@ -110,7 +115,9 @@ export function ProxmoxCoverageTable(props: {
<EmptyState
icon={props.emptyIcon}
title={
!props.hasAnyRows ? props.emptyTitle : 'No workload coverage rows match current filters'
!props.hasAnyRows
? props.emptyTitle
: 'No workload coverage rows match current filters'
}
description={
!props.hasAnyRows
@@ -164,7 +171,7 @@ export function ProxmoxCoverageTable(props: {
/>
<Show when={props.showPbsColumn}>
<SortableHead
label="PBS"
label={pbsSource.coverageColumnLabel}
sortKey="pbs"
currentSort={props.sortKey}
direction={props.sortDirection}
@@ -175,7 +182,7 @@ export function ProxmoxCoverageTable(props: {
</Show>
<Show when={props.showArchiveColumn}>
<SortableHead
label="Archive"
label={archiveSource.coverageColumnLabel}
sortKey="archive"
currentSort={props.sortKey}
direction={props.sortDirection}
@@ -186,7 +193,7 @@ export function ProxmoxCoverageTable(props: {
</Show>
<Show when={props.showSnapshotColumn}>
<SortableHead
label="Snapshot"
label={snapshotSource.coverageColumnLabel}
sortKey="snapshot"
currentSort={props.sortKey}
direction={props.sortDirection}
@@ -284,7 +291,9 @@ export function ProxmoxCoverageTable(props: {
when={row.latestRecovery}
fallback={<span class="text-muted">No restore point</span>}
>
{(artifact) => formatRelativeTime(artifact().createdAt, { compact: true })}
{(artifact) =>
formatRelativeTime(artifact().createdAt, { compact: true })
}
</Show>
</TableCell>
<Show when={props.showPbsColumn}>
@@ -293,7 +302,9 @@ export function ProxmoxCoverageTable(props: {
>
<Show
when={row.latestPBS}
fallback={<span class="text-muted">No PBS</span>}
fallback={
<span class="text-muted">{pbsSource.coverageFallbackLabel}</span>
}
>
{(artifact) =>
formatRelativeTime(artifact().createdAt, { compact: true })
@@ -307,7 +318,9 @@ export function ProxmoxCoverageTable(props: {
>
<Show
when={row.latestArchive}
fallback={<span class="text-muted">No archive</span>}
fallback={
<span class="text-muted">{archiveSource.coverageFallbackLabel}</span>
}
>
{(artifact) =>
formatRelativeTime(artifact().createdAt, { compact: true })
@@ -321,7 +334,9 @@ export function ProxmoxCoverageTable(props: {
>
<Show
when={row.latestSnapshot}
fallback={<span class="text-muted">No snapshot</span>}
fallback={
<span class="text-muted">{snapshotSource.coverageFallbackLabel}</span>
}
>
{(artifact) =>
formatRelativeTime(artifact().createdAt, { compact: true })
@@ -411,7 +426,9 @@ export function ProxmoxCoverageTable(props: {
</span>
</td>
<td class="px-2 py-1 text-right text-base-content">
{formatRelativeTime(artifact.createdAt, { compact: true })}
{formatRelativeTime(artifact.createdAt, {
compact: true,
})}
</td>
<td class="px-2 py-1 text-right tabular-nums text-base-content">
<Show
@@ -145,6 +145,10 @@ describe('ProxmoxBackupsTable', () => {
expect(screen.getByRole('columnheader', { name: /type/i })).toBeInTheDocument();
expect(screen.getAllByText('LXC').length).toBeGreaterThan(0);
expect(screen.getAllByText('PBS').length).toBeGreaterThan(0);
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();
expect(screen.queryByRole('button', { name: 'Archives' })).not.toBeInTheDocument();
expect(screen.getByText('main / minipc')).toBeInTheDocument();
expect(
screen.getByRole('cell', {
@@ -193,7 +197,7 @@ describe('ProxmoxBackupsTable', () => {
screen.getByRole('button', { name: /show restore evidence for pbs-docker/i }),
);
expect(screen.getByText('Restore evidence')).toBeInTheDocument();
expect(screen.getAllByText('PVE archive').length).toBeGreaterThan(0);
expect(screen.getAllByText('PVE file').length).toBeGreaterThan(0);
expect(screen.getAllByText('Snapshot').length).toBeGreaterThan(0);
});
@@ -59,8 +59,8 @@ describe('ProxmoxCoverageTable column visibility', () => {
expect(headers).toContain('Node');
// Source columns gate on their flags.
expect(headers).toContain('PBS');
expect(headers).toContain('Snapshot');
expect(headers).not.toContain('Archive');
expect(headers).toContain('Snapshots');
expect(headers).not.toContain('PVE files');
expect(headers).not.toContain('Latest task');
});
});
@@ -103,7 +103,13 @@ describe('proxmoxBackupRecoveryModel', () => {
expect(row.snapshotCount).toBe(1);
expect(row.latestTask?.label).toBe('OK');
expect(getWorkloadRecoveryPostureLabel(row.posture)).toBe('Current');
expect(model.recoverableArtifacts.map((artifact) => artifact.sourceLabel)).toEqual([
'PBS',
'PVE file',
'Snapshot',
]);
expect(coverageRowMatchesSearch(row, 'pbs-docker')).toBe(true);
expect(coverageRowMatchesSearch(row, 'PVE backup file')).toBe(true);
expect(recoverableArtifactMatchesSearch(model.recoverableArtifacts[0], 'main')).toBe(true);
});
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import {
PROXMOX_BACKUP_SOURCE_KINDS,
getProxmoxArchiveSourceTitle,
getProxmoxBackupSourcePresentation,
} from '../proxmoxBackupSourcePresentation';
describe('proxmoxBackupSourcePresentation', () => {
it('keeps Proxmox backup source vocabulary canonical across filters and badges', () => {
expect(PROXMOX_BACKUP_SOURCE_KINDS).toEqual(['pbs', 'archive', 'snapshot']);
expect(getProxmoxBackupSourcePresentation('pbs')).toMatchObject({
badgeLabel: 'PBS',
filterLabel: 'PBS snapshots',
timelineLabel: 'PBS snapshots',
});
expect(getProxmoxBackupSourcePresentation('archive')).toMatchObject({
badgeLabel: 'PVE file',
filterLabel: 'PVE backup files',
timelineLabel: 'PVE backup files',
});
expect(getProxmoxBackupSourcePresentation('snapshot')).toMatchObject({
badgeLabel: 'Snapshot',
filterLabel: 'Guest snapshots',
timelineLabel: 'Guest snapshots',
});
});
it('explains PVE file rows without making them look like direct PBS rows', () => {
expect(getProxmoxArchiveSourceTitle(false)).toContain('Proxmox VE storage');
expect(getProxmoxArchiveSourceTitle(true)).toBe(
'Backup volume reported by Proxmox VE from a PBS-backed storage target',
);
});
});
@@ -6,6 +6,8 @@ import {
import { getRecoveryTimelineLabelEvery } from '@/utils/recoveryTimelineChartPresentation';
import { getRecoveryTimelineDayFilterStateLabel } from '@/utils/recoveryTimelinePresentation';
import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresentation';
export type BackupActivityRangeDays = 7 | 30 | 90 | 365;
export const BACKUP_ACTIVITY_RANGE_DAYS = [
@@ -31,14 +33,14 @@ interface BackupActivitySegmentPresentation {
const SEGMENT_PRESENTATION: Record<BackupActivitySegmentKind, BackupActivitySegmentPresentation> = {
archive: {
label: 'Archives',
segmentClassName: 'bg-blue-500',
swatchClassName: 'bg-blue-500',
label: getProxmoxBackupSourcePresentation('archive').timelineLabel,
segmentClassName: getProxmoxBackupSourcePresentation('archive').timelineSegmentClassName,
swatchClassName: getProxmoxBackupSourcePresentation('archive').timelineSwatchClassName,
},
pbs: {
label: 'PBS artifacts',
segmentClassName: 'bg-cyan-500',
swatchClassName: 'bg-cyan-500',
label: getProxmoxBackupSourcePresentation('pbs').timelineLabel,
segmentClassName: getProxmoxBackupSourcePresentation('pbs').timelineSegmentClassName,
swatchClassName: getProxmoxBackupSourcePresentation('pbs').timelineSwatchClassName,
},
ok: {
label: 'OK',
@@ -56,9 +58,9 @@ const SEGMENT_PRESENTATION: Record<BackupActivitySegmentKind, BackupActivitySegm
swatchClassName: 'bg-amber-500',
},
snapshot: {
label: 'Snapshots',
segmentClassName: 'bg-violet-500',
swatchClassName: 'bg-violet-500',
label: getProxmoxBackupSourcePresentation('snapshot').timelineLabel,
segmentClassName: getProxmoxBackupSourcePresentation('snapshot').timelineSegmentClassName,
swatchClassName: getProxmoxBackupSourcePresentation('snapshot').timelineSwatchClassName,
},
};
@@ -149,7 +151,7 @@ export interface BackupActivityTooltipRow {
muted: boolean;
}
export type BackupActivityNoun = 'archive' | 'artifact' | 'task' | 'snapshot';
export type BackupActivityNoun = 'archive' | 'artifact' | 'backup' | 'task' | 'snapshot';
function formatActivityValue(value: number, mode: BackupActivityMetricMode): string {
if (mode === 'volume') return formatBytes(Math.max(0, value));
@@ -1,7 +1,13 @@
import type { BackupTask, GuestSnapshot, PBSBackup, StorageBackup } from '@/types/api';
import type { Resource } from '@/types/resource';
export type RecoverableSourceKind = 'pbs' | 'archive' | 'snapshot';
import {
getProxmoxArchiveSourceTitle,
getProxmoxBackupSourcePresentation,
type ProxmoxBackupSourceKind,
} from './proxmoxBackupSourcePresentation';
export type RecoverableSourceKind = ProxmoxBackupSourceKind;
export type WorkloadRecoveryPosture =
| 'current'
@@ -28,6 +34,7 @@ export interface RecoverableArtifact {
nativeId: string;
sourceKind: RecoverableSourceKind;
sourceLabel: string;
sourceTitle?: string;
workload: WorkloadReference;
createdAt: string;
createdMs?: number;
@@ -425,6 +432,9 @@ export function buildProxmoxBackupRecoveryModel(
for (const candidate of candidates) ensureRow(candidate);
const artifacts: RecoverableArtifact[] = [];
const pbsSource = getProxmoxBackupSourcePresentation('pbs');
const archiveSource = getProxmoxBackupSourcePresentation('archive');
const snapshotSource = getProxmoxBackupSourcePresentation('snapshot');
const addArtifact = (artifact: RecoverableArtifact) => {
artifacts.push(artifact);
if (!isCoverageWorkload(artifact.workload)) return;
@@ -446,7 +456,8 @@ export function buildProxmoxBackupRecoveryModel(
id: `pbs:${backup.id}`,
nativeId: backup.id,
sourceKind: 'pbs',
sourceLabel: 'PBS',
sourceLabel: pbsSource.badgeLabel,
sourceTitle: pbsSource.sourceTitle,
workload,
createdAt: backup.backupTime,
createdMs,
@@ -470,13 +481,14 @@ export function buildProxmoxBackupRecoveryModel(
id: `archive:${archive.id}`,
nativeId: archive.id,
sourceKind: 'archive',
sourceLabel: archive.isPBS ? 'PVE PBS archive' : 'PVE archive',
sourceLabel: archiveSource.badgeLabel,
sourceTitle: getProxmoxArchiveSourceTitle(Boolean(archive.isPBS)),
workload,
createdAt: archive.time,
createdMs,
size: archive.size,
location: archive.storage || archive.node || '—',
detail: archive.volid || archive.format || 'Backup archive',
detail: archive.volid || archive.format || archiveSource.detailFallbackLabel,
protected: archive.protected,
verified: archive.isPBS ? archive.verified : undefined,
});
@@ -493,13 +505,14 @@ export function buildProxmoxBackupRecoveryModel(
id: `snapshot:${snapshot.id}`,
nativeId: snapshot.id,
sourceKind: 'snapshot',
sourceLabel: 'Snapshot',
sourceLabel: snapshotSource.badgeLabel,
sourceTitle: snapshotSource.sourceTitle,
workload,
createdAt: snapshot.time,
createdMs,
size: snapshot.sizeBytes,
location: snapshot.node || snapshot.instance || '—',
detail: snapshot.description || snapshot.name || 'Guest snapshot',
detail: snapshot.description || snapshot.name || snapshotSource.detailFallbackLabel,
protected: false,
});
}
@@ -573,7 +586,8 @@ export function buildProxmoxBackupRecoveryModel(
}
export function coverageRowMatchesSearch(row: WorkloadCoverageRow, term: string): boolean {
if (!term) return true;
const normalizedTerm = term.trim().toLowerCase();
if (!normalizedTerm) return true;
const haystack = [
row.workload.label,
row.workload.name,
@@ -586,18 +600,20 @@ export function coverageRowMatchesSearch(row: WorkloadCoverageRow, term: string)
row.latestTask?.error,
...row.artifacts.flatMap((artifact) => [
artifact.sourceLabel,
artifact.sourceTitle,
artifact.location,
artifact.detail,
]),
];
return haystack.filter(Boolean).join(' ').toLowerCase().includes(term);
return haystack.filter(Boolean).join(' ').toLowerCase().includes(normalizedTerm);
}
export function recoverableArtifactMatchesSearch(
artifact: RecoverableArtifact,
term: string,
): boolean {
if (!term) return true;
const normalizedTerm = term.trim().toLowerCase();
if (!normalizedTerm) return true;
const haystack = [
artifact.workload.label,
artifact.workload.name,
@@ -606,6 +622,7 @@ export function recoverableArtifactMatchesSearch(
artifact.workload.node,
artifact.workload.instance,
artifact.sourceLabel,
artifact.sourceTitle,
artifact.location,
artifact.detail,
artifact.verified === true
@@ -615,5 +632,5 @@ export function recoverableArtifactMatchesSearch(
: undefined,
artifact.protected ? 'protected' : 'unprotected',
];
return haystack.filter(Boolean).join(' ').toLowerCase().includes(term);
return haystack.filter(Boolean).join(' ').toLowerCase().includes(normalizedTerm);
}
@@ -0,0 +1,84 @@
export const PROXMOX_BACKUP_SOURCE_KINDS = ['pbs', 'archive', 'snapshot'] as const;
export type ProxmoxBackupSourceKind = (typeof PROXMOX_BACKUP_SOURCE_KINDS)[number];
export interface ProxmoxBackupSourcePresentation {
badgeClassName: string;
badgeLabel: string;
compactFilterLabel: string;
coverageColumnLabel: string;
coverageFallbackLabel: string;
detailFallbackLabel: string;
filterAriaLabel: string;
filterLabel: string;
filterTitle: string;
sourceTitle: string;
stateFallbackLabel: string;
timelineLabel: string;
timelineSegmentClassName: string;
timelineSwatchClassName: string;
}
const SOURCE_PRESENTATION: Record<ProxmoxBackupSourceKind, ProxmoxBackupSourcePresentation> = {
pbs: {
badgeClassName: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200',
badgeLabel: 'PBS',
compactFilterLabel: 'PBS',
coverageColumnLabel: 'PBS',
coverageFallbackLabel: 'No PBS snapshot',
detailFallbackLabel: 'PBS snapshot',
filterAriaLabel: 'PBS snapshots from Proxmox Backup Server',
filterLabel: 'PBS snapshots',
filterTitle: 'Direct inventory from Proxmox Backup Server',
sourceTitle: 'Direct inventory from Proxmox Backup Server',
stateFallbackLabel: 'Snapshot',
timelineLabel: 'PBS snapshots',
timelineSegmentClassName: 'bg-cyan-500',
timelineSwatchClassName: 'bg-cyan-500',
},
archive: {
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',
coverageFallbackLabel: 'No PVE backup file',
detailFallbackLabel: 'PVE backup file',
filterAriaLabel: 'PVE backup files found on Proxmox VE storage',
filterLabel: 'PVE backup files',
filterTitle: 'vzdump backup files or volumes reported by Proxmox VE storage',
sourceTitle: 'PVE backup files reported by Proxmox VE storage',
stateFallbackLabel: 'Backup file',
timelineLabel: 'PVE backup files',
timelineSegmentClassName: 'bg-blue-500',
timelineSwatchClassName: 'bg-blue-500',
},
snapshot: {
badgeClassName: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200',
badgeLabel: 'Snapshot',
compactFilterLabel: 'Snapshots',
coverageColumnLabel: 'Snapshots',
coverageFallbackLabel: 'No guest snapshot',
detailFallbackLabel: 'Guest snapshot',
filterAriaLabel: 'Guest snapshots from Proxmox VE',
filterLabel: 'Guest snapshots',
filterTitle: 'Proxmox VE guest snapshots',
sourceTitle: 'Proxmox VE guest snapshot',
stateFallbackLabel: 'Snapshot',
timelineLabel: 'Guest snapshots',
timelineSegmentClassName: 'bg-violet-500',
timelineSwatchClassName: 'bg-violet-500',
},
};
export function getProxmoxBackupSourcePresentation(
kind: ProxmoxBackupSourceKind,
): ProxmoxBackupSourcePresentation {
return SOURCE_PRESENTATION[kind];
}
export function getProxmoxArchiveSourceTitle(isPbsStorage: boolean): string {
if (isPbsStorage) {
return 'Backup volume reported by Proxmox VE from a PBS-backed storage target';
}
return SOURCE_PRESENTATION.archive.sourceTitle;
}
@@ -8,6 +8,10 @@ import { TableHead } from '@/components/shared/Table';
import { WorkloadTypeBadge as SharedWorkloadTypeBadge } from '@/components/shared/WorkloadTypeBadge';
import type { RecoverableArtifact, WorkloadReference } from './proxmoxBackupRecoveryModel';
import {
getProxmoxBackupSourcePresentation,
type ProxmoxBackupSourceKind,
} from './proxmoxBackupSourcePresentation';
import type {
CoverageFilterValue,
RecoverableFilterValue,
@@ -56,11 +60,26 @@ export const COVERAGE_FILTERS: FilterOption<CoverageFilterValue>[] = [
{ value: 'uncovered', label: 'Uncovered', tone: 'danger', leading: statusDot('bg-red-500') },
];
const recoverableSourceFilterOption = (
value: ProxmoxBackupSourceKind,
): FilterOption<RecoverableFilterValue> => {
const presentation = getProxmoxBackupSourcePresentation(value);
return {
value,
label: presentation.filterLabel,
ariaLabel: presentation.filterAriaLabel,
compactLabel: presentation.compactFilterLabel,
title: presentation.filterTitle,
tone: 'info',
leading: statusDot(presentation.timelineSwatchClassName),
};
};
export const RECOVERABLE_FILTERS: FilterOption<RecoverableFilterValue>[] = [
{ value: 'all', label: 'All' },
{ value: 'pbs', label: 'PBS', tone: 'info', leading: statusDot('bg-cyan-500') },
{ value: 'archive', label: 'Archives', tone: 'info', leading: statusDot('bg-blue-500') },
{ value: 'snapshot', label: 'Snapshots', tone: 'info', leading: statusDot('bg-violet-500') },
recoverableSourceFilterOption('pbs'),
recoverableSourceFilterOption('archive'),
recoverableSourceFilterOption('snapshot'),
{ value: 'verified', label: 'Verified', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'unverified', label: 'Unverified', tone: 'warning', leading: statusDot('bg-amber-500') },
];
@@ -121,11 +140,13 @@ export function RowMetricBar(props: {
// Short state label for a recoverable artifact, paired with ArtifactStateBadge
// for colour. Snapshot and protected take precedence over verification state.
export function artifactStateLabel(artifact: RecoverableArtifact): string {
if (artifact.sourceKind === 'snapshot') return 'Snapshot';
if (artifact.sourceKind === 'snapshot') {
return getProxmoxBackupSourcePresentation('snapshot').stateFallbackLabel;
}
if (artifact.protected) return 'Protected';
if (artifact.verified === true) return 'Verified';
if (artifact.verified === false) return 'Unverified';
return 'Archive';
return getProxmoxBackupSourcePresentation(artifact.sourceKind).stateFallbackLabel;
}
export function ArtifactStateBadge(props: { artifact: RecoverableArtifact; label: string }) {
@@ -161,15 +182,13 @@ export function ArtifactStateBadge(props: { artifact: RecoverableArtifact; label
}
export function ArtifactSourceBadge(props: { artifact: RecoverableArtifact }) {
const presentation = () => getProxmoxBackupSourcePresentation(props.artifact.sourceKind);
const title = () => props.artifact.sourceTitle ?? presentation().sourceTitle;
return (
<span
class={`inline-flex items-center rounded-sm px-1.5 py-0.5 text-[10px] font-semibold ${
props.artifact.sourceKind === 'pbs'
? 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200'
: props.artifact.sourceKind === 'archive'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200'
: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200'
}`}
class={`inline-flex items-center rounded-sm px-1.5 py-0.5 text-[10px] font-semibold ${presentation().badgeClassName}`}
title={title()}
>
{props.artifact.sourceLabel}
</span>