feat: repository doctor always visible

This commit is contained in:
Nicolas Meienberger
2025-11-09 11:31:44 +01:00
parent db0d153610
commit 1152939373
2 changed files with 148 additions and 173 deletions

View File

@@ -1,9 +1,10 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigate, useParams, useSearchParams } from "react-router"; import { redirect, useNavigate, useSearchParams } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { import {
deleteRepositoryMutation, deleteRepositoryMutation,
doctorRepositoryMutation,
getRepositoryOptions, getRepositoryOptions,
listSnapshotsOptions, listSnapshotsOptions,
} from "~/api-client/@tanstack/react-query.gen"; } from "~/api-client/@tanstack/react-query.gen";
@@ -24,6 +25,7 @@ import { cn } from "~/lib/utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { RepositoryInfoTabContent } from "../tabs/info"; import { RepositoryInfoTabContent } from "../tabs/info";
import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
import { Loader2 } from "lucide-react";
export function meta({ params }: Route.MetaArgs) { export function meta({ params }: Route.MetaArgs) {
return [ return [
@@ -38,10 +40,13 @@ export function meta({ params }: Route.MetaArgs) {
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const repository = await getRepository({ path: { name: params.name ?? "" } }); const repository = await getRepository({ path: { name: params.name ?? "" } });
if (repository.data) return repository.data; if (repository.data) return repository.data;
return redirect("/repositories");
}; };
export default function RepositoryDetailsPage({ loaderData }: Route.ComponentProps) { export default function RepositoryDetailsPage({ loaderData }: Route.ComponentProps) {
const { name } = useParams<{ name: string }>(); const [showDoctorResults, setShowDoctorResults] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -50,17 +55,15 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
const activeTab = searchParams.get("tab") || "info"; const activeTab = searchParams.get("tab") || "info";
const { data } = useQuery({ const { data } = useQuery({
...getRepositoryOptions({ path: { name: name ?? "" } }), ...getRepositoryOptions({ path: { name: loaderData.name } }),
initialData: loaderData, initialData: loaderData,
refetchInterval: 10000, refetchInterval: 10000,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}); });
useEffect(() => { useEffect(() => {
if (name) { queryClient.prefetchQuery(listSnapshotsOptions({ path: { name: data.name } }));
queryClient.prefetchQuery(listSnapshotsOptions({ path: { name } })); }, [queryClient, data.name]);
}
}, [name, queryClient]);
const deleteRepo = useMutation({ const deleteRepo = useMutation({
...deleteRepositoryMutation(), ...deleteRepositoryMutation(),
@@ -75,18 +78,48 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
}, },
}); });
const doctorMutation = useMutation({
...doctorRepositoryMutation(),
onSuccess: (data) => {
if (data) {
setShowDoctorResults(true);
if (data.success) {
toast.success("Repository doctor completed successfully");
} else {
toast.warning("Doctor completed with some issues", {
description: "Check the details for more information",
richColors: true,
});
}
}
},
onError: (error) => {
toast.error("Failed to run doctor", {
description: parseError(error)?.message,
});
},
});
const handleConfirmDelete = () => { const handleConfirmDelete = () => {
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
deleteRepo.mutate({ path: { name: name ?? "" } }); deleteRepo.mutate({ path: { name: data.name } });
}; };
if (!name) { const getStepLabel = (step: string) => {
return <div>Repository not found</div>; switch (step) {
} case "unlock":
return "Unlock Repository";
if (!data) { case "check":
return <div>Loading...</div>; return "Check Repository";
} case "repair_index":
return "Repair Index";
case "recheck":
return "Re-check Repository";
default:
return step;
}
};
return ( return (
<> <>
@@ -103,6 +136,20 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
<span className="text-xs bg-primary/10 rounded-md px-2 py-1">{data.type}</span> <span className="text-xs bg-primary/10 rounded-md px-2 py-1">{data.type}</span>
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<Button
onClick={() => doctorMutation.mutate({ path: { name: data.name } })}
disabled={doctorMutation.isPending}
variant={"outline"}
>
{doctorMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Running Doctor...
</>
) : (
"Run Doctor"
)}
</Button>
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}> <Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}>
Delete Delete
</Button> </Button>
@@ -127,8 +174,8 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete repository?</AlertDialogTitle> <AlertDialogTitle>Delete repository?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete the repository <strong>{name}</strong>? This action cannot be undone and Are you sure you want to delete the repository <strong>{data.name}</strong>? This action cannot be undone
will remove all backup data. and will remove all backup data.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
@@ -142,6 +189,46 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
</div> </div>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<AlertDialog open={showDoctorResults} onOpenChange={setShowDoctorResults}>
<AlertDialogContent className="max-w-2xl">
<AlertDialogHeader>
<AlertDialogTitle>Doctor Results</AlertDialogTitle>
<AlertDialogDescription>Repository doctor operation completed</AlertDialogDescription>
</AlertDialogHeader>
{doctorMutation.data && (
<div className="space-y-3 max-h-96 overflow-y-auto">
{doctorMutation.data.steps.map((step) => (
<div
key={step.step}
className={cn("border rounded-md p-3", {
"bg-green-500/10 border-green-500/20": step.success,
"bg-yellow-500/10 border-yellow-500/20": !step.success,
})}
>
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-sm">{getStepLabel(step.step)}</span>
<span
className={cn("text-xs px-2 py-1 rounded", {
"bg-green-500/20 text-green-500": step.success,
"bg-yellow-500/20 text-yellow-500": !step.success,
})}
>
{step.success ? "Success" : "Warning"}
</span>
</div>
{step.error && <p className="text-xs text-red-500 mt-1">{step.error}</p>}
</div>
))}
</div>
)}
<div className="flex justify-end">
<Button onClick={() => setShowDoctorResults(false)}>Close</Button>
</div>
</AlertDialogContent>
</AlertDialog>
</> </>
); );
} }

View File

@@ -1,175 +1,63 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { Card } from "~/components/ui/card"; import { Card } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from "~/components/ui/alert-dialog";
import { Loader2 } from "lucide-react";
import type { Repository } from "~/lib/types"; import type { Repository } from "~/lib/types";
import { parseError } from "~/lib/errors";
import { doctorRepositoryMutation } from "~/api-client/@tanstack/react-query.gen";
import { cn } from "~/lib/utils";
type Props = { type Props = {
repository: Repository; repository: Repository;
}; };
export const RepositoryInfoTabContent = ({ repository }: Props) => { export const RepositoryInfoTabContent = ({ repository }: Props) => {
const [showDoctorResults, setShowDoctorResults] = useState(false);
const doctorMutation = useMutation({
...doctorRepositoryMutation(),
onSuccess: (data) => {
if (data) {
setShowDoctorResults(true);
if (data.success) {
toast.success("Repository doctor completed successfully");
} else {
toast.warning("Doctor completed with some issues", {
description: "Check the details for more information",
richColors: true,
});
}
}
},
onError: (error) => {
toast.error("Failed to run doctor", {
description: parseError(error)?.message,
});
},
});
const handleDoctor = () => {
doctorMutation.mutate({ path: { name: repository.name } });
};
const getStepLabel = (step: string) => {
switch (step) {
case "unlock":
return "Unlock Repository";
case "check":
return "Check Repository";
case "repair_index":
return "Repair Index";
case "recheck":
return "Re-check Repository";
default:
return step;
}
};
return ( return (
<> <Card className="p-6">
<Card className="p-6"> <div className="space-y-6">
<div className="space-y-6"> <div>
<div> <h3 className="text-lg font-semibold mb-4">Repository Information</h3>
<h3 className="text-lg font-semibold mb-4">Repository Information</h3> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="text-sm font-medium text-muted-foreground">Name</div>
<p className="mt-1 text-sm">{repository.name}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Backend</div>
<p className="mt-1 text-sm">{repository.type}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Compression Mode</div>
<p className="mt-1 text-sm">{repository.compressionMode || "off"}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Status</div>
<p className="mt-1 text-sm">{repository.status || "unknown"}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Created At</div>
<p className="mt-1 text-sm">{new Date(repository.createdAt).toLocaleString()}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Last Checked</div>
<p className="mt-1 text-sm">
{repository.lastChecked ? new Date(repository.lastChecked).toLocaleString() : "Never"}
</p>
</div>
</div>
</div>
{repository.lastError && (
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="text-sm font-medium text-muted-foreground">Name</div>
<h3 className="text-lg font-semibold text-red-500">Last Error</h3> <p className="mt-1 text-sm">{repository.name}</p>
<Button onClick={handleDoctor} disabled={doctorMutation.isPending} variant={"outline"} size="sm">
{doctorMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Running Doctor...
</>
) : (
"Run Doctor"
)}
</Button>
</div>
<div className="bg-red-500/10 border border-red-500/20 rounded-md p-4">
<p className="text-sm text-red-500">{repository.lastError}</p>
</div>
</div> </div>
)} <div>
<div className="text-sm font-medium text-muted-foreground">Backend</div>
<div> <p className="mt-1 text-sm">{repository.type}</p>
<h3 className="text-lg font-semibold mb-4">Configuration</h3> </div>
<div className="bg-muted/50 rounded-md p-4"> <div>
<pre className="text-sm overflow-auto">{JSON.stringify(repository.config, null, 2)}</pre> <div className="text-sm font-medium text-muted-foreground">Compression Mode</div>
<p className="mt-1 text-sm">{repository.compressionMode || "off"}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Status</div>
<p className="mt-1 text-sm">{repository.status || "unknown"}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Created At</div>
<p className="mt-1 text-sm">{new Date(repository.createdAt).toLocaleString()}</p>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Last Checked</div>
<p className="mt-1 text-sm">
{repository.lastChecked ? new Date(repository.lastChecked).toLocaleString() : "Never"}
</p>
</div> </div>
</div> </div>
</div> </div>
</Card> {repository.lastError && (
<div>
<AlertDialog open={showDoctorResults} onOpenChange={setShowDoctorResults}> <div className="flex items-center justify-between mb-4">
<AlertDialogContent className="max-w-2xl"> <h3 className="text-lg font-semibold text-red-500">Last Error</h3>
<AlertDialogHeader>
<AlertDialogTitle>Doctor Results</AlertDialogTitle>
<AlertDialogDescription>Repository doctor operation completed</AlertDialogDescription>
</AlertDialogHeader>
{doctorMutation.data && (
<div className="space-y-3 max-h-96 overflow-y-auto">
{doctorMutation.data.steps.map((step) => (
<div
key={step.step}
className={cn("border rounded-md p-3", {
"bg-green-500/10 border-green-500/20": step.success,
"bg-yellow-500/10 border-yellow-500/20": !step.success,
})}
>
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-sm">{getStepLabel(step.step)}</span>
<span
className={cn("text-xs px-2 py-1 rounded", {
"bg-green-500/20 text-green-500": step.success,
"bg-yellow-500/20 text-yellow-500": !step.success,
})}
>
{step.success ? "Success" : "Warning"}
</span>
</div>
{step.error && <p className="text-xs text-red-500 mt-1">{step.error}</p>}
</div>
))}
</div> </div>
)}
<div className="flex justify-end"> <div className="bg-red-500/10 border border-red-500/20 rounded-md p-4">
<Button onClick={() => setShowDoctorResults(false)}>Close</Button> <p className="text-sm text-red-500">{repository.lastError}</p>
</div>
</div> </div>
</AlertDialogContent> )}
</AlertDialog> <div>
</> <h3 className="text-lg font-semibold mb-4">Configuration</h3>
<div className="bg-muted/50 rounded-md p-4">
<pre className="text-sm overflow-auto">{JSON.stringify(repository.config, null, 2)}</pre>
</div>
</div>
</div>
</Card>
); );
}; };