import { useMutation, useQuery } from "@tanstack/react-query"; import { Copy, Plus, Trash2 } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Switch } from "~/client/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; import { Badge } from "~/client/components/ui/badge"; import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip"; import { getScheduleMirrorsOptions, getMirrorCompatibilityOptions, updateScheduleMirrorsMutation, } from "~/client/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors"; import type { Repository } from "~/client/lib/types"; import { RepositoryIcon } from "~/client/components/repository-icon"; import { StatusDot } from "~/client/components/status-dot"; import { formatDistanceToNow } from "date-fns"; import { Link } from "react-router"; import { cn } from "~/client/lib/utils"; type Props = { scheduleId: number; primaryRepositoryId: string; repositories: Repository[]; }; type MirrorAssignment = { repositoryId: string; enabled: boolean; lastCopyAt: number | null; lastCopyStatus: "success" | "error" | null; lastCopyError: string | null; }; export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories }: Props) => { const [assignments, setAssignments] = useState>(new Map()); const [hasChanges, setHasChanges] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false); const { data: currentMirrors } = useQuery({ ...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }), }); const { data: compatibility } = useQuery({ ...getMirrorCompatibilityOptions({ path: { scheduleId: scheduleId.toString() } }), }); const updateMirrors = useMutation({ ...updateScheduleMirrorsMutation(), onSuccess: () => { toast.success("Mirror settings saved successfully"); setHasChanges(false); }, onError: (error) => { toast.error("Failed to save mirror settings", { description: parseError(error)?.message, }); }, }); const compatibilityMap = useMemo(() => { const map = new Map(); if (compatibility) { for (const item of compatibility) { map.set(item.repositoryId, { compatible: item.compatible, reason: item.reason }); } } return map; }, [compatibility]); useEffect(() => { if (currentMirrors) { const map = new Map(); for (const mirror of currentMirrors) { map.set(mirror.repositoryId, { repositoryId: mirror.repositoryId, enabled: mirror.enabled, lastCopyAt: mirror.lastCopyAt, lastCopyStatus: mirror.lastCopyStatus, lastCopyError: mirror.lastCopyError, }); } setAssignments(map); } }, [currentMirrors]); const addRepository = (repositoryId: string) => { const newAssignments = new Map(assignments); newAssignments.set(repositoryId, { repositoryId, enabled: true, lastCopyAt: null, lastCopyStatus: null, lastCopyError: null, }); setAssignments(newAssignments); setHasChanges(true); setIsAddingNew(false); }; const removeRepository = (repositoryId: string) => { const newAssignments = new Map(assignments); newAssignments.delete(repositoryId); setAssignments(newAssignments); setHasChanges(true); }; const toggleEnabled = (repositoryId: string) => { const assignment = assignments.get(repositoryId); if (!assignment) return; const newAssignments = new Map(assignments); newAssignments.set(repositoryId, { ...assignment, enabled: !assignment.enabled, }); setAssignments(newAssignments); setHasChanges(true); }; const handleSave = () => { const mirrorsList = Array.from(assignments.values()).map((a) => ({ repositoryId: a.repositoryId, enabled: a.enabled, })); updateMirrors.mutate({ path: { scheduleId: scheduleId.toString() }, body: { mirrors: mirrorsList, }, }); }; const handleReset = () => { if (currentMirrors) { const map = new Map(); for (const mirror of currentMirrors) { map.set(mirror.repositoryId, { repositoryId: mirror.repositoryId, enabled: mirror.enabled, lastCopyAt: mirror.lastCopyAt, lastCopyStatus: mirror.lastCopyStatus, lastCopyError: mirror.lastCopyError, }); } setAssignments(map); setHasChanges(false); } }; const getRepositoryById = (id: string) => { return repositories?.find((r) => r.id === id); }; const selectableRepositories = repositories?.filter((r) => { if (r.id === primaryRepositoryId) return false; if (assignments.has(r.id)) return false; return true; }) || []; const hasAvailableRepositories = selectableRepositories.some((r) => { const compat = compatibilityMap.get(r.id); return compat?.compatible !== false; }); const assignedRepositories = Array.from(assignments.keys()) .map((id) => getRepositoryById(id)) .filter((r) => r !== undefined); const getStatusVariant = (status: "success" | "error" | null): "success" | "error" | "neutral" => { if (status === "success") return "success"; if (status === "error") return "error"; return "neutral"; }; const getStatusLabel = (assignment: MirrorAssignment): string => { if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) { return assignment.lastCopyError; } if (assignment.lastCopyStatus === "success") { return "Last copy successful"; } return "Never copied"; }; return (
Mirror Repositories Configure secondary repositories where snapshots will be automatically copied after each backup
{!isAddingNew && selectableRepositories.length > 0 && ( )}
{isAddingNew && (
)} {assignedRepositories.length === 0 ? (

No mirror repositories configured for this schedule.

Click "Add mirror" to replicate backups to additional repositories.

) : (
Repository Enabled Last Copy {assignedRepositories.map((repository) => { const assignment = assignments.get(repository.id); if (!assignment) return null; return (
{repository.name} {repository.type}
toggleEnabled(repository.id)} /> {assignment.lastCopyAt ? (
{formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true })}
) : ( Never )}
); })}
)} {hasChanges && (
)}
); };