Pipeline/frontend/src/app/git-projects/connections/page.tsx

184 lines
6.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { AlertCircle, CheckCircle2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/auth/clerk";
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { ForgejoConnectionsTable } from "@/components/git/ForgejoConnectionsTable";
import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog";
import {
getForgejoConnections,
deleteForgejoConnection,
validateConnection,
type ForgejoConnection,
} from "@/lib/api-forgejo";
export default function ForgejoConnectionsPage() {
const router = useRouter();
const auth = useAuth();
const [connections, setConnections] = useState<ForgejoConnection[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<{
tone: "success" | "error";
message: string;
} | null>(null);
const [deleteTarget, setDeleteTarget] = useState<ForgejoConnection | null>(
null,
);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
const fetchConnections = async () => {
try {
setIsLoading(true);
const data = await getForgejoConnections();
setConnections(data);
setError(null);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to load connections",
);
} finally {
setIsLoading(false);
}
};
if (auth.isSignedIn) {
fetchConnections();
}
}, [auth.isSignedIn]);
const handleDelete = (connection: ForgejoConnection) => {
setDeleteError(null);
setDeleteTarget(connection);
};
const confirmDelete = async () => {
if (!deleteTarget) return;
setIsDeleting(true);
setDeleteError(null);
try {
await deleteForgejoConnection(deleteTarget.id);
setConnections((prev) => prev.filter((c) => c.id !== deleteTarget.id));
setNotice({
tone: "success",
message: `Deleted "${deleteTarget.name}".`,
});
setDeleteTarget(null);
} catch (err) {
setDeleteError(
err instanceof Error ? err.message : "Failed to delete connection",
);
} finally {
setIsDeleting(false);
}
};
const handleValidateConnection = async (connection: ForgejoConnection) => {
try {
const result = await validateConnection(connection.id);
if (result.status.ok) {
setNotice({
tone: "success",
message: `"${connection.name}" validated in ${Math.round(result.response_time_ms)}ms.`,
});
} else {
setNotice({
tone: "error",
message: `Connection validation failed: ${result.status.error_message || "Unknown error"}`,
});
}
return result;
} catch (err) {
setNotice({
tone: "error",
message:
err instanceof Error ? err.message : "Failed to validate connection",
});
throw err;
}
};
return (
<>
<DashboardPageLayout
signedOut={{
message: "Sign in to manage Git Project connections.",
forceRedirectUrl: "/git-projects/connections",
signUpForceRedirectUrl: "/git-projects/connections",
}}
title="Git Project Connections"
description={`${connections.length} connection${connections.length === 1 ? "" : "s"} configured for Pipeline.`}
stickyHeader
>
<div className="flex flex-col gap-4">
{notice ? (
<div
className={`flex items-start gap-3 rounded-xl border p-3 text-sm ${
notice.tone === "success"
? "border-[color:rgba(52,211,153,0.35)] bg-[color:rgba(52,211,153,0.08)] text-[color:var(--success)]"
: "border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] text-[color:var(--danger)]"
}`}
>
{notice.tone === "success" ? (
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
) : (
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
)}
<span>{notice.message}</span>
</div>
) : null}
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 className="text-sm font-medium text-muted">Connections</h2>
<Button
onClick={() => router.push("/git-projects/connections/new")}
>
Add Connection
</Button>
</div>
<div className="overflow-hidden rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-lush">
{error ? (
<div className="p-8 text-center">
<p className="text-sm text-[color:var(--danger)]">{error}</p>
</div>
) : (
<ForgejoConnectionsTable
connections={connections}
isLoading={isLoading}
onDelete={handleDelete}
onValidate={handleValidateConnection}
/>
)}
</div>
</div>
</DashboardPageLayout>
<ConfirmActionDialog
open={Boolean(deleteTarget)}
onOpenChange={(open) => {
if (!open) setDeleteTarget(null);
}}
title="Delete Git Project connection"
description={
deleteTarget
? `Delete "${deleteTarget.name}" from Pipeline? Repositories that use this connection will stop syncing.`
: ""
}
onConfirm={confirmDelete}
isConfirming={isDeleting}
errorMessage={deleteError}
confirmLabel="Delete Connection"
confirmingLabel="Deleting…"
confirmClassName="bg-[color:var(--danger)] text-white hover:bg-[color:var(--danger)]/90"
cancelLabel="Keep Connection"
/>
</>
);
}