diff --git a/app/client/components/sortable-card.tsx b/app/client/components/sortable-card.tsx index d5b8897d..1455665d 100644 --- a/app/client/components/sortable-card.tsx +++ b/app/client/components/sortable-card.tsx @@ -1,5 +1,6 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import { ClientOnly } from "@tanstack/react-router"; import { GripVertical } from "lucide-react"; import type { PropsWithChildren } from "react"; @@ -21,13 +22,15 @@ export function SortableCard({ isDragging, uniqueId, children }: PropsWithChildr return (
-
- -
+ +
+ +
+
{children}
); diff --git a/app/client/lib/types.ts b/app/client/lib/types.ts index e921b00d..1afa22a9 100644 --- a/app/client/lib/types.ts +++ b/app/client/lib/types.ts @@ -1,6 +1,8 @@ import type { GetBackupScheduleResponse, GetRepositoryResponse, + GetScheduleMirrorsResponse, + GetScheduleNotificationsResponse, GetVolumeResponse, ListNotificationDestinationsResponse, ListSnapshotsResponse, @@ -17,3 +19,6 @@ export type BackupSchedule = GetBackupScheduleResponse; export type Snapshot = ListSnapshotsResponse[number]; export type NotificationDestination = ListNotificationDestinationsResponse[number]; + +export type ScheduleNotification = GetScheduleNotificationsResponse[number]; +export type ScheduleMirror = GetScheduleMirrorsResponse[number]; diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx index 4de67eb3..c8a98bff 100644 --- a/app/client/modules/backups/components/backup-card.tsx +++ b/app/client/modules/backups/components/backup-card.tsx @@ -1,13 +1,13 @@ import { CalendarClock, Database, HardDrive } from "lucide-react"; -import { Link } from "react-router"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import type { BackupSchedule } from "~/client/lib/types"; import { BackupStatusDot } from "./backup-status-dot"; import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime"; +import { Link } from "@tanstack/react-router"; export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => { return ( - +
diff --git a/app/client/modules/backups/components/schedule-mirrors-config.tsx b/app/client/modules/backups/components/schedule-mirrors-config.tsx index 52879c77..31cefb60 100644 --- a/app/client/modules/backups/components/schedule-mirrors-config.tsx +++ b/app/client/modules/backups/components/schedule-mirrors-config.tsx @@ -1,6 +1,6 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { Copy, Plus, Trash2 } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; +import { 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"; @@ -19,9 +19,9 @@ 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"; import type { GetScheduleMirrorsResponse } from "~/client/api-client"; +import { Link } from "@tanstack/react-router"; type Props = { scheduleId: number; @@ -39,13 +39,23 @@ type MirrorAssignment = { }; export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => { - const [assignments, setAssignments] = useState>(new Map()); + const map = new Map(); + for (const mirror of initialData) { + map.set(mirror.repositoryId, { + repositoryId: mirror.repositoryId, + enabled: mirror.enabled, + lastCopyAt: mirror.lastCopyAt, + lastCopyStatus: mirror.lastCopyStatus, + lastCopyError: mirror.lastCopyError, + }); + } + + const [assignments, setAssignments] = useState>(map); const [hasChanges, setHasChanges] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false); - const { data: currentMirrors } = useQuery({ + const { data: currentMirrors } = useSuspenseQuery({ ...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }), - initialData, }); const { data: compatibility } = useQuery({ @@ -75,23 +85,6 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit return map; }, [compatibility]); - useEffect(() => { - if (currentMirrors && !hasChanges) { - 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, hasChanges]); - const addRepository = (repositoryId: string) => { const newAssignments = new Map(assignments); newAssignments.set(repositoryId, { @@ -288,7 +281,8 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
diff --git a/app/client/modules/backups/components/schedule-notifications-config.tsx b/app/client/modules/backups/components/schedule-notifications-config.tsx index 11a843f1..b326e5e1 100644 --- a/app/client/modules/backups/components/schedule-notifications-config.tsx +++ b/app/client/modules/backups/components/schedule-notifications-config.tsx @@ -31,7 +31,18 @@ type NotificationAssignment = { }; export const ScheduleNotificationsConfig = ({ scheduleId, destinations, initialData }: Props) => { - const [assignments, setAssignments] = useState>(new Map()); + const map = new Map(); + for (const assignment of initialData) { + map.set(assignment.destinationId, { + destinationId: assignment.destinationId, + notifyOnStart: assignment.notifyOnStart, + notifyOnSuccess: assignment.notifyOnSuccess, + notifyOnWarning: assignment.notifyOnWarning, + notifyOnFailure: assignment.notifyOnFailure, + }); + } + + const [assignments, setAssignments] = useState>(map); const [hasChanges, setHasChanges] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false); @@ -53,23 +64,6 @@ export const ScheduleNotificationsConfig = ({ scheduleId, destinations, initialD }, }); - useEffect(() => { - if (currentAssignments) { - const map = new Map(); - for (const assignment of currentAssignments) { - map.set(assignment.destinationId, { - destinationId: assignment.destinationId, - notifyOnStart: assignment.notifyOnStart, - notifyOnSuccess: assignment.notifyOnSuccess, - notifyOnWarning: assignment.notifyOnWarning, - notifyOnFailure: assignment.notifyOnFailure, - }); - } - - setAssignments(map); - } - }, [currentAssignments]); - const addDestination = (destinationId: string) => { const id = Number.parseInt(destinationId, 10); const newAssignments = new Map(assignments); diff --git a/app/client/modules/backups/components/schedule-summary.tsx b/app/client/modules/backups/components/schedule-summary.tsx index 1452310a..16f45aac 100644 --- a/app/client/modules/backups/components/schedule-summary.tsx +++ b/app/client/modules/backups/components/schedule-summary.tsx @@ -19,7 +19,9 @@ import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { handleRepositoryError } from "~/client/lib/errors"; import { Link } from "react-router"; +import { parseError } from "~/client/lib/errors"; import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime"; +import { Link } from "@tanstack/react-router"; type Props = { schedule: BackupSchedule; @@ -93,12 +95,20 @@ export const ScheduleSummary = (props: Props) => {
{schedule.name} - + {schedule.volume.name} → - + {schedule.repository.name} diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index d4d39bf9..bdbb1bd6 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -1,6 +1,5 @@ import { useId, useState } from "react"; -import { useQuery, useMutation } from "@tanstack/react-query"; -import { redirect, useNavigate } from "react-router"; +import { useQuery, useMutation, useSuspenseQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { Save, X } from "lucide-react"; import { Button } from "~/client/components/ui/button"; @@ -27,58 +26,40 @@ import { parseError, handleRepositoryError } 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, - getScheduleMirrors, - getScheduleNotifications, - 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"; +import type { + BackupSchedule, + NotificationDestination, + Repository, + ScheduleMirror, + ScheduleNotification, +} from "~/client/lib/types"; +import { useNavigate } from "@tanstack/react-router"; -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, notifs, repos, scheduleNotifs, mirrors] = await Promise.all([ - getBackupSchedule({ path: { scheduleId: params.id } }), - listNotificationDestinations(), - listRepositories(), - getScheduleNotifications({ path: { scheduleId: params.id } }), - getScheduleMirrors({ path: { scheduleId: params.id } }), - ]); - - if (!schedule.data) return redirect("/backups"); - - return { - schedule: schedule.data, - notifs: notifs.data, - repos: repos.data, - scheduleNotifs: scheduleNotifs.data, - scheduleMirrors: mirrors.data, +// export const handle = { +// breadcrumb: (match: Route.MetaArgs) => { +// const data = match.loaderData; +// return [{ label: "Backups", href: "/backups" }, { label: data.schedule.name }]; +// }, +// }; +type Props = { + loaderData: { + schedule: BackupSchedule; + notifs: NotificationDestination[]; + repos: Repository[]; + scheduleNotifs: ScheduleNotification[]; + mirrors: ScheduleMirror[]; }; + scheduleId: string; }; -export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) { +export function ScheduleDetailsPage(props: Props) { + const { loaderData, scheduleId } = props; + const navigate = useNavigate(); const [isEditMode, setIsEditMode] = useState(false); const formId = useId(); @@ -86,9 +67,8 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [snapshotToDelete, setSnapshotToDelete] = useState(null); - const { data: schedule } = useQuery({ - ...getBackupScheduleOptions({ path: { scheduleId: params.id } }), - initialData: loaderData.schedule, + const { data: schedule } = useSuspenseQuery({ + ...getBackupScheduleOptions({ path: { scheduleId } }), }); const { @@ -136,7 +116,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon ...deleteBackupScheduleMutation(), onSuccess: () => { toast.success("Backup schedule deleted successfully"); - void navigate("/backups"); + void navigate({ to: "/backups" }); }, onError: (error) => { toast.error("Failed to delete backup schedule", { description: parseError(error)?.message }); @@ -267,7 +247,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon scheduleId={schedule.id} primaryRepositoryId={schedule.repositoryId} repositories={loaderData.repos ?? []} - initialData={loaderData.scheduleMirrors ?? []} + initialData={loaderData.mirrors ?? []} />
[{ label: "Backups" }], }; -export function meta(_: Route.MetaArgs) { - return [ - { title: "Zerobyte - Backup Jobs" }, - { - name: "description", - content: "Automate volume backups with scheduled jobs and retention policies.", - }, - ]; -} - export const clientLoader = async () => { const jobs = await listBackupSchedules(); if (jobs.data) return jobs.data; return []; }; -export default function Backups({ loaderData }: Route.ComponentProps) { - const { data: schedules, isLoading } = useQuery({ +export function BackupsPage() { + const { data: schedules, isLoading } = useSuspenseQuery({ ...listBackupSchedulesOptions(), - initialData: loaderData, }); const [items, setItems] = useState(schedules?.map((s) => s.id) ?? []); diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx index fe823d0d..ecd5364c 100644 --- a/app/client/modules/backups/routes/create-backup.tsx +++ b/app/client/modules/backups/routes/create-backup.tsx @@ -1,7 +1,6 @@ import { useId, useState } from "react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; import { Database, HardDrive, Plus } from "lucide-react"; -import { Link, useNavigate } from "react-router"; import { toast } from "sonner"; import { createBackupScheduleMutation, @@ -15,50 +14,30 @@ import { parseError } from "~/client/lib/errors"; import { EmptyState } from "~/client/components/empty-state"; import { getCronExpression } from "~/utils/utils"; import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form"; -import type { Route } from "./+types/create-backup"; -import { listRepositories, listVolumes } from "~/client/api-client"; +import { Link, useNavigate } from "@tanstack/react-router"; -export const handle = { - breadcrumb: () => [{ label: "Backups", href: "/backups" }, { label: "Create" }], -}; +// export const handle = { +// breadcrumb: () => [{ label: "Backups", href: "/backups" }, { label: "Create" }], +// }; -export function meta(_: Route.MetaArgs) { - return [ - { title: "Zerobyte - Create Backup Job" }, - { - name: "description", - content: "Create a new automated backup job for your volumes.", - }, - ]; -} - -export const clientLoader = async () => { - const [volumes, repositories] = await Promise.all([listVolumes(), listRepositories()]); - - if (volumes.data && repositories.data) return { volumes: volumes.data, repositories: repositories.data }; - return { volumes: [], repositories: [] }; -}; - -export default function CreateBackup({ loaderData }: Route.ComponentProps) { +export function CreateBackupPage() { const navigate = useNavigate(); const formId = useId(); const [selectedVolumeId, setSelectedVolumeId] = useState(); - const { data: volumesData, isLoading: loadingVolumes } = useQuery({ + const { data: volumesData, isLoading: loadingVolumes } = useSuspenseQuery({ ...listVolumesOptions(), - initialData: loaderData.volumes, }); - const { data: repositoriesData } = useQuery({ + const { data: repositoriesData } = useSuspenseQuery({ ...listRepositoriesOptions(), - initialData: loaderData.repositories, }); const createSchedule = useMutation({ ...createBackupScheduleMutation(), onSuccess: (data) => { toast.success("Backup job created successfully"); - void navigate(`/backups/${data.id}`); + void navigate({ to: `/backups/${data.id}` }); }, onError: (error) => { toast.error("Failed to create backup job", { diff --git a/app/client/modules/repositories/routes/create-repository.tsx b/app/client/modules/repositories/routes/create-repository.tsx index cad73dc4..85ed1429 100644 --- a/app/client/modules/repositories/routes/create-repository.tsx +++ b/app/client/modules/repositories/routes/create-repository.tsx @@ -1,7 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import { Database, Plus } from "lucide-react"; import { useId } from "react"; -import { useNavigate } from "react-router"; import { toast } from "sonner"; import { createRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { @@ -11,24 +10,14 @@ import { import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; import { parseError } from "~/client/lib/errors"; -import type { Route } from "./+types/create-repository"; import { Alert, AlertDescription } from "~/client/components/ui/alert"; +import { useNavigate } from "@tanstack/react-router"; export const handle = { breadcrumb: () => [{ label: "Repositories", href: "/repositories" }, { label: "Create" }], }; -export function meta(_: Route.MetaArgs) { - return [ - { title: "Zerobyte - Create Repository" }, - { - name: "description", - content: "Create a new backup repository with encryption and compression.", - }, - ]; -} - -export default function CreateRepository() { +export function CreateRepositoryPage() { const navigate = useNavigate(); const formId = useId(); @@ -36,7 +25,7 @@ export default function CreateRepository() { ...createRepositoryMutation(), onSuccess: (data) => { toast.success("Repository created successfully"); - void navigate(`/repositories/${data.repository.shortId}`); + void navigate({ to: `/repositories/${data.repository.shortId}` }); }, }); @@ -78,7 +67,7 @@ export default function CreateRepository() { loading={createRepository.isPending} />
-