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>,
|
||||
) => 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
|
||||
|
||||
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<string>;
|
||||
time: number;
|
||||
hostname?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -1718,6 +1719,7 @@ export type GetSnapshotDetailsResponses = {
|
|||
size: number;
|
||||
tags: Array<string>;
|
||||
time: number;
|
||||
hostname?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Set<string>>(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) => {
|
|||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
hidden={!backup}
|
||||
to={backup ? `/backups/${backup.id}` : "#"}
|
||||
to={backup ? `/backups/${backup.id}` : "."}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="hover:underline"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
<Button onClick={() => navigate("/repositories/create")}>
|
||||
<Button onClick={() => navigate({ to: "/repositories/create" })}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Create repository
|
||||
</Button>
|
||||
|
|
@ -117,7 +97,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
|||
</Button>
|
||||
)}
|
||||
</span>
|
||||
<Button onClick={() => navigate("/repositories/create")}>
|
||||
<Button onClick={() => navigate({ to: "/repositories/create" })}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Create Repository
|
||||
</Button>
|
||||
|
|
@ -126,7 +106,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
|||
<Table className="border-t">
|
||||
<TableHeader className="bg-card-header">
|
||||
<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 hidden sm:table-cell">Compression</TableHead>
|
||||
<TableHead className="uppercase text-center">Status</TableHead>
|
||||
|
|
@ -150,7 +130,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
|||
<TableRow
|
||||
key={repository.id}
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<Tabs value={activeTab} onValueChange={(value) => setSearchParams({ tab: value })}>
|
||||
<Tabs value={activeTab} onValueChange={(value) => navigate({ to: ".", search: (s) => ({ ...s, tab: value }) })}>
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||
<TabsTrigger value="snapshots">Snapshots</TabsTrigger>
|
||||
|
|
@ -58,7 +39,9 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||
<RepositoryInfoTabContent repository={data} />
|
||||
</TabsContent>
|
||||
<TabsContent value="snapshots">
|
||||
<RepositorySnapshotsTabContent repository={data} />
|
||||
<Suspense>
|
||||
<RepositorySnapshotsTabContent repository={data} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<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) {
|
||||
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 (
|
||||
<SnapshotFileBrowser
|
||||
repositoryId={repositoryId}
|
||||
snapshot={{
|
||||
duration: 0,
|
||||
paths: [],
|
||||
short_id: snapshotId,
|
||||
size: 0,
|
||||
tags: [],
|
||||
time: 0,
|
||||
hostname: "",
|
||||
retentionCategories: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-destructive">Invalid snapshot reference</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<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 {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 className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{repository.name}</h1>
|
||||
<p className="text-sm text-muted-foreground">Snapshot: {snapshotId}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<SnapshotError />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -98,29 +96,18 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<SnapshotFileBrowser
|
||||
repositoryId={id}
|
||||
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>;
|
||||
{data ? (
|
||||
<SnapshotFileBrowser repositoryId={repositoryId} snapshot={data} />
|
||||
) : (
|
||||
<FilebrowserFallback repositoryId={repositoryId} snapshotId={snapshotId} />
|
||||
)}
|
||||
|
||||
return <SnapshotFileBrowser repositoryId={id} snapshot={value.data} />;
|
||||
}}
|
||||
</Await>
|
||||
</Suspense>
|
||||
|
||||
{data?.snapshot && (
|
||||
{data && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Snapshot Information</CardTitle>
|
||||
|
|
@ -129,69 +116,58 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<span className="text-muted-foreground">Hostname:</span>
|
||||
<p>{data.snapshot.hostname}</p>
|
||||
<p>{data.hostname}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Time:</span>
|
||||
<p>{formatDateTime(data.snapshot.time)}</p>
|
||||
<p>{formatDateTime(data.time)}</p>
|
||||
</div>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Await resolve={loaderData.snapshot}>
|
||||
{(value) => {
|
||||
if (!value.data) return null;
|
||||
|
||||
const backupSchedule = schedules.data?.find((s) => value.data.tags.includes(s.shortId));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Backup Schedule:</span>
|
||||
<p>
|
||||
<Link to={`/backups/${backupSchedule?.id}`} className="text-primary hover:underline">
|
||||
{backupSchedule?.name}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Volume:</span>
|
||||
<p>
|
||||
<Link
|
||||
to={`/volumes/${backupSchedule?.volume.shortId}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{backupSchedule?.volume.name}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Await>
|
||||
</Suspense>
|
||||
<>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Backup Schedule:</span>
|
||||
<p>
|
||||
<Link to={`/backups/$backupScheduleId`} className="text-primary hover:underline">
|
||||
{backupSchedule?.name}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Volume:</span>
|
||||
<p>
|
||||
<Link
|
||||
to={`/volumes/$volumeId`}
|
||||
className="text-primary hover:underline"
|
||||
params={{ volumeId: backupSchedule?.volume.shortId || "" }}
|
||||
>
|
||||
{backupSchedule?.volume.name}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div className="col-span-2">
|
||||
<span className="text-muted-foreground">Paths:</span>
|
||||
<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">
|
||||
{path}
|
||||
</p>
|
||||
))}
|
||||
{data.snapshot.paths.length > 20 && (
|
||||
{data.paths.length > 20 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllPaths(!showAllPaths)}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ client.setConfig({
|
|||
|
||||
export function getRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
mutationCache: new MutationCache({
|
||||
onSuccess: () => {
|
||||
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)")({
|
||||
component: PathlessLayoutComponent,
|
||||
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||
server: {
|
||||
middleware: [authMiddleware],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { VolumeDetails } from "~/client/modules/volumes/routes/volume-details";
|
|||
|
||||
export const Route = createFileRoute("/(dashboard)/volumes/$volumeId")({
|
||||
component: RouteComponent,
|
||||
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||
loader: async ({ params, context }) => {
|
||||
const res = await context.queryClient.ensureQueryData({
|
||||
...getVolumeOptions({ path: { id: params.volumeId } }),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { VolumesPage } from "~/client/modules/volumes/routes/volumes";
|
|||
|
||||
export const Route = createFileRoute("/(dashboard)/volumes/")({
|
||||
component: VolumesPage,
|
||||
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||
loader: async ({ context }) => {
|
||||
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 { 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) => <div>{e.error.message}</div>,
|
||||
});
|
||||
|
||||
function RootLayout() {
|
||||
|
|
@ -53,6 +55,7 @@ function RootLayout() {
|
|||
<Outlet />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||
<Toaster />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ export const snapshotSchema = type({
|
|||
duration: "number",
|
||||
tags: "string[]",
|
||||
retentionCategories: "string[]",
|
||||
hostname: "string?",
|
||||
});
|
||||
|
||||
const listSnapshotsResponse = snapshotSchema.array();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue