mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Fix : usage convert
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import ProviderLimitCard from "./ProviderLimitCard";
|
||||
import { parseQuotaData, calculatePercentage } from "./utils";
|
||||
import Card from "@/shared/components/Card";
|
||||
import Button from "@/shared/components/Button";
|
||||
import { CardSkeleton } from "@/shared/components/Loading";
|
||||
|
||||
const REFRESH_INTERVAL_MS = 60000; // 60 seconds
|
||||
|
||||
export default function ProviderLimits() {
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [quotaData, setQuotaData] = useState({});
|
||||
const [loading, setLoading] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState(null);
|
||||
const [refreshingAll, setRefreshingAll] = useState(false);
|
||||
const [countdown, setCountdown] = useState(60);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
|
||||
const intervalRef = useRef(null);
|
||||
const countdownRef = useRef(null);
|
||||
|
||||
// Fetch all provider connections
|
||||
const fetchConnections = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/providers/client");
|
||||
if (!response.ok) throw new Error("Failed to fetch connections");
|
||||
|
||||
const data = await response.json();
|
||||
const connectionList = data.connections || [];
|
||||
setConnections(connectionList);
|
||||
return connectionList;
|
||||
} catch (error) {
|
||||
console.error("Error fetching connections:", error);
|
||||
setConnections([]);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch quota for a specific connection
|
||||
const fetchQuota = useCallback(async (connectionId, provider) => {
|
||||
setLoading((prev) => ({ ...prev, [connectionId]: true }));
|
||||
setErrors((prev) => ({ ...prev, [connectionId]: null }));
|
||||
|
||||
try {
|
||||
console.log(`[ProviderLimits] Fetching quota for ${provider} (${connectionId})`);
|
||||
const response = await fetch(`/api/usage/${connectionId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`[ProviderLimits] Got quota for ${provider}:`, data);
|
||||
|
||||
// Parse quota data using provider-specific parser
|
||||
const parsedQuotas = parseQuotaData(provider, data);
|
||||
|
||||
setQuotaData((prev) => ({
|
||||
...prev,
|
||||
[connectionId]: {
|
||||
quotas: parsedQuotas,
|
||||
plan: data.plan || null,
|
||||
message: data.message || null,
|
||||
raw: data,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`[ProviderLimits] Error fetching quota for ${provider} (${connectionId}):`, error);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[connectionId]: error.message || "Failed to fetch quota",
|
||||
}));
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, [connectionId]: false }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Refresh quota for a specific provider
|
||||
const refreshProvider = useCallback(
|
||||
async (connectionId, provider) => {
|
||||
await fetchQuota(connectionId, provider);
|
||||
setLastUpdated(new Date());
|
||||
},
|
||||
[fetchQuota]
|
||||
);
|
||||
|
||||
// Refresh all providers
|
||||
const refreshAll = useCallback(async () => {
|
||||
if (refreshingAll) return;
|
||||
|
||||
setRefreshingAll(true);
|
||||
setCountdown(60);
|
||||
|
||||
try {
|
||||
const conns = await fetchConnections();
|
||||
|
||||
// Fetch quota for all connections (filter by provider support in parseQuotaData)
|
||||
await Promise.all(
|
||||
conns.map((conn) => fetchQuota(conn.id, conn.provider))
|
||||
);
|
||||
|
||||
setLastUpdated(new Date());
|
||||
} catch (error) {
|
||||
console.error("Error refreshing all providers:", error);
|
||||
} finally {
|
||||
setRefreshingAll(false);
|
||||
}
|
||||
}, [refreshingAll, fetchConnections, fetchQuota]);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
const initializeData = async () => {
|
||||
setInitialLoading(true);
|
||||
await refreshAll();
|
||||
setInitialLoading(false);
|
||||
};
|
||||
|
||||
initializeData();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Auto-refresh interval
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
if (countdownRef.current) {
|
||||
clearInterval(countdownRef.current);
|
||||
countdownRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Main refresh interval
|
||||
intervalRef.current = setInterval(() => {
|
||||
refreshAll();
|
||||
}, REFRESH_INTERVAL_MS);
|
||||
|
||||
// Countdown interval
|
||||
countdownRef.current = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) return 60;
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
if (countdownRef.current) clearInterval(countdownRef.current);
|
||||
};
|
||||
}, [autoRefresh, refreshAll]);
|
||||
|
||||
// Pause auto-refresh when tab is hidden (Page Visibility API)
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
if (countdownRef.current) {
|
||||
clearInterval(countdownRef.current);
|
||||
countdownRef.current = null;
|
||||
}
|
||||
} else if (autoRefresh) {
|
||||
// Resume auto-refresh when tab becomes visible
|
||||
intervalRef.current = setInterval(refreshAll, REFRESH_INTERVAL_MS);
|
||||
countdownRef.current = setInterval(() => {
|
||||
setCountdown((prev) => (prev <= 1 ? 60 : prev - 1));
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [autoRefresh, refreshAll]);
|
||||
|
||||
// Format last updated time
|
||||
const formatLastUpdated = useCallback(() => {
|
||||
if (!lastUpdated) return "Never";
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = now - lastUpdated;
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 0) return `${diffDays}d ago`;
|
||||
if (diffHours > 0) return `${diffHours}h ago`;
|
||||
if (diffMinutes > 0) return `${diffMinutes}m ago`;
|
||||
return "Just now";
|
||||
}, [lastUpdated]);
|
||||
|
||||
// Filter only supported providers
|
||||
const supportedProviders = ["antigravity", "kiro", "github", "claude"];
|
||||
const filteredConnections = connections.filter((conn) =>
|
||||
supportedProviders.includes(conn.provider)
|
||||
);
|
||||
|
||||
// Calculate summary stats
|
||||
const totalProviders = filteredConnections.length;
|
||||
const activeWithLimits = Object.values(quotaData).filter(
|
||||
(data) => data?.quotas?.length > 0
|
||||
).length;
|
||||
|
||||
// Count low quotas (remaining < 30%)
|
||||
const lowQuotasCount = Object.values(quotaData).reduce((count, data) => {
|
||||
if (!data?.quotas) return count;
|
||||
|
||||
const hasLowQuota = data.quotas.some((quota) => {
|
||||
const percentage = calculatePercentage(quota.used, quota.total);
|
||||
return percentage < 30 && quota.total > 0;
|
||||
});
|
||||
|
||||
return count + (hasLowQuota ? 1 : 0);
|
||||
}, 0);
|
||||
|
||||
// Initial loading state
|
||||
if (initialLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<CardSkeleton />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (filteredConnections.length === 0) {
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<div className="text-center py-12">
|
||||
<span className="material-symbols-outlined text-[64px] text-text-muted opacity-20">
|
||||
cloud_off
|
||||
</span>
|
||||
<h3 className="mt-4 text-lg font-semibold text-text-primary">
|
||||
No Providers Connected
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-text-muted max-w-md mx-auto">
|
||||
Connect to providers with OAuth to track your API quota limits and usage.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Controls */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
Provider Limits
|
||||
</h2>
|
||||
<span className="text-sm text-text-muted">
|
||||
Last updated: {formatLastUpdated()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Auto-refresh toggle */}
|
||||
<button
|
||||
onClick={() => setAutoRefresh((prev) => !prev)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
title={autoRefresh ? "Disable auto-refresh" : "Enable auto-refresh"}
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[18px] ${
|
||||
autoRefresh ? "text-primary" : "text-text-muted"
|
||||
}`}
|
||||
>
|
||||
{autoRefresh ? "toggle_on" : "toggle_off"}
|
||||
</span>
|
||||
<span className="text-sm text-text-primary">Auto-refresh</span>
|
||||
{autoRefresh && (
|
||||
<span className="text-xs text-text-muted">({countdown}s)</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Refresh all button */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
icon="refresh"
|
||||
onClick={refreshAll}
|
||||
disabled={refreshingAll}
|
||||
loading={refreshingAll}
|
||||
>
|
||||
Refresh All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Provider Cards Grid */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{filteredConnections.map((conn) => {
|
||||
const quota = quotaData[conn.id];
|
||||
const isLoading = loading[conn.id];
|
||||
const error = errors[conn.id];
|
||||
|
||||
return (
|
||||
<ProviderLimitCard
|
||||
key={conn.id}
|
||||
provider={conn.provider}
|
||||
name={conn.name}
|
||||
plan={quota?.plan}
|
||||
quotas={quota?.quotas}
|
||||
message={quota?.message}
|
||||
loading={isLoading}
|
||||
error={error}
|
||||
onRefresh={() => refreshProvider(conn.id, conn.provider)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user