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:
committed by
GitHub
parent
5cab23d92e
commit
3c2503c4b4
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user