mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
Phase 1t of #3501: client 1376→781 LOC (≤800 reached). Original god-component 12,882→781 (−94%). Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
dd0b377b81
commit
37e80306b2
@@ -23,11 +23,11 @@
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1437,
|
||||
"open-sse/mcp-server/server.ts": 1457,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1118,
|
||||
"open-sse/services/accountFallback.ts": 1645,
|
||||
"open-sse/services/accountFallback.ts": 1708,
|
||||
"open-sse/services/batchProcessor.ts": 828,
|
||||
"open-sse/services/browserBackedChat.ts": 850,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 4955,
|
||||
"open-sse/services/combo.ts": 5054,
|
||||
"open-sse/services/rateLimitManager.ts": 1017,
|
||||
"open-sse/services/tokenRefresh.ts": 1997,
|
||||
"open-sse/services/usage.ts": 3341,
|
||||
@@ -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": 1377,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 782,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": 941,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 843,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1171,
|
||||
@@ -64,7 +64,7 @@
|
||||
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 880,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1072,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 981,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 983,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1580,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016,
|
||||
@@ -101,12 +101,13 @@
|
||||
"src/shared/constants/sidebarVisibility.ts": 990,
|
||||
"src/shared/services/cliRuntime.ts": 1084,
|
||||
"src/shared/validation/schemas.ts": 2515,
|
||||
"src/sse/handlers/chat.ts": 1381,
|
||||
"src/sse/services/auth.ts": 2198
|
||||
"src/sse/handlers/chat.ts": 1389,
|
||||
"src/sse/services/auth.ts": 2207
|
||||
},
|
||||
"_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.",
|
||||
"_rebaseline_2026_06_11_phase1f": "Phase 1f (#3501): ProviderDetailPageClient.tsx 4948→4062 (-886 LOC); 3 novos hooks extraídos. useProviderConnections.ts=954 acima do cap=800 — justificado: extração direta do god-component (zero lógica nova), própria redução do cliente supera o custo. useProviderSettings.ts=263 e useProviderModels.ts=154 já abaixo do cap.",
|
||||
"_rebaseline_2026_06_12_review_issues": "Re-baseline consciente do /review-issues v3.8.23: 27 arquivos com crescimento herdado (v3.8.22 nunca reconciliado) + fixes deste round (combo.ts #3685, openai-to-gemini.ts #3688, tokenRefresh.ts #3692, validation/proxies de outras merges). providerLimits.ts (941) adicionado como frozen (split coeso de usage). Shrink endereçado separadamente pelo #3501.",
|
||||
"_rebaseline_2026_06_12_phase1g1j": "Phase 1g-1j (#3501): ProviderDetailPageClient.tsx 4063→3409 (extraídos ProviderPlaygroundPanel, useCommandCodeAuth, useExternalLinkFlow+ExternalLinkModal, useAuthFileHandlers — zero lógica nova). models/route.ts 2344→2426: drift do #3712 (vertex dynamic model discovery) reconciliado aqui.",
|
||||
"_rebaseline_2026_06_12_phase1n1s": "Phase 1n-1s (#3501): ProviderDetailPageClient.tsx 2554→1376 (extraídos ConnectionsListPanel, ConnectionsHeaderToolbar, ZedImportCard, BatchTestResultsModal, AdaptaTutorialModal + hooks/useApiKeySave + 4 helper closures→providerPageHelpers.ts). providerPageHelpers.ts 822→897 justificado: recebe 4 closures do god-component (getApiLabel/getApiDefaultPath/getApiPath/getHeaderIconProviderId), zero lógica nova, cliente encolhe mais do que helpers crescem."
|
||||
"_rebaseline_2026_06_12_phase1n1s": "Phase 1n-1s (#3501): ProviderDetailPageClient.tsx 2554→1376 (extraídos ConnectionsListPanel, ConnectionsHeaderToolbar, ZedImportCard, BatchTestResultsModal, AdaptaTutorialModal + hooks/useApiKeySave + 4 helper closures→providerPageHelpers.ts). providerPageHelpers.ts 822→897 justificado: recebe 4 closures do god-component (getApiLabel/getApiDefaultPath/getApiPath/getHeaderIconProviderId), zero lógica nova, cliente encolhe mais do que helpers crescem.",
|
||||
"_rebaseline_2026_06_12_phase1t": "Phase 1t (#3501): ProviderDetailPageClient.tsx 1377→782 — META ≤800 ATINGIDA (extraídos ProviderPageHeader, CompatibleNodeCard, ProviderModalsPanel, EmptyConnectionsPlaceholder, UpstreamProxyCard, SearchProviderCard + hooks useConnectionGate/useProviderNodeActions). Drift concorrente reconciliado: ResilienceTab/sse-chat/sse-auth/accountFallback/combo (merges #3629 model-lockout etc.)."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
// Phase 1t.2 extraction — Issue #3501
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, Button } from "@/shared/components";
|
||||
import { getApiLabel, getApiPath } from "../providerPageHelpers";
|
||||
import type { ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
interface ProviderNode {
|
||||
baseUrl?: string;
|
||||
apiType?: string;
|
||||
chatPath?: string;
|
||||
prefix?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface CompatibleNodeCardProps {
|
||||
providerId: string;
|
||||
providerNode: ProviderNode;
|
||||
isCcCompatible: boolean;
|
||||
isAnthropicCompatible: boolean;
|
||||
isAnthropicProtocolCompatible: boolean;
|
||||
gateConnectionFlow: (callback: () => void) => void;
|
||||
openApiKeyAddFlow: () => void;
|
||||
onOpenEditNodeModal: () => void;
|
||||
t: ProviderMessageTranslator;
|
||||
}
|
||||
|
||||
export default function CompatibleNodeCard({
|
||||
providerId,
|
||||
providerNode,
|
||||
isCcCompatible,
|
||||
isAnthropicCompatible,
|
||||
isAnthropicProtocolCompatible,
|
||||
gateConnectionFlow,
|
||||
openApiKeyAddFlow,
|
||||
onOpenEditNodeModal,
|
||||
t,
|
||||
}: CompatibleNodeCardProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{isCcCompatible
|
||||
? t("ccCompatibleDetailsTitle")
|
||||
: isAnthropicCompatible
|
||||
? t("anthropicCompatibleDetails")
|
||||
: t("openaiCompatibleDetails")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
{getApiLabel(t, isAnthropicProtocolCompatible, providerNode?.apiType)} ·{" "}
|
||||
{(providerNode.baseUrl || "").replace(/\/$/, "")}/
|
||||
{getApiPath(
|
||||
isCcCompatible,
|
||||
isAnthropicCompatible,
|
||||
providerNode?.apiType,
|
||||
providerNode?.chatPath
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button size="sm" icon="add" onClick={() => gateConnectionFlow(openApiKeyAddFlow)}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="edit"
|
||||
onClick={onOpenEditNodeModal}
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="delete"
|
||||
onClick={async () => {
|
||||
if (
|
||||
!confirm(
|
||||
t("deleteCompatibleNodeConfirm", {
|
||||
type: isCcCompatible
|
||||
? t("ccCompatibleLabel")
|
||||
: isAnthropicCompatible
|
||||
? t("anthropic")
|
||||
: t("openai"),
|
||||
})
|
||||
)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
const res = await fetch(`/api/provider-nodes/${providerId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (res.ok) {
|
||||
router.push("/dashboard/providers");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting provider node:", error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isCcCompatible && (
|
||||
<div className="mb-4 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>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
// Phase 1t.6 extraction — Issue #3501
|
||||
import { Button } from "@/shared/components";
|
||||
import type { ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
interface CommandCodeAuthState {
|
||||
phase: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface EmptyConnectionsPlaceholderProps {
|
||||
isOAuth: boolean;
|
||||
isCompatible: boolean;
|
||||
isCommandCode: boolean;
|
||||
providerId: string;
|
||||
providerSupportsPat: boolean;
|
||||
commandCodeAuthState: CommandCodeAuthState;
|
||||
gateConnectionFlow: (callback: () => void) => void;
|
||||
openApiKeyAddFlow: () => void;
|
||||
openPrimaryAddFlow: () => void;
|
||||
handleOpenCommandCodeConnect: () => void;
|
||||
onOpenOAuthModal: () => void;
|
||||
onOpenImportCodex: () => void;
|
||||
onOpenImportClaude: () => void;
|
||||
onOpenImportGemini: () => void;
|
||||
t: ProviderMessageTranslator;
|
||||
}
|
||||
|
||||
export default function EmptyConnectionsPlaceholder({
|
||||
isOAuth,
|
||||
isCompatible,
|
||||
isCommandCode,
|
||||
providerId,
|
||||
providerSupportsPat,
|
||||
commandCodeAuthState,
|
||||
gateConnectionFlow,
|
||||
openApiKeyAddFlow,
|
||||
openPrimaryAddFlow,
|
||||
handleOpenCommandCodeConnect,
|
||||
onOpenOAuthModal,
|
||||
onOpenImportCodex,
|
||||
onOpenImportClaude,
|
||||
onOpenImportGemini,
|
||||
t,
|
||||
}: EmptyConnectionsPlaceholderProps) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||
<span className="material-symbols-outlined text-[32px]">
|
||||
{isOAuth ? "lock" : "key"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium mb-1">{t("noConnectionsYet")}</p>
|
||||
<p className="text-sm text-text-muted mb-4">{t("addFirstConnectionHint")}</p>
|
||||
{!isCompatible && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{isCommandCode ? (
|
||||
<>
|
||||
<Button
|
||||
icon="open_in_new"
|
||||
loading={
|
||||
commandCodeAuthState.phase === "starting" ||
|
||||
commandCodeAuthState.phase === "polling" ||
|
||||
commandCodeAuthState.phase === "applying"
|
||||
}
|
||||
onClick={() => gateConnectionFlow(handleOpenCommandCodeConnect)}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="add"
|
||||
onClick={() => gateConnectionFlow(openApiKeyAddFlow)}
|
||||
>
|
||||
Manual API key
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button icon="add" onClick={() => gateConnectionFlow(openPrimaryAddFlow)}>
|
||||
{providerSupportsPat ? "Add PAT" : t("addConnection")}
|
||||
</Button>
|
||||
{providerId === "qoder" && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => gateConnectionFlow(onOpenOAuthModal)}
|
||||
>
|
||||
Experimental OAuth
|
||||
</Button>
|
||||
)}
|
||||
{providerId === "codex" && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="upload_file"
|
||||
onClick={() => gateConnectionFlow(onOpenImportCodex)}
|
||||
>
|
||||
{typeof t.has === "function" && t.has("importCodexAuth")
|
||||
? t("importCodexAuth")
|
||||
: "Import auth"}
|
||||
</Button>
|
||||
)}
|
||||
{providerId === "claude" && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="upload_file"
|
||||
onClick={() => gateConnectionFlow(onOpenImportClaude)}
|
||||
>
|
||||
{typeof t.has === "function" && t.has("importClaudeAuth")
|
||||
? t("importClaudeAuth")
|
||||
: "Import auth"}
|
||||
</Button>
|
||||
)}
|
||||
{providerId === "gemini-cli" && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="upload_file"
|
||||
onClick={() => gateConnectionFlow(onOpenImportGemini)}
|
||||
>
|
||||
{typeof t.has === "function" && t.has("importGeminiAuth")
|
||||
? t("importGeminiAuth")
|
||||
: "Import auth"}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
"use client";
|
||||
|
||||
// Phase 1t.5 extraction — Issue #3501
|
||||
// Pure composition of all modal elements rendered by ProviderDetailPageClient.
|
||||
import { ConfirmModal, OAuthModal, KiroOAuthWrapper, CursorAuthModal, TraeAuthModal, ProxyConfigModal } from "@/shared/components";
|
||||
import RiskNoticeModal from "../../components/RiskNoticeModal";
|
||||
import CodexCliGuideModal from "../../components/CodexCliGuideModal";
|
||||
import SiliconFlowEndpointModal from "./SiliconFlowEndpointModal";
|
||||
import AddApiKeyModal from "./modals/AddApiKeyModal";
|
||||
import EditConnectionModal from "./modals/EditConnectionModal";
|
||||
import EditCompatibleNodeModal from "./modals/EditCompatibleNodeModal";
|
||||
import ExternalLinkModal from "./ExternalLinkModal";
|
||||
import BatchTestResultsModal from "./BatchTestResultsModal";
|
||||
import ImportProgressModal from "./ImportProgressModal";
|
||||
import { AdaptaTutorialModal } from "./AdaptaTutorialModal";
|
||||
import {
|
||||
ImportCodexAuthModal,
|
||||
ApplyCodexAuthModal,
|
||||
} from "./modals/ImportCodexAuthModal";
|
||||
import {
|
||||
ImportClaudeAuthModal,
|
||||
ApplyClaudeAuthModal,
|
||||
} from "./modals/ImportClaudeAuthModal";
|
||||
import {
|
||||
ImportGeminiAuthModal,
|
||||
ApplyGeminiAuthModal,
|
||||
} from "./modals/ImportGeminiAuthModal";
|
||||
import { type ConnectionRowConnection } from "./ConnectionRow";
|
||||
import { type BatchTestResults } from "../hooks/useProviderConnections";
|
||||
import { type ImportProgress } from "../hooks/useModelImportHandlers";
|
||||
import type { ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
interface ProviderInfo {
|
||||
name: string;
|
||||
riskNoticeVariant?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ProxyTarget {
|
||||
level: string;
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ProviderModalsPanelProps {
|
||||
providerId: string;
|
||||
providerInfo: ProviderInfo;
|
||||
isCompatible: boolean;
|
||||
isAnthropicProtocolCompatible: boolean;
|
||||
isCcCompatible: boolean;
|
||||
isCommandCode: boolean;
|
||||
isUpstreamProxyProvider: boolean;
|
||||
subscriptionRisk: boolean;
|
||||
// Risk notice
|
||||
showRiskNoticeModal: boolean;
|
||||
handleConfirmRiskNotice: () => void;
|
||||
handleCancelRiskNotice: () => void;
|
||||
// OAuth
|
||||
showOAuthModal: boolean;
|
||||
reauthConnection: ConnectionRowConnection | null;
|
||||
handleOAuthSuccess: () => void;
|
||||
setShowOAuthModal: (show: boolean) => void;
|
||||
// SiliconFlow
|
||||
showSiliconFlowEndpointModal: boolean;
|
||||
setSiliconFlowInitialBaseUrl: (url: string | undefined) => void;
|
||||
setShowSiliconFlowEndpointModal: (open: boolean) => void;
|
||||
setShowAddApiKeyModal: (open: boolean) => void;
|
||||
// AddApiKey
|
||||
showAddApiKeyModal: boolean;
|
||||
siliconFlowInitialBaseUrl: string | undefined;
|
||||
commandCodeAuthState: { phase: string; [key: string]: unknown };
|
||||
handleStartCommandCodeAuth: () => void;
|
||||
handleSaveApiKey: (data: any) => Promise<void>;
|
||||
handleCloseAddApiKeyModal: () => void;
|
||||
// Batch delete confirm
|
||||
batchDeleteConfirmOpen: boolean;
|
||||
setBatchDeleteConfirmOpen: (open: boolean) => void;
|
||||
handleBatchDeleteConfirm: () => void;
|
||||
selectedIds: Set<string>;
|
||||
batchDeleting: boolean;
|
||||
// Codex auth
|
||||
applyCodexModalConnectionId: string | null;
|
||||
setApplyCodexModalConnectionId: (id: string | null) => void;
|
||||
applyingCodexAuthId: string | null;
|
||||
handleApplyCodexAuthLocal: (id: string) => Promise<void>;
|
||||
importCodexModalOpen: boolean;
|
||||
setImportCodexModalOpen: (open: boolean) => void;
|
||||
fetchConnections: () => Promise<void>;
|
||||
// External link
|
||||
externalLinkModalOpen: boolean;
|
||||
setExternalLinkModalOpen: (open: boolean) => void;
|
||||
externalLinkLoading: boolean;
|
||||
externalLinkError: string | null;
|
||||
externalLinkUrl: string | null;
|
||||
externalLinkCopied: boolean;
|
||||
externalLinkCopy: () => void;
|
||||
// Edit connection
|
||||
showEditModal: boolean;
|
||||
setShowEditModal: (open: boolean) => void;
|
||||
selectedConnection: { id: string } | null;
|
||||
handleUpdateConnection: (data: any) => Promise<string | null>;
|
||||
// Edit compatible node
|
||||
showEditNodeModal: boolean;
|
||||
setShowEditNodeModal: (open: boolean) => void;
|
||||
providerNode: any;
|
||||
handleUpdateNode: (data: any) => Promise<void>;
|
||||
// Codex CLI guide
|
||||
codexCliGuideOpen: boolean;
|
||||
setCodexCliGuideOpen: (open: boolean) => void;
|
||||
// Claude auth
|
||||
applyClaudeModalConnectionId: string | null;
|
||||
setApplyClaudeModalConnectionId: (id: string | null) => void;
|
||||
applyingClaudeAuthId: string | null;
|
||||
handleApplyClaudeAuthLocal: (id: string) => Promise<void>;
|
||||
importClaudeModalOpen: boolean;
|
||||
setImportClaudeModalOpen: (open: boolean) => void;
|
||||
// Gemini auth
|
||||
applyGeminiModalConnectionId: string | null;
|
||||
setApplyGeminiModalConnectionId: (id: string | null) => void;
|
||||
applyingGeminiAuthId: string | null;
|
||||
handleApplyGeminiAuthLocal: (id: string) => Promise<void>;
|
||||
importGeminiModalOpen: boolean;
|
||||
setImportGeminiModalOpen: (open: boolean) => void;
|
||||
// Batch test results
|
||||
batchTestResults: BatchTestResults | null;
|
||||
setBatchTestResults: (r: BatchTestResults | null) => void;
|
||||
emailsVisible: boolean;
|
||||
// Proxy config
|
||||
proxyTarget: ProxyTarget | null;
|
||||
setProxyTarget: (t: ProxyTarget | null) => void;
|
||||
fetchProxyConfig: () => Promise<void>;
|
||||
// Import progress
|
||||
importProgress: ImportProgress;
|
||||
showImportModal: boolean;
|
||||
setShowImportModal: (open: boolean) => void;
|
||||
// Tutorial
|
||||
showTutorialModal: boolean;
|
||||
setShowTutorialModal: (open: boolean) => void;
|
||||
t: ProviderMessageTranslator;
|
||||
}
|
||||
|
||||
export default function ProviderModalsPanel({
|
||||
providerId,
|
||||
providerInfo,
|
||||
isCompatible,
|
||||
isAnthropicProtocolCompatible,
|
||||
isCcCompatible,
|
||||
isUpstreamProxyProvider,
|
||||
subscriptionRisk,
|
||||
showRiskNoticeModal,
|
||||
handleConfirmRiskNotice,
|
||||
handleCancelRiskNotice,
|
||||
showOAuthModal,
|
||||
reauthConnection,
|
||||
handleOAuthSuccess,
|
||||
setShowOAuthModal,
|
||||
showSiliconFlowEndpointModal,
|
||||
setSiliconFlowInitialBaseUrl,
|
||||
setShowSiliconFlowEndpointModal,
|
||||
setShowAddApiKeyModal,
|
||||
showAddApiKeyModal,
|
||||
siliconFlowInitialBaseUrl,
|
||||
commandCodeAuthState,
|
||||
handleStartCommandCodeAuth,
|
||||
handleSaveApiKey,
|
||||
handleCloseAddApiKeyModal,
|
||||
isCommandCode,
|
||||
batchDeleteConfirmOpen,
|
||||
setBatchDeleteConfirmOpen,
|
||||
handleBatchDeleteConfirm,
|
||||
selectedIds,
|
||||
batchDeleting,
|
||||
applyCodexModalConnectionId,
|
||||
setApplyCodexModalConnectionId,
|
||||
applyingCodexAuthId,
|
||||
handleApplyCodexAuthLocal,
|
||||
importCodexModalOpen,
|
||||
setImportCodexModalOpen,
|
||||
fetchConnections,
|
||||
externalLinkModalOpen,
|
||||
setExternalLinkModalOpen,
|
||||
externalLinkLoading,
|
||||
externalLinkError,
|
||||
externalLinkUrl,
|
||||
externalLinkCopied,
|
||||
externalLinkCopy,
|
||||
showEditModal,
|
||||
setShowEditModal,
|
||||
selectedConnection,
|
||||
handleUpdateConnection,
|
||||
showEditNodeModal,
|
||||
setShowEditNodeModal,
|
||||
providerNode,
|
||||
handleUpdateNode,
|
||||
codexCliGuideOpen,
|
||||
setCodexCliGuideOpen,
|
||||
applyClaudeModalConnectionId,
|
||||
setApplyClaudeModalConnectionId,
|
||||
applyingClaudeAuthId,
|
||||
handleApplyClaudeAuthLocal,
|
||||
importClaudeModalOpen,
|
||||
setImportClaudeModalOpen,
|
||||
applyGeminiModalConnectionId,
|
||||
setApplyGeminiModalConnectionId,
|
||||
applyingGeminiAuthId,
|
||||
handleApplyGeminiAuthLocal,
|
||||
importGeminiModalOpen,
|
||||
setImportGeminiModalOpen,
|
||||
batchTestResults,
|
||||
setBatchTestResults,
|
||||
emailsVisible,
|
||||
proxyTarget,
|
||||
setProxyTarget,
|
||||
fetchProxyConfig,
|
||||
importProgress,
|
||||
showImportModal,
|
||||
setShowImportModal,
|
||||
showTutorialModal,
|
||||
setShowTutorialModal,
|
||||
t,
|
||||
}: ProviderModalsPanelProps) {
|
||||
return (
|
||||
<>
|
||||
{showRiskNoticeModal && subscriptionRisk && (
|
||||
<RiskNoticeModal
|
||||
variant={(providerInfo.riskNoticeVariant as string) ?? "oauth"}
|
||||
providerId={providerId}
|
||||
providerName={providerInfo.name}
|
||||
onConfirm={handleConfirmRiskNotice}
|
||||
onCancel={handleCancelRiskNotice}
|
||||
/>
|
||||
)}
|
||||
{!isUpstreamProxyProvider &&
|
||||
(providerId === "kiro" || providerId === "amazon-q" ? (
|
||||
<KiroOAuthWrapper
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
providerInfo={{ ...providerInfo, id: providerId }}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
/>
|
||||
) : providerId === "cursor" ? (
|
||||
<CursorAuthModal
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
/>
|
||||
) : providerId === "trae" ? (
|
||||
<TraeAuthModal
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
/>
|
||||
) : (
|
||||
<OAuthModal
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
provider={providerId}
|
||||
providerInfo={providerInfo}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
/>
|
||||
))}
|
||||
{providerId === "siliconflow" && (
|
||||
<SiliconFlowEndpointModal
|
||||
isOpen={showSiliconFlowEndpointModal}
|
||||
onSelect={(baseUrl) => {
|
||||
setSiliconFlowInitialBaseUrl(baseUrl);
|
||||
setShowSiliconFlowEndpointModal(false);
|
||||
setShowAddApiKeyModal(true);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowSiliconFlowEndpointModal(false);
|
||||
setSiliconFlowInitialBaseUrl(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isUpstreamProxyProvider && (
|
||||
<AddApiKeyModal
|
||||
isOpen={showAddApiKeyModal}
|
||||
provider={providerId}
|
||||
providerName={providerInfo.name}
|
||||
initialBaseUrl={siliconFlowInitialBaseUrl}
|
||||
isCompatible={isCompatible}
|
||||
isAnthropic={isAnthropicProtocolCompatible}
|
||||
isCcCompatible={isCcCompatible}
|
||||
isCommandCode={isCommandCode}
|
||||
commandCodeAuthState={commandCodeAuthState}
|
||||
onStartCommandCodeAuth={handleStartCommandCodeAuth}
|
||||
onSave={handleSaveApiKey}
|
||||
onClose={handleCloseAddApiKeyModal}
|
||||
/>
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={batchDeleteConfirmOpen}
|
||||
onClose={() => setBatchDeleteConfirmOpen(false)}
|
||||
onConfirm={handleBatchDeleteConfirm}
|
||||
title={t("batchDeleteConfirmTitle", "Delete connections")}
|
||||
message={t("batchDeleteConfirm", { count: selectedIds.size })}
|
||||
confirmText={t("batchDeleteConfirmButton", "Delete")}
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
loading={batchDeleting}
|
||||
/>
|
||||
{providerId === "codex" && applyCodexModalConnectionId && (
|
||||
<ApplyCodexAuthModal
|
||||
key={applyCodexModalConnectionId}
|
||||
connectionId={applyCodexModalConnectionId}
|
||||
inProgress={!!applyingCodexAuthId}
|
||||
onConfirm={handleApplyCodexAuthLocal}
|
||||
onClose={() => setApplyCodexModalConnectionId(null)}
|
||||
/>
|
||||
)}
|
||||
{!isUpstreamProxyProvider && (
|
||||
<EditConnectionModal
|
||||
isOpen={showEditModal}
|
||||
connection={selectedConnection}
|
||||
onSave={handleUpdateConnection}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
/>
|
||||
)}
|
||||
{!isUpstreamProxyProvider && isCompatible && (
|
||||
<EditCompatibleNodeModal
|
||||
isOpen={showEditNodeModal}
|
||||
node={providerNode}
|
||||
onSave={handleUpdateNode}
|
||||
onClose={() => setShowEditNodeModal(false)}
|
||||
isAnthropic={isAnthropicProtocolCompatible}
|
||||
isCcCompatible={isCcCompatible}
|
||||
/>
|
||||
)}
|
||||
<CodexCliGuideModal isOpen={codexCliGuideOpen} onClose={() => setCodexCliGuideOpen(false)} />
|
||||
{providerId === "codex" && importCodexModalOpen && (
|
||||
<ImportCodexAuthModal
|
||||
key="import-codex-modal"
|
||||
onClose={() => setImportCodexModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
setImportCodexModalOpen(false);
|
||||
void fetchConnections();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{providerId === "codex" && externalLinkModalOpen && (
|
||||
<ExternalLinkModal
|
||||
isOpen={externalLinkModalOpen}
|
||||
onClose={() => setExternalLinkModalOpen(false)}
|
||||
loading={externalLinkLoading}
|
||||
error={externalLinkError}
|
||||
url={externalLinkUrl}
|
||||
copied={externalLinkCopied}
|
||||
onCopy={externalLinkCopy}
|
||||
/>
|
||||
)}
|
||||
{providerId === "claude" && applyClaudeModalConnectionId && (
|
||||
<ApplyClaudeAuthModal
|
||||
key={applyClaudeModalConnectionId}
|
||||
connectionId={applyClaudeModalConnectionId}
|
||||
inProgress={!!applyingClaudeAuthId}
|
||||
onConfirm={handleApplyClaudeAuthLocal}
|
||||
onClose={() => setApplyClaudeModalConnectionId(null)}
|
||||
/>
|
||||
)}
|
||||
{providerId === "claude" && importClaudeModalOpen && (
|
||||
<ImportClaudeAuthModal
|
||||
key="import-claude-modal"
|
||||
onClose={() => setImportClaudeModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
setImportClaudeModalOpen(false);
|
||||
void fetchConnections();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{providerId === "gemini-cli" && applyGeminiModalConnectionId && (
|
||||
<ApplyGeminiAuthModal
|
||||
key={applyGeminiModalConnectionId}
|
||||
connectionId={applyGeminiModalConnectionId}
|
||||
inProgress={!!applyingGeminiAuthId}
|
||||
onConfirm={handleApplyGeminiAuthLocal}
|
||||
onClose={() => setApplyGeminiModalConnectionId(null)}
|
||||
/>
|
||||
)}
|
||||
{providerId === "gemini-cli" && importGeminiModalOpen && (
|
||||
<ImportGeminiAuthModal
|
||||
key="import-gemini-modal"
|
||||
onClose={() => setImportGeminiModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
setImportGeminiModalOpen(false);
|
||||
void fetchConnections();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<BatchTestResultsModal
|
||||
batchTestResults={batchTestResults}
|
||||
providerInfo={providerInfo}
|
||||
providerId={providerId}
|
||||
emailsVisible={emailsVisible}
|
||||
onClose={() => setBatchTestResults(null)}
|
||||
t={t}
|
||||
/>
|
||||
{proxyTarget && (
|
||||
<ProxyConfigModal
|
||||
isOpen={!!proxyTarget}
|
||||
onClose={() => setProxyTarget(null)}
|
||||
level={proxyTarget.level}
|
||||
levelId={proxyTarget.id}
|
||||
levelLabel={proxyTarget.label}
|
||||
onSaved={() => {
|
||||
void fetchProxyConfig();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ImportProgressModal
|
||||
importProgress={importProgress}
|
||||
isOpen={showImportModal}
|
||||
onClose={() => {
|
||||
if (importProgress.phase === "done" || importProgress.phase === "error") {
|
||||
setShowImportModal(false);
|
||||
}
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
{providerId === "adapta-web" && (
|
||||
<AdaptaTutorialModal
|
||||
isOpen={showTutorialModal}
|
||||
onClose={() => setShowTutorialModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
// Phase 1t.1 extraction — Issue #3501
|
||||
import Link from "next/link";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
|
||||
import { getHeaderIconProviderId } from "../providerPageHelpers";
|
||||
import type { ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
interface ProviderInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
website?: string;
|
||||
color: string;
|
||||
apiType?: string;
|
||||
}
|
||||
|
||||
interface ProviderPageHeaderProps {
|
||||
providerId: string;
|
||||
providerInfo: ProviderInfo;
|
||||
connectionsCount: number;
|
||||
isOpenAICompatible: boolean;
|
||||
isAnthropicProtocolCompatible: boolean;
|
||||
onOpenTutorial: () => void;
|
||||
t: ProviderMessageTranslator;
|
||||
}
|
||||
|
||||
export default function ProviderPageHeader({
|
||||
providerId,
|
||||
providerInfo,
|
||||
connectionsCount,
|
||||
isOpenAICompatible,
|
||||
isAnthropicProtocolCompatible,
|
||||
onOpenTutorial,
|
||||
t,
|
||||
}: ProviderPageHeaderProps) {
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
href="/dashboard/providers"
|
||||
className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary transition-colors mb-4"
|
||||
>
|
||||
<span className="material-symbols-outlined text-lg">arrow_back</span>
|
||||
{t("backToProviders")}
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="rounded-lg flex items-center justify-center"
|
||||
style={{ backgroundColor: `${providerInfo.color}15` }}
|
||||
>
|
||||
<ProviderIcon
|
||||
providerId={getHeaderIconProviderId(
|
||||
isOpenAICompatible,
|
||||
isAnthropicProtocolCompatible,
|
||||
providerInfo.id,
|
||||
providerInfo.apiType
|
||||
)}
|
||||
size={48}
|
||||
type="color"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{providerInfo.website ? (
|
||||
<a
|
||||
href={providerInfo.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-3xl font-semibold tracking-tight hover:underline inline-flex items-center gap-2"
|
||||
style={{ color: providerInfo.color }}
|
||||
>
|
||||
{providerInfo.name}
|
||||
<span className="material-symbols-outlined text-lg opacity-60">open_in_new</span>
|
||||
</a>
|
||||
) : (
|
||||
<h1 className="text-3xl font-semibold tracking-tight">{providerInfo.name}</h1>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-text-muted">
|
||||
{t("connectionCountLabel", { count: connectionsCount })}
|
||||
</p>
|
||||
<EmailPrivacyToggle size="md" />
|
||||
{providerId === "adapta-web" && (
|
||||
<button
|
||||
onClick={onOpenTutorial}
|
||||
className="text-sm font-medium underline underline-offset-2 opacity-70 hover:opacity-100 transition-opacity"
|
||||
style={{ color: providerInfo.color }}
|
||||
>
|
||||
Tutorial
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
// Phase 1t.7 extraction — Issue #3501
|
||||
import { Card } from "@/shared/components";
|
||||
import type { ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
interface SearchProviderCardProps {
|
||||
providerId: string;
|
||||
t: ProviderMessageTranslator;
|
||||
}
|
||||
|
||||
export default function SearchProviderCard({ providerId, t }: SearchProviderCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold mb-4">{t("searchProvider")}</h2>
|
||||
<p className="text-sm text-text-muted">{t("searchProviderDesc")}</p>
|
||||
{providerId === "perplexity-search" && (
|
||||
<div className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
||||
<span className="material-symbols-outlined text-sm text-blue-400">link</span>
|
||||
<p className="text-xs text-blue-300">{t("perplexitySearchSharedKeyInfo")}</p>
|
||||
</div>
|
||||
)}
|
||||
{providerId === "google-pse-search" && (
|
||||
<div className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20">
|
||||
<span className="material-symbols-outlined text-sm text-amber-300">tune</span>
|
||||
<p className="text-xs text-amber-200">{t("googlePseInfo")}</p>
|
||||
</div>
|
||||
)}
|
||||
{providerId === "searxng-search" && (
|
||||
<div className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
|
||||
<span className="material-symbols-outlined text-sm text-emerald-300">dns</span>
|
||||
<p className="text-xs text-emerald-200">{t("searxngInfo")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
// Phase 1t.7 extraction — Issue #3501
|
||||
import Link from "next/link";
|
||||
import { Card } from "@/shared/components";
|
||||
import { providerText } from "../providerPageHelpers";
|
||||
import type { ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
interface UpstreamProxyCardProps {
|
||||
t: ProviderMessageTranslator;
|
||||
}
|
||||
|
||||
export default function UpstreamProxyCard({ t }: UpstreamProxyCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{providerText(t, "upstreamProxyManagedTitle", "Managed via Upstream Proxy Settings")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
{providerText(
|
||||
t,
|
||||
"upstreamProxyManagedDescription",
|
||||
"CLIProxyAPI is configured as an upstream proxy layer, not as a direct provider connection. Manage the binary/runtime in CLI Tools and enable proxy routing on each provider via the provider proxy controls."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
href="/dashboard/cli-code"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm text-text-main hover:border-primary/40 hover:text-text-primary transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-base">terminal</span>
|
||||
{t("openCliTools")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard/settings"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm text-text-main hover:border-primary/40 hover:text-text-primary transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-base">settings</span>
|
||||
{t("openSettings")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Phase 1t.3 extraction — Issue #3501
|
||||
// Encapsulates gateConnectionFlow + risk-notice modal state + confirm/cancel handlers.
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import { isRiskAcknowledged, useRiskAcknowledged } from "../../hooks/useRiskAcknowledged";
|
||||
|
||||
interface UseConnectionGateParams {
|
||||
providerId: string;
|
||||
subscriptionRisk: boolean;
|
||||
}
|
||||
|
||||
export function useConnectionGate({ providerId, subscriptionRisk }: UseConnectionGateParams) {
|
||||
const [showRiskNoticeModal, setShowRiskNoticeModal] = useState(false);
|
||||
const pendingRiskActionRef = useRef<(() => void) | null>(null);
|
||||
const { acknowledged: riskAcknowledged, acknowledge: acknowledgeRisk } =
|
||||
useRiskAcknowledged(providerId);
|
||||
|
||||
const gateConnectionFlow = useCallback(
|
||||
(callback: () => void) => {
|
||||
if (subscriptionRisk && !riskAcknowledged && !isRiskAcknowledged(providerId)) {
|
||||
pendingRiskActionRef.current = callback;
|
||||
setShowRiskNoticeModal(true);
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
[providerId, riskAcknowledged, subscriptionRisk]
|
||||
);
|
||||
|
||||
const handleConfirmRiskNotice = useCallback(() => {
|
||||
acknowledgeRisk();
|
||||
setShowRiskNoticeModal(false);
|
||||
const pendingAction = pendingRiskActionRef.current;
|
||||
pendingRiskActionRef.current = null;
|
||||
pendingAction?.();
|
||||
}, [acknowledgeRisk]);
|
||||
|
||||
const handleCancelRiskNotice = useCallback(() => {
|
||||
pendingRiskActionRef.current = null;
|
||||
setShowRiskNoticeModal(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
showRiskNoticeModal,
|
||||
gateConnectionFlow,
|
||||
handleConfirmRiskNotice,
|
||||
handleCancelRiskNotice,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Phase 1t.4 extraction — Issue #3501
|
||||
// Encapsulates handleUpdateNode and handleUpdateConnection async handlers.
|
||||
import type { ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
|
||||
interface UseProviderNodeActionsParams {
|
||||
providerId: string;
|
||||
fetchConnections: () => Promise<void>;
|
||||
selectedConnection: { id: string } | null;
|
||||
setProviderNode: (node: any) => void;
|
||||
setShowEditNodeModal: (open: boolean) => void;
|
||||
setShowEditModal: (open: boolean) => void;
|
||||
t: ProviderMessageTranslator;
|
||||
}
|
||||
|
||||
export function useProviderNodeActions({
|
||||
providerId,
|
||||
fetchConnections,
|
||||
selectedConnection,
|
||||
setProviderNode,
|
||||
setShowEditNodeModal,
|
||||
setShowEditModal,
|
||||
t,
|
||||
}: UseProviderNodeActionsParams) {
|
||||
const handleUpdateNode = async (formData: any) => {
|
||||
try {
|
||||
const res = await fetch(`/api/provider-nodes/${providerId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setProviderNode(data.node);
|
||||
await fetchConnections();
|
||||
setShowEditNodeModal(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error updating provider node:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateConnection = async (formData: any) => {
|
||||
try {
|
||||
const res = await fetch(`/api/providers/${selectedConnection?.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
if (res.ok) {
|
||||
await fetchConnections();
|
||||
setShowEditModal(false);
|
||||
return null;
|
||||
}
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return data.error?.message || data.error || t("failedSaveConnection");
|
||||
} catch (error) {
|
||||
console.log("Error updating connection:", error);
|
||||
return t("failedSaveConnectionRetry");
|
||||
}
|
||||
};
|
||||
|
||||
return { handleUpdateNode, handleUpdateConnection };
|
||||
}
|
||||
Reference in New Issue
Block a user