From 3c2503c4b4235551f780fdd60cc2311c4f25de46 Mon Sep 17 00:00:00 2001 From: Zanuar Tri Romadon Date: Wed, 13 May 2026 15:32:17 +0700 Subject: [PATCH] fix(ui): replace browser confirm dialogs with ConfirmModal component (#1060) Replace 10 instances of native browser confirm() dialogs with the existing ConfirmModal component for consistent UX across the dashboard. Changes: - Add ConfirmModal to 5 files (combos, endpoints, proxy pools, providers, connections) - Maintain same confirmation flow with improved styling - Use 'danger' variant for destructive actions - Preserve all existing functionality Affected areas: - Combo deletion (combos page) - API key deletion/pausing (EndpointPageClient) - Proxy pool management (single/bulk delete, disable dead proxies) - Provider operations (disable all models, delete connection, delete compatible node) - Connection management (ConnectionsCard) All changes manually tested and verified. --- src/app/(dashboard)/dashboard/combos/page.js | 35 +++-- .../dashboard/endpoint/EndpointPageClient.js | 58 +++++--- .../dashboard/providers/[id]/page.js | 87 ++++++++---- .../providers/components/ConnectionsCard.js | 29 +++- .../(dashboard)/dashboard/proxy-pools/page.js | 130 +++++++++++------- 5 files changed, 225 insertions(+), 114 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index bf21475b..a31713ce 100644 --- a/src/app/(dashboard)/dashboard/combos/page.js +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, Toggle } from "@/shared/components"; +import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, Toggle, ConfirmModal } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; @@ -15,6 +15,7 @@ export default function CombosPage() { const [editingCombo, setEditingCombo] = useState(null); const [activeProviders, setActiveProviders] = useState([]); const [comboStrategies, setComboStrategies] = useState({}); + const [confirmState, setConfirmState] = useState(null); const { copied, copy } = useCopyToClipboard(); useEffect(() => { @@ -84,15 +85,21 @@ export default function CombosPage() { }; const handleDelete = async (id) => { - if (!confirm("Delete this combo?")) return; - try { - const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); - if (res.ok) { - setCombos(combos.filter(c => c.id !== id)); + setConfirmState({ + title: "Delete Combo", + message: "Delete this combo?", + onConfirm: async () => { + setConfirmState(null); + try { + const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); + if (res.ok) { + setCombos(combos.filter(c => c.id !== id)); + } + } catch (error) { + console.log("Error deleting combo:", error); + } } - } catch (error) { - console.log("Error deleting combo:", error); - } + }); }; const handleToggleRoundRobin = async (comboName, enabled) => { @@ -189,6 +196,16 @@ export default function CombosPage() { onSave={(data) => handleUpdate(editingCombo.id, data)} activeProviders={activeProviders} /> + + {/* Confirm Delete Modal */} + setConfirmState(null)} + onConfirm={confirmState?.onConfirm} + title={confirmState?.title || "Confirm"} + message={confirmState?.message} + variant="danger" + /> ); } diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 38fb118c..1818667a 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import PropTypes from "prop-types"; -import { Card, Button, Input, Modal, CardSkeleton, Toggle } from "@/shared/components"; +import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; const TUNNEL_BENEFITS = [ @@ -44,6 +44,7 @@ export default function APIPageClient({ machineId }) { const [showAddModal, setShowAddModal] = useState(false); const [newKeyName, setNewKeyName] = useState(""); const [createdKey, setCreatedKey] = useState(null); + const [confirmState, setConfirmState] = useState(null); const [requireApiKey, setRequireApiKey] = useState(false); const [requireLogin, setRequireLogin] = useState(true); @@ -660,22 +661,26 @@ export default function APIPageClient({ machineId }) { }; const handleDeleteKey = async (id) => { - if (!confirm("Delete this API key?")) return; - - try { - const res = await fetch(`/api/keys/${id}`, { method: "DELETE" }); - if (res.ok) { - setKeys(keys.filter((k) => k.id !== id)); - // Clean up visibility state - setVisibleKeys(prev => { - const next = new Set(prev); - next.delete(id); - return next; - }); + setConfirmState({ + title: "Delete API Key", + message: "Delete this API key?", + onConfirm: async () => { + setConfirmState(null); + try { + const res = await fetch(`/api/keys/${id}`, { method: "DELETE" }); + if (res.ok) { + setKeys(keys.filter((k) => k.id !== id)); + setVisibleKeys(prev => { + const next = new Set(prev); + next.delete(id); + return next; + }); + } + } catch (error) { + console.log("Error deleting key:", error); + } } - } catch (error) { - console.log("Error deleting key:", error); - } + }); }; const handleToggleKey = async (id, isActive) => { @@ -1108,9 +1113,14 @@ export default function APIPageClient({ machineId }) { checked={key.isActive ?? true} onChange={(checked) => { if (key.isActive && !checked) { - if (confirm(`Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`)) { - handleToggleKey(key.id, checked); - } + setConfirmState({ + title: "Pause API Key", + message: `Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`, + onConfirm: async () => { + setConfirmState(null); + handleToggleKey(key.id, checked); + } + }); } else { handleToggleKey(key.id, checked); } @@ -1344,6 +1354,16 @@ export default function APIPageClient({ machineId }) { + + {/* Confirm Modal */} + setConfirmState(null)} + onConfirm={confirmState?.onConfirm} + title={confirmState?.title || "Confirm"} + message={confirmState?.message} + variant="danger" + /> ); } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 0f209308..29c41ed6 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; -import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard } from "@/shared/components"; +import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; @@ -42,12 +42,13 @@ export default function ProviderDetailPage() { const [selectedConnectionIds, setSelectedConnectionIds] = useState([]); const [bulkProxyPoolId, setBulkProxyPoolId] = useState("__none__"); const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false); - const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override + const [providerStrategy, setProviderStrategy] = useState(null); const [providerStickyLimit, setProviderStickyLimit] = useState(""); const [thinkingMode, setThinkingMode] = useState("auto"); const [suggestedModels, setSuggestedModels] = useState([]); const [kiloFreeModels, setKiloFreeModels] = useState([]); const [disabledModelIds, setDisabledModelIds] = useState([]); + const [confirmState, setConfirmState] = useState(null); const { copied, copy } = useCopyToClipboard(); const providerInfo = providerNode @@ -110,17 +111,23 @@ export default function ProviderDetailPage() { const handleDisableAll = async (ids) => { if (!ids.length) return; - if (!confirm(`Disable all ${ids.length} model(s)?`)) return; - try { - const res = await fetch("/api/models/disabled", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ providerAlias: providerStorageAlias, ids }), - }); - if (res.ok) await fetchDisabledModels(); - } catch (error) { - console.log("Error disabling all models:", error); - } + setConfirmState({ + title: "Disable All Models", + message: `Disable all ${ids.length} model(s)?`, + onConfirm: async () => { + setConfirmState(null); + try { + const res = await fetch("/api/models/disabled", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ providerAlias: providerStorageAlias, ids }), + }); + if (res.ok) await fetchDisabledModels(); + } catch (error) { + console.log("Error disabling all models:", error); + } + } + }); }; const handleEnableAll = async () => { @@ -338,15 +345,21 @@ export default function ProviderDetailPage() { }; const handleDelete = async (id) => { - if (!confirm("Delete this connection?")) return; - try { - const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); - if (res.ok) { - setConnections(connections.filter(c => c.id !== id)); + setConfirmState({ + title: "Delete Connection", + message: "Delete this connection?", + onConfirm: async () => { + setConfirmState(null); + try { + const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); + if (res.ok) { + setConnections(connections.filter(c => c.id !== id)); + } + } catch (error) { + console.log("Error deleting connection:", error); + } } - } catch (error) { - console.log("Error deleting connection:", error); - } + }); }; const handleOAuthSuccess = () => { @@ -954,15 +967,21 @@ export default function ProviderDetailPage() { variant="secondary" icon="delete" onClick={async () => { - if (!confirm(`Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`)) return; - try { - const res = await fetch(`/api/provider-nodes/${providerId}`, { method: "DELETE" }); - if (res.ok) { - router.push("/dashboard/providers"); + setConfirmState({ + title: "Delete Compatible Node", + message: `Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`, + onConfirm: async () => { + setConfirmState(null); + try { + const res = await fetch(`/api/provider-nodes/${providerId}`, { method: "DELETE" }); + if (res.ok) { + router.push("/dashboard/providers"); + } + } catch (error) { + console.log("Error deleting provider node:", error); + } } - } catch (error) { - console.log("Error deleting provider node:", error); - } + }); }} className="w-full sm:w-auto" > @@ -1222,6 +1241,16 @@ export default function ProviderDetailPage() { onClose={() => setShowAddCustomModel(false)} /> )} + + {/* Confirm Modal */} + setConfirmState(null)} + onConfirm={confirmState?.onConfirm} + title={confirmState?.title || "Confirm"} + message={confirmState?.message} + variant="danger" + /> ); } diff --git a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js index 74686196..b8d196ce 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js +++ b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import PropTypes from "prop-types"; -import { Card, Badge, Button, Modal, Select, Toggle, EditConnectionModal } from "@/shared/components"; +import { Card, Badge, Button, Modal, Select, Toggle, EditConnectionModal, ConfirmModal } from "@/shared/components"; // ── CooldownTimer ────────────────────────────────────────────── function CooldownTimer({ until }) { @@ -308,6 +308,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) { const [selectedConnection, setSelectedConnection] = useState(null); const [providerStrategy, setProviderStrategy] = useState(null); const [providerStickyLimit, setProviderStickyLimit] = useState("1"); + const [confirmState, setConfirmState] = useState(null); const fetch_ = useCallback(async () => { try { @@ -358,11 +359,17 @@ export default function ConnectionsCard({ providerId, isOAuth }) { }; const handleDelete = async (id) => { - if (!confirm("Delete this connection?")) return; - try { - const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); - if (res.ok) setConnections((prev) => prev.filter((c) => c.id !== id)); - } catch (e) { console.log("delete error:", e); } + setConfirmState({ + title: "Delete Connection", + message: "Delete this connection?", + onConfirm: async () => { + setConfirmState(null); + try { + const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); + if (res.ok) setConnections((prev) => prev.filter((c) => c.id !== id)); + } catch (e) { console.log("delete error:", e); } + } + }); }; const handleToggleActive = async (id, isActive) => { @@ -470,6 +477,16 @@ export default function ConnectionsCard({ providerId, isOAuth }) { onSave={handleUpdateConnection} onClose={() => setShowEditModal(false)} /> + + {/* Confirm Modal */} + setConfirmState(null)} + onConfirm={confirmState?.onConfirm} + title={confirmState?.title || "Confirm"} + message={confirmState?.message} + variant="danger" + /> ); } diff --git a/src/app/(dashboard)/dashboard/proxy-pools/page.js b/src/app/(dashboard)/dashboard/proxy-pools/page.js index 6eab6b6c..cae61044 100644 --- a/src/app/(dashboard)/dashboard/proxy-pools/page.js +++ b/src/app/(dashboard)/dashboard/proxy-pools/page.js @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle } from "@/shared/components"; +import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle, ConfirmModal } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; function getStatusVariant(status) { @@ -45,6 +45,7 @@ export default function ProxyPoolsPage() { const [healthChecking, setHealthChecking] = useState(false); const [healthProgress, setHealthProgress] = useState({ current: 0, total: 0 }); const [bulkBusy, setBulkBusy] = useState(false); + const [confirmState, setConfirmState] = useState(null); const notify = useNotificationStore(); const fetchProxyPools = useCallback(async () => { @@ -122,27 +123,31 @@ export default function ProxyPoolsPage() { }; const handleDelete = async (proxyPool) => { - const deleting = confirm(`Delete proxy pool \"${proxyPool.name}\"?`); - if (!deleting) return; + setConfirmState({ + title: "Delete Proxy Pool", + message: `Delete proxy pool "${proxyPool.name}"?`, + onConfirm: async () => { + setConfirmState(null); + try { + const res = await fetch(`/api/proxy-pools/${proxyPool.id}`, { method: "DELETE" }); + if (res.ok) { + setProxyPools((prev) => prev.filter((item) => item.id !== proxyPool.id)); + notify.success("Proxy pool deleted"); + return; + } - try { - const res = await fetch(`/api/proxy-pools/${proxyPool.id}`, { method: "DELETE" }); - if (res.ok) { - setProxyPools((prev) => prev.filter((item) => item.id !== proxyPool.id)); - notify.success("Proxy pool deleted"); - return; + const data = await res.json(); + if (res.status === 409) { + notify.warning(`Cannot delete: ${data.boundConnectionCount || 0} connection(s) are still using this pool.`); + } else { + notify.error(data.error || "Failed to delete proxy pool"); + } + } catch (error) { + console.log("Error deleting proxy pool:", error); + notify.error("Failed to delete proxy pool"); + } } - - const data = await res.json(); - if (res.status === 409) { - notify.warning(`Cannot delete: ${data.boundConnectionCount || 0} connection(s) are still using this pool.`); - } else { - notify.error(data.error || "Failed to delete proxy pool"); - } - } catch (error) { - console.log("Error deleting proxy pool:", error); - notify.error("Failed to delete proxy pool"); - } + }); }; const handleTest = async (proxyPoolId) => { @@ -215,24 +220,30 @@ export default function ProxyPoolsPage() { const bulkDelete = async () => { if (selectedIds.length === 0) return; - if (!confirm(`Delete ${selectedIds.length} proxy pool(s)?`)) return; - setBulkBusy(true); - try { - let ok = 0; let blocked = 0; let failed = 0; - for (const id of selectedIds) { + setConfirmState({ + title: "Delete Proxy Pools", + message: `Delete ${selectedIds.length} proxy pool(s)?`, + onConfirm: async () => { + setConfirmState(null); + setBulkBusy(true); try { - const res = await fetch(`/api/proxy-pools/${id}`, { method: "DELETE" }); - if (res.ok) ok += 1; - else if (res.status === 409) blocked += 1; - else failed += 1; - } catch { failed += 1; } + let ok = 0; let blocked = 0; let failed = 0; + for (const id of selectedIds) { + try { + const res = await fetch(`/api/proxy-pools/${id}`, { method: "DELETE" }); + if (res.ok) ok += 1; + else if (res.status === 409) blocked += 1; + else failed += 1; + } catch { failed += 1; } + } + await fetchProxyPools(); + clearSelection(); + notify.success(`Deleted ${ok}${blocked ? `, ${blocked} bound` : ""}${failed ? `, ${failed} failed` : ""}`); + } finally { + setBulkBusy(false); + } } - await fetchProxyPools(); - clearSelection(); - notify.success(`Deleted ${ok}${blocked ? `, ${blocked} bound` : ""}${failed ? `, ${failed} failed` : ""}`); - } finally { - setBulkBusy(false); - } + }); }; const handleHealthCheck = async () => { @@ -269,23 +280,30 @@ export default function ProxyPoolsPage() { setHealthChecking(false); setHealthProgress({ current: 0, total: 0 }); - if (deadIds.length > 0 && confirm(`Alive: ${alive}, Dead: ${deadIds.length}.\n\nDisable ${deadIds.length} dead proxies?`)) { - setBulkBusy(true); - try { - for (const id of deadIds) { + if (deadIds.length > 0) { + setConfirmState({ + title: "Disable Dead Proxies", + message: `Alive: ${alive}, Dead: ${deadIds.length}.\n\nDisable ${deadIds.length} dead proxies?`, + onConfirm: async () => { + setConfirmState(null); + setBulkBusy(true); try { - await fetch(`/api/proxy-pools/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ isActive: false }), - }); - } catch {} + for (const id of deadIds) { + try { + await fetch(`/api/proxy-pools/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ isActive: false }), + }); + } catch {} + } + await fetchProxyPools(); + notify.success(`Disabled ${deadIds.length} dead proxies`); + } finally { + setBulkBusy(false); + } } - await fetchProxyPools(); - notify.success(`Disabled ${deadIds.length} dead proxies`); - } finally { - setBulkBusy(false); - } + }); } else { notify.success(`Health check done. Alive: ${alive}, Dead: ${deadIds.length}`); } @@ -768,6 +786,16 @@ export default function ProxyPoolsPage() { + + {/* Confirm Modal */} + setConfirmState(null)} + onConfirm={confirmState?.onConfirm} + title={confirmState?.title || "Confirm"} + message={confirmState?.message} + variant="danger" + /> ); }