mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Clarify Proxmox backup source labels
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+36
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user