mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: repository doctor always visible
This commit is contained in:
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +1,12 @@
|
|||||||
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>
|
||||||
@@ -100,28 +40,17 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{repository.lastError && (
|
{repository.lastError && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold text-red-500">Last Error</h3>
|
<h3 className="text-lg font-semibold text-red-500">Last Error</h3>
|
||||||
<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>
|
||||||
|
|
||||||
<div className="bg-red-500/10 border border-red-500/20 rounded-md p-4">
|
<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>
|
<p className="text-sm text-red-500">{repository.lastError}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">Configuration</h3>
|
<h3 className="text-lg font-semibold mb-4">Configuration</h3>
|
||||||
<div className="bg-muted/50 rounded-md p-4">
|
<div className="bg-muted/50 rounded-md p-4">
|
||||||
@@ -130,46 +59,5 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user