refactor: create volume & restore snapshot

This commit is contained in:
Nicolas Meienberger 2026-02-08 15:53:58 +01:00
parent 8a25b79377
commit e424009dd3
11 changed files with 204 additions and 126 deletions

View file

@ -1,6 +1,5 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { ChevronDown, FileIcon, FolderOpen, RotateCcw } from "lucide-react"; import { ChevronDown, FileIcon, FolderOpen, RotateCcw } from "lucide-react";
import { Button } from "~/client/components/ui/button"; import { Button } from "~/client/components/ui/button";
@ -16,6 +15,7 @@ import { useFileBrowser } from "~/client/hooks/use-file-browser";
import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic"; import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic";
import type { Repository, Snapshot } from "~/client/lib/types"; import type { Repository, Snapshot } from "~/client/lib/types";
import { handleRepositoryError } from "~/client/lib/errors"; import { handleRepositoryError } from "~/client/lib/errors";
import { useNavigate } from "@tanstack/react-router";
type RestoreLocation = "original" | "custom"; type RestoreLocation = "original" | "custom";
@ -101,7 +101,7 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re
...restoreSnapshotMutation(), ...restoreSnapshotMutation(),
onSuccess: () => { onSuccess: () => {
toast.success("Restore completed"); toast.success("Restore completed");
void navigate(returnPath); void navigate({ to: returnPath });
}, },
onError: (error) => { onError: (error) => {
handleRepositoryError("Restore failed", error, repository.id); handleRepositoryError("Restore failed", error, repository.id);
@ -156,7 +156,7 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={() => navigate(returnPath)}> <Button variant="outline" onClick={() => navigate({ to: returnPath })}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" onClick={handleRestore} disabled={isRestoring || !canRestore}> <Button variant="primary" onClick={handleRestore} disabled={isRestoring || !canRestore}>

View file

@ -1,62 +0,0 @@
import { redirect } from "react-router";
import { getBackupSchedule, getRepository, getSnapshotDetails } from "~/client/api-client";
import { RestoreForm } from "~/client/components/restore-form";
import type { Route } from "./+types/restore-snapshot";
export const handle = {
breadcrumb: (match: Route.MetaArgs) => [
{ label: "Backups", href: "/backups" },
{ label: `Schedule #${match.params.id}`, href: `/backups/${match.params.id}` },
{ label: match.params.snapshotId },
{ label: "Restore" },
],
};
export function meta({ params }: Route.MetaArgs) {
return [
{ title: `Zerobyte - Restore Snapshot ${params.snapshotId}` },
{
name: "description",
content: "Restore files from a backup snapshot.",
},
];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
if (!schedule.data) return redirect("/backups");
const [snapshot, repository] = await Promise.all([
getSnapshotDetails({
path: {
id: schedule.data.repositoryId,
snapshotId: params.snapshotId,
},
}),
getRepository({ path: { id: schedule.data.repositoryId } }),
]);
if (!snapshot.data) return redirect(`/backups/${params.id}`);
if (!repository.data) return redirect(`/backups/${params.id}`);
return {
snapshot: snapshot.data,
repository: repository.data,
snapshotId: params.snapshotId,
backupId: params.id,
};
};
export default function RestoreSnapshotFromBackupPage({ loaderData }: Route.ComponentProps) {
const { snapshot, repository, snapshotId, backupId } = loaderData;
return (
<RestoreForm
snapshot={snapshot}
repository={repository}
snapshotId={snapshotId}
returnPath={`/backups/${backupId}`}
/>
);
}

View file

@ -1,48 +1,24 @@
import { redirect } from "react-router";
import { getRepository, getSnapshotDetails } from "~/client/api-client";
import { RestoreForm } from "~/client/components/restore-form"; import { RestoreForm } from "~/client/components/restore-form";
import type { Route } from "./+types/restore-snapshot"; import type { Repository, Snapshot } from "~/client/lib/types";
export const handle = { // export const handle = {
breadcrumb: (match: Route.MetaArgs) => [ // breadcrumb: (match: Route.MetaArgs) => [
{ label: "Repositories", href: "/repositories" }, // { label: "Repositories", href: "/repositories" },
{ label: match.loaderData?.repository.name || match.params.id, href: `/repositories/${match.params.id}` }, // { label: match.loaderData?.repository.name || match.params.id, href: `/repositories/${match.params.id}` },
{ label: match.params.snapshotId, href: `/repositories/${match.params.id}/${match.params.snapshotId}` }, // { label: match.params.snapshotId, href: `/repositories/${match.params.id}/${match.params.snapshotId}` },
{ label: "Restore" }, // { label: "Restore" },
], // ],
// };
type Props = {
snapshot: Snapshot;
repository: Repository;
snapshotId: string;
returnPath: string;
}; };
export function meta({ params }: Route.MetaArgs) { export function RestoreSnapshotPage(props: Props) {
return [ const { snapshot, returnPath, snapshotId, repository } = props;
{ title: `Zerobyte - Restore Snapshot ${params.snapshotId}` },
{ return <RestoreForm snapshot={snapshot} repository={repository} snapshotId={snapshotId} returnPath={returnPath} />;
name: "description",
content: "Restore files from a backup snapshot.",
},
];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const [snapshot, repository] = await Promise.all([
getSnapshotDetails({ path: { id: params.id, snapshotId: params.snapshotId } }),
getRepository({ path: { id: params.id } }),
]);
if (!snapshot.data) return redirect("/repositories");
if (!repository.data) return redirect(`/repositories`);
return { snapshot: snapshot.data, id: params.id, repository: repository.data, snapshotId: params.snapshotId };
};
export default function RestoreSnapshotPage({ loaderData }: Route.ComponentProps) {
const { snapshot, id, snapshotId, repository } = loaderData;
return (
<RestoreForm
snapshot={snapshot}
repository={repository}
snapshotId={snapshotId}
returnPath={`/repositories/${id}/${snapshotId}`}
/>
);
} }

View file

@ -1,31 +1,20 @@
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { HardDrive, Plus } from "lucide-react"; import { HardDrive, 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 { createVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { createVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { CreateVolumeForm, type FormValues } from "~/client/modules/volumes/components/create-volume-form"; import { CreateVolumeForm, type FormValues } from "~/client/modules/volumes/components/create-volume-form";
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-volume";
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: "Volumes", href: "/volumes" }, { label: "Create" }], breadcrumb: () => [{ label: "Volumes", href: "/volumes" }, { label: "Create" }],
}; };
export function meta(_: Route.MetaArgs) { export function CreateVolumePage() {
return [
{ title: "Zerobyte - Create Volume" },
{
name: "description",
content: "Create a new storage volume with automatic mounting and health checks.",
},
];
}
export default function CreateVolume() {
const navigate = useNavigate(); const navigate = useNavigate();
const formId = useId(); const formId = useId();
@ -33,7 +22,7 @@ export default function CreateVolume() {
...createVolumeMutation(), ...createVolumeMutation(),
onSuccess: (data) => { onSuccess: (data) => {
toast.success("Volume created successfully"); toast.success("Volume created successfully");
void navigate(`/volumes/${data.shortId}`); void navigate({ to: `/volumes/${data.shortId}` });
}, },
}); });
@ -69,7 +58,7 @@ export default function CreateVolume() {
)} )}
<CreateVolumeForm mode="create" formId={formId} onSubmit={handleSubmit} loading={createVolume.isPending} /> <CreateVolumeForm mode="create" formId={formId} onSubmit={handleSubmit} loading={createVolume.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("/volumes")}> <Button type="button" variant="secondary" onClick={() => navigate({ to: "/volumes" })}>
Cancel Cancel
</Button> </Button>
<Button type="submit" form={formId} loading={createVolume.isPending}> <Button type="submit" form={formId} loading={createVolume.isPending}>

View file

@ -17,12 +17,15 @@ import { Route as authDownloadRecoveryKeyRouteImport } from './routes/(auth)/dow
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 dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index'
import { Route as dashboardVolumesCreateRouteImport } from './routes/(dashboard)/volumes/create'
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 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 dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create'
import { Route as dashboardBackupsScheduleIdRouteImport } from './routes/(dashboard)/backups/$scheduleId' 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'
import { Route as dashboardRepositoriesRepoIdSnapIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repoId.$snapId.restore'
import { Route as dashboardBackupsBackupIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/backups/$backupId.$snapshotId.restore'
const dashboardRouteRoute = dashboardRouteRouteImport.update({ const dashboardRouteRoute = dashboardRouteRouteImport.update({
id: '/(dashboard)', id: '/(dashboard)',
@ -64,6 +67,11 @@ const dashboardBackupsIndexRoute = dashboardBackupsIndexRouteImport.update({
path: '/backups/', path: '/backups/',
getParentRoute: () => dashboardRouteRoute, getParentRoute: () => dashboardRouteRoute,
} as any) } as any)
const dashboardVolumesCreateRoute = dashboardVolumesCreateRouteImport.update({
id: '/volumes/create',
path: '/volumes/create',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardVolumesVolumeIdRoute = const dashboardVolumesVolumeIdRoute =
dashboardVolumesVolumeIdRouteImport.update({ dashboardVolumesVolumeIdRouteImport.update({
id: '/volumes/$volumeId', id: '/volumes/$volumeId',
@ -99,6 +107,18 @@ const dashboardRepositoriesRepoIdSnapshotIdRoute =
path: '/repositories/$repoId/$snapshotId', path: '/repositories/$repoId/$snapshotId',
getParentRoute: () => dashboardRouteRoute, getParentRoute: () => dashboardRouteRoute,
} as any) } as any)
const dashboardRepositoriesRepoIdSnapIdRestoreRoute =
dashboardRepositoriesRepoIdSnapIdRestoreRouteImport.update({
id: '/repositories/$repoId/$snapId/restore',
path: '/repositories/$repoId/$snapId/restore',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardBackupsBackupIdSnapshotIdRestoreRoute =
dashboardBackupsBackupIdSnapshotIdRestoreRouteImport.update({
id: '/backups/$backupId/$snapshotId/restore',
path: '/backups/$backupId/$snapshotId/restore',
getParentRoute: () => dashboardRouteRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
@ -110,10 +130,13 @@ export interface FileRoutesByFullPath {
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute '/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute '/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/volumes/create': typeof dashboardVolumesCreateRoute
'/backups/': typeof dashboardBackupsIndexRoute '/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
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/repositories/$repoId/$snapId/restore': typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
@ -125,10 +148,13 @@ export interface FileRoutesByTo {
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute '/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute '/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/volumes/create': typeof dashboardVolumesCreateRoute
'/backups': typeof dashboardBackupsIndexRoute '/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
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/repositories/$repoId/$snapId/restore': typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
@ -142,10 +168,13 @@ export interface FileRoutesById {
'/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
'/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute '/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute '/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/(dashboard)/volumes/create': typeof dashboardVolumesCreateRoute
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute '/(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
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/(dashboard)/repositories/$repoId/$snapId/restore': typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
@ -159,10 +188,13 @@ export interface FileRouteTypes {
| '/repositories/$repositoryId' | '/repositories/$repositoryId'
| '/repositories/create' | '/repositories/create'
| '/volumes/$volumeId' | '/volumes/$volumeId'
| '/volumes/create'
| '/backups/' | '/backups/'
| '/repositories/' | '/repositories/'
| '/volumes/' | '/volumes/'
| '/repositories/$repoId/$snapshotId' | '/repositories/$repoId/$snapshotId'
| '/backups/$backupId/$snapshotId/restore'
| '/repositories/$repoId/$snapId/restore'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
@ -174,10 +206,13 @@ export interface FileRouteTypes {
| '/repositories/$repositoryId' | '/repositories/$repositoryId'
| '/repositories/create' | '/repositories/create'
| '/volumes/$volumeId' | '/volumes/$volumeId'
| '/volumes/create'
| '/backups' | '/backups'
| '/repositories' | '/repositories'
| '/volumes' | '/volumes'
| '/repositories/$repoId/$snapshotId' | '/repositories/$repoId/$snapshotId'
| '/backups/$backupId/$snapshotId/restore'
| '/repositories/$repoId/$snapId/restore'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@ -190,10 +225,13 @@ export interface FileRouteTypes {
| '/(dashboard)/repositories/$repositoryId' | '/(dashboard)/repositories/$repositoryId'
| '/(dashboard)/repositories/create' | '/(dashboard)/repositories/create'
| '/(dashboard)/volumes/$volumeId' | '/(dashboard)/volumes/$volumeId'
| '/(dashboard)/volumes/create'
| '/(dashboard)/backups/' | '/(dashboard)/backups/'
| '/(dashboard)/repositories/' | '/(dashboard)/repositories/'
| '/(dashboard)/volumes/' | '/(dashboard)/volumes/'
| '/(dashboard)/repositories/$repoId/$snapshotId' | '/(dashboard)/repositories/$repoId/$snapshotId'
| '/(dashboard)/backups/$backupId/$snapshotId/restore'
| '/(dashboard)/repositories/$repoId/$snapId/restore'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
@ -262,6 +300,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardBackupsIndexRouteImport preLoaderRoute: typeof dashboardBackupsIndexRouteImport
parentRoute: typeof dashboardRouteRoute parentRoute: typeof dashboardRouteRoute
} }
'/(dashboard)/volumes/create': {
id: '/(dashboard)/volumes/create'
path: '/volumes/create'
fullPath: '/volumes/create'
preLoaderRoute: typeof dashboardVolumesCreateRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/volumes/$volumeId': { '/(dashboard)/volumes/$volumeId': {
id: '/(dashboard)/volumes/$volumeId' id: '/(dashboard)/volumes/$volumeId'
path: '/volumes/$volumeId' path: '/volumes/$volumeId'
@ -304,6 +349,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRouteImport preLoaderRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRouteImport
parentRoute: typeof dashboardRouteRoute parentRoute: typeof dashboardRouteRoute
} }
'/(dashboard)/repositories/$repoId/$snapId/restore': {
id: '/(dashboard)/repositories/$repoId/$snapId/restore'
path: '/repositories/$repoId/$snapId/restore'
fullPath: '/repositories/$repoId/$snapId/restore'
preLoaderRoute: typeof dashboardRepositoriesRepoIdSnapIdRestoreRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/backups/$backupId/$snapshotId/restore': {
id: '/(dashboard)/backups/$backupId/$snapshotId/restore'
path: '/backups/$backupId/$snapshotId/restore'
fullPath: '/backups/$backupId/$snapshotId/restore'
preLoaderRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRouteImport
parentRoute: typeof dashboardRouteRoute
}
} }
} }
@ -313,10 +372,13 @@ interface dashboardRouteRouteChildren {
dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute
dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
dashboardVolumesCreateRoute: typeof dashboardVolumesCreateRoute
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
dashboardRepositoriesRepoIdSnapIdRestoreRoute: typeof dashboardRepositoriesRepoIdSnapIdRestoreRoute
} }
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = { const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
@ -326,11 +388,16 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardRepositoriesRepositoryIdRoute, dashboardRepositoriesRepositoryIdRoute,
dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute, dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute,
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute, dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
dashboardVolumesCreateRoute: dashboardVolumesCreateRoute,
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute, dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute, dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute, dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
dashboardRepositoriesRepoIdSnapshotIdRoute: dashboardRepositoriesRepoIdSnapshotIdRoute:
dashboardRepositoriesRepoIdSnapshotIdRoute, dashboardRepositoriesRepoIdSnapshotIdRoute,
dashboardBackupsBackupIdSnapshotIdRestoreRoute:
dashboardBackupsBackupIdSnapshotIdRestoreRoute,
dashboardRepositoriesRepoIdSnapIdRestoreRoute:
dashboardRepositoriesRepoIdSnapIdRestoreRoute,
} }
const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren( const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren(

View file

@ -0,0 +1,38 @@
import { createFileRoute } from "@tanstack/react-router";
import { getBackupSchedule } from "~/client/api-client";
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId/restore")({
component: RouteComponent,
loader: async ({ params, context }) => {
const schedule = await getBackupSchedule({ path: { scheduleId: params.backupId } });
if (!schedule.data) {
throw new Response("Not Found", { status: 404 });
}
const [snapshot, repository] = await Promise.all([
context.queryClient.ensureQueryData({
...getSnapshotDetailsOptions({ path: { id: schedule.data?.repositoryId, snapshotId: params.snapshotId } }),
}),
context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { id: schedule.data?.repositoryId } }) }),
]);
return { snapshot, repository };
},
});
function RouteComponent() {
const { backupId, snapshotId } = Route.useParams();
const { snapshot, repository } = Route.useLoaderData();
return (
<RestoreSnapshotPage
returnPath={`/backups/${backupId}`}
snapshotId={snapshotId}
snapshot={snapshot}
repository={repository}
/>
);
}

View file

@ -0,0 +1,41 @@
import { createFileRoute } from "@tanstack/react-router";
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
export const Route = createFileRoute("/(dashboard)/repositories/$repoId/$snapId/restore")({
component: RouteComponent,
errorComponent: (e) => <div>{e.error.message}</div>,
loader: async ({ params, context }) => {
const [snapshot, repository] = await Promise.all([
context.queryClient.ensureQueryData({
...getSnapshotDetailsOptions({ path: { id: params.repoId, snapshotId: params.snapId } }),
}),
context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { id: params.repoId } }) }),
]);
return { snapshot, repository };
},
head: ({ params }) => ({
meta: [
{ title: `Zerobyte - Restore Snapshot ${params.snapId}` },
{
name: "description",
content: "Restore files from a backup snapshot.",
},
],
}),
});
function RouteComponent() {
const { repoId, snapId } = Route.useParams();
const { snapshot, repository } = Route.useLoaderData();
return (
<RestoreSnapshotPage
returnPath={`/repositories/${repoId}/${snapId}`}
snapshot={snapshot}
repository={repository}
snapshotId={snapId}
/>
);
}

View file

@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { CreateVolumePage } from "~/client/modules/volumes/routes/create-volume";
export const Route = createFileRoute("/(dashboard)/volumes/create")({
component: RouteComponent,
head: () => ({
meta: [
{ title: "Zerobyte - Create Volume" },
{
name: "description",
content: "Create a new storage volume with automatic mounting and health checks.",
},
],
}),
});
function RouteComponent() {
return <CreateVolumePage />;
}

View file

@ -5,6 +5,7 @@ import type { QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Toaster } from "~/client/components/ui/sonner"; import { Toaster } from "~/client/components/ui/sonner";
import { useServerEvents } from "~/client/hooks/use-server-events";
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
server: { server: {
@ -38,6 +39,8 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
}); });
function RootLayout() { function RootLayout() {
useServerEvents();
return ( return (
<html lang="en"> <html lang="en">
<head> <head>

View file

@ -1,7 +1,16 @@
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
head: () => ({
meta: [
{ title: "Zerobyte" },
{
name: "description",
content: "Zerobyte - Manage your backups and storage volumes with ease.",
},
],
}),
beforeLoad: () => { beforeLoad: () => {
redirect({ to: "/volumes" }); throw redirect({ to: "/volumes" });
}, },
}); });

View file

@ -315,8 +315,6 @@ const getSnapshotDetails = async (id: string, snapshotId: string) => {
const organizationId = getOrganizationId(); const organizationId = getOrganizationId();
const repository = await findRepository(id); const repository = await findRepository(id);
console.log("Getting snapshot details for:", id, snapshotId);
if (!repository) { if (!repository) {
throw new NotFoundError("Repository not found"); throw new NotFoundError("Repository not found");
} }