From c8e6b07df52cd4cf1fea334bfaa0a3b05208967e Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sun, 9 Aug 2026 09:54:12 -0300 Subject: [PATCH] cherry-pick(pr-9718): feat(src): proxy-pool-toolbar-minor-improvements (#9870) * feat(proxy-pool): streamline pool actions * test(proxy-pool): cover toolbar layout * refactor(settings): extract proxy registry helpers Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(settings): reduce proxy registry component size Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Agnes --- .../components/ProxyRegistryManager.tsx | 427 +++++++++--------- .../components/proxy/ProxyPoolTab.tsx | 102 +---- .../components/proxyRegistryConstants.ts | 88 ++++ .../settings/components/proxyRegistryData.ts | 78 ++++ .../ProxyRegistryManager-tdz-render.test.tsx | 2 + 5 files changed, 391 insertions(+), 306 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/settings/components/proxyRegistryConstants.ts create mode 100644 src/app/(dashboard)/dashboard/settings/components/proxyRegistryData.ts diff --git a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx index b87aa200d8..47d0334041 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx @@ -1,8 +1,7 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslations } from "next-intl"; -import { z } from "zod"; import { Button, Card, Modal } from "@/shared/components"; import { useProxyBatchOperations } from "./useProxyBatchOperations"; import { ProxyStatusBadge } from "./ProxyStatusBadge"; @@ -16,90 +15,32 @@ import { } from "./parseBulkProxyImport"; import { POOL_STRATEGY_OPTIONS, isPoolStrategy, type PoolStrategy } from "./proxyStrategyOptions"; import type { ProxyItem } from "./proxyRegistryTypes"; +import { + BULK_IMPORT_PLACEHOLDER, + EMPTY_FORM, + type HealthInfo, + type ProxyRegistryManagerProps, + type TestResult, + type UsageInfo, +} from "./proxyRegistryConstants"; +import { + loadAllProxyUsage, + loadProxyHealth, + loadProxyUsage, + repairRelayResponseSchema, +} from "./proxyRegistryData"; -type UsageInfo = { - count: number; - assignments: Array<{ scope: string; scopeId: string | null }>; -}; - -type HealthInfo = { - proxyId: string; - totalRequests: number; - successRate: number | null; - avgLatencyMs: number | null; - lastSeenAt: string | null; -}; - -type TestResult = { - success: boolean; - publicIp?: string; - latencyMs?: number; - country?: string; - error?: string; -}; - -const EMPTY_FORM = { - id: "", - name: "", - type: "http", - host: "", - port: "8080", - username: "", - password: "", - region: "", - notes: "", - status: "active", - family: "auto", -}; - -const BULK_IMPORT_TEMPLATE = `# Proxy Bulk Import -# ───────────────────────────────────────────────────────────────────────────── -# FORMAT 1 — Pipe-delimited (full control): -# NAME|HOST|PORT|USERNAME|PASSWORD|TYPE|REGION|STATUS|NOTES -# Required: NAME, HOST, PORT -# Optional: USERNAME, PASSWORD, TYPE (http|https|socks5, default: socks5), REGION, STATUS (active|inactive, default: active), NOTES -# -# FORMAT 2 — Shorthand (one proxy per line, no pipe needed): -# ip:port → no auth, type defaults to socks5 -# ip:port:user:pass → with auth -# user:pass@ip:port → with auth (@-style) -# user:pass:ip:port → with auth (user-pass-first) -# protocol://ip:port → explicit protocol -# protocol://user:pass@ip:port → explicit protocol + auth -# -# FORMAT 3 — Protocol header mode: -# Put a bare protocol (http, https, socks5) on its own line to set -# the default type for all subsequent shorthand lines that don't -# include an explicit protocol:// prefix. -# -# Lines starting with # are ignored. Existing proxies (same host+port) will be updated. -# -# ───────────────────────────────────────────────────────────────────────────── -# Pipe-delimited examples: -# proxy-us|138.99.147.218|50101|myuser|mypass|socks5|US-East|active|US production proxy -# proxy-eu|200.234.177.62|50101|myuser|mypass|socks5|EU-West -# http-proxy|10.0.0.50|8080|||http||active|Internal HTTP proxy -# -# Shorthand examples: -# 138.99.147.218:50101 -# 138.99.147.218:50101:myuser:mypass -# myuser:mypass@138.99.147.218:50101 -# myuser:mypass:138.99.147.218:50101 -# http://10.0.0.50:8080 -# https://admin:secret123@proxy.example.com:443 -# -# Protocol header mode example: -# socks5 -# 138.99.147.218:50101:myuser:mypass -# 200.234.177.62:50101:otheruser:otherpass -#`; - -export default function ProxyRegistryManager({ + export default function ProxyRegistryManager({ onRedeployRelay, -}: { - onRedeployRelay?: (proxy: ProxyItem) => void; -} = {}) { + showVercelRelay = false, + showDenoRelay = false, + showCloudflareRelay = false, + onOpenVercelRelay, + onOpenDenoRelay, + onOpenCloudflareRelay, +}: ProxyRegistryManagerProps = {}) { const t = useTranslations("proxyRegistry"); + const settingsT = useTranslations("settings"); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -135,7 +76,7 @@ export default function ProxyRegistryManager({ const [poolLoaded, setPoolLoaded] = useState(false); const [poolSaving, setPoolSaving] = useState(false); const [bulkImportOpen, setBulkImportOpen] = useState(false); - const [bulkImportText, setBulkImportText] = useState(BULK_IMPORT_TEMPLATE); + const [bulkImportText, setBulkImportText] = useState(""); const [bulkImportParsed, setBulkImportParsed] = useState([]); const [bulkImportErrors, setBulkImportErrors] = useState([]); const [bulkImportSkipped, setBulkImportSkipped] = useState(0); @@ -146,53 +87,40 @@ export default function ProxyRegistryManager({ updated: number; failed: number; } | null>(null); + const [actionsOpen, setActionsOpen] = useState(false); + const [relayMenuOpen, setRelayMenuOpen] = useState(false); + const actionsRef = useRef(null); + const relayRef = useRef(null); + + const showAnyRelay = showVercelRelay || showDenoRelay || showCloudflareRelay; + + useEffect(() => { + if (!actionsOpen && !relayMenuOpen) return; + const onMouseDown = (event: MouseEvent) => { + const target = event.target as Node; + if (actionsOpen && actionsRef.current && !actionsRef.current.contains(target)) { + setActionsOpen(false); + } + if (relayMenuOpen && relayRef.current && !relayRef.current.contains(target)) { + setRelayMenuOpen(false); + } + }; + document.addEventListener("mousedown", onMouseDown); + return () => document.removeEventListener("mousedown", onMouseDown); + }, [actionsOpen, relayMenuOpen]); + + const closeActions = () => { + setActionsOpen(false); + setRelayMenuOpen(false); + }; const editingId = useMemo(() => form.id || "", [form.id]); - const loadHealth = useCallback(async () => { - try { - const res = await fetch("/api/settings/proxies/health?hours=24"); - const data = await res.json().catch(() => ({})); - if (!res.ok) return; - const entries = Array.isArray(data?.items) ? data.items : []; - const mapped = Object.fromEntries( - entries.map((entry: HealthInfo) => [entry.proxyId, entry]) - ) as Record; - setHealthById(mapped); - } catch { - // ignore health loading errors in UI - } - }, []); - - const loadAllUsage = useCallback(async (proxyIds: string[]) => { - if (!proxyIds.length) return; - try { - const results = await Promise.all( - proxyIds.map((id) => - fetch(`/api/settings/proxies/assignments?proxyId=${encodeURIComponent(id)}`) - .then((r) => (r.ok ? r.json() : null)) - .then((data) => { - const rawAssignments: Array<{ scope: string; scopeId: string | null }> = - Array.isArray(data?.items) ? data.items : []; - // Deduplicate by scope+scopeId — prevents double-counting when both - // a provider-scope and account-scope row exist for the same proxy - const seen = new Set(); - const assignments = rawAssignments.filter((a) => { - const key = `${a.scope}:${a.scopeId ?? ""}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - return [id, { count: assignments.length, assignments }] as [string, UsageInfo]; - }) - .catch(() => [id, { count: 0, assignments: [] }] as [string, UsageInfo]) - ) - ); - setUsageById(Object.fromEntries(results)); - } catch { - // ignore - } - }, []); + const loadHealth = useCallback(() => loadProxyHealth(setHealthById), []); + const loadAllUsage = useCallback( + (proxyIds: string[]) => loadAllProxyUsage(proxyIds, setUsageById), + [] + ); const load = useCallback(async () => { setLoading(true); @@ -240,17 +168,9 @@ export default function ProxyRegistryManager({ const allSelected = items.length > 0 && items.every((item) => selectedIds.has(item.id)); - const handleBatchDelete = useCallback(() => { - hookHandleBatchDelete(setError); - }, [hookHandleBatchDelete, setError]); - - const handleBatchActivate = useCallback(() => { - hookHandleBatchActivate(setError, "active"); - }, [hookHandleBatchActivate, setError]); - - const handleAutoTestAll = useCallback(() => { - hookHandleAutoTestAll(setError, setTestById); - }, [hookHandleAutoTestAll, setError, setTestById]); + const handleBatchDelete = () => hookHandleBatchDelete(setError); + const handleBatchActivate = () => hookHandleBatchActivate(setError, "active"); + const handleAutoTestAll = () => hookHandleAutoTestAll(setError, setTestById); useEffect(() => { void load(); @@ -284,33 +204,7 @@ export default function ProxyRegistryManager({ setModalOpen(true); }; - const loadUsage = async (proxyId: string) => { - try { - const res = await fetch( - `/api/settings/proxies/assignments?proxyId=${encodeURIComponent(proxyId)}` - ); - const data = await res.json().catch(() => ({})); - if (!res.ok) return; - const rawAssignments: Array<{ scope: string; scopeId: string | null }> = Array.isArray( - data?.items - ) - ? data.items - : []; - const seen = new Set(); - const assignments = rawAssignments.filter((a) => { - const key = `${a.scope}:${a.scopeId ?? ""}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - setUsageById((prev) => ({ - ...prev, - [proxyId]: { count: assignments.length, assignments }, - })); - } catch { - // ignore usage loading errors in UI - } - }; + const loadUsage = (proxyId: string) => loadProxyUsage(proxyId, setUsageById); const handleTestProxy = async (item: ProxyItem) => { if (testingId) return; @@ -345,12 +239,6 @@ export default function ProxyRegistryManager({ } }; - const repairRelayResponseSchema = z.object({ - repaired: z.boolean().optional(), - mode: z.enum(["noop", "recovered", "redeploy"]).optional(), - error: z.object({ message: z.string() }).optional(), - }); - const handleRepairRelay = async (item: ProxyItem) => { if (repairingId || !item.relayInfo?.isRelay) return; setRepairingId(item.id); @@ -724,7 +612,7 @@ export default function ProxyRegistryManager({ }; const openBulkImport = () => { - setBulkImportText(BULK_IMPORT_TEMPLATE); + setBulkImportText(""); setBulkImportParsed([]); setBulkImportErrors([]); setBulkImportSkipped(0); @@ -736,49 +624,13 @@ export default function ProxyRegistryManager({ return ( <> -
-
+
+

{t("title")}

{t("description")}

-
- - - - +