Files
zerobyte/app/client/modules/backups/components/schedule-summary.tsx
Nico 429b69ec92 fix: show back warnings logs and surface in UI (#677)
fix: show back warnings logs and surface in UI

#544

chore: fix dev login issue
2026-03-18 20:21:14 +01:00

312 lines
11 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 { getBackupProgressOptions, runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { useMutation, useSuspenseQuery } 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";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/client/components/ui/collapsible";
import { cn } from "~/client/lib/utils";
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 { data: initialProgress } = useSuspenseQuery({
...getBackupProgressOptions({ path: { shortId: schedule.shortId } }),
});
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" || schedule.lastBackupStatus === "error") && (
<div className="@medium:col-span-2 @wide:col-span-4">
<Collapsible
className={cn("border border-border/50 rounded-lg overflow-hidden", {
"border-yellow-500/20 bg-yellow-500/5": schedule.lastBackupStatus === "warning",
"border-red-500/20 bg-red-500/5": schedule.lastBackupStatus === "error",
})}
>
<CollapsibleTrigger
className={cn("w-full justify-start p-3 hover:bg-muted/50 transition-colors", {
"hover:bg-yellow-500/10": schedule.lastBackupStatus === "warning",
"hover:bg-red-500/10": schedule.lastBackupStatus === "error",
})}
>
<span>{schedule.lastBackupStatus === "warning" ? "Warning details" : "Error details"}</span>
</CollapsibleTrigger>
<CollapsibleContent
className={cn("border-t border-border/50 bg-muted/30", {
"border-yellow-500/20 bg-yellow-500/8": schedule.lastBackupStatus === "warning",
"border-red-500/20 bg-red-500/8": schedule.lastBackupStatus === "error",
})}
>
<div className="p-3">
<p
className={cn("font-mono text-sm whitespace-pre-wrap wrap-break-word", {
"text-yellow-600": schedule.lastBackupStatus === "warning",
"text-red-600": schedule.lastBackupStatus === "error",
})}
>
{schedule.lastBackupError ??
"No additional details available. check your container logs for more information."}
</p>
</div>
</CollapsibleContent>
</Collapsible>
</div>
)}
</CardContent>
</Card>
{schedule.lastBackupStatus === "in_progress" && (
<BackupProgressCard scheduleShortId={schedule.shortId} initialProgress={initialProgress} />
)}
<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>
);
};