"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 { CardContent } from "@kandev/ui/card";
import { Input } from "@kandev/ui/input";
import { Separator } from "@kandev/ui/separator";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { Spinner } from "@kandev/ui/spinner";
import { WorkspaceScopedSection } from "@/components/integrations/workspace-scoped-section";
import { GitLabEnabledControl } from "@/components/gitlab/gitlab-enabled-control";
import { useToast } from "@/components/toast-provider";
import { SettingsSection } from "@/components/settings/settings-section";
import { SettingsCard } from "@/components/settings/settings-card";
import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider";
import { clearGitLabToken, fetchGitLabStatus, setGitLabConfig } from "@/lib/api/domains/gitlab-api";
import type { GitLabConfig, GitLabStatus } from "@/lib/types/gitlab";
import { GitLabWatchSettings } from "./watch-settings";
import { GitLabActionPresetsSection } from "./action-presets-section";
import { Trans, useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { INTEGRATION_SETTINGS_TARGETS } from "@/lib/settings-discovery/catalog/integrations";
const DEFAULT_HOST = "https://gitlab.com";
// The bare hostname as it reads mid-sentence. Interpolated rather than written
// into the catalog so no locale — including pseudo — can transliterate it into a
// hostname that does not resolve.
const DEFAULT_HOST_NAME = "gitlab.com";
function StatusBadge({ status }: { status: GitLabStatus | null }) {
const { t } = useTranslation();
if (!status) return null;
if (status.authenticated) {
return (
{t("gitlab: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 (
{t("gitlab:unreachable")}
);
}
if (status.token_configured || status.auth_method !== "none") {
return (
{t("gitlab:reconnectRequired")}
);
}
return (
{t("gitlab:notConnected")}
);
}
// 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 }) {
const { t } = useTranslation();
if (!status?.connection_error) return null;
return (
{/* One message rather than a stem plus the host: where the host and the
upstream error sit in the sentence is the translator's call. Both are
server data, so they travel through `values`. */}
Couldn't reach {status.host}:{" "}
{status.connection_error}
{t("gitlab:yourTokenMayStillBeValid")}
);
}
// The record keys are the wire `auth_method` values the backend sends and must
// never be translated; only the values are copy. Built inside the component so
// `t()` runs at render — a module-scope table would freeze at the boot locale.
// The `?? method` fallback deliberately echoes the raw wire value for a method
// this build does not know about.
function AuthMethodBadge({ method }: { method: GitLabStatus["auth_method"] }) {
const { t } = useTranslation();
const labels: Record = {
glab_cli: t("gitlab:glabCli"),
pat: t("gitlab:personalAccessToken"),
environment: t("gitlab:environmentToken"),
none: t("gitlab:notConfigured"),
mock: t("gitlab:mockTest"),
};
return {labels[method] ?? method};
}
function HostForm({
host,
baseline,
onHostChange,
}: {
host: string;
baseline: string;
onHostChange: (host: string) => void;
}) {
const isDirty = host !== baseline;
return (