refactor(dashboard): extract EditCompatibleNodeModal — #3501 Phase 1b (#3638)

#3501 Phase 1b: extract EditCompatibleNodeModal (cycle-safe via leaf constants module).

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-11 09:14:57 -03:00
committed by GitHub
parent 1d147b61da
commit ee53ced36b
6 changed files with 356 additions and 288 deletions

View File

@@ -13,6 +13,7 @@
- **Provider-detail god-component decomposition — Phase 0** ([#3501]): introduced `ProviderDetailPageClient.tsx` and reduced `providers/[id]/page.tsx` to a thin 9-line route wrapper (was 12,882 LOC), following the repo's `*PageClient` convention. Added the first-ever smoke render test for the page (Hard Rule #8) as the safety net every later extraction phase is diffed against. Behavior unchanged; the `check-file-size` ratchet now tracks the extracted client. Foundation for Phases 16 (strangler-fig). Thanks @oyi77 for the parallel modularization effort in #3627.
- **Provider-detail god-component decomposition — Phase 1a** ([#3501]): extracted the three self-contained auth-import modal clusters (Codex/Claude/Gemini `Import*AuthModal` + `Apply*AuthModal` + their co-located helpers, ~2,160 LOC) into `providers/[id]/components/modals/`. `ProviderDetailPageClient.tsx` drops 12,882 → 10,719 LOC. Behavior unchanged (smoke test green; clusters had clean `{ onClose, onSuccess }` / inline-prop interfaces). Co-authored with @oyi77.
- **Provider-detail god-component decomposition — Phase 1b** ([#3501]): extracted `EditCompatibleNodeModal` (+ its node/props types) into `providers/[id]/components/modals/`, and moved the shared `CC_COMPATIBLE_DEFAULT_CHAT_PATH` constant into a leaf `providerDetailConstants.ts` so the page client and the modal can both import it without a circular dependency. Also removed two dangling section comments left by Phase 1a. `ProviderDetailPageClient.tsx`: 10,719 → 10,435 LOC. Behavior unchanged (smoke test + a new standalone modal render test green; `check:cycles` clean). Co-authored with @oyi77.
---

View File

@@ -48,7 +48,7 @@
"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": 10720,
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 10435,
"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

@@ -99,6 +99,8 @@ import { ImportCodexAuthModal, ApplyCodexAuthModal } from "./components/modals/I
import { ImportClaudeAuthModal, ApplyClaudeAuthModal } from "./components/modals/ImportClaudeAuthModal";
import { ImportGeminiAuthModal, ApplyGeminiAuthModal } from "./components/modals/ImportGeminiAuthModal";
import EditCompatibleNodeModal from "./components/modals/EditCompatibleNodeModal";
import { CC_COMPATIBLE_DEFAULT_CHAT_PATH } from "./providerDetailConstants";
type CompatByProtocolMap = Partial<
Record<
ModelCompatProtocolKey,
@@ -905,27 +907,6 @@ interface EditConnectionModalProps {
onSave: (data: unknown) => Promise<void | unknown>;
onClose: () => void;
}
interface EditCompatibleNodeModalNode {
id?: string;
name?: string;
prefix?: string;
apiType?: string;
baseUrl?: string;
chatPath?: string;
modelsPath?: string;
}
interface EditCompatibleNodeModalProps {
isOpen: boolean;
node: EditCompatibleNodeModalNode | null;
onSave: (data: unknown) => Promise<void>;
onClose: () => void;
isAnthropic?: boolean;
isCcCompatible?: boolean;
}
const CC_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true";
const CODEX_REASONING_STRENGTH_OPTIONS = [
{ value: "none", label: "None" },
{ value: "low", label: "Low" },
@@ -9347,8 +9328,6 @@ function AddApiKeyModal({
);
}
// ──── ImportCodexAuthModal ────────────────────────────────────────────────────
function normalizeAndValidateHttpBaseUrl(rawValue, fallbackUrl) {
const value = (typeof rawValue === "string" ? rawValue.trim() : "") || fallbackUrl;
try {
@@ -10453,267 +10432,3 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
</Modal>
);
}
function EditCompatibleNodeModal({
isOpen,
node,
onSave,
onClose,
isAnthropic,
isCcCompatible,
}: EditCompatibleNodeModalProps) {
const t = useTranslations("providers");
const [formData, setFormData] = useState({
name: "",
prefix: "",
apiType: "chat",
baseUrl: "https://api.openai.com/v1",
chatPath: "",
modelsPath: "",
});
const [saving, setSaving] = useState(false);
const [checkKey, setCheckKey] = useState("");
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [showAdvanced, setShowAdvanced] = useState(false);
useEffect(() => {
if (node) {
setFormData({
name: node.name || "",
prefix: node.prefix || "",
apiType: node.apiType || "chat",
baseUrl:
node.baseUrl ||
(isCcCompatible
? "https://api.anthropic.com"
: isAnthropic
? "https://api.anthropic.com/v1"
: "https://api.openai.com/v1"),
chatPath: node.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
modelsPath: isCcCompatible ? "" : node.modelsPath || "",
});
setShowAdvanced(
!!(
node.chatPath ||
(!isCcCompatible && node.modelsPath) ||
(isCcCompatible && !node.chatPath)
)
);
}
}, [node, isAnthropic, isCcCompatible]);
const apiTypeOptions = [
{ value: "chat", label: t("chatCompletions") },
{ value: "responses", label: t("responsesApi") },
{ value: "embeddings", label: t("embeddings") },
{ value: "audio-transcriptions", label: t("audioTranscriptions") },
{ value: "audio-speech", label: t("audioSpeech") },
{ value: "images-generations", label: t("imagesGenerations") },
];
const handleSubmit = async () => {
if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
setSaving(true);
try {
const payload: any = {
name: formData.name,
prefix: formData.prefix,
baseUrl: formData.baseUrl,
chatPath: formData.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
modelsPath: isCcCompatible ? "" : formData.modelsPath,
};
if (!isAnthropic) {
payload.apiType = formData.apiType;
}
await onSave(payload);
} finally {
setSaving(false);
}
};
const handleValidate = async () => {
setValidating(true);
try {
const res = await fetch("/api/provider-nodes/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
baseUrl: formData.baseUrl,
apiKey: checkKey,
type: isAnthropic ? "anthropic-compatible" : "openai-compatible",
compatMode: isCcCompatible ? "cc" : undefined,
chatPath: formData.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
modelsPath: isCcCompatible ? "" : formData.modelsPath,
}),
});
const data = await res.json();
setValidationResult(data.valid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
};
if (!node) return null;
return (
<Modal
isOpen={isOpen}
title={
isCcCompatible
? t("ccCompatibleDetailsTitle")
: t("editCompatibleTitle", { type: isAnthropic ? t("anthropic") : t("openai") })
}
onClose={onClose}
>
<div className="flex flex-col gap-4">
{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>
)}
<Input
label={t("nameLabel")}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={
isCcCompatible
? t("ccCompatibleNamePlaceholder")
: t("compatibleProdPlaceholder", {
type: isAnthropic ? t("anthropic") : t("openai"),
})
}
hint={isCcCompatible ? t("ccCompatibleNameHint") : t("nameHint")}
/>
<Input
label={t("prefixLabel")}
value={formData.prefix}
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
placeholder={
isCcCompatible
? t("ccCompatiblePrefixPlaceholder")
: isAnthropic
? t("anthropicPrefixPlaceholder")
: t("openaiPrefixPlaceholder")
}
hint={isCcCompatible ? t("ccCompatiblePrefixHint") : t("prefixHint")}
/>
{!isAnthropic && (
<Select
label={t("apiTypeLabel")}
options={apiTypeOptions}
value={formData.apiType}
onChange={(e) => setFormData({ ...formData, apiType: e.target.value })}
/>
)}
<Input
label={t("baseUrlLabel")}
value={formData.baseUrl}
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
placeholder={
isCcCompatible
? t("ccCompatibleBaseUrlPlaceholder")
: isAnthropic
? t("anthropicBaseUrlPlaceholder")
: t("openaiBaseUrlPlaceholder")
}
hint={
isCcCompatible
? t("ccCompatibleBaseUrlHint")
: t("compatibleBaseUrlHint", {
type: isAnthropic ? t("anthropic") : t("openai"),
})
}
/>
<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="advanced-settings"
>
<span
className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}
aria-hidden="true"
>
</span>
{t("advancedSettings")}
</button>
{showAdvanced && (
<div id="advanced-settings" className="flex flex-col gap-3 pl-2 border-l-2 border-border">
<Input
label={t("chatPathLabel")}
value={formData.chatPath}
onChange={(e) => setFormData({ ...formData, chatPath: e.target.value })}
placeholder={
isCcCompatible
? CC_COMPATIBLE_DEFAULT_CHAT_PATH
: isAnthropic
? "/messages"
: t("chatPathPlaceholder")
}
hint={isCcCompatible ? t("ccCompatibleChatPathHint") : t("chatPathHint")}
/>
{!isCcCompatible && (
<Input
label={t("modelsPathLabel")}
value={formData.modelsPath}
onChange={(e) => setFormData({ ...formData, modelsPath: e.target.value })}
placeholder={t("modelsPathPlaceholder")}
hint={t("modelsPathHint")}
/>
)}
</div>
)}
<div className="flex gap-2">
<Input
label={t("apiKeyForCheck")}
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
className="flex-1"
/>
<div className="pt-6">
<Button
onClick={handleValidate}
disabled={!checkKey || validating || !formData.baseUrl.trim()}
variant="secondary"
>
{validating ? t("checking") : t("check")}
</Button>
</div>
</div>
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? t("valid") : t("invalid")}
</Badge>
)}
<div className="flex gap-2">
<Button
onClick={handleSubmit}
fullWidth
disabled={
!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || saving
}
>
{saving ? t("saving") : t("save")}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
{t("cancel")}
</Button>
</div>
</div>
</Modal>
);
}
// ──── ImportGeminiAuthModal ────────────────────────────────────────────────────

View File

@@ -0,0 +1,284 @@
"use client";
import { useState, useEffect } from "react";
import { useTranslations } from "next-intl";
import { Button, Badge, Input, Modal, Select } from "@/shared/components";
import { CC_COMPATIBLE_DEFAULT_CHAT_PATH } from "../../providerDetailConstants";
interface EditCompatibleNodeModalNode {
id?: string;
name?: string;
prefix?: string;
apiType?: string;
baseUrl?: string;
chatPath?: string;
modelsPath?: string;
}
interface EditCompatibleNodeModalProps {
isOpen: boolean;
node: EditCompatibleNodeModalNode | null;
onSave: (data: unknown) => Promise<void>;
onClose: () => void;
isAnthropic?: boolean;
isCcCompatible?: boolean;
}
export default function EditCompatibleNodeModal({
isOpen,
node,
onSave,
onClose,
isAnthropic,
isCcCompatible,
}: EditCompatibleNodeModalProps) {
const t = useTranslations("providers");
const [formData, setFormData] = useState({
name: "",
prefix: "",
apiType: "chat",
baseUrl: "https://api.openai.com/v1",
chatPath: "",
modelsPath: "",
});
const [saving, setSaving] = useState(false);
const [checkKey, setCheckKey] = useState("");
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [showAdvanced, setShowAdvanced] = useState(false);
useEffect(() => {
if (node) {
setFormData({
name: node.name || "",
prefix: node.prefix || "",
apiType: node.apiType || "chat",
baseUrl:
node.baseUrl ||
(isCcCompatible
? "https://api.anthropic.com"
: isAnthropic
? "https://api.anthropic.com/v1"
: "https://api.openai.com/v1"),
chatPath: node.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
modelsPath: isCcCompatible ? "" : node.modelsPath || "",
});
setShowAdvanced(
!!(
node.chatPath ||
(!isCcCompatible && node.modelsPath) ||
(isCcCompatible && !node.chatPath)
)
);
}
}, [node, isAnthropic, isCcCompatible]);
const apiTypeOptions = [
{ value: "chat", label: t("chatCompletions") },
{ value: "responses", label: t("responsesApi") },
{ value: "embeddings", label: t("embeddings") },
{ value: "audio-transcriptions", label: t("audioTranscriptions") },
{ value: "audio-speech", label: t("audioSpeech") },
{ value: "images-generations", label: t("imagesGenerations") },
];
const handleSubmit = async () => {
if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
setSaving(true);
try {
const payload: any = {
name: formData.name,
prefix: formData.prefix,
baseUrl: formData.baseUrl,
chatPath: formData.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
modelsPath: isCcCompatible ? "" : formData.modelsPath,
};
if (!isAnthropic) {
payload.apiType = formData.apiType;
}
await onSave(payload);
} finally {
setSaving(false);
}
};
const handleValidate = async () => {
setValidating(true);
try {
const res = await fetch("/api/provider-nodes/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
baseUrl: formData.baseUrl,
apiKey: checkKey,
type: isAnthropic ? "anthropic-compatible" : "openai-compatible",
compatMode: isCcCompatible ? "cc" : undefined,
chatPath: formData.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
modelsPath: isCcCompatible ? "" : formData.modelsPath,
}),
});
const data = await res.json();
setValidationResult(data.valid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
};
if (!node) return null;
return (
<Modal
isOpen={isOpen}
title={
isCcCompatible
? t("ccCompatibleDetailsTitle")
: t("editCompatibleTitle", { type: isAnthropic ? t("anthropic") : t("openai") })
}
onClose={onClose}
>
<div className="flex flex-col gap-4">
{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>
)}
<Input
label={t("nameLabel")}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={
isCcCompatible
? t("ccCompatibleNamePlaceholder")
: t("compatibleProdPlaceholder", {
type: isAnthropic ? t("anthropic") : t("openai"),
})
}
hint={isCcCompatible ? t("ccCompatibleNameHint") : t("nameHint")}
/>
<Input
label={t("prefixLabel")}
value={formData.prefix}
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
placeholder={
isCcCompatible
? t("ccCompatiblePrefixPlaceholder")
: isAnthropic
? t("anthropicPrefixPlaceholder")
: t("openaiPrefixPlaceholder")
}
hint={isCcCompatible ? t("ccCompatiblePrefixHint") : t("prefixHint")}
/>
{!isAnthropic && (
<Select
label={t("apiTypeLabel")}
options={apiTypeOptions}
value={formData.apiType}
onChange={(e) => setFormData({ ...formData, apiType: e.target.value })}
/>
)}
<Input
label={t("baseUrlLabel")}
value={formData.baseUrl}
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
placeholder={
isCcCompatible
? t("ccCompatibleBaseUrlPlaceholder")
: isAnthropic
? t("anthropicBaseUrlPlaceholder")
: t("openaiBaseUrlPlaceholder")
}
hint={
isCcCompatible
? t("ccCompatibleBaseUrlHint")
: t("compatibleBaseUrlHint", {
type: isAnthropic ? t("anthropic") : t("openai"),
})
}
/>
<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="advanced-settings"
>
<span
className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}
aria-hidden="true"
>
</span>
{t("advancedSettings")}
</button>
{showAdvanced && (
<div id="advanced-settings" className="flex flex-col gap-3 pl-2 border-l-2 border-border">
<Input
label={t("chatPathLabel")}
value={formData.chatPath}
onChange={(e) => setFormData({ ...formData, chatPath: e.target.value })}
placeholder={
isCcCompatible
? CC_COMPATIBLE_DEFAULT_CHAT_PATH
: isAnthropic
? "/messages"
: t("chatPathPlaceholder")
}
hint={isCcCompatible ? t("ccCompatibleChatPathHint") : t("chatPathHint")}
/>
{!isCcCompatible && (
<Input
label={t("modelsPathLabel")}
value={formData.modelsPath}
onChange={(e) => setFormData({ ...formData, modelsPath: e.target.value })}
placeholder={t("modelsPathPlaceholder")}
hint={t("modelsPathHint")}
/>
)}
</div>
)}
<div className="flex gap-2">
<Input
label={t("apiKeyForCheck")}
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
className="flex-1"
/>
<div className="pt-6">
<Button
onClick={handleValidate}
disabled={!checkKey || validating || !formData.baseUrl.trim()}
variant="secondary"
>
{validating ? t("checking") : t("check")}
</Button>
</div>
</div>
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? t("valid") : t("invalid")}
</Badge>
)}
<div className="flex gap-2">
<Button
onClick={handleSubmit}
fullWidth
disabled={
!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || saving
}
>
{saving ? t("saving") : t("save")}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
{t("cancel")}
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,62 @@
// @vitest-environment jsdom
//
// Phase 1b regression test for Issue #3501. EditCompatibleNodeModal was extracted
// out of the god-component into a standalone file (its node/props types co-moved,
// the shared CC_COMPATIBLE_DEFAULT_CHAT_PATH constant pulled into a leaf module to
// keep the import graph acyclic). Proves it mounts in isolation (Hard Rule #8).
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import EditCompatibleNodeModal from "../EditCompatibleNodeModal";
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("EditCompatibleNodeModal (Phase 1b extraction)", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanups.length) cleanups.pop()?.();
document.body.innerHTML = "";
});
it("mounts standalone when open", () => {
const c = renderModal(
<EditCompatibleNodeModal
isOpen
node={{ id: "n1", name: "Node", baseUrl: "https://x" }}
onSave={async () => {}}
onClose={() => {}}
isCcCompatible
/>
);
expect(c.querySelector("*")).not.toBeNull();
});
it("renders nothing harmful when closed", () => {
expect(() =>
renderModal(
<EditCompatibleNodeModal isOpen={false} node={null} onSave={async () => {}} onClose={() => {}} />
)
).not.toThrow();
});
});

View File

@@ -0,0 +1,6 @@
// Shared constants for the provider-detail page and its extracted modals
// (Issue #3501 strangler-fig decomposition). Kept in a leaf module so both the
// page client and the colocated modals can import without a circular dependency.
/** Default chat path for Claude-Code-compatible (Anthropic-shaped) custom nodes. */
export const CC_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true";