refactor(dashboard): extract AddApiKeyModal + EditConnectionModal — #3501 Phase 1c (#3674)

#3501 Phase 1c: extract AddApiKeyModal, EditConnectionModal, WebSessionCredentialGuide into components/; god-component 10,166->8,092 LOC. Reconciles the v3.8.22 file-size drift for this file.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-11 13:41:39 -03:00
committed by GitHub
parent acadb593d8
commit 9196a5427e
8 changed files with 2343 additions and 2093 deletions

View File

@@ -14,6 +14,7 @@
### ♻️ Code Quality
- **providers/[id]: extract AddApiKeyModal + EditConnectionModal (+ WebSessionCredentialGuide) from the god-component into components/** ([#3501] Phase 1c): extracted the two heaviest inline modals — `AddApiKeyModal` (~787-LOC body) and `EditConnectionModal` (~1091-LOC body) — plus shared `WebSessionCredentialGuide` (~103 LOC) into standalone files under `providers/[id]/components/modals/` and `providers/[id]/components/` respectively. Added `ERROR_TYPE_LABELS` and `formatTimeAgo` to `providerPageHelpers.ts` (leaf) so `EditConnectionModal` and `ConnectionRow` share them without cycles. Pruned 14 now-unused imports from the god-component. `ProviderDetailPageClient.tsx`: 9,981 → 8,092 LOC (1,889 lines). Frozen baselines: `AddApiKeyModal.tsx: 842`, `EditConnectionModal.tsx: 1170`. 6 new Phase-1c smoke tests; all 21 vitest modal tests pass; typecheck/cycles/lint green.
- **refactor: small db/utils cleanup** ([#3523] — thanks @androw): table-driven `compression_analytics` column migration (replaces 17 repeated `ALTER TABLE` calls), a single merged `serializeJsonField` helper in `db/providers.ts` (folded two byte-identical serializers), and removal of the dead no-op `syncProviderDataToCloud`/`getProvidersNeedingRefresh` stubs from `shared/utils/machine.ts` (no remaining callers). Pure refactor; behavior unchanged.
- **Provider-detail god-component decomposition — Phase 2b (remaining shared helpers→leaf)** ([#3501]): extended `providers/[id]/providerPageHelpers.ts` with all remaining pure helpers needed by the heavy modals (`AddApiKeyModal`/`EditConnectionModal`) before they can be extracted. Moved 22 symbols: web-session credential label/hint/check/title helpers; upstream-headers helpers (`upstreamHeadersRecordsEqual`, `headerRowsToRecord`, `effectiveUpstreamHeadersForProtocol`, `anyUpstreamHeadersBadge`, `getProtoSlice`) plus their `HeaderDraftRow`/`CompatModelRow`/`CompatModelMap`/`CompatByProtocolMap` types; Codex consts and helpers (`CODEX_REASONING_STRENGTH_OPTIONS`, `CODEX_ACCOUNT_SERVICE_TIER_VALUES`, `CODEX_GLOBAL_SERVICE_MODE_VALUES`, `getCodexServiceTierLabel`, `normalizeCodexLimitPolicy`, `getCodexRequestDefaults`, `getClaudeCodeCompatibleRequestDefaults`); misc helpers (`compatProtocolLabelKey`, `extractCommandCodeCredentialInput`, `normalizeAndValidateHttpBaseUrl`, `SILICONFLOW_ENDPOINTS`, `CommandCodeAuthFlowState`). New transitive imports wired into the leaf: `MODEL_COMPAT_PROTOCOL_KEYS` (`@/shared/constants/modelCompat`), `CodexServiceTier`/`getCodexRequestDefaults`/`getClaudeCodeCompatibleRequestDefaults` (`@/lib/providers/requestDefaults`), `CodexGlobalServiceMode` (`@/lib/providers/codexFastTier`), `WebSessionCredentialRequirement` (`./webSessionCredentials`). `ProviderDetailPageClient.tsx`: 10,288 → 9,980 LOC. Leaf module: 589 LOC (acyclic). 25-assertion unit test suite passes; smoke test 3/3; no import cycles. Co-authored with @oyi77.
- **Provider-detail god-component decomposition — Phase 2 (helpers→lib)** ([#3501]): extracted the pure shared helpers — `ProviderMessageTranslator`/`LocalProviderMetadata` types, `providerText`/`providerCountText`/`readBooleanToggle`, and the provider base-URL + routing-tag/excluded-model parse/format block — into a new leaf `providers/[id]/providerPageHelpers.ts` (imports only `@/shared`, so the client and modals share them with no import cycle). `ProviderDetailPageClient.tsx`: 10,435 → 10,288 LOC. Unblocks extracting the heavier `AddApiKeyModal`/`EditConnectionModal` (which depend on these helpers) without cycling. The Phase 0 smoke test caught a missing transitive import (`isSelfHostedChatProvider`) at mount — now wired + locked by a new helpers unit test (12 assertions). Co-authored with @oyi77.

View File

@@ -48,7 +48,9 @@
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570,
"src/app/(dashboard)/dashboard/health/page.tsx": 1091,
"src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847,
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 9981,
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 8093,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 843,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1171,
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906,
"src/app/(dashboard)/dashboard/providers/page.tsx": 1925,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1127,

View File

@@ -0,0 +1,114 @@
"use client";
// Issue #3501 Phase 1c — extracted from the god-component.
// Shared by AddApiKeyModal and EditConnectionModal; imports only leaf modules
// (no cycle risk).
import type { WebSessionCredentialRequirement } from "../webSessionCredentials";
import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers";
export interface WebSessionCredentialGuideProps {
requirement: WebSessionCredentialRequirement;
providerName: string;
t: ProviderMessageTranslator;
}
export default function WebSessionCredentialGuide({
requirement,
providerName,
t,
}: WebSessionCredentialGuideProps) {
if (requirement.kind === "none") {
return (
<div className="rounded-lg border border-emerald-500/25 bg-emerald-500/10 px-3 py-3 text-sm text-text-muted">
<div className="flex items-start gap-2">
<span className="material-symbols-outlined mt-0.5 text-[18px] text-emerald-500">
check_circle
</span>
<div>
<p className="font-medium text-text-main">
{providerText(t, "webNoAuthGuideTitle", "No credential required")}
</p>
<p className="mt-1">
{providerText(
t,
"webNoAuthGuideBody",
"{provider} does not need an API key or cookie. Save the connection to use its free web endpoint.",
{ provider: providerName }
)}
</p>
</div>
</div>
</div>
);
}
const requiredCredentialKey =
requirement.kind === "token" ? "webTokenRequiredCredential" : "webCookieRequiredCredential";
const requiredCredentialFallback =
requirement.kind === "token" ? "Required token: {credential}" : "Required cookie: {credential}";
return (
<div className="rounded-lg border border-purple-500/25 bg-purple-500/10 px-3 py-3 text-sm text-text-muted">
<div className="flex items-start gap-2">
<span className="material-symbols-outlined mt-0.5 text-[18px] text-purple-500">cookie</span>
<div className="space-y-2">
<div>
<p className="font-medium text-text-main">
{providerText(t, "webSessionGuideTitle", "How to get the session credential")}
</p>
<p className="mt-1">
{providerText(
t,
"webSessionGuideIntro",
"{provider} uses a browser web session instead of an API key.",
{ provider: providerName }
)}
</p>
</div>
<p className="font-medium text-text-main">
{providerText(t, requiredCredentialKey, requiredCredentialFallback, {
credential: requirement.credentialName,
})}
</p>
<ol className="list-decimal space-y-1 pl-5">
<li>
{providerText(t, "webSessionGuideStep1", "Sign in to {provider} in your browser.", {
provider: providerName,
})}
</li>
<li>
{providerText(
t,
"webSessionGuideStep2",
"Open the browser developer tools and inspect a request made by the web app."
)}
</li>
<li>
{providerText(
t,
"webSessionGuideStep3",
"Copy the required credential from the provider's own domain. For cookies, copy only the Cookie header value and omit Cookie:.",
{ credential: requirement.credentialName }
)}
</li>
<li>
{providerText(
t,
"webSessionGuideStep4",
"Paste it here and check the connection. If it stops working, sign in again and replace it with a fresh value."
)}
</li>
</ol>
<p className="text-xs text-amber-700 dark:text-amber-300">
{providerText(
t,
"webSessionSecurityHint",
"Treat this like a password: it may access your signed-in web account until it expires or is revoked."
)}
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,842 @@
"use client";
// Issue #3501 Phase 1c — extracted from the god-component.
// ~787-LOC modal for adding a new API key / credential to a provider.
import { useState, useEffect, useRef } from "react";
import { useTranslations } from "next-intl";
import { Button, Badge, Input, Modal, Toggle } from "@/shared/components";
import {
providerAllowsOptionalApiKey,
supportsBulkApiKey,
} from "@/shared/constants/providers";
import { parseBulkApiKeys } from "@/shared/utils/bulkApiKeyParser";
import {
isBaseUrlConfigurableProvider,
getProviderBaseUrlDefault,
getProviderBaseUrlHint,
getProviderBaseUrlPlaceholder,
isGlmProvider,
parseRoutingTagsInput,
parseExcludedModelsInput,
getWebSessionCredentialLabel,
getWebSessionCredentialHint,
getWebSessionCredentialCheckLabel,
getAddCredentialModalTitle,
getLocalProviderMetadata,
normalizeAndValidateHttpBaseUrl,
extractCommandCodeCredentialInput,
providerText,
type CommandCodeAuthFlowState,
} from "../../providerPageHelpers";
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
export interface AddApiKeyModalProps {
isOpen: boolean;
provider?: string;
providerName?: string;
initialBaseUrl?: string;
isCompatible?: boolean;
isAnthropic?: boolean;
isCcCompatible?: boolean;
isCommandCode?: boolean;
commandCodeAuthState?: CommandCodeAuthFlowState;
onStartCommandCodeAuth?: () => void;
onSave: (data: {
name: string;
apiKey?: string;
priority: number;
baseUrl?: string;
providerSpecificData?: Record<string, unknown>;
}) => Promise<void | unknown>;
onClose: () => void;
}
export default function AddApiKeyModal({
isOpen,
provider,
providerName,
initialBaseUrl,
isCompatible,
isAnthropic,
isCcCompatible,
isCommandCode,
commandCodeAuthState,
onStartCommandCodeAuth,
onSave,
onClose,
}: AddApiKeyModalProps) {
const t = useTranslations("providers");
const usesBaseUrl = isBaseUrlConfigurableProvider(provider);
const defaultBaseUrl = getProviderBaseUrlDefault(provider);
const isVertex = provider === "vertex" || provider === "vertex-partner";
const isBedrock = provider === "bedrock";
const showsRegion = isVertex || isBedrock;
const defaultRegion = isBedrock ? "eu-west-2" : "us-central1";
const isGlm = isGlmProvider(provider);
const isQoder = provider === "qoder";
const isCloudflare = provider === "cloudflare-ai";
const localProviderMetadata = getLocalProviderMetadata(provider);
const isLocalSelfHostedProvider = !!localProviderMetadata;
const isGooglePse = provider === "google-pse-search";
const webSessionCredential = getWebSessionCredentialRequirement(provider);
const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none";
const isWebSessionCredential = !!webSessionCredential && webSessionCredential.kind !== "none";
const providerDisplayName = providerName || provider || "";
const apiKeyOptional =
providerAllowsOptionalApiKey(provider) || Boolean(isNoAuthWebSessionCredential);
const commandCodeAuthPhaseLabel = commandCodeAuthState
? {
idle: "Ready",
starting: "Starting…",
polling: "Waiting for browser…",
received: "Browser approved",
applying: "Applying key…",
applied: "Connected",
expired: "Link expired",
error: "Connection failed",
}[commandCodeAuthState.phase]
: null;
const [formData, setFormData] = useState({
name: "",
apiKey: "",
priority: 1,
baseUrl: initialBaseUrl || defaultBaseUrl,
cx: "",
region: showsRegion ? defaultRegion : "",
apiRegion: "international",
validationModelId: "",
routingTags: "",
excludedModels: "",
customUserAgent: "",
accountId: "",
consoleApiKey: "",
ccCompatibleContext1m: false,
passthroughModels: false,
});
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [showAdvanced, setShowAdvanced] = useState(false);
const [copiedCommandCodeField, setCopiedCommandCodeField] = useState<string | null>(null);
const wasOpenRef = useRef(false);
useEffect(() => {
const wasOpen = wasOpenRef.current;
wasOpenRef.current = isOpen;
if (!isOpen || wasOpen) return;
setFormData((current) => ({
...current,
baseUrl: initialBaseUrl || defaultBaseUrl,
}));
}, [defaultBaseUrl, initialBaseUrl, isOpen]);
const bulkSupported = supportsBulkApiKey(provider);
const [mode, setMode] = useState<"single" | "bulk">("single");
const [bulkText, setBulkText] = useState("");
const [bulkValidateKeys, setBulkValidateKeys] = useState(false);
const [bulkResult, setBulkResult] = useState<{
success: number;
failed: number;
total: number;
errors: Array<{ index: number; name: string; message: string }>;
} | null>(null);
const [bulkWarnings, setBulkWarnings] = useState<string[]>([]);
const apiCredentialLabel = isQoder
? t("personalAccessTokenLabel")
: webSessionCredential
? getWebSessionCredentialLabel(t, webSessionCredential, apiKeyOptional)
: apiKeyOptional
? `${t("apiKeyLabel")} (${t("optional").toLowerCase()})`
: t("apiKeyLabel");
const apiCredentialPlaceholder = isVertex
? t("vertexServiceAccountPlaceholder")
: isWebSessionCredential
? webSessionCredential.placeholder
: isQoder
? t("qoderPatPlaceholder")
: apiKeyOptional
? t("optional")
: undefined;
const apiCredentialHint = isQoder
? t("qoderPatHint")
: isWebSessionCredential
? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false)
: isLocalSelfHostedProvider
? t("localProviderApiKeyOptionalHint", {
provider: localProviderMetadata?.name || providerName || provider || "",
})
: apiKeyOptional
? t("apiKeyOptionalHint")
: undefined;
const credentialValidationFailedMessage = isWebSessionCredential
? providerText(
t,
"webSessionCredentialValidationFailed",
"Session credential validation failed. Sign in again, copy a fresh credential, and try again."
)
: t("apiKeyValidationFailed");
const handleValidate = async () => {
setValidating(true);
setSaveError(null);
try {
const credentialInput = isCommandCode
? extractCommandCodeCredentialInput(formData.apiKey)
: formData.apiKey;
const res = await fetch("/api/providers/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider,
apiKey: credentialInput,
validationModelId: formData.validationModelId || undefined,
customUserAgent: formData.customUserAgent.trim() || undefined,
baseUrl: formData.baseUrl.trim() || undefined,
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
cx: formData.cx.trim() || undefined,
}),
});
const data = await res.json();
setValidationResult(data.valid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
};
const copyCommandCodeValue = async (value: string | undefined, key: string) => {
if (!value) return;
try {
await navigator.clipboard.writeText(value);
setCopiedCommandCodeField(key);
window.setTimeout(() => setCopiedCommandCodeField(null), 1500);
} catch {
setSaveError("Copy failed. Select the text and copy it manually.");
}
};
const handleSubmit = async () => {
const credentialInput = isCommandCode
? extractCommandCodeCredentialInput(formData.apiKey)
: formData.apiKey;
if (!provider || (!isCompatible && !apiKeyOptional && !credentialInput)) return;
setSaving(true);
setSaveError(null);
try {
if (isGooglePse && !formData.cx.trim()) {
setSaveError(t("searchEngineIdRequired"));
return;
}
let validatedBaseUrl = null;
if (usesBaseUrl) {
const checked = normalizeAndValidateHttpBaseUrl(formData.baseUrl, defaultBaseUrl);
if (checked.error) {
setSaveError(checked.error);
return;
}
validatedBaseUrl = checked.value;
}
let isValid = Boolean(isNoAuthWebSessionCredential && !credentialInput);
let validationError: string | null = null;
if (!isValid) {
try {
setValidating(true);
setValidationResult(null);
const res = await fetch("/api/providers/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider,
apiKey: credentialInput,
validationModelId: formData.validationModelId || undefined,
customUserAgent: formData.customUserAgent.trim() || undefined,
baseUrl: formData.baseUrl.trim() || undefined,
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
cx: formData.cx.trim() || undefined,
}),
});
const data = await res.json();
isValid = !!data.valid;
if (!isValid && data.error) {
validationError = data.error;
}
setValidationResult(isValid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
}
if (!isValid) {
if (apiKeyOptional && !credentialInput) {
// Bypass validation block for local/optional providers when no key is provided
console.debug("Validation failed but apiKey is optional; proceeding to save.");
} else {
setSaveError(validationError || credentialValidationFailedMessage);
return;
}
}
const providerSpecificData: Record<string, unknown> = {};
if (formData.customUserAgent.trim()) {
providerSpecificData.customUserAgent = formData.customUserAgent.trim();
}
if (formData.routingTags.trim()) {
providerSpecificData.tags = parseRoutingTagsInput(formData.routingTags);
}
if (formData.excludedModels.trim()) {
providerSpecificData.excludedModels = parseExcludedModelsInput(formData.excludedModels);
}
if (formData.passthroughModels) {
providerSpecificData.passthroughModels = true;
}
if (provider === "bailian-coding-plan" && formData.consoleApiKey.trim()) {
providerSpecificData.consoleApiKey = formData.consoleApiKey.trim();
}
if (isGooglePse && formData.cx.trim()) {
providerSpecificData.cx = formData.cx.trim();
}
if (usesBaseUrl) {
providerSpecificData.baseUrl = validatedBaseUrl;
} else if (showsRegion) {
providerSpecificData.region = formData.region.trim() || defaultRegion;
} else if (isGlm) {
providerSpecificData.apiRegion = formData.apiRegion;
} else if (isCloudflare && formData.accountId.trim()) {
providerSpecificData.accountId = formData.accountId.trim();
}
if (isCcCompatible && formData.ccCompatibleContext1m) {
providerSpecificData.requestDefaults = { context1m: true };
}
const payload = {
name: formData.name,
apiKey: credentialInput.trim() || undefined,
priority: formData.priority,
testStatus: "active",
providerSpecificData:
Object.keys(providerSpecificData).length > 0 ? providerSpecificData : undefined,
};
const error = await onSave(payload);
if (error) {
setSaveError(typeof error === "string" ? error : t("failedSaveConnection"));
}
} finally {
setSaving(false);
}
};
const handleBulkSubmit = async () => {
if (!provider) return;
const parsed = parseBulkApiKeys(bulkText);
setBulkWarnings(parsed.warnings);
if (parsed.entries.length === 0) return;
setSaving(true);
setBulkResult(null);
setSaveError(null);
try {
let providerSpecificData: Record<string, unknown> | undefined;
if (usesBaseUrl) {
const checked = normalizeAndValidateHttpBaseUrl(formData.baseUrl, defaultBaseUrl);
if (checked.error) {
setSaveError(checked.error);
return;
}
providerSpecificData = { baseUrl: checked.value };
}
const res = await fetch("/api/providers/bulk", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider,
entries: parsed.entries.map((e) => ({ name: e.name, apiKey: e.apiKey })),
priority: formData.priority || 1,
providerSpecificData,
validateKeys: bulkValidateKeys,
}),
});
const data = await res.json();
if (!res.ok) {
setSaveError(typeof data?.error === "string" ? data.error : t("failedSaveConnection"));
return;
}
setBulkResult({
success: data.success || 0,
failed: data.failed || 0,
total: data.total || 0,
errors: Array.isArray(data.errors) ? data.errors : [],
});
} catch (err) {
setSaveError(err instanceof Error ? err.message : t("failedSaveConnection"));
} finally {
setSaving(false);
}
};
if (!provider) return null;
return (
<Modal
isOpen={isOpen}
title={getAddCredentialModalTitle(t, providerDisplayName, webSessionCredential)}
onClose={onClose}
>
<div className="flex flex-col gap-4">
{bulkSupported && (
<div className="flex gap-1 border-b border-border">
<button
type="button"
onClick={() => {
setMode("single");
setBulkResult(null);
setBulkWarnings([]);
}}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
mode === "single"
? "border-b-2 border-primary text-text-main"
: "text-text-muted hover:text-text-main"
}`}
>
{t("bulkTabSingle")}
</button>
<button
type="button"
onClick={() => {
setMode("bulk");
setSaveError(null);
}}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
mode === "bulk"
? "border-b-2 border-primary text-text-main"
: "text-text-muted hover:text-text-main"
}`}
>
{t("bulkTabBulkAdd")}
</button>
</div>
)}
{bulkSupported && mode === "bulk" && (
<div className="flex flex-col gap-3">
<p className="text-xs text-text-muted">{t("bulkAddFormatHint")}</p>
<textarea
className="w-full rounded border border-border bg-background p-2 text-sm font-mono resize-y min-h-[140px] focus:outline-none focus:ring-1 focus:ring-primary"
placeholder={"name1|sk-key1\nname2|sk-key2\nsk-key-only-auto-named"}
value={bulkText}
onChange={(e) => setBulkText(e.target.value)}
/>
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<label className="text-sm text-text-muted">{t("priorityLabel")}</label>
<input
type="number"
min={1}
max={100}
value={formData.priority}
onChange={(e) =>
setFormData({
...formData,
priority: Number.parseInt(e.target.value) || 1,
})
}
className="w-20 px-2 py-1 text-sm border border-border rounded bg-background"
/>
</div>
<label className="flex items-center gap-2 text-sm text-text-muted cursor-pointer">
<input
type="checkbox"
checked={bulkValidateKeys}
onChange={(e) => setBulkValidateKeys(e.target.checked)}
className="rounded border-border"
/>
{t("bulkValidateKeys")}
</label>
</div>
{bulkWarnings.length > 0 && (
<div className="rounded border border-amber-500/25 bg-amber-500/10 p-2 text-xs text-amber-200 space-y-1">
{bulkWarnings.map((w, i) => (
<div key={i}>{w}</div>
))}
</div>
)}
{bulkResult && (
<div
className={`text-sm font-medium ${
bulkResult.failed > 0 ? "text-amber-300" : "text-emerald-400"
}`}
>
{t("bulkAddedCount", { count: bulkResult.success })}
{bulkResult.failed > 0 && (
<>, {t("bulkFailedCount", { count: bulkResult.failed })}</>
)}
{bulkResult.errors.length > 0 && (
<ul className="mt-2 list-disc pl-5 text-xs text-text-muted font-normal space-y-0.5">
{bulkResult.errors.slice(0, 10).map((err, i) => (
<li key={i}>
{err.name}: {err.message}
</li>
))}
{bulkResult.errors.length > 10 && (
<li> {bulkResult.errors.length - 10} more</li>
)}
</ul>
)}
</div>
)}
{saveError && <div className="text-sm text-rose-400">{saveError}</div>}
<div className="flex gap-2">
<Button onClick={handleBulkSubmit} fullWidth disabled={saving || !bulkText.trim()}>
{saving ? t("adding") : t("bulkAddAllKeys")}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
{t("cancel")}
</Button>
</div>
</div>
)}
{(!bulkSupported || mode === "single") && (
<>
{isCcCompatible && (
<div className="rounded-lg border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-sm text-text-muted">
<div className="flex items-start gap-2">
<span className="material-symbols-outlined mt-0.5 text-[18px] text-amber-500">
warning
</span>
<p>{t("ccCompatibleValidationHint")}</p>
</div>
</div>
)}
{isCommandCode && onStartCommandCodeAuth && (
<div className="rounded-lg border border-sky-500/20 bg-sky-500/10 px-3 py-3 text-sm">
<div className="flex items-start gap-3">
<span className="material-symbols-outlined mt-0.5 text-[18px] text-sky-500">
open_in_new
</span>
<div className="min-w-0 flex-1">
<p className="font-medium text-text-main">
{t("providerDetailBrowserManualConnect")}
</p>
<p className="mt-1 text-xs text-text-muted">
Open Command Code Studio, then paste the returned key/JSON/URL into the API
key field below.
</p>
{commandCodeAuthState?.message && (
<p className="mt-2 text-xs text-text-muted">
{commandCodeAuthPhaseLabel}: {commandCodeAuthState.message}
</p>
)}
{commandCodeAuthState?.authUrl && (
<div className="mt-3 space-y-2">
<div>
<p className="mb-1 text-xs font-medium text-text-main">
{t("providerDetailAuthUrl")}
</p>
<div className="flex gap-2">
<Input
value={commandCodeAuthState.authUrl}
readOnly
className="flex-1 font-mono text-xs"
/>
<Button
variant="ghost"
size="sm"
icon={copiedCommandCodeField === "authUrl" ? "check" : "content_copy"}
onClick={() =>
copyCommandCodeValue(commandCodeAuthState.authUrl, "authUrl")
}
/>
</div>
</div>
{commandCodeAuthState.callbackUrl && (
<div>
<p className="mb-1 text-xs font-medium text-text-main">
{t("providerDetailCallbackUrl")}
</p>
<div className="flex gap-2">
<Input
value={commandCodeAuthState.callbackUrl}
readOnly
className="flex-1 font-mono text-xs"
/>
<Button
variant="ghost"
size="sm"
icon={
copiedCommandCodeField === "callbackUrl"
? "check"
: "content_copy"
}
onClick={() =>
copyCommandCodeValue(
commandCodeAuthState.callbackUrl,
"callbackUrl"
)
}
/>
</div>
</div>
)}
</div>
)}
</div>
<Button
variant="secondary"
size="sm"
icon="open_in_new"
loading={
commandCodeAuthState?.phase === "starting" ||
commandCodeAuthState?.phase === "polling" ||
commandCodeAuthState?.phase === "applying"
}
onClick={onStartCommandCodeAuth}
>
Connect in browser
</Button>
</div>
</div>
)}
<Input
label={t("nameLabel")}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={isQoder ? t("personalAccessTokenLabel") : t("productionKey")}
/>
{webSessionCredential && (
<WebSessionCredentialGuide
requirement={webSessionCredential}
providerName={providerDisplayName}
t={t}
/>
)}
{!isNoAuthWebSessionCredential && (
<div className="flex gap-2">
<Input
label={apiCredentialLabel}
type="password"
value={formData.apiKey}
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
className="flex-1"
placeholder={apiCredentialPlaceholder}
hint={apiCredentialHint}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
<div className="pt-6">
<Button
onClick={handleValidate}
disabled={
(!isCompatible && !apiKeyOptional && !formData.apiKey) ||
(isGooglePse && !formData.cx.trim()) ||
validating ||
saving
}
variant="secondary"
>
{validating
? t("checking")
: webSessionCredential
? getWebSessionCredentialCheckLabel(t, webSessionCredential)
: t("check")}
</Button>
</div>
</div>
)}
{isGooglePse && (
<Input
label={t("searchEngineIdLabel")}
value={formData.cx}
onChange={(e) => setFormData({ ...formData, cx: e.target.value })}
placeholder="012345678901234567890:abc123xyz"
hint={t("searchEngineIdHint")}
/>
)}
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? t("valid") : t("invalid")}
</Badge>
)}
{saveError && (
<div className="text-sm text-red-500 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
{saveError}
</div>
)}
{isCcCompatible && (
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
<Toggle
checked={formData.ccCompatibleContext1m}
onChange={(checked) =>
setFormData({ ...formData, ccCompatibleContext1m: checked })
}
label={t("ccCompatibleContext1mLabel")}
description={t("ccCompatibleContext1mDescription")}
/>
</div>
)}
{isCompatible && !isCcCompatible && (
<p className="text-xs text-text-muted">
{isAnthropic
? t("validationChecksAnthropicCompatible", {
provider: providerName || t("anthropicCompatibleName"),
})
: t("validationChecksOpenAiCompatible", {
provider: providerName || t("openaiCompatibleName"),
})}
</p>
)}
<button
type="button"
className="text-sm text-text-muted hover:text-text-primary flex items-center gap-1"
onClick={() => setShowAdvanced(!showAdvanced)}
aria-expanded={showAdvanced}
aria-controls="add-api-key-advanced-settings"
>
<span
className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}
aria-hidden="true"
>
</span>
{t("advancedSettings")}
</button>
{showAdvanced && (
<div
id="add-api-key-advanced-settings"
className="flex flex-col gap-3 pl-2 border-l-2 border-border"
>
<Input
label={t("customUserAgentLabel")}
value={formData.customUserAgent}
onChange={(e) => setFormData({ ...formData, customUserAgent: e.target.value })}
placeholder="my-app/1.0"
hint={t("customUserAgentHint")}
/>
<Input
label={t("routingTagsLabel")}
value={formData.routingTags}
onChange={(e) => setFormData({ ...formData, routingTags: e.target.value })}
placeholder={t("routingTagsPlaceholder")}
hint={t("routingTagsHint")}
/>
<Input
label={t("excludedModelsLabel")}
value={formData.excludedModels}
onChange={(e) => setFormData({ ...formData, excludedModels: e.target.value })}
placeholder={t("excludedModelsPlaceholder")}
hint={t("excludedModelsHint")}
/>
<Toggle
size="sm"
checked={formData.passthroughModels}
onChange={(checked) => setFormData({ ...formData, passthroughModels: checked })}
label={t("perModelQuotaLabel")}
description={t("perModelQuotaDescription")}
/>
{provider === "bailian-coding-plan" && (
<Input
label={t("consoleApiKeyOracleLabel")}
value={formData.consoleApiKey}
onChange={(e) => setFormData({ ...formData, consoleApiKey: e.target.value })}
placeholder={t("consoleApiKeyOraclePlaceholder")}
hint={t("consoleApiKeyOracleHint")}
type="password"
/>
)}
</div>
)}
<Input
label={t("validationModelIdLabel")}
placeholder={t("validationModelIdPlaceholder")}
value={formData.validationModelId}
onChange={(e) => setFormData({ ...formData, validationModelId: e.target.value })}
hint={t("validationModelIdHint")}
/>
<Input
label={t("priorityLabel")}
type="number"
value={formData.priority}
onChange={(e) =>
setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })
}
/>
{usesBaseUrl && (
<Input
label={t("baseUrlLabel")}
value={formData.baseUrl}
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
placeholder={getProviderBaseUrlPlaceholder(provider)}
hint={getProviderBaseUrlHint(provider, t)}
/>
)}
{showsRegion && (
<Input
label={t("regionLabel")}
value={formData.region}
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
placeholder={defaultRegion}
hint={t("regionHint")}
/>
)}
{isCloudflare && (
<Input
label={t("accountIdLabel")}
value={formData.accountId}
onChange={(e) => setFormData({ ...formData, accountId: e.target.value })}
placeholder={t("accountIdPlaceholder")}
hint={t("accountIdHint")}
/>
)}
{isGlm && (
<div>
<label className="text-sm font-medium text-text-main mb-1 block">
{t("apiRegionLabel")}
</label>
<select
value={formData.apiRegion}
onChange={(e) => setFormData({ ...formData, apiRegion: e.target.value })}
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="international">{t("apiRegionInternational")}</option>
<option value="china">{t("apiRegionChina")}</option>
</select>
<p className="text-xs text-text-muted mt-1">{t("apiRegionHint")}</p>
</div>
)}
<div className="flex gap-2">
<Button
onClick={handleSubmit}
fullWidth
disabled={
!formData.name ||
(!isCompatible && !apiKeyOptional && !formData.apiKey) ||
(isGooglePse && !formData.cx.trim()) ||
saving ||
(usesBaseUrl && !formData.baseUrl.trim() && !defaultBaseUrl)
}
>
{saving ? t("saving") : t("save")}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
{t("cancel")}
</Button>
</div>
</>
)}
</div>
</Modal>
);
}

View File

@@ -0,0 +1,155 @@
// @vitest-environment jsdom
//
// Phase 1c regression tests for Issue #3501. AddApiKeyModal and EditConnectionModal
// were extracted from the god-component. This proves each mounts in isolation with
// its clean Props interface (Hard Rule #8, Rule #18 TDD gate).
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import AddApiKeyModal from "../AddApiKeyModal";
import EditConnectionModal from "../EditConnectionModal";
vi.mock("next/navigation", () => ({
useParams: () => ({ id: "openai" }),
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));
vi.mock("next-intl", () => ({
useTranslations: (ns?: string) => (k: string) => (ns ? `${ns}.${k}` : k),
}));
const cleanups: Array<() => void> = [];
function renderModal(node: React.ReactElement) {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
act(() => root.render(node));
cleanups.push(() => {
act(() => root.unmount());
container.remove();
});
return container;
}
describe("conn-modals (Phase 1c extraction)", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({ ok: true, json: async () => ({}), text: async () => "" } as Response)
)
);
vi.stubGlobal("localStorage", {
getItem: () => null,
setItem: () => undefined,
removeItem: () => undefined,
clear: () => undefined,
});
});
afterEach(() => {
while (cleanups.length) cleanups.pop()?.();
document.body.innerHTML = "";
vi.unstubAllGlobals();
});
it("AddApiKeyModal mounts standalone when isOpen=false", () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const c = renderModal(
<AddApiKeyModal
isOpen={false}
provider="openai"
providerName="OpenAI"
isCompatible={false}
onSave={onSave}
onClose={vi.fn()}
/>
);
// When isOpen=false the modal renders nothing (null body) — no throw is the assertion.
expect(c).toBeDefined();
});
it("AddApiKeyModal mounts standalone when isOpen=true", () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const c = renderModal(
<AddApiKeyModal
isOpen={true}
provider="openai"
providerName="OpenAI"
isCompatible={false}
onSave={onSave}
onClose={vi.fn()}
/>
);
expect(c.querySelector("*")).not.toBeNull();
});
it("AddApiKeyModal returns null when provider is falsy", () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const c = renderModal(
<AddApiKeyModal isOpen={true} onSave={onSave} onClose={vi.fn()} />
);
// No provider → renders null
expect(c.textContent).toBe("");
});
it("EditConnectionModal mounts standalone when connection=null", () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const c = renderModal(
<EditConnectionModal
isOpen={false}
connection={null}
onSave={onSave}
onClose={vi.fn()}
/>
);
// connection=null → renders null — no throw is the assertion
expect(c).toBeDefined();
});
it("EditConnectionModal mounts standalone with a connection", () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const connection = {
id: "conn-1",
name: "Test Connection",
provider: "openai",
authType: "apikey",
priority: 1,
};
const c = renderModal(
<EditConnectionModal
isOpen={true}
connection={connection}
onSave={onSave}
onClose={vi.fn()}
/>
);
expect(c.querySelector("*")).not.toBeNull();
});
it("EditConnectionModal renders without ReferenceError for oauth connection", () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const connection = {
id: "conn-oauth",
name: "OAuth Account",
email: "user@example.com",
provider: "claude",
authType: "oauth",
priority: 1,
};
// Must not throw; tests that ERROR_TYPE_LABELS and formatTimeAgo are properly imported
expect(() =>
renderModal(
<EditConnectionModal
isOpen={true}
connection={connection}
onSave={onSave}
onClose={vi.fn()}
/>
)
).not.toThrow();
});
});

View File

@@ -629,3 +629,43 @@ export function shouldSwitchToVisibleFilter(opts: {
}): boolean {
return opts.autoHideFailed && opts.hiddenCount > 0;
}
// ---------------------------------------------------------------------------
// Error-type label map — shared by ConnectionRow and EditConnectionModal
// ---------------------------------------------------------------------------
export const ERROR_TYPE_LABELS: Record<
string,
{ labelKey: string; variant: string }
> = {
runtime_error: { labelKey: "errorTypeRuntime", variant: "warning" },
upstream_auth_error: { labelKey: "errorTypeUpstreamAuth", variant: "error" },
account_deactivated: { labelKey: "Account Deactivated", variant: "error" },
auth_missing: { labelKey: "errorTypeMissingCredential", variant: "warning" },
token_refresh_failed: { labelKey: "errorTypeRefreshFailed", variant: "warning" },
token_expired: { labelKey: "errorTypeTokenExpired", variant: "warning" },
upstream_rate_limited: { labelKey: "errorTypeRateLimited", variant: "warning" },
upstream_unavailable: { labelKey: "errorTypeUpstreamUnavailable", variant: "error" },
network_error: { labelKey: "errorTypeNetworkError", variant: "warning" },
unsupported: { labelKey: "errorTypeTestUnsupported", variant: "default" },
upstream_error: { labelKey: "errorTypeUpstreamError", variant: "error" },
banned: { labelKey: "errorTypeBanned", variant: "error" },
credits_exhausted: { labelKey: "errorTypeCreditsExhausted", variant: "warning" },
};
// ---------------------------------------------------------------------------
// formatTimeAgo — used in EditConnectionModal's extra-key health display
// ---------------------------------------------------------------------------
export function formatTimeAgo(dateStr: string): string {
const now = Date.now();
const date = new Date(dateStr).getTime();
const diff = now - date;
if (diff < 0) return "just now";
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}d ago`;
return new Date(dateStr).toLocaleDateString();
}