import { useId, useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { redirect, useNavigate } from "react-router"; import { toast } from "sonner"; import { Save, X } from "lucide-react"; import { Button } from "~/client/components/ui/button"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "~/client/components/ui/alert-dialog"; import { getBackupScheduleOptions, runBackupNowMutation, deleteBackupScheduleMutation, listSnapshotsOptions, updateBackupScheduleMutation, stopBackupMutation, deleteSnapshotMutation, } from "~/client/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors"; import { getCronExpression } from "~/utils/utils"; import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form"; import { ScheduleSummary } from "../components/schedule-summary"; import type { Route } from "./+types/backup-details"; import { SnapshotFileBrowser } from "../components/snapshot-file-browser"; import { SnapshotTimeline } from "../components/snapshot-timeline"; import { getBackupSchedule, listNotificationDestinations, listRepositories } from "~/client/api-client"; import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config"; import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config"; import { cn } from "~/client/lib/utils"; export const handle = { breadcrumb: (match: Route.MetaArgs) => { const data = match.loaderData; return [{ label: "Backups", href: "/backups" }, { label: data.schedule.name }]; }, }; export function meta(_: Route.MetaArgs) { return [ { title: "Zerobyte - Backup Job Details" }, { name: "description", content: "View and manage backup job configuration, schedule, and snapshots.", }, ]; } export const clientLoader = async ({ params }: Route.LoaderArgs) => { const schedule = await getBackupSchedule({ path: { scheduleId: params.id } }); const notifs = await listNotificationDestinations(); const repos = await listRepositories(); if (!schedule.data) return redirect("/backups"); return { schedule: schedule.data, notifs: notifs.data, repos: repos.data }; }; export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) { const navigate = useNavigate(); const [isEditMode, setIsEditMode] = useState(false); const formId = useId(); const [selectedSnapshotId, setSelectedSnapshotId] = useState(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [snapshotToDelete, setSnapshotToDelete] = useState(null); const { data: schedule } = useQuery({ ...getBackupScheduleOptions({ path: { scheduleId: params.id } }), initialData: loaderData.schedule, }); const { data: snapshots, isLoading, failureReason, } = useQuery({ ...listSnapshotsOptions({ path: { name: schedule.repository.name }, query: { backupId: schedule.id.toString() } }), }); const updateSchedule = useMutation({ ...updateBackupScheduleMutation(), onSuccess: () => { toast.success("Backup schedule saved successfully"); setIsEditMode(false); }, onError: (error) => { toast.error("Failed to save backup schedule", { description: parseError(error)?.message, }); }, }); const runBackupNow = useMutation({ ...runBackupNowMutation(), onSuccess: () => { toast.success("Backup started successfully"); }, onError: (error) => { toast.error("Failed to start backup", { description: parseError(error)?.message }); }, }); const stopBackup = useMutation({ ...stopBackupMutation(), onSuccess: () => { toast.success("Backup stopped successfully"); }, onError: (error) => { toast.error("Failed to stop backup", { description: parseError(error)?.message }); }, }); const deleteSchedule = useMutation({ ...deleteBackupScheduleMutation(), onSuccess: () => { toast.success("Backup schedule deleted successfully"); navigate("/backups"); }, onError: (error) => { toast.error("Failed to delete backup schedule", { description: parseError(error)?.message }); }, }); const deleteSnapshot = useMutation({ ...deleteSnapshotMutation(), onSuccess: () => { setShowDeleteConfirm(false); setSnapshotToDelete(null); if (selectedSnapshotId === snapshotToDelete) { setSelectedSnapshotId(undefined); } }, }); const handleSubmit = (formValues: BackupScheduleFormValues) => { if (!schedule) return; const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay); const retentionPolicy: Record = {}; if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast; if (formValues.keepHourly) retentionPolicy.keepHourly = formValues.keepHourly; if (formValues.keepDaily) retentionPolicy.keepDaily = formValues.keepDaily; if (formValues.keepWeekly) retentionPolicy.keepWeekly = formValues.keepWeekly; if (formValues.keepMonthly) retentionPolicy.keepMonthly = formValues.keepMonthly; if (formValues.keepYearly) retentionPolicy.keepYearly = formValues.keepYearly; updateSchedule.mutate({ path: { scheduleId: schedule.id.toString() }, body: { name: formValues.name, repositoryId: formValues.repositoryId, enabled: schedule.enabled, cronExpression, retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined, includePatterns: formValues.includePatterns, excludePatterns: formValues.excludePatterns, excludeIfPresent: formValues.excludeIfPresent, }, }); }; const handleToggleEnabled = (enabled: boolean) => { updateSchedule.mutate({ path: { scheduleId: schedule.id.toString() }, body: { repositoryId: schedule.repositoryId, enabled, cronExpression: schedule.cronExpression, retentionPolicy: schedule.retentionPolicy || undefined, includePatterns: schedule.includePatterns || [], excludePatterns: schedule.excludePatterns || [], excludeIfPresent: schedule.excludeIfPresent || [], }, }); }; const handleDeleteSnapshot = (snapshotId: string) => { setSnapshotToDelete(snapshotId); setShowDeleteConfirm(true); }; const handleConfirmDelete = () => { if (snapshotToDelete) { toast.promise( deleteSnapshot.mutateAsync({ path: { name: schedule.repository.name, snapshotId: snapshotToDelete }, }), { loading: "Deleting snapshot...", success: "Snapshot deleted successfully", error: (error) => parseError(error)?.message || "Failed to delete snapshot", }, ); } }; if (isEditMode) { return (
); } const selectedSnapshot = snapshots?.find((s) => s.short_id === selectedSnapshotId); return (
runBackupNow.mutate({ path: { scheduleId: schedule.id.toString() } })} handleStopBackup={() => stopBackup.mutate({ path: { scheduleId: schedule.id.toString() } })} handleDeleteSchedule={() => deleteSchedule.mutate({ path: { scheduleId: schedule.id.toString() } })} setIsEditMode={setIsEditMode} schedule={schedule} />
{selectedSnapshot && ( )} Delete snapshot? This action cannot be undone. This will permanently delete the snapshot and all its data from the repository. Cancel Delete snapshot
); }