ai_api / src /app /(dashboard) /dashboard /settings /components /proxy /GlobalConfigTab.tsx
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
9.78 kB
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { Card, Button, Toggle, ProxyConfigModal } from "@/shared/components";
import { useTranslations } from "next-intl";
type GlobalProxyConfig = { type: string; host: string; port: number } | null;
type HealthcheckResult = {
proxyUrl: string;
ok: boolean;
latencyMs: number | null;
};
type HealthcheckSummary = {
total: number;
working: number;
failed: number;
};
export default function GlobalConfigTab() {
const [proxyModalOpen, setProxyModalOpen] = useState(false);
const [globalProxy, setGlobalProxy] = useState<GlobalProxyConfig>(null);
const [perKeyProxyEnabled, setPerKeyProxyEnabled] = useState(false);
const [perKeyLoading, setPerKeyLoading] = useState(true);
const [targetUrl, setTargetUrl] = useState("https://api.openai.com/v1/models");
const [testing, setTesting] = useState(false);
const [results, setResults] = useState<HealthcheckResult[] | null>(null);
const [summary, setSummary] = useState<HealthcheckSummary | null>(null);
const [error, setError] = useState<string | null>(null);
const mountedRef = useRef(true);
const t = useTranslations("settings");
const tc = useTranslations("common");
const loadGlobalProxy = useCallback(async () => {
try {
const res = await fetch("/api/settings/proxy?level=global");
if (res.ok) {
const data = await res.json();
setGlobalProxy(data.proxy || null);
}
} catch {}
}, []);
const loadPerKeyProxyEnabled = useCallback(async () => {
try {
const res = await fetch("/api/settings", { cache: "no-store" });
if (res.ok) {
const data = await res.json();
if (mountedRef.current) setPerKeyProxyEnabled(data.perKeyProxyEnabled === true);
}
} catch {
/* leave default */
} finally {
if (mountedRef.current) setPerKeyLoading(false);
}
}, []);
useEffect(() => {
mountedRef.current = true;
loadGlobalProxy();
loadPerKeyProxyEnabled();
return () => {
mountedRef.current = false;
};
}, [loadGlobalProxy, loadPerKeyProxyEnabled]);
const handleTogglePerKeyProxyEnabled = async () => {
const newValue = !perKeyProxyEnabled;
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ perKeyProxyEnabled: newValue }),
});
if (res.ok) {
setPerKeyProxyEnabled(newValue);
}
} catch (err) {
console.error("Failed to update per-key proxy setting:", err);
}
};
const runHealthcheck = async () => {
setTesting(true);
setResults(null);
setSummary(null);
setError(null);
try {
const res = await fetch("/api/proxy-fallback/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetUrl }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Request failed" }));
setError(err.error || `HTTP ${res.status}`);
return;
}
const data = await res.json();
setResults(data.results);
setSummary(data.summary);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : t("healthcheckFailed"));
} finally {
setTesting(false);
}
};
return (
<>
<Card className="p-0 overflow-hidden">
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
vpn_lock
</span>
<h2 className="text-lg font-bold">{t("globalProxy")}</h2>
</div>
<p className="text-sm text-text-muted mb-4">{t("globalProxyDesc")}</p>
<div className="flex items-center gap-3">
{globalProxy ? (
<span className="px-2.5 py-1 rounded text-xs font-bold uppercase bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
{globalProxy.type}://{globalProxy.host}:{globalProxy.port}
</span>
) : (
<span className="text-sm text-text-muted">{t("noGlobalProxy")}</span>
)}
<Button
size="sm"
variant={globalProxy ? "secondary" : "primary"}
icon="settings"
onClick={() => {
loadGlobalProxy();
setProxyModalOpen(true);
}}
>
{globalProxy ? tc("edit") : t("configure")}
</Button>
</div>
</div>
</Card>
<Card className="p-0 overflow-hidden">
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<span
className="material-symbols-outlined text-xl text-violet-500"
aria-hidden="true"
>
key
</span>
<div>
<h2 className="text-lg font-bold">{t("perKeyProxyEnabled")}</h2>
<p className="text-sm text-text-muted">{t("perKeyProxyEnabledDesc")}</p>
</div>
</div>
<Toggle
checked={perKeyProxyEnabled}
disabled={perKeyLoading}
onChange={handleTogglePerKeyProxyEnabled}
/>
</div>
</div>
</Card>
<Card className="p-0 overflow-hidden">
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
network_check
</span>
<h2 className="text-lg font-bold">{t("bulkHealthcheck")}</h2>
</div>
<p className="text-sm text-text-muted mb-4">{t("bulkHealthcheckDesc")}</p>
<div className="flex items-center gap-3 mb-4">
<input
type="text"
value={targetUrl}
onChange={(e) => setTargetUrl(e.target.value)}
placeholder="https://api.openai.com/v1/models"
className="flex-1 px-3 py-2 rounded-lg bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
<Button
size="sm"
variant="primary"
icon={testing ? "refresh" : "play_arrow"}
disabled={testing}
onClick={runHealthcheck}
>
{testing ? t("healthcheckTesting") : t("healthcheckAll")}
</Button>
</div>
{error && (
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-sm text-red-400">
{error}
</div>
)}
{testing && !results && (
<div className="flex items-center gap-2 text-sm text-text-muted py-2">
<span className="material-symbols-outlined animate-spin text-lg">refresh</span>
{t("healthcheckTestingAll")}
</div>
)}
{summary && (
<div className="flex items-center gap-4 mb-3 text-sm">
<span className="text-text-muted">
{t("healthcheckTotal")}: <strong>{summary.total}</strong>
</span>
<span className="text-emerald-400">
{t("healthcheckWorking")}: <strong>{summary.working}</strong>
</span>
<span className="text-red-400">
{t("healthcheckFailedLabel")}: <strong>{summary.failed}</strong>
</span>
</div>
)}
{results && results.length > 0 && (
<div className="max-h-64 overflow-y-auto rounded-lg border border-black/10 dark:border-white/10">
<table className="w-full text-xs">
<thead>
<tr className="bg-black/5 dark:bg-white/5">
<th className="text-left px-3 py-2 font-medium text-text-muted">
{t("healthcheckStatus")}
</th>
<th className="text-left px-3 py-2 font-medium text-text-muted">
{t("healthcheckProxyUrl")}
</th>
<th className="text-right px-3 py-2 font-medium text-text-muted">
{t("healthcheckLatency")}
</th>
</tr>
</thead>
<tbody>
{results.map((r, i) => (
<tr key={i} className="border-t border-black/5 dark:border-white/5">
<td className="px-3 py-1.5">
{r.ok ? (
<span className="text-emerald-400 text-sm"></span>
) : (
<span className="text-red-400 text-sm"></span>
)}
</td>
<td className="px-3 py-1.5 font-mono truncate max-w-xs">{r.proxyUrl}</td>
<td className="px-3 py-1.5 text-right text-text-muted">
{r.latencyMs !== null ? `${r.latencyMs}ms` : "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</Card>
<ProxyConfigModal
isOpen={proxyModalOpen}
onClose={() => setProxyModalOpen(false)}
level="global"
levelLabel={t("globalLabel")}
onSaved={loadGlobalProxy}
/>
</>
);
}