refactor: backups, create repo, volumes
This commit is contained in:
parent
551618d192
commit
8a25b79377
17 changed files with 310 additions and 175 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { ClientOnly } from "@tanstack/react-router";
|
||||||
import { GripVertical } from "lucide-react";
|
import { GripVertical } from "lucide-react";
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
|
@ -21,13 +22,15 @@ export function SortableCard({ isDragging, uniqueId, children }: PropsWithChildr
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setNodeRef} style={style} className="relative group">
|
<div ref={setNodeRef} style={style} className="relative group">
|
||||||
<div
|
<ClientOnly>
|
||||||
{...attributes}
|
<div
|
||||||
{...listeners}
|
{...attributes}
|
||||||
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"
|
{...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>
|
<GripVertical className="h-4 w-4 text-muted-foreground rotate-90" />
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type {
|
import type {
|
||||||
GetBackupScheduleResponse,
|
GetBackupScheduleResponse,
|
||||||
GetRepositoryResponse,
|
GetRepositoryResponse,
|
||||||
|
GetScheduleMirrorsResponse,
|
||||||
|
GetScheduleNotificationsResponse,
|
||||||
GetVolumeResponse,
|
GetVolumeResponse,
|
||||||
ListNotificationDestinationsResponse,
|
ListNotificationDestinationsResponse,
|
||||||
ListSnapshotsResponse,
|
ListSnapshotsResponse,
|
||||||
|
|
@ -17,3 +19,6 @@ export type BackupSchedule = GetBackupScheduleResponse;
|
||||||
export type Snapshot = ListSnapshotsResponse[number];
|
export type Snapshot = ListSnapshotsResponse[number];
|
||||||
|
|
||||||
export type NotificationDestination = ListNotificationDestinationsResponse[number];
|
export type NotificationDestination = ListNotificationDestinationsResponse[number];
|
||||||
|
|
||||||
|
export type ScheduleNotification = GetScheduleNotificationsResponse[number];
|
||||||
|
export type ScheduleMirror = GetScheduleMirrorsResponse[number];
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { CalendarClock, Database, HardDrive } from "lucide-react";
|
import { CalendarClock, Database, HardDrive } from "lucide-react";
|
||||||
import { Link } from "react-router";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import type { BackupSchedule } from "~/client/lib/types";
|
import type { BackupSchedule } from "~/client/lib/types";
|
||||||
import { BackupStatusDot } from "./backup-status-dot";
|
import { BackupStatusDot } from "./backup-status-dot";
|
||||||
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
|
export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
|
||||||
return (
|
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">
|
<Card key={schedule.id} className="flex flex-col h-full">
|
||||||
<CardHeader className="pb-3 overflow-hidden">
|
<CardHeader className="pb-3 overflow-hidden">
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
<div className="flex items-center justify-between gap-2 w-full">
|
||||||
|
|
|
||||||
|
|
@ -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 { Copy, Plus, Trash2 } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
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 { RepositoryIcon } from "~/client/components/repository-icon";
|
||||||
import { StatusDot } from "~/client/components/status-dot";
|
import { StatusDot } from "~/client/components/status-dot";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { Link } from "react-router";
|
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
import type { GetScheduleMirrorsResponse } from "~/client/api-client";
|
import type { GetScheduleMirrorsResponse } from "~/client/api-client";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
scheduleId: number;
|
scheduleId: number;
|
||||||
|
|
@ -39,13 +39,23 @@ type MirrorAssignment = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => {
|
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 [hasChanges, setHasChanges] = useState(false);
|
||||||
const [isAddingNew, setIsAddingNew] = useState(false);
|
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||||
|
|
||||||
const { data: currentMirrors } = useQuery({
|
const { data: currentMirrors } = useSuspenseQuery({
|
||||||
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
|
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
|
||||||
initialData,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: compatibility } = useQuery({
|
const { data: compatibility } = useQuery({
|
||||||
|
|
@ -75,23 +85,6 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
|
||||||
return map;
|
return map;
|
||||||
}, [compatibility]);
|
}, [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 addRepository = (repositoryId: string) => {
|
||||||
const newAssignments = new Map(assignments);
|
const newAssignments = new Map(assignments);
|
||||||
newAssignments.set(repositoryId, {
|
newAssignments.set(repositoryId, {
|
||||||
|
|
@ -288,7 +281,8 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
to={`/repositories/${repository.shortId}`}
|
to="/repositories/$repositoryId"
|
||||||
|
params={{ repositoryId: repository.id }}
|
||||||
className="hover:underline flex items-center gap-2"
|
className="hover:underline flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<RepositoryIcon backend={repository.type} className="h-4 w-4" />
|
<RepositoryIcon backend={repository.type} className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,18 @@ type NotificationAssignment = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScheduleNotificationsConfig = ({ scheduleId, destinations, initialData }: Props) => {
|
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 [hasChanges, setHasChanges] = useState(false);
|
||||||
const [isAddingNew, setIsAddingNew] = 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 addDestination = (destinationId: string) => {
|
||||||
const id = Number.parseInt(destinationId, 10);
|
const id = Number.parseInt(destinationId, 10);
|
||||||
const newAssignments = new Map(assignments);
|
const newAssignments = new Map(assignments);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { handleRepositoryError } from "~/client/lib/errors";
|
import { handleRepositoryError } from "~/client/lib/errors";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
import { parseError } from "~/client/lib/errors";
|
||||||
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
schedule: BackupSchedule;
|
schedule: BackupSchedule;
|
||||||
|
|
@ -93,12 +95,20 @@ export const ScheduleSummary = (props: Props) => {
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>{schedule.name}</CardTitle>
|
<CardTitle>{schedule.name}</CardTitle>
|
||||||
<CardDescription className="mt-1">
|
<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" />
|
<HardDrive className="inline h-4 w-4 mr-2" />
|
||||||
<span>{schedule.volume.name}</span>
|
<span>{schedule.volume.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mx-2">→</span>
|
<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" />
|
<Database className="inline h-4 w-4 mr-2 text-strong-accent" />
|
||||||
<span className="text-strong-accent">{schedule.repository.name}</span>
|
<span className="text-strong-accent">{schedule.repository.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useId, useState } from "react";
|
import { useId, useState } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { redirect, useNavigate } from "react-router";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Save, X } from "lucide-react";
|
import { Save, X } from "lucide-react";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
|
@ -27,58 +26,40 @@ import { parseError, handleRepositoryError } from "~/client/lib/errors";
|
||||||
import { getCronExpression } from "~/utils/utils";
|
import { getCronExpression } from "~/utils/utils";
|
||||||
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
|
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
|
||||||
import { ScheduleSummary } from "../components/schedule-summary";
|
import { ScheduleSummary } from "../components/schedule-summary";
|
||||||
import type { Route } from "./+types/backup-details";
|
|
||||||
import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
|
import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
|
||||||
import { SnapshotTimeline } from "../components/snapshot-timeline";
|
import { SnapshotTimeline } from "../components/snapshot-timeline";
|
||||||
import {
|
|
||||||
getBackupSchedule,
|
|
||||||
getScheduleMirrors,
|
|
||||||
getScheduleNotifications,
|
|
||||||
listNotificationDestinations,
|
|
||||||
listRepositories,
|
|
||||||
} from "~/client/api-client";
|
|
||||||
import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config";
|
import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config";
|
||||||
import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
|
import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
|
||||||
import { cn } from "~/client/lib/utils";
|
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 = {
|
// export const handle = {
|
||||||
breadcrumb: (match: Route.MetaArgs) => {
|
// breadcrumb: (match: Route.MetaArgs) => {
|
||||||
const data = match.loaderData;
|
// const data = match.loaderData;
|
||||||
return [{ label: "Backups", href: "/backups" }, { label: data.schedule.name }];
|
// return [{ label: "Backups", href: "/backups" }, { label: data.schedule.name }];
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
|
type Props = {
|
||||||
export function meta(_: Route.MetaArgs) {
|
loaderData: {
|
||||||
return [
|
schedule: BackupSchedule;
|
||||||
{ title: "Zerobyte - Backup Job Details" },
|
notifs: NotificationDestination[];
|
||||||
{
|
repos: Repository[];
|
||||||
name: "description",
|
scheduleNotifs: ScheduleNotification[];
|
||||||
content: "View and manage backup job configuration, schedule, and snapshots.",
|
mirrors: ScheduleMirror[];
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
scheduleId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) {
|
export function ScheduleDetailsPage(props: Props) {
|
||||||
|
const { loaderData, scheduleId } = props;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
|
|
@ -86,9 +67,8 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: schedule } = useQuery({
|
const { data: schedule } = useSuspenseQuery({
|
||||||
...getBackupScheduleOptions({ path: { scheduleId: params.id } }),
|
...getBackupScheduleOptions({ path: { scheduleId } }),
|
||||||
initialData: loaderData.schedule,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -136,7 +116,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||||
...deleteBackupScheduleMutation(),
|
...deleteBackupScheduleMutation(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Backup schedule deleted successfully");
|
toast.success("Backup schedule deleted successfully");
|
||||||
void navigate("/backups");
|
void navigate({ to: "/backups" });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to delete backup schedule", { description: parseError(error)?.message });
|
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}
|
scheduleId={schedule.id}
|
||||||
primaryRepositoryId={schedule.repositoryId}
|
primaryRepositoryId={schedule.repositoryId}
|
||||||
repositories={loaderData.repos ?? []}
|
repositories={loaderData.repos ?? []}
|
||||||
initialData={loaderData.scheduleMirrors ?? []}
|
initialData={loaderData.mirrors ?? []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SnapshotTimeline
|
<SnapshotTimeline
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
|
|
@ -10,12 +10,10 @@ import {
|
||||||
} from "@dnd-kit/core";
|
} from "@dnd-kit/core";
|
||||||
import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy } from "@dnd-kit/sortable";
|
import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy } from "@dnd-kit/sortable";
|
||||||
import { CalendarClock, Plus } from "lucide-react";
|
import { CalendarClock, Plus } from "lucide-react";
|
||||||
import { Link } from "react-router";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Card, CardContent } from "~/client/components/ui/card";
|
import { Card, CardContent } from "~/client/components/ui/card";
|
||||||
import type { Route } from "./+types/backups";
|
|
||||||
import { listBackupSchedules } from "~/client/api-client";
|
import { listBackupSchedules } from "~/client/api-client";
|
||||||
import {
|
import {
|
||||||
listBackupSchedulesOptions,
|
listBackupSchedulesOptions,
|
||||||
|
|
@ -23,31 +21,21 @@ import {
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { SortableCard } from "~/client/components/sortable-card";
|
import { SortableCard } from "~/client/components/sortable-card";
|
||||||
import { BackupCard } from "../components/backup-card";
|
import { BackupCard } from "../components/backup-card";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: () => [{ label: "Backups" }],
|
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 () => {
|
export const clientLoader = async () => {
|
||||||
const jobs = await listBackupSchedules();
|
const jobs = await listBackupSchedules();
|
||||||
if (jobs.data) return jobs.data;
|
if (jobs.data) return jobs.data;
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Backups({ loaderData }: Route.ComponentProps) {
|
export function BackupsPage() {
|
||||||
const { data: schedules, isLoading } = useQuery({
|
const { data: schedules, isLoading } = useSuspenseQuery({
|
||||||
...listBackupSchedulesOptions(),
|
...listBackupSchedulesOptions(),
|
||||||
initialData: loaderData,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [items, setItems] = useState(schedules?.map((s) => s.id) ?? []);
|
const [items, setItems] = useState(schedules?.map((s) => s.id) ?? []);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useId, useState } from "react";
|
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 { Database, HardDrive, Plus } from "lucide-react";
|
||||||
import { Link, useNavigate } from "react-router";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
createBackupScheduleMutation,
|
createBackupScheduleMutation,
|
||||||
|
|
@ -15,50 +14,30 @@ import { parseError } from "~/client/lib/errors";
|
||||||
import { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
import { getCronExpression } from "~/utils/utils";
|
import { getCronExpression } from "~/utils/utils";
|
||||||
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
|
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
|
||||||
import type { Route } from "./+types/create-backup";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { listRepositories, listVolumes } from "~/client/api-client";
|
|
||||||
|
|
||||||
export const handle = {
|
// export const handle = {
|
||||||
breadcrumb: () => [{ label: "Backups", href: "/backups" }, { label: "Create" }],
|
// breadcrumb: () => [{ label: "Backups", href: "/backups" }, { label: "Create" }],
|
||||||
};
|
// };
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function CreateBackupPage() {
|
||||||
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) {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
const [selectedVolumeId, setSelectedVolumeId] = useState<number | undefined>();
|
const [selectedVolumeId, setSelectedVolumeId] = useState<number | undefined>();
|
||||||
|
|
||||||
const { data: volumesData, isLoading: loadingVolumes } = useQuery({
|
const { data: volumesData, isLoading: loadingVolumes } = useSuspenseQuery({
|
||||||
...listVolumesOptions(),
|
...listVolumesOptions(),
|
||||||
initialData: loaderData.volumes,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: repositoriesData } = useQuery({
|
const { data: repositoriesData } = useSuspenseQuery({
|
||||||
...listRepositoriesOptions(),
|
...listRepositoriesOptions(),
|
||||||
initialData: loaderData.repositories,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createSchedule = useMutation({
|
const createSchedule = useMutation({
|
||||||
...createBackupScheduleMutation(),
|
...createBackupScheduleMutation(),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success("Backup job created successfully");
|
toast.success("Backup job created successfully");
|
||||||
void navigate(`/backups/${data.id}`);
|
void navigate({ to: `/backups/${data.id}` });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to create backup job", {
|
toast.error("Failed to create backup job", {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Database, Plus } from "lucide-react";
|
import { Database, Plus } from "lucide-react";
|
||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { createRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { createRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,24 +10,14 @@ import {
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import type { Route } from "./+types/create-repository";
|
|
||||||
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: () => [{ label: "Repositories", href: "/repositories" }, { label: "Create" }],
|
breadcrumb: () => [{ label: "Repositories", href: "/repositories" }, { label: "Create" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function CreateRepositoryPage() {
|
||||||
return [
|
|
||||||
{ title: "Zerobyte - Create Repository" },
|
|
||||||
{
|
|
||||||
name: "description",
|
|
||||||
content: "Create a new backup repository with encryption and compression.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CreateRepository() {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
|
|
||||||
|
|
@ -36,7 +25,7 @@ export default function CreateRepository() {
|
||||||
...createRepositoryMutation(),
|
...createRepositoryMutation(),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success("Repository created successfully");
|
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}
|
loading={createRepository.isPending}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
<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
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form={formId} loading={createRepository.isPending}>
|
<Button type="submit" form={formId} loading={createRepository.isPending}>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
<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 ">
|
<span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap ">
|
||||||
<Input
|
<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…"
|
placeholder="Search volumes…"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<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" />
|
<SelectValue placeholder="All status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -92,7 +92,7 @@ export function VolumesPage() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
<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" />
|
<SelectValue placeholder="All backends" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -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 authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key'
|
||||||
import { Route as dashboardVolumesIndexRouteImport } from './routes/(dashboard)/volumes/index'
|
import { Route as dashboardVolumesIndexRouteImport } from './routes/(dashboard)/volumes/index'
|
||||||
import { Route as dashboardRepositoriesIndexRouteImport } from './routes/(dashboard)/repositories/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 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 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'
|
import { Route as dashboardRepositoriesRepoIdSnapshotIdRouteImport } from './routes/(dashboard)/repositories/$repoId.$snapshotId'
|
||||||
|
|
||||||
const dashboardRouteRoute = dashboardRouteRouteImport.update({
|
const dashboardRouteRoute = dashboardRouteRouteImport.update({
|
||||||
|
|
@ -55,18 +59,40 @@ const dashboardRepositoriesIndexRoute =
|
||||||
path: '/repositories/',
|
path: '/repositories/',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const dashboardBackupsIndexRoute = dashboardBackupsIndexRouteImport.update({
|
||||||
|
id: '/backups/',
|
||||||
|
path: '/backups/',
|
||||||
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
const dashboardVolumesVolumeIdRoute =
|
const dashboardVolumesVolumeIdRoute =
|
||||||
dashboardVolumesVolumeIdRouteImport.update({
|
dashboardVolumesVolumeIdRouteImport.update({
|
||||||
id: '/volumes/$volumeId',
|
id: '/volumes/$volumeId',
|
||||||
path: '/volumes/$volumeId',
|
path: '/volumes/$volumeId',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const dashboardRepositoriesCreateRoute =
|
||||||
|
dashboardRepositoriesCreateRouteImport.update({
|
||||||
|
id: '/repositories/create',
|
||||||
|
path: '/repositories/create',
|
||||||
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
const dashboardRepositoriesRepositoryIdRoute =
|
const dashboardRepositoriesRepositoryIdRoute =
|
||||||
dashboardRepositoriesRepositoryIdRouteImport.update({
|
dashboardRepositoriesRepositoryIdRouteImport.update({
|
||||||
id: '/repositories/$repositoryId',
|
id: '/repositories/$repositoryId',
|
||||||
path: '/repositories/$repositoryId',
|
path: '/repositories/$repositoryId',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} 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 =
|
const dashboardRepositoriesRepoIdSnapshotIdRoute =
|
||||||
dashboardRepositoriesRepoIdSnapshotIdRouteImport.update({
|
dashboardRepositoriesRepoIdSnapshotIdRouteImport.update({
|
||||||
id: '/repositories/$repoId/$snapshotId',
|
id: '/repositories/$repoId/$snapshotId',
|
||||||
|
|
@ -79,8 +105,12 @@ export interface FileRoutesByFullPath {
|
||||||
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/onboarding': typeof authOnboardingRoute
|
'/onboarding': typeof authOnboardingRoute
|
||||||
|
'/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute
|
||||||
|
'/backups/create': typeof dashboardBackupsCreateRoute
|
||||||
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
|
'/repositories/create': typeof dashboardRepositoriesCreateRoute
|
||||||
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||||
|
'/backups/': typeof dashboardBackupsIndexRoute
|
||||||
'/repositories/': typeof dashboardRepositoriesIndexRoute
|
'/repositories/': typeof dashboardRepositoriesIndexRoute
|
||||||
'/volumes/': typeof dashboardVolumesIndexRoute
|
'/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
|
|
@ -90,8 +120,12 @@ export interface FileRoutesByTo {
|
||||||
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/onboarding': typeof authOnboardingRoute
|
'/onboarding': typeof authOnboardingRoute
|
||||||
|
'/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute
|
||||||
|
'/backups/create': typeof dashboardBackupsCreateRoute
|
||||||
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
|
'/repositories/create': typeof dashboardRepositoriesCreateRoute
|
||||||
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||||
|
'/backups': typeof dashboardBackupsIndexRoute
|
||||||
'/repositories': typeof dashboardRepositoriesIndexRoute
|
'/repositories': typeof dashboardRepositoriesIndexRoute
|
||||||
'/volumes': typeof dashboardVolumesIndexRoute
|
'/volumes': typeof dashboardVolumesIndexRoute
|
||||||
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
|
|
@ -103,8 +137,12 @@ export interface FileRoutesById {
|
||||||
'/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/(auth)/login': typeof authLoginRoute
|
'/(auth)/login': typeof authLoginRoute
|
||||||
'/(auth)/onboarding': typeof authOnboardingRoute
|
'/(auth)/onboarding': typeof authOnboardingRoute
|
||||||
|
'/(dashboard)/backups/$scheduleId': typeof dashboardBackupsScheduleIdRoute
|
||||||
|
'/(dashboard)/backups/create': typeof dashboardBackupsCreateRoute
|
||||||
'/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
'/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
|
'/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute
|
||||||
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||||
|
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute
|
||||||
'/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute
|
'/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute
|
||||||
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
'/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
'/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
|
|
@ -116,8 +154,12 @@ export interface FileRouteTypes {
|
||||||
| '/download-recovery-key'
|
| '/download-recovery-key'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
|
| '/backups/$scheduleId'
|
||||||
|
| '/backups/create'
|
||||||
| '/repositories/$repositoryId'
|
| '/repositories/$repositoryId'
|
||||||
|
| '/repositories/create'
|
||||||
| '/volumes/$volumeId'
|
| '/volumes/$volumeId'
|
||||||
|
| '/backups/'
|
||||||
| '/repositories/'
|
| '/repositories/'
|
||||||
| '/volumes/'
|
| '/volumes/'
|
||||||
| '/repositories/$repoId/$snapshotId'
|
| '/repositories/$repoId/$snapshotId'
|
||||||
|
|
@ -127,8 +169,12 @@ export interface FileRouteTypes {
|
||||||
| '/download-recovery-key'
|
| '/download-recovery-key'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
|
| '/backups/$scheduleId'
|
||||||
|
| '/backups/create'
|
||||||
| '/repositories/$repositoryId'
|
| '/repositories/$repositoryId'
|
||||||
|
| '/repositories/create'
|
||||||
| '/volumes/$volumeId'
|
| '/volumes/$volumeId'
|
||||||
|
| '/backups'
|
||||||
| '/repositories'
|
| '/repositories'
|
||||||
| '/volumes'
|
| '/volumes'
|
||||||
| '/repositories/$repoId/$snapshotId'
|
| '/repositories/$repoId/$snapshotId'
|
||||||
|
|
@ -139,8 +185,12 @@ export interface FileRouteTypes {
|
||||||
| '/(auth)/download-recovery-key'
|
| '/(auth)/download-recovery-key'
|
||||||
| '/(auth)/login'
|
| '/(auth)/login'
|
||||||
| '/(auth)/onboarding'
|
| '/(auth)/onboarding'
|
||||||
|
| '/(dashboard)/backups/$scheduleId'
|
||||||
|
| '/(dashboard)/backups/create'
|
||||||
| '/(dashboard)/repositories/$repositoryId'
|
| '/(dashboard)/repositories/$repositoryId'
|
||||||
|
| '/(dashboard)/repositories/create'
|
||||||
| '/(dashboard)/volumes/$volumeId'
|
| '/(dashboard)/volumes/$volumeId'
|
||||||
|
| '/(dashboard)/backups/'
|
||||||
| '/(dashboard)/repositories/'
|
| '/(dashboard)/repositories/'
|
||||||
| '/(dashboard)/volumes/'
|
| '/(dashboard)/volumes/'
|
||||||
| '/(dashboard)/repositories/$repoId/$snapshotId'
|
| '/(dashboard)/repositories/$repoId/$snapshotId'
|
||||||
|
|
@ -205,6 +255,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardRepositoriesIndexRouteImport
|
preLoaderRoute: typeof dashboardRepositoriesIndexRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
parentRoute: typeof dashboardRouteRoute
|
||||||
}
|
}
|
||||||
|
'/(dashboard)/backups/': {
|
||||||
|
id: '/(dashboard)/backups/'
|
||||||
|
path: '/backups'
|
||||||
|
fullPath: '/backups/'
|
||||||
|
preLoaderRoute: typeof dashboardBackupsIndexRouteImport
|
||||||
|
parentRoute: typeof dashboardRouteRoute
|
||||||
|
}
|
||||||
'/(dashboard)/volumes/$volumeId': {
|
'/(dashboard)/volumes/$volumeId': {
|
||||||
id: '/(dashboard)/volumes/$volumeId'
|
id: '/(dashboard)/volumes/$volumeId'
|
||||||
path: '/volumes/$volumeId'
|
path: '/volumes/$volumeId'
|
||||||
|
|
@ -212,6 +269,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardVolumesVolumeIdRouteImport
|
preLoaderRoute: typeof dashboardVolumesVolumeIdRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
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': {
|
'/(dashboard)/repositories/$repositoryId': {
|
||||||
id: '/(dashboard)/repositories/$repositoryId'
|
id: '/(dashboard)/repositories/$repositoryId'
|
||||||
path: '/repositories/$repositoryId'
|
path: '/repositories/$repositoryId'
|
||||||
|
|
@ -219,6 +283,20 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardRepositoriesRepositoryIdRouteImport
|
preLoaderRoute: typeof dashboardRepositoriesRepositoryIdRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
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': {
|
'/(dashboard)/repositories/$repoId/$snapshotId': {
|
||||||
id: '/(dashboard)/repositories/$repoId/$snapshotId'
|
id: '/(dashboard)/repositories/$repoId/$snapshotId'
|
||||||
path: '/repositories/$repoId/$snapshotId'
|
path: '/repositories/$repoId/$snapshotId'
|
||||||
|
|
@ -230,17 +308,25 @@ declare module '@tanstack/react-router' {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface dashboardRouteRouteChildren {
|
interface dashboardRouteRouteChildren {
|
||||||
|
dashboardBackupsScheduleIdRoute: typeof dashboardBackupsScheduleIdRoute
|
||||||
|
dashboardBackupsCreateRoute: typeof dashboardBackupsCreateRoute
|
||||||
dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute
|
dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
|
dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute
|
||||||
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
|
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
|
||||||
|
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
|
||||||
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
|
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
|
||||||
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
||||||
dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
|
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
|
||||||
|
dashboardBackupsScheduleIdRoute: dashboardBackupsScheduleIdRoute,
|
||||||
|
dashboardBackupsCreateRoute: dashboardBackupsCreateRoute,
|
||||||
dashboardRepositoriesRepositoryIdRoute:
|
dashboardRepositoriesRepositoryIdRoute:
|
||||||
dashboardRepositoriesRepositoryIdRoute,
|
dashboardRepositoriesRepositoryIdRoute,
|
||||||
|
dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute,
|
||||||
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
|
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
|
||||||
|
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
|
||||||
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
|
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
|
||||||
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
||||||
dashboardRepositoriesRepoIdSnapshotIdRoute:
|
dashboardRepositoriesRepoIdSnapshotIdRoute:
|
||||||
|
|
|
||||||
47
app/routes/(dashboard)/backups/$scheduleId.tsx
Normal file
47
app/routes/(dashboard)/backups/$scheduleId.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
17
app/routes/(dashboard)/backups/create.tsx
Normal file
17
app/routes/(dashboard)/backups/create.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
25
app/routes/(dashboard)/backups/index.tsx
Normal file
25
app/routes/(dashboard)/backups/index.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
19
app/routes/(dashboard)/repositories/create.tsx
Normal file
19
app/routes/(dashboard)/repositories/create.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { reactRouter } from "@react-router/dev/vite";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||||
|
|
@ -10,13 +9,13 @@ export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
// babel({
|
babel({
|
||||||
// filter: /\.[jt]sx?$/,
|
filter: /\.[jt]sx?$/,
|
||||||
// babelConfig: {
|
babelConfig: {
|
||||||
// presets: ["@babel/preset-typescript"],
|
presets: ["@babel/preset-typescript"],
|
||||||
// plugins: [["babel-plugin-react-compiler"]],
|
plugins: [["babel-plugin-react-compiler"]],
|
||||||
// },
|
},
|
||||||
// }),
|
}),
|
||||||
tanstackStart({
|
tanstackStart({
|
||||||
srcDirectory: "app",
|
srcDirectory: "app",
|
||||||
router: {
|
router: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue