refactor: backups, create repo, volumes

This commit is contained in:
Nicolas Meienberger 2026-02-08 15:53:55 +01:00
parent 551618d192
commit 8a25b79377
17 changed files with 310 additions and 175 deletions

View file

@ -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 (
<div ref={setNodeRef} style={style} className="relative group">
<div
{...attributes}
{...listeners}
className="absolute left-1/2 -translate-x-1/2 top-1 z-10 cursor-grab active:cursor-grabbing opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-muted/50 bg-background/80 backdrop-blur-sm"
>
<GripVertical className="h-4 w-4 text-muted-foreground rotate-90" />
</div>
<ClientOnly>
<div
{...attributes}
{...listeners}
className="absolute left-1/2 -translate-x-1/2 top-1 z-10 cursor-grab active:cursor-grabbing opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-muted/50 bg-background/80 backdrop-blur-sm"
>
<GripVertical className="h-4 w-4 text-muted-foreground rotate-90" />
</div>
</ClientOnly>
{children}
</div>
);

View file

@ -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];

View file

@ -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 (
<Link key={schedule.id} to={`/backups/${schedule.id}`}>
<Link key={schedule.id} to="/backups/$scheduleId" params={{ scheduleId: schedule.id.toString() }}>
<Card key={schedule.id} className="flex flex-col h-full">
<CardHeader className="pb-3 overflow-hidden">
<div className="flex items-center justify-between gap-2 w-full">

View file

@ -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<Map<string, MirrorAssignment>>(new Map());
const map = new Map<string, MirrorAssignment>();
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<string, MirrorAssignment>>(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<string, MirrorAssignment>();
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
<TableCell>
<div className="flex items-center gap-2">
<Link
to={`/repositories/${repository.shortId}`}
to="/repositories/$repositoryId"
params={{ repositoryId: repository.id }}
className="hover:underline flex items-center gap-2"
>
<RepositoryIcon backend={repository.type} className="h-4 w-4" />

View file

@ -31,7 +31,18 @@ type NotificationAssignment = {
};
export const ScheduleNotificationsConfig = ({ scheduleId, destinations, initialData }: Props) => {
const [assignments, setAssignments] = useState<Map<number, NotificationAssignment>>(new Map());
const map = new Map<number, NotificationAssignment>();
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<number, NotificationAssignment>>(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<number, NotificationAssignment>();
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);

View file

@ -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) => {
<div>
<CardTitle>{schedule.name}</CardTitle>
<CardDescription className="mt-1">
<Link to={`/volumes/${schedule.volume.shortId}`} className="hover:underline">
<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/${schedule.repository.shortId}`} className="hover:underline">
<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>

View file

@ -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<string | null>(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 ?? []}
/>
</div>
<SnapshotTimeline

View file

@ -1,4 +1,4 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import {
DndContext,
closestCenter,
@ -10,12 +10,10 @@ import {
} from "@dnd-kit/core";
import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy } from "@dnd-kit/sortable";
import { CalendarClock, Plus } from "lucide-react";
import { Link } from "react-router";
import { useState, useEffect } from "react";
import { EmptyState } from "~/client/components/empty-state";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent } from "~/client/components/ui/card";
import type { Route } from "./+types/backups";
import { listBackupSchedules } from "~/client/api-client";
import {
listBackupSchedulesOptions,
@ -23,31 +21,21 @@ import {
} from "~/client/api-client/@tanstack/react-query.gen";
import { SortableCard } from "~/client/components/sortable-card";
import { BackupCard } from "../components/backup-card";
import { Link } from "@tanstack/react-router";
export const handle = {
breadcrumb: () => [{ 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) ?? []);

View file

@ -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<number | undefined>();
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", {

View file

@ -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}
/>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button type="button" variant="secondary" onClick={() => navigate("/repositories")}>
<Button type="button" variant="secondary" onClick={() => navigate({ to: "/repositories" })}>
Cancel
</Button>
<Button type="submit" form={formId} loading={createRepository.isPending}>

View file

@ -76,13 +76,13 @@ export function VolumesPage() {
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4">
<span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap ">
<Input
className="w-full lg:w-[180px] min-w-[180px] -mr-px -mt-px"
className="w-full lg:w-45 min-w-45 -mr-px -mt-px"
placeholder="Search volumes…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] -mr-px -mt-px">
<SelectTrigger className="w-full lg:w-45 min-w-45 -mr-px -mt-px">
<SelectValue placeholder="All status" />
</SelectTrigger>
<SelectContent>
@ -92,7 +92,7 @@ export function VolumesPage() {
</SelectContent>
</Select>
<Select value={backendFilter} onValueChange={setBackendFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] -mt-px">
<SelectTrigger className="w-full lg:w-45 min-w-45 -mt-px">
<SelectValue placeholder="All backends" />
</SelectTrigger>
<SelectContent>

View file

@ -16,8 +16,12 @@ import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key'
import { Route as dashboardVolumesIndexRouteImport } from './routes/(dashboard)/volumes/index'
import { Route as dashboardRepositoriesIndexRouteImport } from './routes/(dashboard)/repositories/index'
import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index'
import { Route as dashboardVolumesVolumeIdRouteImport } from './routes/(dashboard)/volumes/$volumeId'
import { Route as dashboardRepositoriesCreateRouteImport } from './routes/(dashboard)/repositories/create'
import { Route as dashboardRepositoriesRepositoryIdRouteImport } from './routes/(dashboard)/repositories/$repositoryId'
import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create'
import { Route as dashboardBackupsScheduleIdRouteImport } from './routes/(dashboard)/backups/$scheduleId'
import { Route as dashboardRepositoriesRepoIdSnapshotIdRouteImport } from './routes/(dashboard)/repositories/$repoId.$snapshotId'
const dashboardRouteRoute = dashboardRouteRouteImport.update({
@ -55,18 +59,40 @@ const dashboardRepositoriesIndexRoute =
path: '/repositories/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardBackupsIndexRoute = dashboardBackupsIndexRouteImport.update({
id: '/backups/',
path: '/backups/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardVolumesVolumeIdRoute =
dashboardVolumesVolumeIdRouteImport.update({
id: '/volumes/$volumeId',
path: '/volumes/$volumeId',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesCreateRoute =
dashboardRepositoriesCreateRouteImport.update({
id: '/repositories/create',
path: '/repositories/create',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesRepositoryIdRoute =
dashboardRepositoriesRepositoryIdRouteImport.update({
id: '/repositories/$repositoryId',
path: '/repositories/$repositoryId',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardBackupsCreateRoute = dashboardBackupsCreateRouteImport.update({
id: '/backups/create',
path: '/backups/create',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardBackupsScheduleIdRoute =
dashboardBackupsScheduleIdRouteImport.update({
id: '/backups/$scheduleId',
path: '/backups/$scheduleId',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesRepoIdSnapshotIdRoute =
dashboardRepositoriesRepoIdSnapshotIdRouteImport.update({
id: '/repositories/$repoId/$snapshotId',
@ -79,8 +105,12 @@ export interface FileRoutesByFullPath {
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
'/login': typeof authLoginRoute
'/onboarding': typeof authOnboardingRoute
'/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute
'/backups/create': typeof dashboardBackupsCreateRoute
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/backups/': typeof dashboardBackupsIndexRoute
'/repositories/': typeof dashboardRepositoriesIndexRoute
'/volumes/': typeof dashboardVolumesIndexRoute
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
@ -90,8 +120,12 @@ export interface FileRoutesByTo {
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
'/login': typeof authLoginRoute
'/onboarding': typeof authOnboardingRoute
'/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute
'/backups/create': typeof dashboardBackupsCreateRoute
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/backups': typeof dashboardBackupsIndexRoute
'/repositories': typeof dashboardRepositoriesIndexRoute
'/volumes': typeof dashboardVolumesIndexRoute
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
@ -103,8 +137,12 @@ export interface FileRoutesById {
'/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute
'/(auth)/login': typeof authLoginRoute
'/(auth)/onboarding': typeof authOnboardingRoute
'/(dashboard)/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute
'/(dashboard)/backups/create': typeof dashboardBackupsCreateRoute
'/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
'/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute
'/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
'/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
@ -116,8 +154,12 @@ export interface FileRouteTypes {
| '/download-recovery-key'
| '/login'
| '/onboarding'
| '/backups/$scheduleId'
| '/backups/create'
| '/repositories/$repositoryId'
| '/repositories/create'
| '/volumes/$volumeId'
| '/backups/'
| '/repositories/'
| '/volumes/'
| '/repositories/$repoId/$snapshotId'
@ -127,8 +169,12 @@ export interface FileRouteTypes {
| '/download-recovery-key'
| '/login'
| '/onboarding'
| '/backups/$scheduleId'
| '/backups/create'
| '/repositories/$repositoryId'
| '/repositories/create'
| '/volumes/$volumeId'
| '/backups'
| '/repositories'
| '/volumes'
| '/repositories/$repoId/$snapshotId'
@ -139,8 +185,12 @@ export interface FileRouteTypes {
| '/(auth)/download-recovery-key'
| '/(auth)/login'
| '/(auth)/onboarding'
| '/(dashboard)/backups/$scheduleId'
| '/(dashboard)/backups/create'
| '/(dashboard)/repositories/$repositoryId'
| '/(dashboard)/repositories/create'
| '/(dashboard)/volumes/$volumeId'
| '/(dashboard)/backups/'
| '/(dashboard)/repositories/'
| '/(dashboard)/volumes/'
| '/(dashboard)/repositories/$repoId/$snapshotId'
@ -205,6 +255,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardRepositoriesIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/backups/': {
id: '/(dashboard)/backups/'
path: '/backups'
fullPath: '/backups/'
preLoaderRoute: typeof dashboardBackupsIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/volumes/$volumeId': {
id: '/(dashboard)/volumes/$volumeId'
path: '/volumes/$volumeId'
@ -212,6 +269,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardVolumesVolumeIdRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/repositories/create': {
id: '/(dashboard)/repositories/create'
path: '/repositories/create'
fullPath: '/repositories/create'
preLoaderRoute: typeof dashboardRepositoriesCreateRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/repositories/$repositoryId': {
id: '/(dashboard)/repositories/$repositoryId'
path: '/repositories/$repositoryId'
@ -219,6 +283,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardRepositoriesRepositoryIdRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/backups/create': {
id: '/(dashboard)/backups/create'
path: '/backups/create'
fullPath: '/backups/create'
preLoaderRoute: typeof dashboardBackupsCreateRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/backups/$scheduleId': {
id: '/(dashboard)/backups/$scheduleId'
path: '/backups/$scheduleId'
fullPath: '/backups/$scheduleId'
preLoaderRoute: typeof dashboardBackupsScheduleIdRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/repositories/$repoId/$snapshotId': {
id: '/(dashboard)/repositories/$repoId/$snapshotId'
path: '/repositories/$repoId/$snapshotId'
@ -230,17 +308,25 @@ declare module '@tanstack/react-router' {
}
interface dashboardRouteRouteChildren {
dashboardBackupsScheduleIdRoute: typeof dashboardBackupsScheduleIdRoute
dashboardBackupsCreateRoute: typeof dashboardBackupsCreateRoute
dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute
dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute
}
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardBackupsScheduleIdRoute: dashboardBackupsScheduleIdRoute,
dashboardBackupsCreateRoute: dashboardBackupsCreateRoute,
dashboardRepositoriesRepositoryIdRoute:
dashboardRepositoriesRepositoryIdRoute,
dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute,
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
dashboardRepositoriesRepoIdSnapshotIdRoute:

View file

@ -0,0 +1,47 @@
import { createFileRoute } from "@tanstack/react-router";
import {
getBackupScheduleOptions,
getScheduleMirrorsOptions,
getScheduleNotificationsOptions,
listNotificationDestinationsOptions,
listRepositoriesOptions,
listSnapshotsOptions,
} from "~/client/api-client/@tanstack/react-query.gen";
import { ScheduleDetailsPage } from "~/client/modules/backups/routes/backup-details";
export const Route = createFileRoute("/(dashboard)/backups/$scheduleId")({
component: RouteComponent,
loader: async ({ params, context }) => {
const { scheduleId } = params;
const [schedule, notifs, repos, scheduleNotifs, mirrors] = await Promise.all([
context.queryClient.ensureQueryData({ ...getBackupScheduleOptions({ path: { scheduleId } }) }),
context.queryClient.ensureQueryData({ ...listNotificationDestinationsOptions() }),
context.queryClient.ensureQueryData({ ...listRepositoriesOptions() }),
context.queryClient.ensureQueryData({ ...getScheduleNotificationsOptions({ path: { scheduleId } }) }),
context.queryClient.ensureQueryData({ ...getScheduleMirrorsOptions({ path: { scheduleId } }) }),
]);
context.queryClient.prefetchQuery({
...listSnapshotsOptions({ path: { id: schedule.repository.id }, query: { backupId: schedule.shortId } }),
});
return { schedule, notifs, repos, scheduleNotifs, mirrors };
},
head: ({ loaderData }) => ({
meta: [
{ title: `Zerobyte - ${loaderData?.schedule.name || "Backup Job Details"}` },
{
name: "description",
content: "View and manage backup job configuration, schedule, and snapshots.",
},
],
}),
});
function RouteComponent() {
const loaderData = Route.useLoaderData();
const { scheduleId } = Route.useParams();
return <ScheduleDetailsPage loaderData={loaderData} scheduleId={scheduleId} />;
}

View file

@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { listRepositoriesOptions, listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { CreateBackupPage } from "~/client/modules/backups/routes/create-backup";
export const Route = createFileRoute("/(dashboard)/backups/create")({
loader: async ({ context }) => {
await Promise.all([
context.queryClient.ensureQueryData({ ...listVolumesOptions() }),
context.queryClient.ensureQueryData({ ...listRepositoriesOptions() }),
]);
},
component: RouteComponent,
});
function RouteComponent() {
return <CreateBackupPage />;
}

View file

@ -0,0 +1,25 @@
import { createFileRoute } from "@tanstack/react-router";
import { listBackupSchedulesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { BackupsPage } from "~/client/modules/backups/routes/backups";
export const Route = createFileRoute("/(dashboard)/backups/")({
component: RouteComponent,
loader: async ({ context }) => {
await context.queryClient.ensureQueryData({
...listBackupSchedulesOptions(),
});
},
head: () => ({
meta: [
{ title: "Zerobyte - Backup Jobs" },
{
name: "description",
content: "Automate volume backups with scheduled jobs and retention policies.",
},
],
}),
});
function RouteComponent() {
return <BackupsPage />;
}

View file

@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { CreateRepositoryPage } from "~/client/modules/repositories/routes/create-repository";
export const Route = createFileRoute("/(dashboard)/repositories/create")({
component: RouteComponent,
head: () => ({
meta: [
{ title: "Zerobyte - Create Repository" },
{
name: "description",
content: "Create a new backup repository with encryption and compression.",
},
],
}),
});
function RouteComponent() {
return <CreateRepositoryPage />;
}

View file

@ -1,4 +1,3 @@
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
@ -10,13 +9,13 @@ export default defineConfig({
plugins: [
tailwindcss(),
tsconfigPaths(),
// babel({
// filter: /\.[jt]sx?$/,
// babelConfig: {
// presets: ["@babel/preset-typescript"],
// plugins: [["babel-plugin-react-compiler"]],
// },
// }),
babel({
filter: /\.[jt]sx?$/,
babelConfig: {
presets: ["@babel/preset-typescript"],
plugins: [["babel-plugin-react-compiler"]],
},
}),
tanstackStart({
srcDirectory: "app",
router: {