481 lines
23 KiB
JavaScript
481 lines
23 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import PropTypes from "prop-types";
|
|
import { Card, Badge, Button, Modal, Select, Toggle, EditConnectionModal } from "@/shared/components";
|
|
|
|
// ── CooldownTimer ──────────────────────────────────────────────
|
|
function CooldownTimer({ until }) {
|
|
const [remaining, setRemaining] = useState("");
|
|
|
|
useEffect(() => {
|
|
const update = () => {
|
|
const diff = new Date(until).getTime() - Date.now();
|
|
if (diff <= 0) { setRemaining(""); return; }
|
|
const s = Math.floor(diff / 1000);
|
|
if (s < 60) setRemaining(`${s}s`);
|
|
else if (s < 3600) setRemaining(`${Math.floor(s / 60)}m ${s % 60}s`);
|
|
else setRemaining(`${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`);
|
|
};
|
|
update();
|
|
const t = setInterval(update, 1000);
|
|
return () => clearInterval(t);
|
|
}, [until]);
|
|
|
|
if (!remaining) return null;
|
|
return <span className="text-xs text-orange-500 font-mono">⏱ {remaining}</span>;
|
|
}
|
|
|
|
CooldownTimer.propTypes = { until: PropTypes.string.isRequired };
|
|
|
|
// ── ConnectionRow ──────────────────────────────────────────────
|
|
function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete }) {
|
|
const [showProxyDropdown, setShowProxyDropdown] = useState(false);
|
|
const [updatingProxy, setUpdatingProxy] = useState(false);
|
|
const [isCooldown, setIsCooldown] = useState(false);
|
|
const proxyDropdownRef = useRef(null);
|
|
|
|
const proxyPoolMap = new Map((proxyPools || []).map((p) => [p.id, p]));
|
|
const boundProxyPoolId = connection.providerSpecificData?.proxyPoolId || null;
|
|
const boundProxyPool = boundProxyPoolId ? proxyPoolMap.get(boundProxyPoolId) : null;
|
|
const hasLegacyProxy = connection.providerSpecificData?.connectionProxyEnabled === true && !!connection.providerSpecificData?.connectionProxyUrl;
|
|
const hasAnyProxy = !!boundProxyPoolId || hasLegacyProxy;
|
|
|
|
const proxyDisplayText = boundProxyPool
|
|
? `Pool: ${boundProxyPool.name}`
|
|
: boundProxyPoolId ? `Pool: ${boundProxyPoolId} (inactive/missing)`
|
|
: hasLegacyProxy ? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}` : "";
|
|
|
|
let maskedProxyUrl = "";
|
|
const rawProxyUrl = boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl;
|
|
if (rawProxyUrl) {
|
|
try {
|
|
const p = new URL(rawProxyUrl);
|
|
maskedProxyUrl = `${p.protocol}//${p.hostname}${p.port ? `:${p.port}` : ""}`;
|
|
} catch { maskedProxyUrl = rawProxyUrl; }
|
|
}
|
|
|
|
const noProxyText = boundProxyPool?.noProxy || connection.providerSpecificData?.connectionNoProxy || "";
|
|
const proxyBadgeVariant = boundProxyPool?.isActive === true ? "success" : (boundProxyPoolId || hasLegacyProxy) ? "error" : "default";
|
|
|
|
const modelLockUntil = Object.entries(connection)
|
|
.filter(([k]) => k.startsWith("modelLock_"))
|
|
.map(([, v]) => v).filter(Boolean).sort()[0] || null;
|
|
|
|
useEffect(() => {
|
|
const check = () => {
|
|
const until = Object.entries(connection)
|
|
.filter(([k]) => k.startsWith("modelLock_"))
|
|
.map(([, v]) => v).filter(v => v && new Date(v).getTime() > Date.now()).sort()[0] || null;
|
|
setIsCooldown(!!until);
|
|
};
|
|
check();
|
|
const t = modelLockUntil ? setInterval(check, 1000) : null;
|
|
return () => { if (t) clearInterval(t); };
|
|
}, [modelLockUntil]);
|
|
|
|
useEffect(() => {
|
|
if (!showProxyDropdown) return;
|
|
const handler = (e) => {
|
|
if (proxyDropdownRef.current && !proxyDropdownRef.current.contains(e.target))
|
|
setShowProxyDropdown(false);
|
|
};
|
|
document.addEventListener("mousedown", handler);
|
|
return () => document.removeEventListener("mousedown", handler);
|
|
}, [showProxyDropdown]);
|
|
|
|
const effectiveStatus = connection.testStatus === "unavailable" && !isCooldown ? "active" : connection.testStatus;
|
|
|
|
const getStatusVariant = () => {
|
|
if (connection.isActive === false) return "default";
|
|
if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
|
|
if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
|
|
return "default";
|
|
};
|
|
|
|
const displayName = isOAuth
|
|
? connection.name || connection.email || connection.displayName || "OAuth Account"
|
|
: connection.name;
|
|
|
|
const handleSelectProxy = async (poolId) => {
|
|
setUpdatingProxy(true);
|
|
try { await onUpdateProxy(poolId === "__none__" ? null : poolId); }
|
|
finally { setUpdatingProxy(false); setShowProxyDropdown(false); }
|
|
};
|
|
|
|
return (
|
|
<div className={`group flex flex-col gap-3 p-2 rounded-lg sm:flex-row sm:items-center sm:justify-between hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
|
|
<div className="flex w-full min-w-0 flex-1 items-start gap-3 sm:items-center">
|
|
<div className="flex flex-col">
|
|
<button onClick={onMoveUp} disabled={isFirst} className={`p-0.5 rounded ${isFirst ? "text-text-muted/30 cursor-not-allowed" : "hover:bg-sidebar text-text-muted hover:text-primary"}`}>
|
|
<span className="material-symbols-outlined text-sm">keyboard_arrow_up</span>
|
|
</button>
|
|
<button onClick={onMoveDown} disabled={isLast} className={`p-0.5 rounded ${isLast ? "text-text-muted/30 cursor-not-allowed" : "hover:bg-sidebar text-text-muted hover:text-primary"}`}>
|
|
<span className="material-symbols-outlined text-sm">keyboard_arrow_down</span>
|
|
</button>
|
|
</div>
|
|
<span className="material-symbols-outlined text-base text-text-muted">{isOAuth ? "lock" : "key"}</span>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{displayName}</p>
|
|
<div className="flex flex-wrap items-center gap-2 mt-1">
|
|
<Badge variant={getStatusVariant()} size="sm" dot>
|
|
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
|
|
</Badge>
|
|
{hasAnyProxy && <Badge variant={proxyBadgeVariant} size="sm">Proxy</Badge>}
|
|
{isCooldown && connection.isActive !== false && <CooldownTimer until={modelLockUntil} />}
|
|
{connection.lastError && connection.isActive !== false && (
|
|
<span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>{connection.lastError}</span>
|
|
)}
|
|
<span className="text-xs text-text-muted">#{connection.priority}</span>
|
|
</div>
|
|
{hasAnyProxy && (
|
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
<span className="text-[11px] text-text-muted truncate max-w-[420px]" title={proxyDisplayText}>{proxyDisplayText}</span>
|
|
{maskedProxyUrl && <code className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded text-text-muted">{maskedProxyUrl}</code>}
|
|
{noProxyText && <span className="text-[11px] text-text-muted truncate max-w-[320px]" title={noProxyText}>no_proxy: {noProxyText}</span>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex w-full flex-wrap items-center justify-between gap-2 sm:w-auto sm:justify-end">
|
|
<div className="flex flex-wrap gap-1">
|
|
{(proxyPools || []).length > 0 && (
|
|
<div className="relative" ref={proxyDropdownRef}>
|
|
<button
|
|
onClick={() => setShowProxyDropdown((v) => !v)}
|
|
className={`flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors ${hasAnyProxy ? "text-primary" : "text-text-muted hover:text-primary"}`}
|
|
disabled={updatingProxy}
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">{updatingProxy ? "progress_activity" : "lan"}</span>
|
|
<span className="text-[10px] leading-tight">Proxy</span>
|
|
</button>
|
|
{showProxyDropdown && (
|
|
<div className="absolute right-0 top-full mt-1 z-50 bg-bg border border-border rounded-lg shadow-lg py-1 min-w-[160px]">
|
|
<button onClick={() => handleSelectProxy("__none__")} className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${!boundProxyPoolId ? "text-primary font-medium" : "text-text-main"}`}>None</button>
|
|
{(proxyPools || []).map((pool) => (
|
|
<button key={pool.id} onClick={() => handleSelectProxy(pool.id)} className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${boundProxyPoolId === pool.id ? "text-primary font-medium" : "text-text-main"}`}>{pool.name}</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
<button onClick={onEdit} className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary">
|
|
<span className="material-symbols-outlined text-[18px]">edit</span>
|
|
<span className="text-[10px] leading-tight">Edit</span>
|
|
</button>
|
|
<button onClick={onDelete} className="flex flex-col items-center px-2 py-1 rounded hover:bg-red-500/10 text-red-500">
|
|
<span className="material-symbols-outlined text-[18px]">delete</span>
|
|
<span className="text-[10px] leading-tight">Delete</span>
|
|
</button>
|
|
</div>
|
|
<Toggle size="sm" checked={connection.isActive ?? true} onChange={onToggleActive} title={(connection.isActive ?? true) ? "Disable" : "Enable"} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ConnectionRow.propTypes = {
|
|
connection: PropTypes.shape({
|
|
id: PropTypes.string,
|
|
name: PropTypes.string,
|
|
email: PropTypes.string,
|
|
displayName: PropTypes.string,
|
|
testStatus: PropTypes.string,
|
|
isActive: PropTypes.bool,
|
|
lastError: PropTypes.string,
|
|
priority: PropTypes.number,
|
|
}).isRequired,
|
|
proxyPools: PropTypes.array,
|
|
isOAuth: PropTypes.bool.isRequired,
|
|
isFirst: PropTypes.bool.isRequired,
|
|
isLast: PropTypes.bool.isRequired,
|
|
onMoveUp: PropTypes.func.isRequired,
|
|
onMoveDown: PropTypes.func.isRequired,
|
|
onToggleActive: PropTypes.func.isRequired,
|
|
onUpdateProxy: PropTypes.func,
|
|
onEdit: PropTypes.func.isRequired,
|
|
onDelete: PropTypes.func.isRequired,
|
|
};
|
|
|
|
// ── AddApiKeyModal ─────────────────────────────────────────────
|
|
function AddApiKeyModal({ isOpen, provider, providerName, proxyPools, onSave, onClose }) {
|
|
const NONE = "__none__";
|
|
const [formData, setFormData] = useState({ name: "", apiKey: "", priority: 1, proxyPoolId: NONE });
|
|
const [validating, setValidating] = useState(false);
|
|
const [validationResult, setValidationResult] = useState(null);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const handleValidate = async () => {
|
|
setValidating(true);
|
|
try {
|
|
const res = await fetch("/api/providers/validate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ provider, apiKey: formData.apiKey }),
|
|
});
|
|
const data = await res.json();
|
|
setValidationResult(data.valid ? "success" : "failed");
|
|
} catch { setValidationResult("failed"); }
|
|
finally { setValidating(false); }
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!provider || !formData.apiKey) return;
|
|
setSaving(true);
|
|
try {
|
|
let isValid = false;
|
|
try {
|
|
setValidating(true); setValidationResult(null);
|
|
const res = await fetch("/api/providers/validate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ provider, apiKey: formData.apiKey }),
|
|
});
|
|
const data = await res.json();
|
|
isValid = !!data.valid;
|
|
setValidationResult(isValid ? "success" : "failed");
|
|
} catch { setValidationResult("failed"); }
|
|
finally { setValidating(false); }
|
|
await onSave({
|
|
name: formData.name,
|
|
apiKey: formData.apiKey,
|
|
priority: formData.priority,
|
|
proxyPoolId: formData.proxyPoolId === NONE ? null : formData.proxyPoolId,
|
|
testStatus: isValid ? "active" : "unknown",
|
|
});
|
|
} finally { setSaving(false); }
|
|
};
|
|
|
|
if (!provider) return null;
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} title={`Add ${providerName || provider} API Key`} onClose={onClose}>
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<label className="text-xs text-text-muted mb-1 block">Name</label>
|
|
<input className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} placeholder="Production Key" />
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<label className="text-xs text-text-muted mb-1 block">API Key</label>
|
|
<input type="password" className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" value={formData.apiKey} onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })} />
|
|
</div>
|
|
<div className="pt-6">
|
|
<Button onClick={handleValidate} disabled={!formData.apiKey || validating || saving} variant="secondary">
|
|
{validating ? "Checking..." : "Check"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{validationResult && (
|
|
<Badge variant={validationResult === "success" ? "success" : "error"}>
|
|
{validationResult === "success" ? "Valid" : "Invalid"}
|
|
</Badge>
|
|
)}
|
|
<div>
|
|
<label className="text-xs text-text-muted mb-1 block">Priority</label>
|
|
<input type="number" className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" value={formData.priority} onChange={(e) => setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} />
|
|
</div>
|
|
<Select label="Proxy Pool" value={formData.proxyPoolId} onChange={(e) => setFormData({ ...formData, proxyPoolId: e.target.value })}
|
|
options={[{ value: NONE, label: "None" }, ...(proxyPools || []).map((p) => ({ value: p.id, label: p.name }))]} />
|
|
<div className="flex gap-2">
|
|
<Button onClick={handleSubmit} fullWidth disabled={!formData.name || !formData.apiKey || saving}>
|
|
{saving ? "Saving..." : "Save"}
|
|
</Button>
|
|
<Button onClick={onClose} variant="ghost" fullWidth>Cancel</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
AddApiKeyModal.propTypes = {
|
|
isOpen: PropTypes.bool.isRequired,
|
|
provider: PropTypes.string,
|
|
providerName: PropTypes.string,
|
|
proxyPools: PropTypes.array,
|
|
onSave: PropTypes.func.isRequired,
|
|
onClose: PropTypes.func.isRequired,
|
|
};
|
|
|
|
// ── ConnectionsCard ────────────────────────────────────────────
|
|
// Self-contained card: fetches, displays and manages all connections for a provider.
|
|
export default function ConnectionsCard({ providerId, isOAuth }) {
|
|
const [connections, setConnections] = useState([]);
|
|
const [proxyPools, setProxyPools] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [selectedConnection, setSelectedConnection] = useState(null);
|
|
const [providerStrategy, setProviderStrategy] = useState(null);
|
|
const [providerStickyLimit, setProviderStickyLimit] = useState("1");
|
|
|
|
const fetch_ = useCallback(async () => {
|
|
try {
|
|
const [connRes, proxyRes, settingsRes] = await Promise.all([
|
|
fetch("/api/providers", { cache: "no-store" }),
|
|
fetch("/api/proxy-pools?isActive=true", { cache: "no-store" }),
|
|
fetch("/api/settings", { cache: "no-store" }),
|
|
]);
|
|
const connData = await connRes.json();
|
|
const proxyData = await proxyRes.json();
|
|
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
|
if (connRes.ok) setConnections((connData.connections || []).filter((c) => c.provider === providerId));
|
|
if (proxyRes.ok) setProxyPools(proxyData.proxyPools || []);
|
|
const override = (settingsData.providerStrategies || {})[providerId] || {};
|
|
setProviderStrategy(override.fallbackStrategy || null);
|
|
setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1");
|
|
} catch (e) { console.log("ConnectionsCard fetch error:", e); }
|
|
finally { setLoading(false); }
|
|
}, [providerId]);
|
|
|
|
useEffect(() => { fetch_(); }, [fetch_]);
|
|
|
|
const saveStrategy = async (strategy, stickyLimit) => {
|
|
try {
|
|
const res = await fetch("/api/settings", { cache: "no-store" });
|
|
const data = res.ok ? await res.json() : {};
|
|
const current = data.providerStrategies || {};
|
|
const override = {};
|
|
if (strategy) override.fallbackStrategy = strategy;
|
|
if (strategy === "round-robin" && stickyLimit !== "") override.stickyRoundRobinLimit = Number(stickyLimit) || 3;
|
|
const updated = { ...current };
|
|
if (Object.keys(override).length === 0) delete updated[providerId];
|
|
else updated[providerId] = override;
|
|
await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerStrategies: updated }) });
|
|
} catch (e) { console.log("saveStrategy error:", e); }
|
|
};
|
|
|
|
const handleSwapPriority = async (i1, i2) => {
|
|
const next = [...connections];
|
|
[next[i1], next[i2]] = [next[i2], next[i1]];
|
|
setConnections(next);
|
|
try {
|
|
await Promise.all([
|
|
fetch(`/api/providers/${next[i1].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priority: i1 }) }),
|
|
fetch(`/api/providers/${next[i2].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priority: i2 }) }),
|
|
]);
|
|
} catch { await fetch_(); }
|
|
};
|
|
|
|
const handleDelete = async (id) => {
|
|
if (!confirm("Delete this connection?")) return;
|
|
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) => {
|
|
try {
|
|
const res = await fetch(`/api/providers/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive }) });
|
|
if (res.ok) setConnections((prev) => prev.map((c) => c.id === id ? { ...c, isActive } : c));
|
|
} catch (e) { console.log("toggle error:", e); }
|
|
};
|
|
|
|
const handleUpdateProxy = async (connId, proxyPoolId) => {
|
|
try {
|
|
const res = await fetch(`/api/providers/${connId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxyPoolId: proxyPoolId || null }) });
|
|
if (res.ok) setConnections((prev) => prev.map((c) => c.id === connId ? { ...c, providerSpecificData: { ...c.providerSpecificData, proxyPoolId: proxyPoolId || null } } : c));
|
|
} catch (e) { console.log("proxy error:", e); }
|
|
};
|
|
|
|
const handleSaveApiKey = async (formData) => {
|
|
try {
|
|
const res = await fetch("/api/providers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: providerId, ...formData }) });
|
|
if (res.ok) { await fetch_(); setShowAddModal(false); }
|
|
} catch (e) { console.log("save apikey error:", e); }
|
|
};
|
|
|
|
const handleUpdateConnection = async (formData) => {
|
|
try {
|
|
const res = await fetch(`/api/providers/${selectedConnection.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData) });
|
|
if (res.ok) { await fetch_(); setShowEditModal(false); }
|
|
} catch (e) { console.log("update connection error:", e); }
|
|
};
|
|
|
|
if (loading) return <Card><div className="h-20 animate-pulse bg-black/5 rounded-lg" /></Card>;
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Connections</h2>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
|
<Toggle
|
|
checked={providerStrategy === "round-robin"}
|
|
onChange={(enabled) => {
|
|
const strategy = enabled ? "round-robin" : null;
|
|
setProviderStrategy(strategy);
|
|
if (enabled && !providerStickyLimit) setProviderStickyLimit("1");
|
|
saveStrategy(strategy, enabled ? (providerStickyLimit || "1") : providerStickyLimit);
|
|
}}
|
|
/>
|
|
{providerStrategy === "round-robin" && (
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<span className="text-xs text-text-muted">Sticky:</span>
|
|
<input
|
|
type="number" min={1} value={providerStickyLimit}
|
|
onChange={(e) => { setProviderStickyLimit(e.target.value); saveStrategy("round-robin", e.target.value); }}
|
|
className="w-16 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{connections.length === 0 ? (
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="text-sm text-text-muted">No connections yet</p>
|
|
<Button size="sm" icon="add" onClick={() => setShowAddModal(true)}>Add Connection</Button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
|
{connections.map((conn, idx) => (
|
|
<ConnectionRow
|
|
key={conn.id}
|
|
connection={conn}
|
|
proxyPools={proxyPools}
|
|
isOAuth={isOAuth}
|
|
isFirst={idx === 0}
|
|
isLast={idx === connections.length - 1}
|
|
onMoveUp={() => handleSwapPriority(idx, idx - 1)}
|
|
onMoveDown={() => handleSwapPriority(idx, idx + 1)}
|
|
onToggleActive={(isActive) => handleToggleActive(conn.id, isActive)}
|
|
onUpdateProxy={(poolId) => handleUpdateProxy(conn.id, poolId)}
|
|
onEdit={() => { setSelectedConnection(conn); setShowEditModal(true); }}
|
|
onDelete={() => handleDelete(conn.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="mt-4 flex justify-stretch sm:justify-start">
|
|
<Button size="sm" icon="add" onClick={() => setShowAddModal(true)}>Add</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</Card>
|
|
|
|
<AddApiKeyModal
|
|
isOpen={showAddModal}
|
|
provider={providerId}
|
|
proxyPools={proxyPools}
|
|
onSave={handleSaveApiKey}
|
|
onClose={() => setShowAddModal(false)}
|
|
/>
|
|
<EditConnectionModal
|
|
isOpen={showEditModal}
|
|
connection={selectedConnection}
|
|
proxyPools={proxyPools}
|
|
onSave={handleUpdateConnection}
|
|
onClose={() => setShowEditModal(false)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
ConnectionsCard.propTypes = {
|
|
providerId: PropTypes.string.isRequired,
|
|
isOAuth: PropTypes.bool,
|
|
};
|