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 { 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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) ?? []);
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
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 { 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: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue