"use client"; import { useCallback, useEffect, useState } from "react"; import { IconAlertTriangle, IconBrandGitlab, IconCheck, IconEye, IconEyeOff, IconKey, IconRefresh, IconTrash, IconWorld, IconX, } from "@tabler/icons-react"; import { Alert, AlertDescription } from "@kandev/ui/alert"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Card, CardContent } from "@kandev/ui/card"; import { Input } from "@kandev/ui/input"; import { Separator } from "@kandev/ui/separator"; import { Spinner } from "@kandev/ui/spinner"; import { WorkspaceScopedSection } from "@/components/integrations/workspace-scoped-section"; import { useToast } from "@/components/toast-provider"; import { SettingsSection } from "@/components/settings/settings-section"; import { clearGitLabToken, configureGitLabHost, configureGitLabToken, fetchGitLabStatus, } from "@/lib/api/domains/gitlab-api"; import type { GitLabStatus } from "@/lib/types/gitlab"; const DEFAULT_HOST = "https://gitlab.com"; function StatusBadge({ status }: { status: GitLabStatus | null }) { if (!status) return null; if (status.authenticated) { return ( Connected ); } // A non-empty connection_error means the probe failed for transport reasons // (network / 5xx / parse) — distinct from "no token configured", which has // an empty connection_error and authenticated=false. if (status.connection_error) { return ( Unreachable ); } return ( Not connected ); } // ConnectionErrorAlert renders the per-host transport failure separately from // the "bad token" path so users see "GitLab is currently unreachable" instead // of "your token is broken" during an outage. Hidden when the probe succeeded // or when no token is configured (nothing to probe). function ConnectionErrorAlert({ status }: { status: GitLabStatus | null }) { if (!status?.connection_error) return null; return ( Couldn't reach {status.host}:{" "} {status.connection_error} Your token may still be valid — this looks like a network or upstream issue. ); } function AuthMethodBadge({ method }: { method: GitLabStatus["auth_method"] }) { const labels: Record = { glab_cli: "glab CLI", pat: "Personal access token", none: "Not configured", mock: "Mock (test)", }; return {labels[method] ?? method}; } function HostForm({ initial, onSaved }: { initial: string; onSaved: () => void }) { const [host, setHost] = useState(initial); const [saving, setSaving] = useState(false); const { toast } = useToast(); useEffect(() => { setHost(initial); }, [initial]); const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!host.trim()) return; setSaving(true); try { await configureGitLabHost(host.trim()); toast({ description: "GitLab host updated", variant: "success" }); onSaved(); } catch (err) { toast({ description: err instanceof Error ? err.message : "Failed to update host", variant: "error", }); } finally { setSaving(false); } }, [host, toast, onSaved], ); return (
setHost(e.target.value)} className="font-mono text-sm" disabled={saving} /> ); } function TokenForm({ onSuccess }: { onSuccess: () => void }) { const [token, setToken] = useState(""); const [showToken, setShowToken] = useState(false); const [saving, setSaving] = useState(false); const { toast } = useToast(); const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!token.trim()) return; setSaving(true); try { await configureGitLabToken(token.trim()); toast({ description: "GitLab token configured", variant: "success" }); setToken(""); onSuccess(); } catch (err) { toast({ description: err instanceof Error ? err.message : "Failed to save token", variant: "error", }); } finally { setSaving(false); } }, [token, toast, onSuccess], ); return (
setToken(e.target.value)} className="font-mono text-sm pr-9" disabled={saving} autoComplete="off" />
); } function ClearTokenButton({ onCleared }: { onCleared: () => void }) { const [busy, setBusy] = useState(false); const { toast } = useToast(); return ( ); } type GitLabIntegrationPageProps = { workspaceId?: string; }; export function GitLabIntegrationPage({ workspaceId }: GitLabIntegrationPageProps = {}) { return ( {(ws) => } ); } function GitLabConnectionSection() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const reload = useCallback(async () => { setLoading(true); try { const next = await fetchGitLabStatus({ cache: "no-store" }); setStatus(next); } catch { setStatus(null); } finally { setLoading(false); } }, []); useEffect(() => { void reload(); }, [reload]); return ( } action={ } >
{status && } {status?.glab_version && ( glab {status.glab_version} )}
{status?.username && ( Logged in as {status.username} )}

GitLab host URL. Override for self-managed instances; leave at the default for gitlab.com.

void reload()} />

Personal access token. Required scopes: api, read_user. Stored encrypted in the kandev secret store.

{status?.token_configured && void reload()} />}
void reload()} />
); }