mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
- Introduce default MITM router base URL and update related components to handle it.
- Add input for MITM router base URL in MitmServerCard component.
This commit is contained in:
@@ -2,11 +2,12 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
import ProviderLimitCard from "./ProviderLimitCard";
|
||||
import QuotaTable from "./QuotaTable";
|
||||
import Toggle from "@/shared/components/Toggle";
|
||||
import { parseQuotaData, calculatePercentage } from "./utils";
|
||||
import Card from "@/shared/components/Card";
|
||||
import Button from "@/shared/components/Button";
|
||||
import { EditConnectionModal } from "@/shared/components";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||
|
||||
const REFRESH_INTERVAL_MS = 60000; // 60 seconds
|
||||
@@ -21,6 +22,11 @@ export default function ProviderLimits() {
|
||||
const [refreshingAll, setRefreshingAll] = useState(false);
|
||||
const [countdown, setCountdown] = useState(60);
|
||||
const [connectionsLoading, setConnectionsLoading] = useState(true);
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
const [togglingId, setTogglingId] = useState(null);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [selectedConnection, setSelectedConnection] = useState(null);
|
||||
const [proxyPools, setProxyPools] = useState([]);
|
||||
|
||||
const intervalRef = useRef(null);
|
||||
const countdownRef = useRef(null);
|
||||
@@ -123,6 +129,97 @@ export default function ProviderLimits() {
|
||||
[fetchQuota],
|
||||
);
|
||||
|
||||
const handleDeleteConnection = useCallback(async (id) => {
|
||||
if (!confirm("Delete this connection?")) return;
|
||||
setDeletingId(id);
|
||||
try {
|
||||
const res = await fetch(`/api/providers/${id}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
setConnections((prev) => prev.filter((c) => c.id !== id));
|
||||
setQuotaData((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
setLoading((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting connection:", error);
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleConnectionActive = useCallback(async (id, isActive) => {
|
||||
setTogglingId(id);
|
||||
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 (error) {
|
||||
console.error("Error updating connection status:", error);
|
||||
} finally {
|
||||
setTogglingId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleUpdateConnection = useCallback(
|
||||
async (formData) => {
|
||||
if (!selectedConnection?.id) return;
|
||||
const connectionId = selectedConnection.id;
|
||||
const provider = selectedConnection.provider;
|
||||
try {
|
||||
const res = await fetch(`/api/providers/${connectionId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
if (res.ok) {
|
||||
await fetchConnections();
|
||||
setShowEditModal(false);
|
||||
setSelectedConnection(null);
|
||||
if (USAGE_SUPPORTED_PROVIDERS.includes(provider)) {
|
||||
await fetchQuota(connectionId, provider);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving connection:", error);
|
||||
}
|
||||
},
|
||||
[selectedConnection, fetchConnections, fetchQuota],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch("/api/proxy-pools?isActive=true", { cache: "no-store" })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (!cancelled && data?.proxyPools) {
|
||||
setProxyPools(data.proxyPools);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Refresh all providers
|
||||
const refreshAll = useCallback(async () => {
|
||||
if (refreshingAll) return;
|
||||
@@ -358,81 +455,148 @@ export default function ProviderLimits() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Provider Cards Grid */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Provider cards: 2 columns, compact */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{sortedConnections.map((conn) => {
|
||||
const quota = quotaData[conn.id];
|
||||
const isLoading = loading[conn.id];
|
||||
const error = errors[conn.id];
|
||||
|
||||
// Use table layout for all providers
|
||||
const isInactive = conn.isActive === false;
|
||||
const rowBusy = deletingId === conn.id || togglingId === conn.id;
|
||||
|
||||
return (
|
||||
<Card key={conn.id} padding="none">
|
||||
<div className="p-6 border-b border-black/10 dark:border-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg flex items-center justify-center overflow-hidden">
|
||||
<Card
|
||||
key={conn.id}
|
||||
padding="none"
|
||||
className={`min-w-0 ${isInactive ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="px-4 py-3 border-b border-black/10 dark:border-white/10">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="w-8 h-8 shrink-0 rounded-md flex items-center justify-center overflow-hidden">
|
||||
<ProviderIcon
|
||||
src={`/providers/${conn.provider}.png`}
|
||||
alt={conn.provider}
|
||||
size={40}
|
||||
size={32}
|
||||
className="object-contain"
|
||||
fallbackText={
|
||||
conn.provider?.slice(0, 2).toUpperCase() || "PR"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-text-primary capitalize">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-text-primary capitalize truncate">
|
||||
{conn.provider}
|
||||
</h3>
|
||||
{conn.name && (
|
||||
<p className="text-sm text-text-muted">{conn.name}</p>
|
||||
<p className="text-xs text-text-muted truncate">
|
||||
{conn.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => refreshProvider(conn.id, conn.provider)}
|
||||
disabled={isLoading}
|
||||
className="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
|
||||
title="Refresh quota"
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[20px] text-text-muted ${isLoading ? "animate-spin" : ""}`}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refreshProvider(conn.id, conn.provider)}
|
||||
disabled={isLoading || rowBusy}
|
||||
className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
|
||||
title="Refresh quota"
|
||||
>
|
||||
refresh
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[18px] text-text-muted ${isLoading ? "animate-spin" : ""}`}
|
||||
>
|
||||
refresh
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedConnection(conn);
|
||||
setShowEditModal(true);
|
||||
}}
|
||||
disabled={rowBusy}
|
||||
className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors disabled:opacity-50"
|
||||
title="Edit connection"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">
|
||||
edit
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteConnection(conn.id)}
|
||||
disabled={rowBusy}
|
||||
className="p-1.5 rounded-lg hover:bg-red-500/10 text-red-500 transition-colors disabled:opacity-50"
|
||||
title="Delete connection"
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[18px] ${deletingId === conn.id ? "animate-pulse" : ""}`}
|
||||
>
|
||||
delete
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
className="inline-flex items-center pl-0.5"
|
||||
title={
|
||||
(conn.isActive ?? true)
|
||||
? "Disable connection"
|
||||
: "Enable connection"
|
||||
}
|
||||
>
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={conn.isActive ?? true}
|
||||
disabled={rowBusy}
|
||||
onChange={(nextActive) =>
|
||||
handleToggleConnectionActive(conn.id, nextActive)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="px-3 py-3">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-text-muted">
|
||||
<span className="material-symbols-outlined text-[32px] animate-spin">
|
||||
<div className="text-center py-5 text-text-muted">
|
||||
<span className="material-symbols-outlined text-[28px] animate-spin">
|
||||
progress_activity
|
||||
</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-8">
|
||||
<span className="material-symbols-outlined text-[32px] text-red-500">
|
||||
<div className="text-center py-5">
|
||||
<span className="material-symbols-outlined text-[28px] text-red-500">
|
||||
error
|
||||
</span>
|
||||
<p className="mt-2 text-sm text-text-muted">{error}</p>
|
||||
<p className="mt-1.5 text-xs text-text-muted">{error}</p>
|
||||
</div>
|
||||
) : quota?.message ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-sm text-text-muted">{quota.message}</p>
|
||||
<div className="text-center py-5">
|
||||
<p className="text-xs text-text-muted">{quota.message}</p>
|
||||
</div>
|
||||
) : (
|
||||
<QuotaTable quotas={quota?.quotas} />
|
||||
<QuotaTable quotas={quota?.quotas} compact />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<EditConnectionModal
|
||||
isOpen={showEditModal}
|
||||
connection={selectedConnection}
|
||||
proxyPools={proxyPools}
|
||||
onSave={handleUpdateConnection}
|
||||
onClose={() => {
|
||||
setShowEditModal(false);
|
||||
setSelectedConnection(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user