"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { CheckCircle2, ExternalLink, KeyRound, Loader2, Pencil, Plug, Plus, Trash2, XCircle, Zap, } from "lucide-react"; import type { GitProvider } from "@prisma/client"; import { Modal } from "@/components/ui/Modal"; import { PROVIDER_META } from "@/lib/git/meta"; import { cn, formatRelative } from "@/lib/utils"; import type { ConnectionWithCount } from "@/types"; const PROVIDERS: GitProvider[] = ["GITHUB", "GITLAB", "GITEA"]; interface FormState { name: string; provider: GitProvider; baseUrl: string; username: string; token: string; } const EMPTY: FormState = { name: "", provider: "GITHUB", baseUrl: PROVIDER_META.GITHUB.defaultBaseUrl, username: "", token: "", }; export function ConnectionsManager({ connections }: { connections: ConnectionWithCount[] }) { const router = useRouter(); const [dialogOpen, setDialogOpen] = useState(false); const [editing, setEditing] = useState(null); const [form, setForm] = useState(EMPTY); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [busyId, setBusyId] = useState(null); const [checkResult, setCheckResult] = useState>({}); function openCreate() { setEditing(null); setForm(EMPTY); setError(null); setDialogOpen(true); } function openEdit(connection: ConnectionWithCount) { setEditing(connection); setForm({ name: connection.name, provider: connection.provider, baseUrl: connection.baseUrl, username: connection.username ?? "", token: "", }); setError(null); setDialogOpen(true); } function update(key: K, value: FormState[K]) { setForm((previous) => { if (key === "provider") { const provider = value as GitProvider; const wasDefault = Object.values(PROVIDER_META).some( (meta) => meta.defaultBaseUrl === previous.baseUrl, ); return { ...previous, provider, baseUrl: wasDefault || !previous.baseUrl ? PROVIDER_META[provider].defaultBaseUrl : previous.baseUrl, }; } return { ...previous, [key]: value }; }); } async function save(event: React.FormEvent) { event.preventDefault(); setSaving(true); setError(null); // Beim Bearbeiten bedeutet ein leeres Token-Feld "unveraendert lassen". const body: Record = { name: form.name, provider: form.provider, baseUrl: form.baseUrl, username: form.username, }; if (!editing || form.token.trim()) body.token = form.token; try { const response = await fetch( editing ? `/api/connections/${editing.id}` : "/api/connections", { method: editing ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }, ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload.error ?? "Speichern fehlgeschlagen."); setDialogOpen(false); router.refresh(); } catch (caught) { setError(caught instanceof Error ? caught.message : String(caught)); } finally { setSaving(false); } } async function check(connection: ConnectionWithCount) { setBusyId(connection.id); try { const response = await fetch(`/api/connections/${connection.id}/check`, { method: "POST" }); const payload = await response.json().catch(() => ({})); setCheckResult((previous) => ({ ...previous, [connection.id]: { ok: Boolean(payload.ok), message: payload.message ?? payload.error ?? "Unbekannte Antwort.", }, })); router.refresh(); } finally { setBusyId(null); } } async function remove(connection: ConnectionWithCount) { const linked = connection._count?.repositories ?? 0; const hint = linked > 0 ? `\n\n${linked} Repository(s) verlieren die Verknuepfung.` : ""; if (!window.confirm(`Verbindung „${connection.name}“ loeschen?${hint}`)) return; setBusyId(connection.id); await fetch(`/api/connections/${connection.id}`, { method: "DELETE" }).catch(() => undefined); setBusyId(null); router.refresh(); } return (

Git-Verbindungen

Pro Git-System eine Verbindung mit Basis-URL und Access Token. Tokens werden AES-256-GCM-verschluesselt in der Datenbank abgelegt und verlassen den Server nie.

{connections.length === 0 ? (

Noch keine Verbindung

Verbinde GitHub, GitLab oder deine eigene Gitea-Instanz, um Branches, Tags, Commits und Releases direkt im Dashboard zu sehen.

) : (
{connections.map((connection, index) => { const meta = PROVIDER_META[connection.provider]; const result = checkResult[connection.id]; const ok = result ? result.ok : connection.lastCheckOk; const message = result?.message ?? connection.lastCheckMessage; return (

{connection.name}

{meta.label}

{ok === true ? ( ) : ok === false ? ( ) : ( )} {ok === true ? "verbunden" : ok === false ? "Fehler" : "ungeprueft"}
Basis-URL
{connection.baseUrl}
Token
{connection.tokenHint ? ( {connection.tokenHint} ) : ( keines )}
{connection.username ? (
Benutzer
{connection.username}
) : null}
Repositories
{connection._count?.repositories ?? 0}
{connection.lastCheckedAt ? (
Geprueft
{formatRelative(connection.lastCheckedAt)}
) : null}
{message ? (

{message}

) : null}
); })}
)} setDialogOpen(false)} title={editing ? "Verbindung bearbeiten" : "Neue Git-Verbindung"} description="Der Token wird verschluesselt gespeichert und nur serverseitig entschluesselt." >
Git-System
{PROVIDERS.map((provider) => ( ))}
update("name", event.target.value)} placeholder="z. B. Firmen-Gitea" required maxLength={80} />
update("baseUrl", event.target.value)} placeholder="https://git.example.com" required />

Ohne /api – Nebula haengt den API-Pfad selbst an.

update("token", event.target.value)} placeholder={editing ? "••••••••" : "ghp_… / glpat-… / gitea token"} autoComplete="off" />

{PROVIDER_META[form.provider].tokenHelp} Doku

update("username", event.target.value)} placeholder="wird beim Test automatisch gefuellt" />
{error ? (

{error}

) : null}
); }