184 lines
6.0 KiB
TypeScript
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"
|
|
/>
|
|
</>
|
|
);
|
|
}
|