- Add new "Quota Tracker" item to the sidebar navigation.

This commit is contained in:
decolua 2026-03-03 09:53:30 +07:00
parent 4e92a66379
commit bfd9614fa2
7 changed files with 127 additions and 68 deletions

View file

@ -0,0 +1,11 @@
import { Suspense } from "react";
import { CardSkeleton } from "@/shared/components/Loading";
import ProviderLimits from "../usage/components/ProviderLimits";
export default function QuotaPage() {
return (
<Suspense fallback={<CardSkeleton />}>
<ProviderLimits />
</Suspense>
);
}

View file

@ -7,7 +7,6 @@ import QuotaTable from "./QuotaTable";
import { parseQuotaData, calculatePercentage } from "./utils";
import Card from "@/shared/components/Card";
import Button from "@/shared/components/Button";
import { CardSkeleton } from "@/shared/components/Loading";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
const REFRESH_INTERVAL_MS = 60000; // 60 seconds
@ -21,7 +20,7 @@ export default function ProviderLimits() {
const [lastUpdated, setLastUpdated] = useState(null);
const [refreshingAll, setRefreshingAll] = useState(false);
const [countdown, setCountdown] = useState(60);
const [initialLoading, setInitialLoading] = useState(true);
const [connectionsLoading, setConnectionsLoading] = useState(true);
const intervalRef = useRef(null);
const countdownRef = useRef(null);
@ -142,12 +141,26 @@ export default function ProviderLimits() {
}
}, [refreshingAll, fetchConnections, fetchQuota]);
// Initial load
// Initial load: fetch connections first so cards render immediately, then fetch quotas
useEffect(() => {
const initializeData = async () => {
setInitialLoading(true);
await refreshAll();
setInitialLoading(false);
setConnectionsLoading(true);
const conns = await fetchConnections();
setConnectionsLoading(false);
const oauthConnections = conns.filter(
(conn) => USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth"
);
// Mark all as loading before fetching
const loadingState = {};
oauthConnections.forEach((conn) => { loadingState[conn.id] = true; });
setLoading(loadingState);
await Promise.all(
oauthConnections.map((conn) => fetchQuota(conn.id, conn.provider))
);
setLastUpdated(new Date());
};
initializeData();
@ -271,22 +284,8 @@ export default function ProviderLimits() {
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 (sortedConnections.length === 0) {
if (!connectionsLoading && sortedConnections.length === 0) {
return (
<Card padding="lg">
<div className="text-center py-12">

View file

@ -3,7 +3,6 @@
import { Suspense, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components";
import ProviderLimits from "./components/ProviderLimits";
import RequestDetailsTab from "./components/RequestDetailsTab";
export default function UsagePage() {
@ -21,7 +20,7 @@ function UsageContent() {
const [tabLoading, setTabLoading] = useState(false);
const tabFromUrl = searchParams.get("tab");
const activeTab = tabFromUrl && ["overview", "logs", "limits", "details"].includes(tabFromUrl)
const activeTab = tabFromUrl && ["overview", "logs", "details"].includes(tabFromUrl)
? tabFromUrl
: "overview";
@ -40,7 +39,6 @@ function UsageContent() {
<SegmentedControl
options={[
{ value: "overview", label: "Overview" },
{ value: "limits", label: "Limits" },
{ value: "details", label: "Details" },
]}
value={activeTab}
@ -57,11 +55,6 @@ function UsageContent() {
</Suspense>
)}
{activeTab === "logs" && <RequestLogger />}
{activeTab === "limits" && (
<Suspense fallback={<CardSkeleton />}>
<ProviderLimits />
</Suspense>
)}
{activeTab === "details" && <RequestDetailsTab />}
</>
)}