From bfd9614fa2d8816071a834fe7eae2b3ea8e1a27d Mon Sep 17 00:00:00 2001 From: decolua Date: Tue, 3 Mar 2026 09:53:30 +0700 Subject: [PATCH] - Add new "Quota Tracker" item to the sidebar navigation. --- package.json | 2 +- src/app/(dashboard)/dashboard/quota/page.js | 11 +++ .../usage/components/ProviderLimits/index.js | 41 ++++---- src/app/(dashboard)/dashboard/usage/page.js | 9 +- src/shared/components/Sidebar.js | 1 + src/shared/components/UsageStats.js | 36 ++++--- src/shared/constants/cliTools.js | 95 ++++++++++++++----- 7 files changed, 127 insertions(+), 68 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/quota/page.js diff --git a/package.json b/package.json index 54f8835..1ba5cee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.24", + "version": "0.3.27", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/quota/page.js b/src/app/(dashboard)/dashboard/quota/page.js new file mode 100644 index 0000000..ee5e7e7 --- /dev/null +++ b/src/app/(dashboard)/dashboard/quota/page.js @@ -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 ( + }> + + + ); +} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js index d90261d..c9b5d12 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js @@ -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 ( -
- -
- - - -
-
- ); - } - // Empty state - if (sortedConnections.length === 0) { + if (!connectionsLoading && sortedConnections.length === 0) { return (
diff --git a/src/app/(dashboard)/dashboard/usage/page.js b/src/app/(dashboard)/dashboard/usage/page.js index e150571..3ee827f 100644 --- a/src/app/(dashboard)/dashboard/usage/page.js +++ b/src/app/(dashboard)/dashboard/usage/page.js @@ -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() { )} {activeTab === "logs" && } - {activeTab === "limits" && ( - }> - - - )} {activeTab === "details" && } )} diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index 7e53315..ce68d9b 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -14,6 +14,7 @@ const navItems = [ { href: "/dashboard/providers", label: "Providers", icon: "dns" }, { href: "/dashboard/combos", label: "Combos", icon: "layers" }, { href: "/dashboard/usage", label: "Usage", icon: "bar_chart" }, + { href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" }, { href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" }, ]; diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index 89f918e..f08a1cf 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -2,7 +2,6 @@ import { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams, useRouter } from "next/navigation"; -import { CardSkeleton } from "./Loading"; import Badge from "./Badge"; import Card from "./Card"; import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards"; @@ -348,27 +347,34 @@ export default function UsageStats() { } }, [stats, tableView, sortBy, sortOrder]); - if (loading) return ; - if (!stats) return
Failed to load usage statistics.
; + if (!stats && !loading) return
Failed to load usage statistics.
; + + const spinner = ( +
+ progress_activity +
+ ); return (
{/* Overview cards */} - + {loading ? spinner : } {/* Provider topology + Recent Requests */} -
- - -
+ {loading ? spinner : ( +
+ + +
+ )} {/* Token / Cost chart */} - + {loading ? spinner : } {/* Table with dropdown selector */}
@@ -383,7 +389,7 @@ export default function UsageStats() { ))}
- {activeTableConfig && ( + {loading ? spinner : activeTableConfig && (