From 4dadab9d5fdde8e670ab8571d5e8239ce7b3697b Mon Sep 17 00:00:00 2001
From: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
Date: Sat, 4 Jul 2026 23:26:49 +0700
Subject: [PATCH] feat: add provider quota visibility settings
---
.../components/ProviderLimits/QuotaTable.js | 20 +++-
.../usage/components/ProviderLimits/index.js | 91 ++++++++++++++++++-
.../usage/components/ProviderLimits/utils.js | 24 +++++
src/lib/db/repos/settingsRepo.js | 1 +
tests/unit/provider-quota-visibility.test.js | 55 +++++++++++
5 files changed, 185 insertions(+), 6 deletions(-)
create mode 100644 tests/unit/provider-quota-visibility.test.js
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js
index 8f2a1bc3..299445ef 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js
@@ -89,6 +89,7 @@ export default function QuotaTable({
compact = false,
sortMode = "default",
showSortLabel = false,
+ onHideQuota = null,
}) {
const [page, setPage] = useState(1);
@@ -132,6 +133,7 @@ export default function QuotaTable({
const resetPrimary = compact ? "text-[11px]" : "text-sm";
const resetSecondary = compact ? "text-[10px] leading-tight" : "text-xs";
const sortLabel = "Sorted by account remaining";
+ const hasHideAction = typeof onHideQuota === "function";
return (
@@ -195,7 +197,7 @@ export default function QuotaTable({
-
+ |
{countdown !== "-" || resetDisplay ? (
compact ? (
N/A
)}
|
+
+ {hasHideAction && (
+
+
+ |
+ )}
);
})}
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
index 95300c92..babfa6e2 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
@@ -8,6 +8,9 @@ import Tooltip from "@/shared/components/Tooltip";
import {
parseQuotaData,
calculatePercentage,
+ filterQuotasByVisibility,
+ getHiddenQuotaRows,
+ getQuotaVisibilityKey,
getConnectionLabel,
getConnectionQuotaRemaining,
sortVisibleConnections,
@@ -146,6 +149,7 @@ export default function ProviderLimits() {
const [providerOptions, setProviderOptions] = useState([]);
const [accountFilter, setAccountFilter] = useState("all");
const [quotaSortMode, setQuotaSortMode] = useState("default");
+ const [quotaVisibility, setQuotaVisibility] = useState({});
const [expiringFirst, setExpiringFirst] = useState(false);
const [providerMenuOpen, setProviderMenuOpen] = useState(false);
const [bulkToggling, setBulkToggling] = useState(false);
@@ -536,10 +540,13 @@ export default function ProviderLimits() {
useEffect(() => {
fetch("/api/settings", { cache: "no-store" })
.then((r) => (r.ok ? r.json() : {}))
- .then((s) => setAutoPingMaps({
- claude: s?.claudeAutoPing?.connections || {},
- codex: s?.codexAutoPing?.connections || {},
- }))
+ .then((s) => {
+ setAutoPingMaps({
+ claude: s?.claudeAutoPing?.connections || {},
+ codex: s?.codexAutoPing?.connections || {},
+ });
+ setQuotaVisibility(s?.quotaVisibility || {});
+ })
.catch(() => {});
}, []);
@@ -565,6 +572,57 @@ export default function ProviderLimits() {
}
}, [autoPingMaps]);
+ const updateQuotaVisibility = useCallback(async (nextVisibility, previousVisibility) => {
+ setQuotaVisibility(nextVisibility);
+ try {
+ const response = await fetch("/api/settings", {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ quotaVisibility: nextVisibility }),
+ });
+ if (!response.ok) throw new Error("Failed to update quota visibility");
+ } catch (error) {
+ console.error("Error updating quota visibility:", error);
+ setQuotaVisibility(previousVisibility);
+ }
+ }, []);
+
+ const handleHideQuota = useCallback((provider, quota) => {
+ const key = getQuotaVisibilityKey(quota);
+ if (!provider || !key) return;
+
+ const previous = quotaVisibility;
+ const providerVisibility = previous[provider] || {};
+ const hidden = new Set(providerVisibility.hidden || []);
+ hidden.add(key);
+ const next = {
+ ...previous,
+ [provider]: {
+ ...providerVisibility,
+ hidden: [...hidden],
+ },
+ };
+ updateQuotaVisibility(next, previous);
+ }, [quotaVisibility, updateQuotaVisibility]);
+
+ const handleShowQuota = useCallback((provider, quota) => {
+ const key = getQuotaVisibilityKey(quota);
+ if (!provider || !key) return;
+
+ const previous = quotaVisibility;
+ const providerVisibility = previous[provider] || {};
+ const hidden = new Set(providerVisibility.hidden || []);
+ hidden.delete(key);
+ const next = {
+ ...previous,
+ [provider]: {
+ ...providerVisibility,
+ hidden: [...hidden],
+ },
+ };
+ updateQuotaVisibility(next, previous);
+ }, [quotaVisibility, updateQuotaVisibility]);
+
// Auto-refresh interval
useEffect(() => {
if (!hasHydratedAutoRefresh || !autoRefresh) {
@@ -973,6 +1031,9 @@ export default function ProviderLimits() {
const resetCreditCount = getCodexResetCreditCount(quota);
const isResettingLimit = resettingLimitId === conn.id;
const rowBusy = deletingId === conn.id || togglingId === conn.id || isResettingLimit;
+ const rawQuotas = quota?.quotas || [];
+ const visibleQuotas = filterQuotasByVisibility(conn.provider, rawQuotas, quotaVisibility);
+ const hiddenQuotaRows = getHiddenQuotaRows(conn.provider, rawQuotas, quotaVisibility);
return (
) : (
handleHideQuota(conn.provider, quotaRow)}
/>
)}
+ {hiddenQuotaRows.length > 0 && (
+
+
+ visibility_off
+
+ Hidden:
+ {hiddenQuotaRows.map((quotaRow) => (
+
+ ))}
+
+ )}
);
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js
index 688f0ab7..19fdaa8e 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js
@@ -300,6 +300,30 @@ export function getRemainingPercentage(quota) {
return calculatePercentage(quota?.used, quota?.total);
}
+export function getQuotaVisibilityKey(quota) {
+ if (!quota || typeof quota !== "object") return "";
+ return String(quota.modelKey || quota.name || "").trim();
+}
+
+function getProviderHiddenQuotaSet(provider, quotaVisibility) {
+ const hidden = quotaVisibility?.[provider]?.hidden;
+ return new Set(Array.isArray(hidden) ? hidden.map(String) : []);
+}
+
+export function filterQuotasByVisibility(provider, quotas = [], quotaVisibility = {}) {
+ if (!Array.isArray(quotas) || quotas.length === 0) return [];
+ const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility);
+ if (hidden.size === 0) return quotas;
+ return quotas.filter((quota) => !hidden.has(getQuotaVisibilityKey(quota)));
+}
+
+export function getHiddenQuotaRows(provider, quotas = [], quotaVisibility = {}) {
+ if (!Array.isArray(quotas) || quotas.length === 0) return [];
+ const hidden = getProviderHiddenQuotaSet(provider, quotaVisibility);
+ if (hidden.size === 0) return [];
+ return quotas.filter((quota) => hidden.has(getQuotaVisibilityKey(quota)));
+}
+
/**
* Parse provider-specific quota structures into normalized array
* @param {string} provider - Provider name (github, antigravity, codex, kiro, claude)
diff --git a/src/lib/db/repos/settingsRepo.js b/src/lib/db/repos/settingsRepo.js
index 0057cc1c..60a5054b 100644
--- a/src/lib/db/repos/settingsRepo.js
+++ b/src/lib/db/repos/settingsRepo.js
@@ -13,6 +13,7 @@ const DEFAULT_SETTINGS = {
tailscaleUrl: "",
stickyRoundRobinLimit: 3,
providerStrategies: {},
+ quotaVisibility: {},
comboStrategy: "fallback",
comboStickyRoundRobinLimit: 1,
comboStrategies: {},
diff --git a/tests/unit/provider-quota-visibility.test.js b/tests/unit/provider-quota-visibility.test.js
new file mode 100644
index 00000000..d6616b81
--- /dev/null
+++ b/tests/unit/provider-quota-visibility.test.js
@@ -0,0 +1,55 @@
+import { describe, expect, it } from "vitest";
+import {
+ filterQuotasByVisibility,
+ getHiddenQuotaRows,
+ parseQuotaData,
+} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
+
+describe("provider quota visibility", () => {
+ const data = {
+ quotas: {
+ "gemini-pro-agent": {
+ displayName: "Gemini 3.1 Pro (High)",
+ used: 200,
+ total: 1000,
+ resetAt: "2026-07-04T00:00:00Z",
+ },
+ "claude-opus-4-6-thinking": {
+ displayName: "Claude Opus 4.6 (Thinking)",
+ used: 100,
+ total: 1000,
+ resetAt: "2026-07-04T00:00:00Z",
+ },
+ },
+ };
+
+ it("keeps Antigravity modelKey so hidden settings use stable quota ids", () => {
+ const quotas = parseQuotaData("antigravity", data);
+ expect(quotas.map((q) => q.modelKey)).toEqual([
+ "gemini-pro-agent",
+ "claude-opus-4-6-thinking",
+ ]);
+ });
+
+ it("shows all quotas by default and hides configured provider rows", () => {
+ const quotas = parseQuotaData("antigravity", data);
+ expect(filterQuotasByVisibility("antigravity", quotas, {})).toHaveLength(2);
+
+ const visibility = {
+ antigravity: { hidden: ["claude-opus-4-6-thinking"] },
+ };
+ const visible = filterQuotasByVisibility("antigravity", quotas, visibility);
+ const hidden = getHiddenQuotaRows("antigravity", quotas, visibility);
+
+ expect(visible.map((q) => q.modelKey)).toEqual(["gemini-pro-agent"]);
+ expect(hidden.map((q) => q.modelKey)).toEqual(["claude-opus-4-6-thinking"]);
+ });
+
+ it("does not apply one provider hidden list to another provider", () => {
+ const quotas = parseQuotaData("antigravity", data);
+ const visibility = {
+ codex: { hidden: ["gemini-pro-agent"] },
+ };
+ expect(filterQuotasByVisibility("antigravity", quotas, visibility)).toHaveLength(2);
+ });
+});