feat(proxy-pools): add support for deno deploy relays and fix layout overflow issues in proxy pools dashboard (#1437)

Co-authored-by: TD <tho.din@inno.ai.vn>
This commit is contained in:
@TDandTD authored and GitHub committed 2026-05-26 11:21:49 +07:00
1 parent 13a28452fb
commit 57b3e94e40
5 files changed
+343 -10

No files matched your search

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle, ConfirmModal } from "@/shared/components";
import { useNotificationStore } from "@/store/notificationStore";
@@ -34,11 +34,14 @@ export default function ProxyPoolsPage() {
const [showBatchImportModal, setShowBatchImportModal] = useState(false);
const [showVercelModal, setShowVercelModal] = useState(false);
const [showCloudflareModal, setShowCloudflareModal] = useState(false);
const [showDenoModal, setShowDenoModal] = useState(false);
const [showRelayMenu, setShowRelayMenu] = useState(false);
const [editingProxyPool, setEditingProxyPool] = useState(null);
const [formData, setFormData] = useState(normalizeFormData());
const [batchImportText, setBatchImportText] = useState("");
const [vercelForm, setVercelForm] = useState({ vercelToken: "", projectName: "vercel-relay" });
const [cloudflareForm, setCloudflareForm] = useState({ accountId: "", apiToken: "", projectName: "cloudflare-relay" });
const [denoForm, setDenoForm] = useState({ denoToken: "", orgDomain: "", projectName: "" });
const [saving, setSaving] = useState(false);
const [importing, setImporting] = useState(false);
const [deploying, setDeploying] = useState(false);
@@ -48,8 +51,21 @@ export default function ProxyPoolsPage() {
const [healthProgress, setHealthProgress] = useState({ current: 0, total: 0 });
const [bulkBusy, setBulkBusy] = useState(false);
const [confirmState, setConfirmState] = useState(null);
const relayMenuRef = useRef(null);
const notify = useNotificationStore();
useEffect(() => {
const handleClickOutside = (e) => {
if (relayMenuRef.current && !relayMenuRef.current.contains(e.target)) {
setShowRelayMenu(false);
}
};
if (showRelayMenu) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [showRelayMenu]);
const fetchProxyPools = useCallback(async () => {
try {
const res = await fetch("/api/proxy-pools?includeUsage=true", { cache: "no-store" });
@@ -346,6 +362,16 @@ export default function ProxyPoolsPage() {
setShowCloudflareModal(false);
};
const openDenoModal = () => {
setDenoForm({ denoToken: "", orgDomain: "", projectName: "" });
setShowDenoModal(true);
};
const closeDenoModal = () => {
if (deploying) return;
setShowDenoModal(false);
};
const handleVercelDeploy = async () => {
if (!vercelForm.vercelToken.trim()) return;
setDeploying(true);
@@ -396,6 +422,31 @@ export default function ProxyPoolsPage() {
}
};
const handleDenoDeploy = async () => {
if (!denoForm.denoToken.trim()) return;
setDeploying(true);
try {
const res = await fetch("/api/proxy-pools/deno-deploy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(denoForm),
});
const data = await res.json();
if (res.ok) {
await fetchProxyPools();
closeDenoModal();
notify.success(`Deployed: ${data.deployUrl}`);
} else {
notify.error(data.error || "Deploy failed");
}
} catch (error) {
console.log("Error deploying Deno relay:", error);
notify.error("Deploy failed");
} finally {
setDeploying(false);
}
};
const parseProxyLine = (line) => {
const trimmed = line.trim();
if (!trimmed) return null;
@@ -532,12 +583,55 @@ export default function ProxyPoolsPage() {
</div>
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
<Button size="sm" variant="secondary" icon="cloud" onClick={openCloudflareModal}>
Cloudflare Relay
</Button>
<Button size="sm" variant="secondary" icon="cloud_upload" onClick={openVercelModal}>
Vercel Relay
</Button>
<div className="relative" ref={relayMenuRef}>
<Button
size="sm"
variant="secondary"
icon="rocket_launch"
onClick={() => setShowRelayMenu(!showRelayMenu)}
>
Deploy Relay
<span className="material-symbols-outlined ml-1 text-[18px]">
{showRelayMenu ? "expand_less" : "expand_more"}
</span>
</Button>
{showRelayMenu && (
<div className="absolute left-0 top-full z-50 mt-1 w-48 rounded-xl border border-black/10 bg-white p-1 shadow-xl dark:border-white/10 dark:bg-zinc-900 sm:left-auto sm:right-0">
<button
onClick={() => {
openCloudflareModal();
setShowRelayMenu(false);
}}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-text-main transition-colors hover:bg-black/5 dark:hover:bg-white/5"
>
<span className="material-symbols-outlined text-[20px] text-orange-500">cloud</span>
Cloudflare Relay
</button>
<button
onClick={() => {
openVercelModal();
setShowRelayMenu(false);
}}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-text-main transition-colors hover:bg-black/5 dark:hover:bg-white/5"
>
<span className="material-symbols-outlined text-[20px] text-blue-500">cloud_upload</span>
Vercel Relay
</button>
<button
onClick={() => {
openDenoModal();
setShowRelayMenu(false);
}}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-text-main transition-colors hover:bg-black/5 dark:hover:bg-white/5"
>
<span className="material-symbols-outlined text-[20px] text-green-500">terminal</span>
Deno Relay
</button>
</div>
)}
</div>
<Button size="sm" variant="secondary" icon="upload" onClick={openBatchImportModal}>
Batch Import
</Button>
@@ -829,6 +923,70 @@ export default function ProxyPoolsPage() {
</div>
</Modal>
<Modal
isOpen={showDenoModal}
title="Deploy Deno Relay"
onClose={closeDenoModal}
>
<div className="flex flex-col gap-4">
<div className="rounded-lg bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 p-3 flex flex-col gap-1.5">
<p className="text-sm text-text-main font-medium">What is Deno Relay?</p>
<p className="text-xs text-text-muted">
Deploys a relay worker to Deno Deploy&apos;s global edge network. All AI provider requests are forwarded through Deno&apos;s edge, masking your real IP.
</p>
<ul className="text-xs text-text-muted list-disc pl-4 space-y-0.5">
<li>Deno Deploy v2 runs on a high-performance global edge network</li>
<li>Free tier: 1M requests & 100GiB outbound traffic per month</li>
<li>No per-request CPU time limits (unlike Vercel/Cloudflare)</li>
<li>Support up to 20 active apps & 50 custom domains</li>
<li>Deploy multiple relays for maximum IP diversity</li>
</ul>
<div className="mt-2 pt-2 border-t border-black/10 dark:border-white/10 text-xs text-text-muted">
<p className="font-medium text-text-main mb-1">How to generate API token:</p>
<ol className="list-decimal pl-4 space-y-0.5">
<li>Go to <b>console.deno.com</b></li>
<li>Select your <b>Organization</b> → <b>Settings</b> → <b>Organization Tokens</b></li>
<li>Create a <b>Organization Token</b> (prefix <b>ddo_</b>)</li>
</ol>
</div>
</div>
<Input
label="Deno Deploy API Token"
value={denoForm.denoToken}
onChange={(e) => setDenoForm((prev) => ({ ...prev, denoToken: e.target.value }))}
placeholder="ddo_xxxxxxxxxxxxxxxx"
hint={<>Token is used once for deployment, not stored. Found in Organization Settings.</>}
type="password"
/>
<Input
label="Organization Domain"
value={denoForm.orgDomain}
onChange={(e) => setDenoForm((prev) => ({ ...prev, orgDomain: e.target.value }))}
placeholder="your-org.deno.net"
hint="Organization's default domain. Your relay URL will be in the format: https://my-relay.your-org.deno.net"
/>
<Input
label="App Name"
value={denoForm.projectName}
onChange={(e) => setDenoForm((prev) => ({ ...prev, projectName: e.target.value }))}
placeholder="deno-relay"
hint="Unique app name. Leave empty for auto-generated name."
/>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<Button
fullWidth
onClick={handleDenoDeploy}
disabled={!denoForm.denoToken.trim() || !denoForm.orgDomain.trim() || deploying}
>
{deploying ? "Deploying..." : "Deploy Relay"}
</Button>
<Button fullWidth variant="ghost" onClick={closeDenoModal} disabled={deploying}>
Cancel
</Button>
</div>
</div>
</Modal>
<Modal
isOpen={showFormModal}
title={editingProxyPool ? "Edit Proxy Pool" : "Add Proxy Pool"}