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.
This commit is contained in:
Zanuar Tri Romadon
2026-05-13 15:32:17 +07:00
committed by GitHub
parent 5cab23d92e
commit 3c2503c4b4
5 changed files with 225 additions and 114 deletions

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react"; 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 { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
@@ -15,6 +15,7 @@ export default function CombosPage() {
const [editingCombo, setEditingCombo] = useState(null); const [editingCombo, setEditingCombo] = useState(null);
const [activeProviders, setActiveProviders] = useState([]); const [activeProviders, setActiveProviders] = useState([]);
const [comboStrategies, setComboStrategies] = useState({}); const [comboStrategies, setComboStrategies] = useState({});
const [confirmState, setConfirmState] = useState(null);
const { copied, copy } = useCopyToClipboard(); const { copied, copy } = useCopyToClipboard();
useEffect(() => { useEffect(() => {
@@ -84,15 +85,21 @@ export default function CombosPage() {
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (!confirm("Delete this combo?")) return; setConfirmState({
try { title: "Delete Combo",
const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); message: "Delete this combo?",
if (res.ok) { onConfirm: async () => {
setCombos(combos.filter(c => c.id !== id)); 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) => { const handleToggleRoundRobin = async (comboName, enabled) => {
@@ -189,6 +196,16 @@ export default function CombosPage() {
onSave={(data) => handleUpdate(editingCombo.id, data)} onSave={(data) => handleUpdate(editingCombo.id, data)}
activeProviders={activeProviders} activeProviders={activeProviders}
/> />
{/* Confirm Delete Modal */}
<ConfirmModal
isOpen={!!confirmState}
onClose={() => setConfirmState(null)}
onConfirm={confirmState?.onConfirm}
title={confirmState?.title || "Confirm"}
message={confirmState?.message}
variant="danger"
/>
</div> </div>
); );
} }

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import PropTypes from "prop-types"; 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"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
const TUNNEL_BENEFITS = [ const TUNNEL_BENEFITS = [
@@ -44,6 +44,7 @@ export default function APIPageClient({ machineId }) {
const [showAddModal, setShowAddModal] = useState(false); const [showAddModal, setShowAddModal] = useState(false);
const [newKeyName, setNewKeyName] = useState(""); const [newKeyName, setNewKeyName] = useState("");
const [createdKey, setCreatedKey] = useState(null); const [createdKey, setCreatedKey] = useState(null);
const [confirmState, setConfirmState] = useState(null);
const [requireApiKey, setRequireApiKey] = useState(false); const [requireApiKey, setRequireApiKey] = useState(false);
const [requireLogin, setRequireLogin] = useState(true); const [requireLogin, setRequireLogin] = useState(true);
@@ -660,22 +661,26 @@ export default function APIPageClient({ machineId }) {
}; };
const handleDeleteKey = async (id) => { const handleDeleteKey = async (id) => {
if (!confirm("Delete this API key?")) return; setConfirmState({
title: "Delete API Key",
try { message: "Delete this API key?",
const res = await fetch(`/api/keys/${id}`, { method: "DELETE" }); onConfirm: async () => {
if (res.ok) { setConfirmState(null);
setKeys(keys.filter((k) => k.id !== id)); try {
// Clean up visibility state const res = await fetch(`/api/keys/${id}`, { method: "DELETE" });
setVisibleKeys(prev => { if (res.ok) {
const next = new Set(prev); setKeys(keys.filter((k) => k.id !== id));
next.delete(id); setVisibleKeys(prev => {
return next; 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) => { const handleToggleKey = async (id, isActive) => {
@@ -1108,9 +1113,14 @@ export default function APIPageClient({ machineId }) {
checked={key.isActive ?? true} checked={key.isActive ?? true}
onChange={(checked) => { onChange={(checked) => {
if (key.isActive && !checked) { if (key.isActive && !checked) {
if (confirm(`Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`)) { setConfirmState({
handleToggleKey(key.id, checked); 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 { } else {
handleToggleKey(key.id, checked); handleToggleKey(key.id, checked);
} }
@@ -1344,6 +1354,16 @@ export default function APIPageClient({ machineId }) {
</div> </div>
</div> </div>
</Modal> </Modal>
{/* Confirm Modal */}
<ConfirmModal
isOpen={!!confirmState}
onClose={() => setConfirmState(null)}
onConfirm={confirmState?.onConfirm}
title={confirmState?.title || "Confirm"}
message={confirmState?.message}
variant="danger"
/>
</div> </div>
); );
} }

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; 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 { 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 { getModelsByProviderId } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
@@ -42,12 +42,13 @@ export default function ProviderDetailPage() {
const [selectedConnectionIds, setSelectedConnectionIds] = useState([]); const [selectedConnectionIds, setSelectedConnectionIds] = useState([]);
const [bulkProxyPoolId, setBulkProxyPoolId] = useState("__none__"); const [bulkProxyPoolId, setBulkProxyPoolId] = useState("__none__");
const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false); 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 [providerStickyLimit, setProviderStickyLimit] = useState("");
const [thinkingMode, setThinkingMode] = useState("auto"); const [thinkingMode, setThinkingMode] = useState("auto");
const [suggestedModels, setSuggestedModels] = useState([]); const [suggestedModels, setSuggestedModels] = useState([]);
const [kiloFreeModels, setKiloFreeModels] = useState([]); const [kiloFreeModels, setKiloFreeModels] = useState([]);
const [disabledModelIds, setDisabledModelIds] = useState([]); const [disabledModelIds, setDisabledModelIds] = useState([]);
const [confirmState, setConfirmState] = useState(null);
const { copied, copy } = useCopyToClipboard(); const { copied, copy } = useCopyToClipboard();
const providerInfo = providerNode const providerInfo = providerNode
@@ -110,17 +111,23 @@ export default function ProviderDetailPage() {
const handleDisableAll = async (ids) => { const handleDisableAll = async (ids) => {
if (!ids.length) return; if (!ids.length) return;
if (!confirm(`Disable all ${ids.length} model(s)?`)) return; setConfirmState({
try { title: "Disable All Models",
const res = await fetch("/api/models/disabled", { message: `Disable all ${ids.length} model(s)?`,
method: "POST", onConfirm: async () => {
headers: { "Content-Type": "application/json" }, setConfirmState(null);
body: JSON.stringify({ providerAlias: providerStorageAlias, ids }), try {
}); const res = await fetch("/api/models/disabled", {
if (res.ok) await fetchDisabledModels(); method: "POST",
} catch (error) { headers: { "Content-Type": "application/json" },
console.log("Error disabling all models:", error); body: JSON.stringify({ providerAlias: providerStorageAlias, ids }),
} });
if (res.ok) await fetchDisabledModels();
} catch (error) {
console.log("Error disabling all models:", error);
}
}
});
}; };
const handleEnableAll = async () => { const handleEnableAll = async () => {
@@ -338,15 +345,21 @@ export default function ProviderDetailPage() {
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (!confirm("Delete this connection?")) return; setConfirmState({
try { title: "Delete Connection",
const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); message: "Delete this connection?",
if (res.ok) { onConfirm: async () => {
setConnections(connections.filter(c => c.id !== id)); 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 = () => { const handleOAuthSuccess = () => {
@@ -954,15 +967,21 @@ export default function ProviderDetailPage() {
variant="secondary" variant="secondary"
icon="delete" icon="delete"
onClick={async () => { onClick={async () => {
if (!confirm(`Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`)) return; setConfirmState({
try { title: "Delete Compatible Node",
const res = await fetch(`/api/provider-nodes/${providerId}`, { method: "DELETE" }); message: `Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`,
if (res.ok) { onConfirm: async () => {
router.push("/dashboard/providers"); 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" className="w-full sm:w-auto"
> >
@@ -1222,6 +1241,16 @@ export default function ProviderDetailPage() {
onClose={() => setShowAddCustomModel(false)} onClose={() => setShowAddCustomModel(false)}
/> />
)} )}
{/* Confirm Modal */}
<ConfirmModal
isOpen={!!confirmState}
onClose={() => setConfirmState(null)}
onConfirm={confirmState?.onConfirm}
title={confirmState?.title || "Confirm"}
message={confirmState?.message}
variant="danger"
/>
</div> </div>
); );
} }

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import PropTypes from "prop-types"; 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 ────────────────────────────────────────────── // ── CooldownTimer ──────────────────────────────────────────────
function CooldownTimer({ until }) { function CooldownTimer({ until }) {
@@ -308,6 +308,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
const [selectedConnection, setSelectedConnection] = useState(null); const [selectedConnection, setSelectedConnection] = useState(null);
const [providerStrategy, setProviderStrategy] = useState(null); const [providerStrategy, setProviderStrategy] = useState(null);
const [providerStickyLimit, setProviderStickyLimit] = useState("1"); const [providerStickyLimit, setProviderStickyLimit] = useState("1");
const [confirmState, setConfirmState] = useState(null);
const fetch_ = useCallback(async () => { const fetch_ = useCallback(async () => {
try { try {
@@ -358,11 +359,17 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (!confirm("Delete this connection?")) return; setConfirmState({
try { title: "Delete Connection",
const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); message: "Delete this connection?",
if (res.ok) setConnections((prev) => prev.filter((c) => c.id !== id)); onConfirm: async () => {
} catch (e) { console.log("delete error:", e); } 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) => { const handleToggleActive = async (id, isActive) => {
@@ -470,6 +477,16 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
onSave={handleUpdateConnection} onSave={handleUpdateConnection}
onClose={() => setShowEditModal(false)} onClose={() => setShowEditModal(false)}
/> />
{/* Confirm Modal */}
<ConfirmModal
isOpen={!!confirmState}
onClose={() => setConfirmState(null)}
onConfirm={confirmState?.onConfirm}
title={confirmState?.title || "Confirm"}
message={confirmState?.message}
variant="danger"
/>
</> </>
); );
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo, useState } from "react"; 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"; import { useNotificationStore } from "@/store/notificationStore";
function getStatusVariant(status) { function getStatusVariant(status) {
@@ -45,6 +45,7 @@ export default function ProxyPoolsPage() {
const [healthChecking, setHealthChecking] = useState(false); const [healthChecking, setHealthChecking] = useState(false);
const [healthProgress, setHealthProgress] = useState({ current: 0, total: 0 }); const [healthProgress, setHealthProgress] = useState({ current: 0, total: 0 });
const [bulkBusy, setBulkBusy] = useState(false); const [bulkBusy, setBulkBusy] = useState(false);
const [confirmState, setConfirmState] = useState(null);
const notify = useNotificationStore(); const notify = useNotificationStore();
const fetchProxyPools = useCallback(async () => { const fetchProxyPools = useCallback(async () => {
@@ -122,27 +123,31 @@ export default function ProxyPoolsPage() {
}; };
const handleDelete = async (proxyPool) => { const handleDelete = async (proxyPool) => {
const deleting = confirm(`Delete proxy pool \"${proxyPool.name}\"?`); setConfirmState({
if (!deleting) return; 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 data = await res.json();
const res = await fetch(`/api/proxy-pools/${proxyPool.id}`, { method: "DELETE" }); if (res.status === 409) {
if (res.ok) { notify.warning(`Cannot delete: ${data.boundConnectionCount || 0} connection(s) are still using this pool.`);
setProxyPools((prev) => prev.filter((item) => item.id !== proxyPool.id)); } else {
notify.success("Proxy pool deleted"); notify.error(data.error || "Failed to delete proxy pool");
return; }
} 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) => { const handleTest = async (proxyPoolId) => {
@@ -215,24 +220,30 @@ export default function ProxyPoolsPage() {
const bulkDelete = async () => { const bulkDelete = async () => {
if (selectedIds.length === 0) return; if (selectedIds.length === 0) return;
if (!confirm(`Delete ${selectedIds.length} proxy pool(s)?`)) return; setConfirmState({
setBulkBusy(true); title: "Delete Proxy Pools",
try { message: `Delete ${selectedIds.length} proxy pool(s)?`,
let ok = 0; let blocked = 0; let failed = 0; onConfirm: async () => {
for (const id of selectedIds) { setConfirmState(null);
setBulkBusy(true);
try { try {
const res = await fetch(`/api/proxy-pools/${id}`, { method: "DELETE" }); let ok = 0; let blocked = 0; let failed = 0;
if (res.ok) ok += 1; for (const id of selectedIds) {
else if (res.status === 409) blocked += 1; try {
else failed += 1; const res = await fetch(`/api/proxy-pools/${id}`, { method: "DELETE" });
} catch { failed += 1; } 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 () => { const handleHealthCheck = async () => {
@@ -269,23 +280,30 @@ export default function ProxyPoolsPage() {
setHealthChecking(false); setHealthChecking(false);
setHealthProgress({ current: 0, total: 0 }); setHealthProgress({ current: 0, total: 0 });
if (deadIds.length > 0 && confirm(`Alive: ${alive}, Dead: ${deadIds.length}.\n\nDisable ${deadIds.length} dead proxies?`)) { if (deadIds.length > 0) {
setBulkBusy(true); setConfirmState({
try { title: "Disable Dead Proxies",
for (const id of deadIds) { message: `Alive: ${alive}, Dead: ${deadIds.length}.\n\nDisable ${deadIds.length} dead proxies?`,
onConfirm: async () => {
setConfirmState(null);
setBulkBusy(true);
try { try {
await fetch(`/api/proxy-pools/${id}`, { for (const id of deadIds) {
method: "PUT", try {
headers: { "Content-Type": "application/json" }, await fetch(`/api/proxy-pools/${id}`, {
body: JSON.stringify({ isActive: false }), method: "PUT",
}); headers: { "Content-Type": "application/json" },
} catch {} 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 { } else {
notify.success(`Health check done. Alive: ${alive}, Dead: ${deadIds.length}`); notify.success(`Health check done. Alive: ${alive}, Dead: ${deadIds.length}`);
} }
@@ -768,6 +786,16 @@ export default function ProxyPoolsPage() {
</div> </div>
</div> </div>
</Modal> </Modal>
{/* Confirm Modal */}
<ConfirmModal
isOpen={!!confirmState}
onClose={() => setConfirmState(null)}
onConfirm={confirmState?.onConfirm}
title={confirmState?.title || "Confirm"}
message={confirmState?.message}
variant="danger"
/>
</div> </div>
); );
} }