soteria/web/src/components/BackupPoliciesPanel.tsx

123 lines
4.6 KiB
TypeScript

import type { BackupPolicy } from '../soteria-types';
import { formatTimestamp } from '../soteria-ui-helpers';
interface BackupPoliciesPanelProps {
policies: BackupPolicy[];
policyError: string;
namespaceOptions: string[];
policyNamespace: string;
policyPVC: string;
policyIntervalHours: number;
policyEnabled: boolean;
policyDedupe: boolean;
policyKeepLast: number;
busy: boolean;
onPolicyNamespaceChange: (value: string) => void;
onPolicyPVCChange: (value: string) => void;
onPolicyIntervalHoursChange: (value: number) => void;
onPolicyEnabledChange: (value: boolean) => void;
onPolicyDedupeChange: (value: boolean) => void;
onPolicyKeepLastChange: (value: number) => void;
onSavePolicy: () => void | Promise<void>;
onDeletePolicy: (policyID: string) => void | Promise<void>;
onLoadPolicy: (policy: BackupPolicy) => void;
}
export function BackupPoliciesPanel({
policies,
policyError,
namespaceOptions,
policyNamespace,
policyPVC,
policyIntervalHours,
policyEnabled,
policyDedupe,
policyKeepLast,
busy,
onPolicyNamespaceChange,
onPolicyPVCChange,
onPolicyIntervalHoursChange,
onPolicyEnabledChange,
onPolicyDedupeChange,
onPolicyKeepLastChange,
onSavePolicy,
onDeletePolicy,
onLoadPolicy
}: BackupPoliciesPanelProps) {
return (
<section className="panel scroll-panel">
<h2>Backup Policies</h2>
<p className="subtle tiny">
Policy backups create new restic snapshots. `Keep last` controls version retention per PVC: 1 means only newest copy
remains after each run. With dedupe on, unchanged blocks are reused in the shared repository. With dedupe off,
Soteria isolates each PVC to its own repository path.
</p>
<div className="stack">
<label>
Namespace
<select value={policyNamespace} onChange={(event) => onPolicyNamespaceChange(event.target.value)}>
{namespaceOptions.map((item) => <option key={item} value={item}>{item}</option>)}
</select>
</label>
<label>
PVC (optional)
<input value={policyPVC} onChange={(event) => onPolicyPVCChange(event.target.value)} placeholder="blank means all PVCs in namespace" />
</label>
<label>
Interval hours
<input
type="number"
min={1}
value={policyIntervalHours}
onChange={(event) => onPolicyIntervalHoursChange(Math.max(1, Number(event.target.value || 1)))}
/>
</label>
<label className="checkbox-row">
<input type="checkbox" checked={policyEnabled} onChange={(event) => onPolicyEnabledChange(event.target.checked)} />
Enabled
</label>
<label className="checkbox-row">
<input type="checkbox" checked={policyDedupe} onChange={(event) => onPolicyDedupeChange(event.target.checked)} />
Dedupe unchanged blocks
</label>
<label>
Keep last snapshots per PVC (0 = keep all)
<input
type="number"
min={0}
max={1000}
value={policyKeepLast}
onChange={(event) => onPolicyKeepLastChange(Math.max(0, Math.min(1000, Number(event.target.value || 0))))}
/>
</label>
<button type="button" onClick={() => void onSavePolicy()} disabled={busy || !policyNamespace}>Save policy</button>
</div>
{policyError && <p className="error">{policyError}</p>}
{!policyError && policies.length === 0 && <p className="subtle">No policies yet.</p>}
<div className="policy-list">
{policies.map((policy) => (
<article key={policy.id} className="policy-item">
<div className="policy-head">
<strong>{policy.namespace}/{policy.pvc || '*'}</strong>
<span className={`chip ${policy.enabled ? 'good' : 'bad'}`}>{policy.enabled ? 'Enabled' : 'Disabled'}</span>
</div>
<p className="subtle tiny">
Every {policy.interval_hours}h | Dedupe: {policy.dedupe === false ? 'off' : 'on'} | Keep last: {policy.keep_last ?? 0}
{' '}| Updated {formatTimestamp(policy.updated_at || policy.created_at)}
</p>
<div className="actions">
<button type="button" className="secondary" onClick={() => onLoadPolicy(policy)}>
Load
</button>
<button type="button" className="secondary" onClick={() => void onDeletePolicy(policy.id)} disabled={busy}>
Delete
</button>
</div>
</article>
))}
</div>
</section>
);
}