mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
222 lines
7.8 KiB
TypeScript
222 lines
7.8 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { Database, Pencil, Play, Trash2 } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
import { listSnapshotsOptions } from "~/api-client/@tanstack/react-query.gen";
|
|
import { ByteSize } from "~/components/bytes-size";
|
|
import { OnOff } from "~/components/onoff";
|
|
import { SnapshotsTable } from "~/components/snapshots-table";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "~/components/ui/alert-dialog";
|
|
import type { BackupSchedule, Repository, Volume } from "~/lib/types";
|
|
|
|
type Props = {
|
|
volume: Volume;
|
|
schedule: BackupSchedule;
|
|
repository: Repository;
|
|
handleToggleEnabled: (enabled: boolean) => void;
|
|
handleRunBackupNow: () => void;
|
|
handleDeleteSchedule: () => void;
|
|
setIsEditMode: (isEdit: boolean) => void;
|
|
isDeleting?: boolean;
|
|
};
|
|
|
|
export const ScheduleSummary = (props: Props) => {
|
|
const {
|
|
volume,
|
|
schedule,
|
|
repository,
|
|
handleToggleEnabled,
|
|
handleRunBackupNow,
|
|
handleDeleteSchedule,
|
|
setIsEditMode,
|
|
isDeleting,
|
|
} = props;
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
|
|
const { data: snapshots, isLoading: loadingSnapshots } = useQuery({
|
|
...listSnapshotsOptions({
|
|
path: { name: repository.name },
|
|
query: { volumeId: volume.id.toString() },
|
|
}),
|
|
refetchInterval: 10000,
|
|
refetchOnWindowFocus: true,
|
|
});
|
|
|
|
const summary = useMemo(() => {
|
|
const scheduleLabel = schedule ? schedule.cronExpression : "-";
|
|
|
|
const retentionParts: string[] = [];
|
|
if (schedule?.retentionPolicy) {
|
|
const rp = schedule.retentionPolicy;
|
|
if (rp.keepLast) retentionParts.push(`${rp.keepLast} last`);
|
|
if (rp.keepHourly) retentionParts.push(`${rp.keepHourly} hourly`);
|
|
if (rp.keepDaily) retentionParts.push(`${rp.keepDaily} daily`);
|
|
if (rp.keepWeekly) retentionParts.push(`${rp.keepWeekly} weekly`);
|
|
if (rp.keepMonthly) retentionParts.push(`${rp.keepMonthly} monthly`);
|
|
if (rp.keepYearly) retentionParts.push(`${rp.keepYearly} yearly`);
|
|
}
|
|
|
|
return {
|
|
vol: volume.name,
|
|
scheduleLabel,
|
|
repositoryLabel: schedule.repositoryId || "No repository selected",
|
|
retentionLabel: retentionParts.length > 0 ? retentionParts.join(" • ") : "No retention policy",
|
|
};
|
|
}, [schedule, volume.name]);
|
|
|
|
const handleConfirmDelete = () => {
|
|
setShowDeleteConfirm(false);
|
|
handleDeleteSchedule();
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
|
<div>
|
|
<CardTitle>Backup schedule</CardTitle>
|
|
<CardDescription>
|
|
Automated backup configuration for volume <strong className="text-strong-accent">{volume.name}</strong>
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<OnOff isOn={schedule.enabled} toggle={handleToggleEnabled} enabledLabel="Enabled" disabledLabel="Paused" />
|
|
<Button variant="default" size="sm" onClick={handleRunBackupNow}>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
Backup Now
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => setIsEditMode(true)}>
|
|
<Pencil className="h-4 w-4 mr-2" />
|
|
Edit schedule
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
disabled={isDeleting}
|
|
className="text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<div>
|
|
<p className="text-xs uppercase text-muted-foreground">Schedule</p>
|
|
<p className="font-medium">{summary.scheduleLabel}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase text-muted-foreground">Repository</p>
|
|
<p className="font-medium">{repository.name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase text-muted-foreground">Last backup</p>
|
|
<p className="font-medium">
|
|
{schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleString() : "Never"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs uppercase text-muted-foreground">Next backup</p>
|
|
<p className="font-medium">
|
|
{schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleString() : "Never"}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-xs uppercase text-muted-foreground">Status</p>
|
|
<p className="font-medium">
|
|
{schedule.lastBackupStatus === "success" && "✓ Success"}
|
|
{schedule.lastBackupStatus === "error" && "✗ Error"}
|
|
{!schedule.lastBackupStatus && "—"}
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete backup schedule?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete this backup schedule for <strong>{volume.name}</strong>? This action
|
|
cannot be undone. Existing snapshots will not be deleted.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<div className="flex gap-3 justify-end">
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleConfirmDelete}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
Delete schedule
|
|
</AlertDialogAction>
|
|
</div>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<Card className="p-0 gap-0">
|
|
<CardHeader className="p-4 bg-card-header">
|
|
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-4 justify-between">
|
|
<div className="flex-1">
|
|
<CardTitle>Snapshots</CardTitle>
|
|
<CardDescription className="mt-1">
|
|
Backup snapshots for this volume. Total: {snapshots?.snapshots.length}
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
{loadingSnapshots && !snapshots ? (
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
<p className="text-muted-foreground">Loading snapshots...</p>
|
|
</CardContent>
|
|
) : !snapshots ? (
|
|
<CardContent className="flex flex-col items-center justify-center text-center py-16 px-4">
|
|
<div className="relative mb-8">
|
|
<div className="absolute inset-0 animate-pulse">
|
|
<div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" />
|
|
</div>
|
|
<div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
|
|
<Database className="w-16 h-16 text-primary/70" strokeWidth={1.5} />
|
|
</div>
|
|
</div>
|
|
<div className="max-w-md space-y-3">
|
|
<h3 className="text-2xl font-semibold text-foreground">No snapshots yet</h3>
|
|
<p className="text-muted-foreground text-sm">
|
|
Snapshots are point-in-time backups of your data. The next scheduled backup will create the first
|
|
snapshot.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
) : (
|
|
<>
|
|
<SnapshotsTable snapshots={snapshots.snapshots} repositoryName={repository.name} />
|
|
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-between border-t">
|
|
<span>{`Showing ${snapshots.snapshots.length} of ${snapshots.snapshots.length}`}</span>
|
|
<span>
|
|
Total size:
|
|
<span className="text-strong-accent font-medium">
|
|
<ByteSize
|
|
bytes={snapshots.snapshots.reduce((sum, s) => sum + s.size, 0)}
|
|
base={1024}
|
|
maximumFractionDigits={1}
|
|
/>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|