diff --git a/app/client/api-client/client.gen.ts b/app/client/api-client/client.gen.ts index 457f3c3d..fb37e2b1 100644 --- a/app/client/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -16,4 +16,4 @@ export type CreateClientConfig = ( override?: Config, ) => Config & T>; -export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })); +export const client = createClient(createConfig({ baseUrl: "http://192.168.2.42:4096" })); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 46689078..da9b6e98 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -2,7 +2,7 @@ // This file is auto-generated by @hey-api/openapi-ts export type ClientOptions = { - baseUrl: "http://localhost:4096" | (string & {}); + baseUrl: "http://192.168.2.42:4096" | (string & {}); }; export type GetStatusData = { @@ -1649,6 +1649,7 @@ export type ListSnapshotsResponses = { size: number; tags: Array; time: number; + hostname?: string; }>; }; @@ -1718,6 +1719,7 @@ export type GetSnapshotDetailsResponses = { size: number; tags: Array; time: number; + hostname?: string; }; }; diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 613c54eb..fb195df5 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Calendar, Clock, Database, HardDrive, Tag, Trash2, X } from "lucide-react"; -import { Link, useNavigate } from "react-router"; import { toast } from "sonner"; import { ByteSize } from "~/client/components/bytes-size"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; @@ -32,6 +31,7 @@ import { deleteSnapshotsMutation, tagSnapshotsMutation } from "~/client/api-clie import { parseError } from "~/client/lib/errors"; import type { BackupSchedule, Snapshot } from "../lib/types"; import { cn } from "../lib/utils"; +import { Link, useNavigate } from "@tanstack/react-router"; type Props = { snapshots: Snapshot[]; @@ -40,8 +40,8 @@ type Props = { }; export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => { - const navigate = useNavigate(); const queryClient = useQueryClient(); + const navigate = useNavigate(); const [selectedIds, setSelectedIds] = useState>(new Set()); const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false); @@ -71,7 +71,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => { }); const handleRowClick = (snapshotId: string) => { - void navigate(`/repositories/${repositoryId}/${snapshotId}`); + void navigate({ to: `/repositories/${repositoryId}/${snapshotId}` }); }; const toggleSelectAll = () => { @@ -172,7 +172,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
e.stopPropagation()} className="hover:underline" > diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index 9e62a816..d15c5cb1 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -1,7 +1,6 @@ import { useCallback } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { FileIcon, RotateCcw, Trash2 } from "lucide-react"; -import { Link } from "react-router"; import { FileTree } from "~/client/components/file-tree"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Button, buttonVariants } from "~/client/components/ui/button"; @@ -10,6 +9,7 @@ import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-qu import { formatDateTime } from "~/client/lib/datetime"; import { useFileBrowser } from "~/client/hooks/use-file-browser"; import { cn } from "~/client/lib/utils"; +import { Link } from "@tanstack/react-router"; interface Props { snapshot: Snapshot; diff --git a/app/client/modules/repositories/routes/repositories.tsx b/app/client/modules/repositories/routes/repositories.tsx index 008efd10..49a323d2 100644 --- a/app/client/modules/repositories/routes/repositories.tsx +++ b/app/client/modules/repositories/routes/repositories.tsx @@ -1,8 +1,6 @@ -import { useQuery } from "@tanstack/react-query"; +import { useSuspenseQuery } from "@tanstack/react-query"; import { Database, Plus, RotateCcw } from "lucide-react"; import { useState } from "react"; -import { useNavigate } from "react-router"; -import { listRepositories } from "~/client/api-client/sdk.gen"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { RepositoryIcon } from "~/client/components/repository-icon"; import { Button } from "~/client/components/ui/button"; @@ -10,31 +8,15 @@ import { Card } from "~/client/components/ui/card"; import { Input } from "~/client/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; -import type { Route } from "./+types/repositories"; import { cn } from "~/client/lib/utils"; import { EmptyState } from "~/client/components/empty-state"; +import { useNavigate } from "@tanstack/react-router"; export const handle = { breadcrumb: () => [{ label: "Repositories" }], }; -export function meta(_: Route.MetaArgs) { - return [ - { title: "Zerobyte - Repositories" }, - { - name: "description", - content: "Manage your backup repositories with encryption and compression.", - }, - ]; -} - -export const clientLoader = async () => { - const repositories = await listRepositories(); - if (repositories.data) return repositories.data; - return []; -}; - -export default function Repositories({ loaderData }: Route.ComponentProps) { +export function RepositoriesPage() { const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [backendFilter, setBackendFilter] = useState(""); @@ -47,20 +29,18 @@ export default function Repositories({ loaderData }: Route.ComponentProps) { const navigate = useNavigate(); - const { data } = useQuery({ + const { data } = useSuspenseQuery({ ...listRepositoriesOptions(), - initialData: loaderData, }); - const filteredRepositories = - data?.filter((repository) => { - const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesStatus = !statusFilter || repository.status === statusFilter; - const matchesBackend = !backendFilter || repository.type === backendFilter; - return matchesSearch && matchesStatus && matchesBackend; - }) || []; + const filteredRepositories = data.filter((repository) => { + const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesStatus = !statusFilter || repository.status === statusFilter; + const matchesBackend = !backendFilter || repository.type === backendFilter; + return matchesSearch && matchesStatus && matchesBackend; + }); - const hasNoRepositories = data?.length === 0; + const hasNoRepositories = data.length === 0; const hasNoFilteredRepositories = filteredRepositories.length === 0 && !hasNoRepositories; if (hasNoRepositories) { @@ -70,7 +50,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) { title="No repository" description="Repositories are remote storage locations where you can backup your volumes securely. Encrypted and optimized for storage efficiency." button={ - @@ -117,7 +97,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) { )} - @@ -126,7 +106,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) { - Name + Name Backend Compression Status @@ -150,7 +130,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) { navigate(`/repositories/${repository.shortId}`)} + onClick={() => navigate({ to: `/repositories/${repository.shortId}` })} > {repository.name} diff --git a/app/client/modules/repositories/routes/repository-details.tsx b/app/client/modules/repositories/routes/repository-details.tsx index b685ea88..5d691a31 100644 --- a/app/client/modules/repositories/routes/repository-details.tsx +++ b/app/client/modules/repositories/routes/repository-details.tsx @@ -1,46 +1,27 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { redirect, useSearchParams } from "react-router"; -import { useEffect } from "react"; +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { Suspense, useEffect } from "react"; import { getRepositoryOptions, listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen"; -import { getRepository } from "~/client/api-client/sdk.gen"; -import type { Route } from "./+types/repository-details"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; import { RepositoryInfoTabContent } from "../tabs/info"; import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; +import { useNavigate, useSearch } from "@tanstack/react-router"; -export const handle = { - breadcrumb: (match: Route.MetaArgs) => [ - { label: "Repositories", href: "/repositories" }, - { label: match.loaderData?.name || match.params.id }, - ], -}; +// export const handle = { +// breadcrumb: (match: Route.MetaArgs) => [ +// { label: "Repositories", href: "/repositories" }, +// { label: match.loaderData?.name || match.params.id }, +// ], +// }; -export function meta({ params, loaderData }: Route.MetaArgs) { - return [ - { title: `Zerobyte - ${loaderData?.name || params.id}` }, - { - name: "description", - content: "View repository configuration, status, and snapshots.", - }, - ]; -} - -export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { - const repository = await getRepository({ path: { id: params.id ?? "" } }); - if (repository.data) return repository.data; - - return redirect("/repositories"); -}; - -export default function RepositoryDetailsPage({ loaderData }: Route.ComponentProps) { +export default function RepositoryDetailsPage({ repositoryId }: { repositoryId: string }) { const queryClient = useQueryClient(); - const [searchParams, setSearchParams] = useSearchParams(); - const activeTab = searchParams.get("tab") || "info"; + const navigate = useNavigate(); + const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId" }); + const activeTab = tab || "info"; - const { data } = useQuery({ - ...getRepositoryOptions({ path: { id: loaderData.id } }), - initialData: loaderData, + const { data } = useSuspenseQuery({ + ...getRepositoryOptions({ path: { id: repositoryId } }), }); useEffect(() => { @@ -49,7 +30,7 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro return ( <> - setSearchParams({ tab: value })}> + navigate({ to: ".", search: (s) => ({ ...s, tab: value }) })}> Configuration Snapshots @@ -58,7 +39,9 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro - + + + diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index cd382073..28721f16 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -1,96 +1,94 @@ -import { useQuery } from "@tanstack/react-query"; -import { redirect, useParams, Link, Await } from "react-router"; -import { listBackupSchedulesOptions, listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { + getRepositoryOptions, + getSnapshotDetailsOptions, + listBackupSchedulesOptions, +} from "~/client/api-client/@tanstack/react-query.gen"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser"; import { formatDateTime } from "~/client/lib/datetime"; -import { parseError } from "~/client/lib/errors"; -import { getRepository, getSnapshotDetails } from "~/client/api-client"; -import type { Route } from "./+types/snapshot-details"; -import { Suspense, useState } from "react"; +import { useState } from "react"; import { Database } from "lucide-react"; +import { Link, useParams } from "@tanstack/react-router"; -export const handle = { - breadcrumb: (match: Route.MetaArgs) => [ - { label: "Repositories", href: "/repositories" }, - { label: match.loaderData?.repository.name || match.params.id, href: `/repositories/${match.params.id}` }, - { label: match.params.snapshotId }, - ], +// export const handle = { +// breadcrumb: (match: Route.MetaArgs) => [ +// { label: "Repositories", href: "/repositories" }, +// { label: match.loaderData?.repository.name || match.params.id, href: `/repositories/${match.params.id}` }, +// { label: match.params.snapshotId }, +// ], +// }; + +export const SnapshotError = () => { + const { repoId } = useParams({ from: "/(dashboard)/repositories/$repoId/$snapshotId" }); + + return ( + + + +

Snapshot not found

+

This snapshot does not exist in this repository

+

It may have been deleted manually outside of Zerobyte.

+
+ ({ tab: "snapshots" })} + params={{ repositoryId: repoId }} + > + + +
+
+
+ ); }; -export function meta({ params }: Route.MetaArgs) { - return [ - { title: `Zerobyte - Snapshot ${params.snapshotId}` }, - { - name: "description", - content: "Browse and 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 (!repository.data) return redirect("/repositories"); - - return { - snapshot: snapshot, - repository: repository.data, - snapshotError: parseError(snapshot.error)?.message ?? null, - }; +const FilebrowserFallback = ({ repositoryId, snapshotId }: { repositoryId: string; snapshotId: string }) => { + return ( + + ); }; -export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps) { - const { id, snapshotId } = useParams<{ - id: string; - snapshotId: string; - }>(); - +export function SnapshotDetailsPage({ repositoryId, snapshotId }: { repositoryId: string; snapshotId: string }) { const [showAllPaths, setShowAllPaths] = useState(false); - const { data } = useQuery({ - ...listSnapshotFilesOptions({ - path: { id: id ?? "", snapshotId: snapshotId ?? "" }, - query: { path: "/" }, - }), - enabled: !!id && !!snapshotId && !loaderData.snapshotError, + const { data: repository } = useSuspenseQuery({ + ...getRepositoryOptions({ path: { id: repositoryId } }), }); - const schedules = useQuery({ + const { data: schedules } = useSuspenseQuery({ ...listBackupSchedulesOptions(), }); - if (!id || !snapshotId) { - return ( -
-

Invalid snapshot reference

-
- ); - } + const { data, error } = useQuery({ + ...getSnapshotDetailsOptions({ path: { id: repositoryId, snapshotId: snapshotId } }), + }); + const backupSchedule = schedules?.find((s) => data?.tags.includes(s.shortId)); - if (loaderData.snapshotError) { + if (error) { return ( - - - -

Snapshot not found

-

- This snapshot does not exist in {loaderData.repository.name}. -

-

It may have been deleted manually outside of Zerobyte.

-
- - - + <> +
+
+

{repository.name}

+

Snapshot: {snapshotId}

- - +
+ + ); } @@ -98,29 +96,18 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
-

{loaderData.repository.name}

+

{repository.name}

Snapshot: {snapshotId}

- - } - > - - {(value) => { - if (!value.data) return
Snapshot data not found.
; + {data ? ( + + ) : ( + + )} - return ; - }} -
-
- - {data?.snapshot && ( + {data && ( Snapshot Information @@ -129,69 +116,58 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
Snapshot ID: -

{data.snapshot.id}

+

{data.short_id}

Short ID: -

{data.snapshot.short_id}

+

{data.short_id}

Hostname: -

{data.snapshot.hostname}

+

{data.hostname}

Time: -

{formatDateTime(data.snapshot.time)}

+

{formatDateTime(data.time)}

- Loading...
}> - - {(value) => { - if (!value.data) return null; - - const backupSchedule = schedules.data?.find((s) => value.data.tags.includes(s.shortId)); - - return ( - <> -
- Backup Schedule: -

- - {backupSchedule?.name} - -

-
-
- Volume: -

- - {backupSchedule?.volume.name} - -

-
- - ); - }} -
- + <> +
+ Backup Schedule: +

+ + {backupSchedule?.name} + +

+
+
+ Volume: +

+ + {backupSchedule?.volume.name} + +

+
+
Paths:
- {data.snapshot.paths.slice(0, showAllPaths ? undefined : 20).map((path) => ( + {data.paths.slice(0, showAllPaths ? undefined : 20).map((path) => (

{path}

))} - {data.snapshot.paths.length > 20 && ( + {data.paths.length > 20 && ( )}
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx index f71572a1..41c1c66a 100644 --- a/app/client/modules/repositories/tabs/info.tsx +++ b/app/client/modules/repositories/tabs/info.tsx @@ -30,7 +30,7 @@ import { import type { CompressionMode, RepositoryConfig } from "~/schemas/restic"; import { DoctorReport } from "../components/doctor-report"; import { parseError } from "~/client/lib/errors"; -import { useNavigate } from "react-router"; +import { useNavigate } from "@tanstack/react-router"; type Props = { repository: Repository; @@ -73,7 +73,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => { ...deleteRepositoryMutation(), onSuccess: () => { toast.success("Repository deleted successfully"); - void navigate("/repositories"); + void navigate({ to: "/repositories" }); }, onError: (error) => { toast.error("Failed to delete repository", { @@ -115,7 +115,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setShowConfirmDialog(true); }; diff --git a/app/routeTree.gen.ts b/app/routeTree.gen.ts index 760276ea..8f4c2ebc 100644 --- a/app/routeTree.gen.ts +++ b/app/routeTree.gen.ts @@ -15,7 +15,10 @@ import { Route as authOnboardingRouteImport } from './routes/(auth)/onboarding' 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 dashboardVolumesVolumeIdRouteImport } from './routes/(dashboard)/volumes/$volumeId' +import { Route as dashboardRepositoriesRepositoryIdRouteImport } from './routes/(dashboard)/repositories/$repositoryId' +import { Route as dashboardRepositoriesRepoIdSnapshotIdRouteImport } from './routes/(dashboard)/repositories/$repoId.$snapshotId' const dashboardRouteRoute = dashboardRouteRouteImport.update({ id: '/(dashboard)', @@ -46,28 +49,52 @@ const dashboardVolumesIndexRoute = dashboardVolumesIndexRouteImport.update({ path: '/volumes/', getParentRoute: () => dashboardRouteRoute, } as any) +const dashboardRepositoriesIndexRoute = + dashboardRepositoriesIndexRouteImport.update({ + id: '/repositories/', + path: '/repositories/', + getParentRoute: () => dashboardRouteRoute, + } as any) const dashboardVolumesVolumeIdRoute = dashboardVolumesVolumeIdRouteImport.update({ id: '/volumes/$volumeId', path: '/volumes/$volumeId', getParentRoute: () => dashboardRouteRoute, } as any) +const dashboardRepositoriesRepositoryIdRoute = + dashboardRepositoriesRepositoryIdRouteImport.update({ + id: '/repositories/$repositoryId', + path: '/repositories/$repositoryId', + getParentRoute: () => dashboardRouteRoute, + } as any) +const dashboardRepositoriesRepoIdSnapshotIdRoute = + dashboardRepositoriesRepoIdSnapshotIdRouteImport.update({ + id: '/repositories/$repoId/$snapshotId', + path: '/repositories/$repoId/$snapshotId', + getParentRoute: () => dashboardRouteRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/download-recovery-key': typeof authDownloadRecoveryKeyRoute '/login': typeof authLoginRoute '/onboarding': typeof authOnboardingRoute + '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute + '/repositories/': typeof dashboardRepositoriesIndexRoute '/volumes/': typeof dashboardVolumesIndexRoute + '/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/download-recovery-key': typeof authDownloadRecoveryKeyRoute '/login': typeof authLoginRoute '/onboarding': typeof authOnboardingRoute + '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute + '/repositories': typeof dashboardRepositoriesIndexRoute '/volumes': typeof dashboardVolumesIndexRoute + '/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -76,8 +103,11 @@ export interface FileRoutesById { '/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute '/(auth)/login': typeof authLoginRoute '/(auth)/onboarding': typeof authOnboardingRoute + '/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute '/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute + '/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute '/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute + '/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -86,16 +116,22 @@ export interface FileRouteTypes { | '/download-recovery-key' | '/login' | '/onboarding' + | '/repositories/$repositoryId' | '/volumes/$volumeId' + | '/repositories/' | '/volumes/' + | '/repositories/$repoId/$snapshotId' fileRoutesByTo: FileRoutesByTo to: | '/' | '/download-recovery-key' | '/login' | '/onboarding' + | '/repositories/$repositoryId' | '/volumes/$volumeId' + | '/repositories' | '/volumes' + | '/repositories/$repoId/$snapshotId' id: | '__root__' | '/' @@ -103,8 +139,11 @@ export interface FileRouteTypes { | '/(auth)/download-recovery-key' | '/(auth)/login' | '/(auth)/onboarding' + | '/(dashboard)/repositories/$repositoryId' | '/(dashboard)/volumes/$volumeId' + | '/(dashboard)/repositories/' | '/(dashboard)/volumes/' + | '/(dashboard)/repositories/$repoId/$snapshotId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -159,6 +198,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof dashboardVolumesIndexRouteImport parentRoute: typeof dashboardRouteRoute } + '/(dashboard)/repositories/': { + id: '/(dashboard)/repositories/' + path: '/repositories' + fullPath: '/repositories/' + preLoaderRoute: typeof dashboardRepositoriesIndexRouteImport + parentRoute: typeof dashboardRouteRoute + } '/(dashboard)/volumes/$volumeId': { id: '/(dashboard)/volumes/$volumeId' path: '/volumes/$volumeId' @@ -166,17 +212,39 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof dashboardVolumesVolumeIdRouteImport parentRoute: typeof dashboardRouteRoute } + '/(dashboard)/repositories/$repositoryId': { + id: '/(dashboard)/repositories/$repositoryId' + path: '/repositories/$repositoryId' + fullPath: '/repositories/$repositoryId' + preLoaderRoute: typeof dashboardRepositoriesRepositoryIdRouteImport + parentRoute: typeof dashboardRouteRoute + } + '/(dashboard)/repositories/$repoId/$snapshotId': { + id: '/(dashboard)/repositories/$repoId/$snapshotId' + path: '/repositories/$repoId/$snapshotId' + fullPath: '/repositories/$repoId/$snapshotId' + preLoaderRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRouteImport + parentRoute: typeof dashboardRouteRoute + } } } interface dashboardRouteRouteChildren { + dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute + dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute + dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute } const dashboardRouteRouteChildren: dashboardRouteRouteChildren = { + dashboardRepositoriesRepositoryIdRoute: + dashboardRepositoriesRepositoryIdRoute, dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute, + dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute, dashboardVolumesIndexRoute: dashboardVolumesIndexRoute, + dashboardRepositoriesRepoIdSnapshotIdRoute: + dashboardRepositoriesRepoIdSnapshotIdRoute, } const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren( diff --git a/app/router.tsx b/app/router.tsx index d75937a0..73d11cf6 100644 --- a/app/router.tsx +++ b/app/router.tsx @@ -11,6 +11,11 @@ client.setConfig({ export function getRouter() { const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, mutationCache: new MutationCache({ onSuccess: () => { void queryClient.invalidateQueries(); diff --git a/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx b/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx new file mode 100644 index 00000000..21542f24 --- /dev/null +++ b/app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx @@ -0,0 +1,44 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { + getRepositoryOptions, + getSnapshotDetailsOptions, + listSnapshotFilesOptions, +} from "~/client/api-client/@tanstack/react-query.gen"; +import { SnapshotDetailsPage } from "~/client/modules/repositories/routes/snapshot-details"; + +export const Route = createFileRoute("/(dashboard)/repositories/$repoId/$snapshotId")({ + component: RouteComponent, + errorComponent: (e) =>
{e.error.message}
, + loader: async ({ params, context }) => { + await context.queryClient.ensureQueryData({ + ...getRepositoryOptions({ path: { id: params.repoId } }), + }); + + context.queryClient.prefetchQuery({ + ...getSnapshotDetailsOptions({ + path: { id: params.repoId, snapshotId: params.snapshotId }, + }), + }); + context.queryClient.prefetchQuery({ + ...listSnapshotFilesOptions({ + path: { id: params.repoId, snapshotId: params.snapshotId }, + query: { path: "/" }, + }), + }); + }, + head: ({ params }) => ({ + meta: [ + { title: `Zerobyte - ${params.snapshotId}` }, + { + name: "description", + content: "Browse and restore files from a backup snapshot.", + }, + ], + }), +}); + +function RouteComponent() { + const { repoId, snapshotId } = Route.useParams(); + + return ; +} diff --git a/app/routes/(dashboard)/repositories/$repositoryId.tsx b/app/routes/(dashboard)/repositories/$repositoryId.tsx new file mode 100644 index 00000000..93dc8450 --- /dev/null +++ b/app/routes/(dashboard)/repositories/$repositoryId.tsx @@ -0,0 +1,43 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { type } from "arktype"; +import { + getRepositoryOptions, + listBackupSchedulesOptions, + listSnapshotsOptions, +} from "~/client/api-client/@tanstack/react-query.gen"; +import RepositoryDetailsPage from "~/client/modules/repositories/routes/repository-details"; + +export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId")({ + component: RouteComponent, + errorComponent: (e) =>
{e.error.message}
, + loader: async ({ params, context }) => { + context.queryClient.prefetchQuery({ + ...listSnapshotsOptions({ path: { id: params.repositoryId } }), + }) + context.queryClient.prefetchQuery({ + ...listBackupSchedulesOptions(), + }) + + const res = await context.queryClient.ensureQueryData({ + ...getRepositoryOptions({ path: { id: params.repositoryId } }), + }) + + return res; + }, + validateSearch: type({ tab: "string?" }), + head: ({ loaderData }) => ({ + meta: [ + { title: `Zerobyte - ${loaderData?.name}` }, + { + name: "description", + content: "View repository configuration, status, and snapshots.", + }, + ], + }), +}); + +function RouteComponent() { + const { repositoryId } = Route.useParams(); + + return ; +} diff --git a/app/routes/(dashboard)/repositories/index.tsx b/app/routes/(dashboard)/repositories/index.tsx new file mode 100644 index 00000000..f78591f5 --- /dev/null +++ b/app/routes/(dashboard)/repositories/index.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { RepositoriesPage } from "~/client/modules/repositories/routes/repositories"; + +export const Route = createFileRoute("/(dashboard)/repositories/")({ + component: RouteComponent, + loader: async ({ context }) => { + await context.queryClient.ensureQueryData({ + ...listRepositoriesOptions(), + }); + }, + errorComponent: (e) =>
{e.error.message}
, + head: () => ({ + meta: [ + { title: "Zerobyte - Repositories" }, + { + name: "description", + content: "Manage your backup repositories with encryption and compression.", + }, + ], + }), +}); + +function RouteComponent() { + return ; +} diff --git a/app/routes/(dashboard)/route.tsx b/app/routes/(dashboard)/route.tsx index 7194f797..86be3267 100644 --- a/app/routes/(dashboard)/route.tsx +++ b/app/routes/(dashboard)/route.tsx @@ -15,6 +15,7 @@ const fetchUser = createServerFn({ method: "GET" }).handler(async () => { export const Route = createFileRoute("/(dashboard)")({ component: PathlessLayoutComponent, + errorComponent: (e) =>
{e.error.message}
, server: { middleware: [authMiddleware], }, diff --git a/app/routes/(dashboard)/volumes/$volumeId.tsx b/app/routes/(dashboard)/volumes/$volumeId.tsx index b4905e98..7e97269e 100644 --- a/app/routes/(dashboard)/volumes/$volumeId.tsx +++ b/app/routes/(dashboard)/volumes/$volumeId.tsx @@ -5,6 +5,7 @@ import { VolumeDetails } from "~/client/modules/volumes/routes/volume-details"; export const Route = createFileRoute("/(dashboard)/volumes/$volumeId")({ component: RouteComponent, + errorComponent: (e) =>
{e.error.message}
, loader: async ({ params, context }) => { const res = await context.queryClient.ensureQueryData({ ...getVolumeOptions({ path: { id: params.volumeId } }), diff --git a/app/routes/(dashboard)/volumes/index.tsx b/app/routes/(dashboard)/volumes/index.tsx index 5d34f7e5..5fb24041 100644 --- a/app/routes/(dashboard)/volumes/index.tsx +++ b/app/routes/(dashboard)/volumes/index.tsx @@ -4,6 +4,7 @@ import { VolumesPage } from "~/client/modules/volumes/routes/volumes"; export const Route = createFileRoute("/(dashboard)/volumes/")({ component: VolumesPage, + errorComponent: (e) =>
{e.error.message}
, loader: async ({ context }) => { await context.queryClient.ensureQueryData(listVolumesOptions()); }, @@ -16,5 +17,4 @@ export const Route = createFileRoute("/(dashboard)/volumes/")({ }, ], }), - errorComponent: (err) =>
Failed to load volumes {JSON.stringify(err.error)}
, }); diff --git a/app/routes/__root.tsx b/app/routes/__root.tsx index d555e230..90415a43 100644 --- a/app/routes/__root.tsx +++ b/app/routes/__root.tsx @@ -4,6 +4,7 @@ import { apiClientMiddleware } from "~/middleware/api-client"; import type { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import { Toaster } from "~/client/components/ui/sonner"; export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ server: { @@ -33,6 +34,7 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()( ], }), component: RootLayout, + errorComponent: (e) =>
{e.error.message}
, }); function RootLayout() { @@ -53,6 +55,7 @@ function RootLayout() { + diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts index c9a3d051..2f762cef 100644 --- a/app/server/modules/repositories/repositories.controller.ts +++ b/app/server/modules/repositories/repositories.controller.ts @@ -142,6 +142,7 @@ export const repositoriesController = new Hono() duration, time: new Date(snapshot.time).getTime(), paths: snapshot.paths, + hostname: snapshot.hostname, size: snapshot.summary?.total_bytes_processed || 0, tags: snapshot.tags ?? [], retentionCategories: [], diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts index 73a8b22a..543337bc 100644 --- a/app/server/modules/repositories/repositories.dto.ts +++ b/app/server/modules/repositories/repositories.dto.ts @@ -179,6 +179,7 @@ export const snapshotSchema = type({ duration: "number", tags: "string[]", retentionCategories: "string[]", + hostname: "string?", }); const listSnapshotsResponse = snapshotSchema.array(); diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index ae49493b..215407c1 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -315,6 +315,8 @@ const getSnapshotDetails = async (id: string, snapshotId: string) => { const organizationId = getOrganizationId(); const repository = await findRepository(id); + console.log("Getting snapshot details for:", id, snapshotId); + if (!repository) { throw new NotFoundError("Repository not found"); }