137 lines
6.5 KiB
TypeScript
137 lines
6.5 KiB
TypeScript
|
|
import type { InventoryResponse } from '../soteria-types';
|
||
|
|
import { formatBytes, formatLabel, formatTimestamp, progressChipClass } from '../soteria-ui-helpers';
|
||
|
|
|
||
|
|
interface PVCInventoryPanelProps {
|
||
|
|
inventory: InventoryResponse | null;
|
||
|
|
inventoryError: string;
|
||
|
|
manualDedupe: boolean;
|
||
|
|
manualKeepLast: number;
|
||
|
|
busy: boolean;
|
||
|
|
onManualDedupeChange: (value: boolean) => void;
|
||
|
|
onManualKeepLastChange: (value: number) => void;
|
||
|
|
onTriggerNamespaceBackup: (namespace: string) => void | Promise<void>;
|
||
|
|
onOpenNamespaceSelection: (namespace: string) => void;
|
||
|
|
onTriggerBackup: (namespace: string, pvc: string) => void | Promise<void>;
|
||
|
|
onOpenPVCSelection: (namespace: string, pvc: string) => void | Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function PVCInventoryPanel({
|
||
|
|
inventory,
|
||
|
|
inventoryError,
|
||
|
|
manualDedupe,
|
||
|
|
manualKeepLast,
|
||
|
|
busy,
|
||
|
|
onManualDedupeChange,
|
||
|
|
onManualKeepLastChange,
|
||
|
|
onTriggerNamespaceBackup,
|
||
|
|
onOpenNamespaceSelection,
|
||
|
|
onTriggerBackup,
|
||
|
|
onOpenPVCSelection
|
||
|
|
}: PVCInventoryPanelProps) {
|
||
|
|
return (
|
||
|
|
<section className="panel scroll-panel">
|
||
|
|
<div className="panel-header">
|
||
|
|
<h2>PVC Inventory</h2>
|
||
|
|
<span className="subtle">
|
||
|
|
{inventory?.generated_at ? `Updated ${formatTimestamp(inventory.generated_at)}` : 'No inventory yet'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<label className="checkbox-row">
|
||
|
|
<input type="checkbox" checked={manualDedupe} onChange={(event) => onManualDedupeChange(event.target.checked)} />
|
||
|
|
Dedupe unchanged blocks (default)
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
Keep last snapshots per PVC (0 = keep all)
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min={0}
|
||
|
|
max={1000}
|
||
|
|
value={manualKeepLast}
|
||
|
|
onChange={(event) => onManualKeepLastChange(Math.max(0, Math.min(1000, Number(event.target.value || 0))))}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
<p className="subtle tiny">This setting applies to both `Backup now` and `Backup namespace` actions.</p>
|
||
|
|
{inventoryError && <p className="error">{inventoryError}</p>}
|
||
|
|
{!inventory && !inventoryError && <p className="subtle">Loading inventory...</p>}
|
||
|
|
{inventory?.namespaces.map((namespace) => (
|
||
|
|
<article key={namespace.name} className="namespace-block">
|
||
|
|
<div className="namespace-row">
|
||
|
|
<h3>{namespace.name}</h3>
|
||
|
|
<div className="actions">
|
||
|
|
<button type="button" className="secondary" onClick={() => void onTriggerNamespaceBackup(namespace.name)} disabled={busy}>
|
||
|
|
Backup namespace
|
||
|
|
</button>
|
||
|
|
<button type="button" className="secondary" onClick={() => onOpenNamespaceSelection(namespace.name)}>
|
||
|
|
Restore namespace
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="pvc-grid">
|
||
|
|
{namespace.pvcs.map((pvc) => {
|
||
|
|
const healthClass = pvc.healthy ? 'good' : (pvc.health_reason === 'in_progress' ? 'warn' : 'bad');
|
||
|
|
const healthLabel = pvc.healthy ? 'Healthy' : formatLabel(pvc.health_reason);
|
||
|
|
const progressPct = Math.max(0, Math.min(100, Number(pvc.last_job_progress_pct || 0)));
|
||
|
|
const progressClass = progressChipClass(pvc.last_job_state);
|
||
|
|
const showProgress = Boolean(pvc.last_job_name) || (pvc.active_backups || 0) > 0;
|
||
|
|
const latestSizeLabel = formatBytes(pvc.last_backup_size_bytes);
|
||
|
|
const totalStoredLabel = formatBytes(pvc.total_backup_size_bytes);
|
||
|
|
const showResticSizeHint = pvc.driver === 'restic'
|
||
|
|
&& (pvc.last_backup_size_bytes === undefined || pvc.total_backup_size_bytes === undefined);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<article key={`${pvc.namespace}/${pvc.pvc}`} className="pvc-card">
|
||
|
|
<div className="pvc-title-row">
|
||
|
|
<div>
|
||
|
|
<h4>{pvc.pvc}</h4>
|
||
|
|
<p className="subtle tiny">
|
||
|
|
{pvc.volume || 'unknown volume'} | {pvc.storage_class || 'no class'} | {pvc.capacity || 'unknown size'}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<span className={`chip ${healthClass}`}>{healthLabel}</span>
|
||
|
|
</div>
|
||
|
|
<p className="subtle tiny">
|
||
|
|
Last backup: {pvc.last_backup_at ? `${formatTimestamp(pvc.last_backup_at)} (${(pvc.last_backup_age_hours || 0).toFixed(1)}h ago)` : 'never'}
|
||
|
|
</p>
|
||
|
|
<p className="subtle tiny">
|
||
|
|
Backups: {pvc.completed_backups}/{pvc.backup_count} completed | Latest size: {latestSizeLabel} | Total stored: {totalStoredLabel}
|
||
|
|
</p>
|
||
|
|
{showResticSizeHint && (
|
||
|
|
<p className="subtle tiny">
|
||
|
|
Per-PVC storage is estimated from restic upload summaries persisted by Soteria. Older backups created
|
||
|
|
before tracking may show n/a until a new backup runs.
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{showProgress && (
|
||
|
|
<div className="backup-progress">
|
||
|
|
<div className="progress-header">
|
||
|
|
<p className="subtle tiny">
|
||
|
|
Job: {pvc.last_job_name || 'n/a'}
|
||
|
|
{pvc.last_job_started_at ? ` | Started ${formatTimestamp(pvc.last_job_started_at)}` : ''}
|
||
|
|
</p>
|
||
|
|
<span className={`chip ${progressClass}`}>{pvc.last_job_state || 'Unknown'}</span>
|
||
|
|
</div>
|
||
|
|
<div className="progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={progressPct}>
|
||
|
|
<div className={`progress-fill ${progressClass} ${pvc.active_backups > 0 ? 'active' : ''}`} style={{ width: `${progressPct}%` }} />
|
||
|
|
</div>
|
||
|
|
<p className="subtle tiny">Progress {progressPct}% | Active jobs: {pvc.active_backups || 0}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{pvc.error && <p className="error tiny">{pvc.error}</p>}
|
||
|
|
<div className="actions">
|
||
|
|
<button type="button" onClick={() => void onTriggerBackup(pvc.namespace, pvc.pvc)} disabled={busy}>
|
||
|
|
Backup now
|
||
|
|
</button>
|
||
|
|
<button type="button" className="secondary" onClick={() => void onOpenPVCSelection(pvc.namespace, pvc.pvc)}>
|
||
|
|
Restore
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
))}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|