Files
zerobyte/app/client/modules/backups/components/schedule-summary.tsx

285 lines
10 KiB
TypeScript

import { Check, Database, Eraser, HardDrive, Pencil, Play, Square, Trash2, X } from "lucide-react";
import { useMemo, useState } from "react";
import { OnOff } from "~/client/components/onoff";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import type { BackupSchedule } from "~/client/lib/types";
import { BackupProgressCard } from "./backup-progress-card";
import { runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { handleRepositoryError } from "~/client/lib/errors";
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
import { Link } from "@tanstack/react-router";
type Props = {
schedule: BackupSchedule;
handleToggleEnabled: (enabled: boolean) => void;
handleRunBackupNow: () => void;
handleStopBackup: () => void;
handleDeleteSchedule: () => void;
setIsEditMode: (isEdit: boolean) => void;
};
export const ScheduleSummary = (props: Props) => {
const { schedule, handleToggleEnabled, handleRunBackupNow, handleStopBackup, handleDeleteSchedule, setIsEditMode } =
props;
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showForgetConfirm, setShowForgetConfirm] = useState(false);
const [showStopConfirm, setShowStopConfirm] = useState(false);
const runForget = useMutation({
...runForgetMutation(),
onSuccess: () => {
toast.success("Retention policy applied successfully");
},
onError: (error) => {
handleRepositoryError("Failed to apply retention policy", error, schedule.repository.shortId);
},
});
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: schedule.volume.name,
scheduleLabel,
repositoryLabel: schedule.repositoryId || "No repository selected",
retentionLabel: retentionParts.length > 0 ? retentionParts.join(" • ") : "No retention policy",
};
}, [schedule]);
const handleConfirmDelete = () => {
setShowDeleteConfirm(false);
handleDeleteSchedule();
};
const handleConfirmForget = () => {
setShowForgetConfirm(false);
runForget.mutate({ path: { shortId: schedule.shortId } });
};
const handleConfirmStop = () => {
setShowStopConfirm(false);
if (schedule.lastBackupStatus !== "in_progress") return;
handleStopBackup();
};
return (
<div className="space-y-4">
<Card className="@container">
<CardHeader className="space-y-4">
<div className="flex flex-col @medium:flex-row @medium:items-center @medium:justify-between gap-4">
<div>
<CardTitle>{schedule.name}</CardTitle>
<CardDescription className="mt-1">
<Link
to="/volumes/$volumeId"
className="hover:underline"
params={{ volumeId: schedule.volume.shortId }}
>
<HardDrive className="inline h-4 w-4 mr-2" />
<span>{schedule.volume.name}</span>
</Link>
<span className="mx-2"></span>
<Link
to="/repositories/$repositoryId"
className="hover:underline"
params={{ repositoryId: schedule.repository.shortId }}
>
<Database className="inline h-4 w-4 mr-2 text-strong-accent" />
<span className="text-strong-accent">{schedule.repository.name}</span>
</Link>
</CardDescription>
</div>
<div className="flex items-center gap-2 justify-between @medium:justify-start">
<OnOff
isOn={schedule.enabled}
toggle={handleToggleEnabled}
enabledLabel="Enabled"
disabledLabel="Paused"
/>
</div>
</div>
<div className="flex flex-col @wide:flex-row gap-2">
{schedule.lastBackupStatus === "in_progress" ? (
<Button
variant="destructive"
size="sm"
onClick={() => setShowStopConfirm(true)}
className="w-full @medium:w-auto"
>
<Square className="h-4 w-4 mr-2" />
<span>Stop backup</span>
</Button>
) : (
<Button variant="default" size="sm" onClick={handleRunBackupNow} className="w-full @medium:w-auto">
<Play className="h-4 w-4 mr-2" />
<span>Backup now</span>
</Button>
)}
{schedule.retentionPolicy && (
<Button
variant="outline"
size="sm"
loading={runForget.isPending}
onClick={() => setShowForgetConfirm(true)}
className="w-full @medium:w-auto"
>
<Eraser className="h-4 w-4 mr-2" />
<span>Run cleanup</span>
</Button>
)}
<Button variant="outline" size="sm" onClick={() => setIsEditMode(true)} className="w-full @medium:w-auto">
<Pencil className="h-4 w-4 mr-2" />
<span>Edit schedule</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowDeleteConfirm(true)}
className="text-destructive hover:text-destructive w-full @medium:w-auto"
>
<Trash2 className="h-4 w-4 mr-2" />
<span>Delete</span>
</Button>
</div>
</CardHeader>
<CardContent className="grid gap-4 grid-cols-1 @medium:grid-cols-2 @wide: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">{schedule.repository.name}</p>
</div>
<div>
<p className="text-xs uppercase text-muted-foreground">Last backup</p>
<p className="font-medium">{formatTimeAgo(schedule.lastBackupAt)}</p>
</div>
<div>
<p className="text-xs uppercase text-muted-foreground">Next backup</p>
<p className="font-medium">{formatShortDateTime(schedule.nextBackupAt)}</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 === "in_progress" && "⟳ in progress..."}
{schedule.lastBackupStatus === "warning" && "! Warning"}
{!schedule.lastBackupStatus && "—"}
</p>
</div>
{schedule.lastBackupStatus === "warning" && (
<div className="@medium:col-span-2 @wide:col-span-4">
<p className="text-xs uppercase text-muted-foreground">Warning Details</p>
<p className="font-mono text-sm text-yellow-600 whitespace-pre-wrap wrap-break-word">
{schedule.lastBackupError ??
"Last backup completed with warnings. Check your container logs for more details."}
</p>
</div>
)}
{schedule.lastBackupError && schedule.lastBackupStatus === "error" && (
<div className="@medium:col-span-2 @wide:col-span-4">
<p className="text-xs uppercase text-muted-foreground">Error details</p>
<p className="font-mono text-sm text-red-600 whitespace-pre-wrap wrap-break-word">
{schedule.lastBackupError}
</p>
</div>
)}
</CardContent>
</Card>
{schedule.lastBackupStatus === "in_progress" && <BackupProgressCard scheduleShortId={schedule.shortId} />}
<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>{schedule.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>
<AlertDialog open={showForgetConfirm} onOpenChange={setShowForgetConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Run retention policy cleanup?</AlertDialogTitle>
<AlertDialogDescription>
This will apply the retention policy and permanently delete old snapshots according to the configured
rules ({summary.retentionLabel}). This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex gap-3 justify-end">
<AlertDialogCancel>
<X className="h-4 w-4 mr-2" />
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirmForget}>
<Check className="h-4 w-4 mr-2" />
Run cleanup
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={showStopConfirm} onOpenChange={setShowStopConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Stop running backup?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to stop the current backup for <strong>{schedule.name}</strong>?
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex gap-3 justify-end">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmStop}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Stop backup
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialog>
</div>
);
};