refactor: repo & snapshot details
This commit is contained in:
parent
86a20cff2e
commit
551618d192
20 changed files with 349 additions and 213 deletions
|
|
@ -16,4 +16,4 @@ export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
|
||||||
override?: Config<ClientOptions & T>,
|
override?: Config<ClientOptions & T>,
|
||||||
) => Config<Required<ClientOptions> & T>;
|
) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }));
|
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: "http://192.168.2.42:4096" }));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
export type ClientOptions = {
|
export type ClientOptions = {
|
||||||
baseUrl: "http://localhost:4096" | (string & {});
|
baseUrl: "http://192.168.2.42:4096" | (string & {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetStatusData = {
|
export type GetStatusData = {
|
||||||
|
|
@ -1649,6 +1649,7 @@ export type ListSnapshotsResponses = {
|
||||||
size: number;
|
size: number;
|
||||||
tags: Array<string>;
|
tags: Array<string>;
|
||||||
time: number;
|
time: number;
|
||||||
|
hostname?: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1718,6 +1719,7 @@ export type GetSnapshotDetailsResponses = {
|
||||||
size: number;
|
size: number;
|
||||||
tags: Array<string>;
|
tags: Array<string>;
|
||||||
time: number;
|
time: number;
|
||||||
|
hostname?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Calendar, Clock, Database, HardDrive, Tag, Trash2, X } from "lucide-react";
|
import { Calendar, Clock, Database, HardDrive, Tag, Trash2, X } from "lucide-react";
|
||||||
import { Link, useNavigate } from "react-router";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
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 { parseError } from "~/client/lib/errors";
|
||||||
import type { BackupSchedule, Snapshot } from "../lib/types";
|
import type { BackupSchedule, Snapshot } from "../lib/types";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
snapshots: Snapshot[];
|
snapshots: Snapshot[];
|
||||||
|
|
@ -40,8 +40,8 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
|
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
|
||||||
|
|
@ -71,7 +71,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRowClick = (snapshotId: string) => {
|
const handleRowClick = (snapshotId: string) => {
|
||||||
void navigate(`/repositories/${repositoryId}/${snapshotId}`);
|
void navigate({ to: `/repositories/${repositoryId}/${snapshotId}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSelectAll = () => {
|
const toggleSelectAll = () => {
|
||||||
|
|
@ -172,7 +172,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
hidden={!backup}
|
hidden={!backup}
|
||||||
to={backup ? `/backups/${backup.id}` : "#"}
|
to={backup ? `/backups/${backup.id}` : "."}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="hover:underline"
|
className="hover:underline"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { FileIcon, RotateCcw, Trash2 } from "lucide-react";
|
import { FileIcon, RotateCcw, Trash2 } from "lucide-react";
|
||||||
import { Link } from "react-router";
|
|
||||||
import { FileTree } from "~/client/components/file-tree";
|
import { FileTree } from "~/client/components/file-tree";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { Button, buttonVariants } from "~/client/components/ui/button";
|
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 { formatDateTime } from "~/client/lib/datetime";
|
||||||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
snapshot: Snapshot;
|
snapshot: Snapshot;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Database, Plus, RotateCcw } from "lucide-react";
|
import { Database, Plus, RotateCcw } from "lucide-react";
|
||||||
import { useState } from "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 { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { RepositoryIcon } from "~/client/components/repository-icon";
|
import { RepositoryIcon } from "~/client/components/repository-icon";
|
||||||
import { Button } from "~/client/components/ui/button";
|
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 { Input } from "~/client/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
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 { cn } from "~/client/lib/utils";
|
||||||
import { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: () => [{ label: "Repositories" }],
|
breadcrumb: () => [{ label: "Repositories" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function RepositoriesPage() {
|
||||||
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) {
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState("");
|
const [statusFilter, setStatusFilter] = useState("");
|
||||||
const [backendFilter, setBackendFilter] = useState("");
|
const [backendFilter, setBackendFilter] = useState("");
|
||||||
|
|
@ -47,20 +29,18 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useSuspenseQuery({
|
||||||
...listRepositoriesOptions(),
|
...listRepositoriesOptions(),
|
||||||
initialData: loaderData,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredRepositories =
|
const filteredRepositories = data.filter((repository) => {
|
||||||
data?.filter((repository) => {
|
const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase());
|
const matchesStatus = !statusFilter || repository.status === statusFilter;
|
||||||
const matchesStatus = !statusFilter || repository.status === statusFilter;
|
const matchesBackend = !backendFilter || repository.type === backendFilter;
|
||||||
const matchesBackend = !backendFilter || repository.type === backendFilter;
|
return matchesSearch && matchesStatus && matchesBackend;
|
||||||
return matchesSearch && matchesStatus && matchesBackend;
|
});
|
||||||
}) || [];
|
|
||||||
|
|
||||||
const hasNoRepositories = data?.length === 0;
|
const hasNoRepositories = data.length === 0;
|
||||||
const hasNoFilteredRepositories = filteredRepositories.length === 0 && !hasNoRepositories;
|
const hasNoFilteredRepositories = filteredRepositories.length === 0 && !hasNoRepositories;
|
||||||
|
|
||||||
if (hasNoRepositories) {
|
if (hasNoRepositories) {
|
||||||
|
|
@ -70,7 +50,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||||
title="No repository"
|
title="No repository"
|
||||||
description="Repositories are remote storage locations where you can backup your volumes securely. Encrypted and optimized for storage efficiency."
|
description="Repositories are remote storage locations where you can backup your volumes securely. Encrypted and optimized for storage efficiency."
|
||||||
button={
|
button={
|
||||||
<Button onClick={() => navigate("/repositories/create")}>
|
<Button onClick={() => navigate({ to: "/repositories/create" })}>
|
||||||
<Plus size={16} className="mr-2" />
|
<Plus size={16} className="mr-2" />
|
||||||
Create repository
|
Create repository
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -117,7 +97,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<Button onClick={() => navigate("/repositories/create")}>
|
<Button onClick={() => navigate({ to: "/repositories/create" })}>
|
||||||
<Plus size={16} className="mr-2" />
|
<Plus size={16} className="mr-2" />
|
||||||
Create Repository
|
Create Repository
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -126,7 +106,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||||
<Table className="border-t">
|
<Table className="border-t">
|
||||||
<TableHeader className="bg-card-header">
|
<TableHeader className="bg-card-header">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
<TableHead className="w-25 uppercase">Name</TableHead>
|
||||||
<TableHead className="uppercase text-left">Backend</TableHead>
|
<TableHead className="uppercase text-left">Backend</TableHead>
|
||||||
<TableHead className="uppercase hidden sm:table-cell">Compression</TableHead>
|
<TableHead className="uppercase hidden sm:table-cell">Compression</TableHead>
|
||||||
<TableHead className="uppercase text-center">Status</TableHead>
|
<TableHead className="uppercase text-center">Status</TableHead>
|
||||||
|
|
@ -150,7 +130,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||||
<TableRow
|
<TableRow
|
||||||
key={repository.id}
|
key={repository.id}
|
||||||
className="hover:bg-accent/50 hover:cursor-pointer"
|
className="hover:bg-accent/50 hover:cursor-pointer"
|
||||||
onClick={() => navigate(`/repositories/${repository.shortId}`)}
|
onClick={() => navigate({ to: `/repositories/${repository.shortId}` })}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium text-strong-accent">{repository.name}</TableCell>
|
<TableCell className="font-medium text-strong-accent">{repository.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,27 @@
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { redirect, useSearchParams } from "react-router";
|
import { Suspense, useEffect } from "react";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { getRepositoryOptions, listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||||
import { RepositoryInfoTabContent } from "../tabs/info";
|
import { RepositoryInfoTabContent } from "../tabs/info";
|
||||||
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
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?.name || match.params.id },
|
// { label: match.loaderData?.name || match.params.id },
|
||||||
],
|
// ],
|
||||||
};
|
// };
|
||||||
|
|
||||||
export function meta({ params, loaderData }: Route.MetaArgs) {
|
export default function RepositoryDetailsPage({ repositoryId }: { repositoryId: string }) {
|
||||||
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) {
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const navigate = useNavigate();
|
||||||
const activeTab = searchParams.get("tab") || "info";
|
const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId" });
|
||||||
|
const activeTab = tab || "info";
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useSuspenseQuery({
|
||||||
...getRepositoryOptions({ path: { id: loaderData.id } }),
|
...getRepositoryOptions({ path: { id: repositoryId } }),
|
||||||
initialData: loaderData,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -49,7 +30,7 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs value={activeTab} onValueChange={(value) => setSearchParams({ tab: value })}>
|
<Tabs value={activeTab} onValueChange={(value) => navigate({ to: ".", search: (s) => ({ ...s, tab: value }) })}>
|
||||||
<TabsList className="mb-2">
|
<TabsList className="mb-2">
|
||||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||||
<TabsTrigger value="snapshots">Snapshots</TabsTrigger>
|
<TabsTrigger value="snapshots">Snapshots</TabsTrigger>
|
||||||
|
|
@ -58,7 +39,9 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
||||||
<RepositoryInfoTabContent repository={data} />
|
<RepositoryInfoTabContent repository={data} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="snapshots">
|
<TabsContent value="snapshots">
|
||||||
<RepositorySnapshotsTabContent repository={data} />
|
<Suspense>
|
||||||
|
<RepositorySnapshotsTabContent repository={data} />
|
||||||
|
</Suspense>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,94 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { redirect, useParams, Link, Await } from "react-router";
|
import {
|
||||||
import { listBackupSchedulesOptions, listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
getRepositoryOptions,
|
||||||
|
getSnapshotDetailsOptions,
|
||||||
|
listBackupSchedulesOptions,
|
||||||
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
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 { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
|
import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
|
||||||
import { formatDateTime } from "~/client/lib/datetime";
|
import { formatDateTime } from "~/client/lib/datetime";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { useState } from "react";
|
||||||
import { getRepository, getSnapshotDetails } from "~/client/api-client";
|
|
||||||
import type { Route } from "./+types/snapshot-details";
|
|
||||||
import { Suspense, useState } from "react";
|
|
||||||
import { Database } from "lucide-react";
|
import { Database } from "lucide-react";
|
||||||
|
import { Link, useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
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 },
|
// { label: match.params.snapshotId },
|
||||||
],
|
// ],
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const SnapshotError = () => {
|
||||||
|
const { repoId } = useParams({ from: "/(dashboard)/repositories/$repoId/$snapshotId" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center justify-center text-center py-12">
|
||||||
|
<Database className="mb-4 h-12 w-12 text-destructive" />
|
||||||
|
<p className="text-destructive font-semibold">Snapshot not found</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">This snapshot does not exist in this repository</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">It may have been deleted manually outside of Zerobyte.</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Link
|
||||||
|
to={`/repositories/$repositoryId`}
|
||||||
|
search={() => ({ tab: "snapshots" })}
|
||||||
|
params={{ repositoryId: repoId }}
|
||||||
|
>
|
||||||
|
<Button variant="outline">Back to repository</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function meta({ params }: Route.MetaArgs) {
|
const FilebrowserFallback = ({ repositoryId, snapshotId }: { repositoryId: string; snapshotId: string }) => {
|
||||||
return [
|
return (
|
||||||
{ title: `Zerobyte - Snapshot ${params.snapshotId}` },
|
<SnapshotFileBrowser
|
||||||
{
|
repositoryId={repositoryId}
|
||||||
name: "description",
|
snapshot={{
|
||||||
content: "Browse and restore files from a backup snapshot.",
|
duration: 0,
|
||||||
},
|
paths: [],
|
||||||
];
|
short_id: snapshotId,
|
||||||
}
|
size: 0,
|
||||||
|
tags: [],
|
||||||
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
|
time: 0,
|
||||||
const [snapshot, repository] = await Promise.all([
|
hostname: "",
|
||||||
getSnapshotDetails({
|
retentionCategories: [],
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps) {
|
export function SnapshotDetailsPage({ repositoryId, snapshotId }: { repositoryId: string; snapshotId: string }) {
|
||||||
const { id, snapshotId } = useParams<{
|
|
||||||
id: string;
|
|
||||||
snapshotId: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const [showAllPaths, setShowAllPaths] = useState(false);
|
const [showAllPaths, setShowAllPaths] = useState(false);
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data: repository } = useSuspenseQuery({
|
||||||
...listSnapshotFilesOptions({
|
...getRepositoryOptions({ path: { id: repositoryId } }),
|
||||||
path: { id: id ?? "", snapshotId: snapshotId ?? "" },
|
|
||||||
query: { path: "/" },
|
|
||||||
}),
|
|
||||||
enabled: !!id && !!snapshotId && !loaderData.snapshotError,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const schedules = useQuery({
|
const { data: schedules } = useSuspenseQuery({
|
||||||
...listBackupSchedulesOptions(),
|
...listBackupSchedulesOptions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!id || !snapshotId) {
|
const { data, error } = useQuery({
|
||||||
return (
|
...getSnapshotDetailsOptions({ path: { id: repositoryId, snapshotId: snapshotId } }),
|
||||||
<div className="flex items-center justify-center h-full">
|
});
|
||||||
<p className="text-destructive">Invalid snapshot reference</p>
|
const backupSchedule = schedules?.find((s) => data?.tags.includes(s.shortId));
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loaderData.snapshotError) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<>
|
||||||
<CardContent className="flex flex-col items-center justify-center text-center py-12">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<Database className="mb-4 h-12 w-12 text-destructive" />
|
<div>
|
||||||
<p className="text-destructive font-semibold">Snapshot not found</p>
|
<h1 className="text-2xl font-bold">{repository.name}</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
<p className="text-sm text-muted-foreground">Snapshot: {snapshotId}</p>
|
||||||
This snapshot does not exist in {loaderData.repository.name}.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">It may have been deleted manually outside of Zerobyte.</p>
|
|
||||||
<div className="mt-4">
|
|
||||||
<Link to={`/repositories/${id}?tab=snapshots`}>
|
|
||||||
<Button variant="outline">Back to repository</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
<SnapshotError />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,29 +96,18 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">{loaderData.repository.name}</h1>
|
<h1 className="text-2xl font-bold">{repository.name}</h1>
|
||||||
<p className="text-sm text-muted-foreground">Snapshot: {snapshotId}</p>
|
<p className="text-sm text-muted-foreground">Snapshot: {snapshotId}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Suspense
|
{data ? (
|
||||||
fallback={
|
<SnapshotFileBrowser repositoryId={repositoryId} snapshot={data} />
|
||||||
<SnapshotFileBrowser
|
) : (
|
||||||
repositoryId={id}
|
<FilebrowserFallback repositoryId={repositoryId} snapshotId={snapshotId} />
|
||||||
snapshot={{ duration: 0, paths: [], short_id: "", size: 0, tags: [], time: 0, retentionCategories: [] }}
|
)}
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Await resolve={loaderData.snapshot}>
|
|
||||||
{(value) => {
|
|
||||||
if (!value.data) return <div className="text-destructive">Snapshot data not found.</div>;
|
|
||||||
|
|
||||||
return <SnapshotFileBrowser repositoryId={id} snapshot={value.data} />;
|
{data && (
|
||||||
}}
|
|
||||||
</Await>
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{data?.snapshot && (
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Snapshot Information</CardTitle>
|
<CardTitle>Snapshot Information</CardTitle>
|
||||||
|
|
@ -129,69 +116,58 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Snapshot ID:</span>
|
<span className="text-muted-foreground">Snapshot ID:</span>
|
||||||
<p className="font-mono break-all">{data.snapshot.id}</p>
|
<p className="font-mono break-all">{data.short_id}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Short ID:</span>
|
<span className="text-muted-foreground">Short ID:</span>
|
||||||
<p className="font-mono break-all">{data.snapshot.short_id}</p>
|
<p className="font-mono break-all">{data.short_id}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Hostname:</span>
|
<span className="text-muted-foreground">Hostname:</span>
|
||||||
<p>{data.snapshot.hostname}</p>
|
<p>{data.hostname}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Time:</span>
|
<span className="text-muted-foreground">Time:</span>
|
||||||
<p>{formatDateTime(data.snapshot.time)}</p>
|
<p>{formatDateTime(data.time)}</p>
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<>
|
||||||
<Await resolve={loaderData.snapshot}>
|
<div>
|
||||||
{(value) => {
|
<span className="text-muted-foreground">Backup Schedule:</span>
|
||||||
if (!value.data) return null;
|
<p>
|
||||||
|
<Link to={`/backups/$backupScheduleId`} className="text-primary hover:underline">
|
||||||
const backupSchedule = schedules.data?.find((s) => value.data.tags.includes(s.shortId));
|
{backupSchedule?.name}
|
||||||
|
</Link>
|
||||||
return (
|
</p>
|
||||||
<>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Backup Schedule:</span>
|
<span className="text-muted-foreground">Volume:</span>
|
||||||
<p>
|
<p>
|
||||||
<Link to={`/backups/${backupSchedule?.id}`} className="text-primary hover:underline">
|
<Link
|
||||||
{backupSchedule?.name}
|
to={`/volumes/$volumeId`}
|
||||||
</Link>
|
className="text-primary hover:underline"
|
||||||
</p>
|
params={{ volumeId: backupSchedule?.volume.shortId || "" }}
|
||||||
</div>
|
>
|
||||||
<div>
|
{backupSchedule?.volume.name}
|
||||||
<span className="text-muted-foreground">Volume:</span>
|
</Link>
|
||||||
<p>
|
</p>
|
||||||
<Link
|
</div>
|
||||||
to={`/volumes/${backupSchedule?.volume.shortId}`}
|
</>
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{backupSchedule?.volume.name}
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Await>
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<span className="text-muted-foreground">Paths:</span>
|
<span className="text-muted-foreground">Paths:</span>
|
||||||
<div className="space-y-1 mt-1">
|
<div className="space-y-1 mt-1">
|
||||||
{data.snapshot.paths.slice(0, showAllPaths ? undefined : 20).map((path) => (
|
{data.paths.slice(0, showAllPaths ? undefined : 20).map((path) => (
|
||||||
<p key={path} className="font-mono text-xs bg-muted px-2 py-1 rounded break-all">
|
<p key={path} className="font-mono text-xs bg-muted px-2 py-1 rounded break-all">
|
||||||
{path}
|
{path}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
{data.snapshot.paths.length > 20 && (
|
{data.paths.length > 20 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAllPaths(!showAllPaths)}
|
onClick={() => setShowAllPaths(!showAllPaths)}
|
||||||
className="text-xs text-primary hover:underline mt-1"
|
className="text-xs text-primary hover:underline mt-1"
|
||||||
>
|
>
|
||||||
{showAllPaths ? "Show less" : `+ ${data.snapshot.paths.length - 20} more`}
|
{showAllPaths ? "Show less" : `+ ${data.paths.length - 20} more`}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
||||||
import { DoctorReport } from "../components/doctor-report";
|
import { DoctorReport } from "../components/doctor-report";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
|
|
@ -73,7 +73,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
||||||
...deleteRepositoryMutation(),
|
...deleteRepositoryMutation(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Repository deleted successfully");
|
toast.success("Repository deleted successfully");
|
||||||
void navigate("/repositories");
|
void navigate({ to: "/repositories" });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to delete repository", {
|
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();
|
e.preventDefault();
|
||||||
setShowConfirmDialog(true);
|
setShowConfirmDialog(true);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ import { Route as authOnboardingRouteImport } from './routes/(auth)/onboarding'
|
||||||
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
||||||
import { Route as authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key'
|
import { Route as authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key'
|
||||||
import { Route as dashboardVolumesIndexRouteImport } from './routes/(dashboard)/volumes/index'
|
import { Route as dashboardVolumesIndexRouteImport } from './routes/(dashboard)/volumes/index'
|
||||||
|
import { Route as dashboardRepositoriesIndexRouteImport } from './routes/(dashboard)/repositories/index'
|
||||||
import { Route as dashboardVolumesVolumeIdRouteImport } from './routes/(dashboard)/volumes/$volumeId'
|
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({
|
const dashboardRouteRoute = dashboardRouteRouteImport.update({
|
||||||
id: '/(dashboard)',
|
id: '/(dashboard)',
|
||||||
|
|
@ -46,28 +49,52 @@ const dashboardVolumesIndexRoute = dashboardVolumesIndexRouteImport.update({
|
||||||
path: '/volumes/',
|
path: '/volumes/',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const dashboardRepositoriesIndexRoute =
|
||||||
|
dashboardRepositoriesIndexRouteImport.update({
|
||||||
|
id: '/repositories/',
|
||||||
|
path: '/repositories/',
|
||||||
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
const dashboardVolumesVolumeIdRoute =
|
const dashboardVolumesVolumeIdRoute =
|
||||||
dashboardVolumesVolumeIdRouteImport.update({
|
dashboardVolumesVolumeIdRouteImport.update({
|
||||||
id: '/volumes/$volumeId',
|
id: '/volumes/$volumeId',
|
||||||
path: '/volumes/$volumeId',
|
path: '/volumes/$volumeId',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const 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 {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/onboarding': typeof authOnboardingRoute
|
'/onboarding': typeof authOnboardingRoute
|
||||||
|
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||||
|
'/repositories/': typeof dashboardRepositoriesIndexRoute
|
||||||
'/volumes/': typeof dashboardVolumesIndexRoute
|
'/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
|
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/onboarding': typeof authOnboardingRoute
|
'/onboarding': typeof authOnboardingRoute
|
||||||
|
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||||
|
'/repositories': typeof dashboardRepositoriesIndexRoute
|
||||||
'/volumes': typeof dashboardVolumesIndexRoute
|
'/volumes': typeof dashboardVolumesIndexRoute
|
||||||
|
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
|
|
@ -76,8 +103,11 @@ export interface FileRoutesById {
|
||||||
'/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/(auth)/login': typeof authLoginRoute
|
'/(auth)/login': typeof authLoginRoute
|
||||||
'/(auth)/onboarding': typeof authOnboardingRoute
|
'/(auth)/onboarding': typeof authOnboardingRoute
|
||||||
|
'/(dashboard)/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||||
|
'/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute
|
||||||
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
|
'/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
|
@ -86,16 +116,22 @@ export interface FileRouteTypes {
|
||||||
| '/download-recovery-key'
|
| '/download-recovery-key'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
|
| '/repositories/$repositoryId'
|
||||||
| '/volumes/$volumeId'
|
| '/volumes/$volumeId'
|
||||||
|
| '/repositories/'
|
||||||
| '/volumes/'
|
| '/volumes/'
|
||||||
|
| '/repositories/$repoId/$snapshotId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/download-recovery-key'
|
| '/download-recovery-key'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
|
| '/repositories/$repositoryId'
|
||||||
| '/volumes/$volumeId'
|
| '/volumes/$volumeId'
|
||||||
|
| '/repositories'
|
||||||
| '/volumes'
|
| '/volumes'
|
||||||
|
| '/repositories/$repoId/$snapshotId'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
|
@ -103,8 +139,11 @@ export interface FileRouteTypes {
|
||||||
| '/(auth)/download-recovery-key'
|
| '/(auth)/download-recovery-key'
|
||||||
| '/(auth)/login'
|
| '/(auth)/login'
|
||||||
| '/(auth)/onboarding'
|
| '/(auth)/onboarding'
|
||||||
|
| '/(dashboard)/repositories/$repositoryId'
|
||||||
| '/(dashboard)/volumes/$volumeId'
|
| '/(dashboard)/volumes/$volumeId'
|
||||||
|
| '/(dashboard)/repositories/'
|
||||||
| '/(dashboard)/volumes/'
|
| '/(dashboard)/volumes/'
|
||||||
|
| '/(dashboard)/repositories/$repoId/$snapshotId'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
|
|
@ -159,6 +198,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardVolumesIndexRouteImport
|
preLoaderRoute: typeof dashboardVolumesIndexRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
parentRoute: typeof dashboardRouteRoute
|
||||||
}
|
}
|
||||||
|
'/(dashboard)/repositories/': {
|
||||||
|
id: '/(dashboard)/repositories/'
|
||||||
|
path: '/repositories'
|
||||||
|
fullPath: '/repositories/'
|
||||||
|
preLoaderRoute: typeof dashboardRepositoriesIndexRouteImport
|
||||||
|
parentRoute: typeof dashboardRouteRoute
|
||||||
|
}
|
||||||
'/(dashboard)/volumes/$volumeId': {
|
'/(dashboard)/volumes/$volumeId': {
|
||||||
id: '/(dashboard)/volumes/$volumeId'
|
id: '/(dashboard)/volumes/$volumeId'
|
||||||
path: '/volumes/$volumeId'
|
path: '/volumes/$volumeId'
|
||||||
|
|
@ -166,17 +212,39 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardVolumesVolumeIdRouteImport
|
preLoaderRoute: typeof dashboardVolumesVolumeIdRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
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 {
|
interface dashboardRouteRouteChildren {
|
||||||
|
dashboardRepositoriesRepositoryIdRoute: typeof dashboardRepositoriesRepositoryIdRoute
|
||||||
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
|
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
|
||||||
|
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
|
||||||
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
||||||
|
dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
|
const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
|
||||||
|
dashboardRepositoriesRepositoryIdRoute:
|
||||||
|
dashboardRepositoriesRepositoryIdRoute,
|
||||||
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
|
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
|
||||||
|
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
|
||||||
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
||||||
|
dashboardRepositoriesRepoIdSnapshotIdRoute:
|
||||||
|
dashboardRepositoriesRepoIdSnapshotIdRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren(
|
const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ client.setConfig({
|
||||||
|
|
||||||
export function getRouter() {
|
export function getRouter() {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
mutationCache: new MutationCache({
|
mutationCache: new MutationCache({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries();
|
void queryClient.invalidateQueries();
|
||||||
|
|
|
||||||
44
app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx
Normal file
44
app/routes/(dashboard)/repositories/$repoId.$snapshotId.tsx
Normal file
|
|
@ -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) => <div>{e.error.message}</div>,
|
||||||
|
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 <SnapshotDetailsPage repositoryId={repoId} snapshotId={snapshotId} />;
|
||||||
|
}
|
||||||
43
app/routes/(dashboard)/repositories/$repositoryId.tsx
Normal file
43
app/routes/(dashboard)/repositories/$repositoryId.tsx
Normal file
|
|
@ -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) => <div>{e.error.message}</div>,
|
||||||
|
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 <RepositoryDetailsPage repositoryId={repositoryId} />;
|
||||||
|
}
|
||||||
26
app/routes/(dashboard)/repositories/index.tsx
Normal file
26
app/routes/(dashboard)/repositories/index.tsx
Normal file
|
|
@ -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) => <div>{e.error.message}</div>,
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ title: "Zerobyte - Repositories" },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Manage your backup repositories with encryption and compression.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <RepositoriesPage />;
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
|
||||||
|
|
||||||
export const Route = createFileRoute("/(dashboard)")({
|
export const Route = createFileRoute("/(dashboard)")({
|
||||||
component: PathlessLayoutComponent,
|
component: PathlessLayoutComponent,
|
||||||
|
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||||
server: {
|
server: {
|
||||||
middleware: [authMiddleware],
|
middleware: [authMiddleware],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { VolumeDetails } from "~/client/modules/volumes/routes/volume-details";
|
||||||
|
|
||||||
export const Route = createFileRoute("/(dashboard)/volumes/$volumeId")({
|
export const Route = createFileRoute("/(dashboard)/volumes/$volumeId")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||||
loader: async ({ params, context }) => {
|
loader: async ({ params, context }) => {
|
||||||
const res = await context.queryClient.ensureQueryData({
|
const res = await context.queryClient.ensureQueryData({
|
||||||
...getVolumeOptions({ path: { id: params.volumeId } }),
|
...getVolumeOptions({ path: { id: params.volumeId } }),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { VolumesPage } from "~/client/modules/volumes/routes/volumes";
|
||||||
|
|
||||||
export const Route = createFileRoute("/(dashboard)/volumes/")({
|
export const Route = createFileRoute("/(dashboard)/volumes/")({
|
||||||
component: VolumesPage,
|
component: VolumesPage,
|
||||||
|
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
await context.queryClient.ensureQueryData(listVolumesOptions());
|
await context.queryClient.ensureQueryData(listVolumesOptions());
|
||||||
},
|
},
|
||||||
|
|
@ -16,5 +17,4 @@ export const Route = createFileRoute("/(dashboard)/volumes/")({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
errorComponent: (err) => <div>Failed to load volumes {JSON.stringify(err.error)}</div>,
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { apiClientMiddleware } from "~/middleware/api-client";
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
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";
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
||||||
server: {
|
server: {
|
||||||
|
|
@ -33,6 +34,7 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
component: RootLayout,
|
component: RootLayout,
|
||||||
|
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RootLayout() {
|
function RootLayout() {
|
||||||
|
|
@ -53,6 +55,7 @@ function RootLayout() {
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<TanStackRouterDevtools position="bottom-right" />
|
<TanStackRouterDevtools position="bottom-right" />
|
||||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||||
|
<Toaster />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ export const repositoriesController = new Hono()
|
||||||
duration,
|
duration,
|
||||||
time: new Date(snapshot.time).getTime(),
|
time: new Date(snapshot.time).getTime(),
|
||||||
paths: snapshot.paths,
|
paths: snapshot.paths,
|
||||||
|
hostname: snapshot.hostname,
|
||||||
size: snapshot.summary?.total_bytes_processed || 0,
|
size: snapshot.summary?.total_bytes_processed || 0,
|
||||||
tags: snapshot.tags ?? [],
|
tags: snapshot.tags ?? [],
|
||||||
retentionCategories: [],
|
retentionCategories: [],
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ export const snapshotSchema = type({
|
||||||
duration: "number",
|
duration: "number",
|
||||||
tags: "string[]",
|
tags: "string[]",
|
||||||
retentionCategories: "string[]",
|
retentionCategories: "string[]",
|
||||||
|
hostname: "string?",
|
||||||
});
|
});
|
||||||
|
|
||||||
const listSnapshotsResponse = snapshotSchema.array();
|
const listSnapshotsResponse = snapshotSchema.array();
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,8 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue