From bf943e0a7cdb57609b1dd46eafbfb608cd5e9149 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:10:42 -0300 Subject: [PATCH] [needs-vps] feat(dashboard): add per-operator quota row visibility on usage tab (#7251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(dashboard): add per-operator quota row visibility on usage tab Adds a "hide this quota row" action (visibility_off icon button) to each model-quota row on the provider limits card, and a "Hidden: …" strip at the bottom of the card to restore any hidden row. The visibility preference is keyed per-provider (settings.quotaVisibility) and persisted via PATCH /api/settings, so it survives refresh/reload. Distinct from the existing model-catalog isHidden/isDeleted mechanism (collectHiddenQuotaModelIds/filterHiddenModelQuotas in ProviderLimits/utils.tsx), which hides rows the ADMIN marked hidden in the model catalog. This is a personal dashboard preference an operator can toggle without touching the catalog — e.g. temporarily decluttering a quota card with many low-signal rows. New pure helpers (getQuotaVisibilityKey, filterQuotasByVisibility, getHiddenQuotaRows) live in ProviderLimits/utils.tsx and are unit tested directly; the settings key is added to DEFAULT_SETTINGS and validated via updateSettingsSchema like the existing providerStrategies field. New UI strings are filled in en.json and synced to the other 42 locales as `__MISSING__` placeholders. Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Inspired-by: https://github.com/decolua/9router/pull/2371 * chore(changelog): fragment for #7251 * refactor(usage): extract useQuotaVisibility hook (file-size budget) ProviderLimits/index.tsx grew past its frozen 1127-line budget with the quota-visibility wiring; extract the state + persistence + hide/show handlers into a dedicated hook (same pattern as useCodexResetCreditRedemption). No behavior change — the live validation evidence on the PR covers this flow end-to-end. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(dashboard): shed QuotaCard complexity to baseline + type useQuotaVisibility settings fetch (#7251 CI green) * test(dashboard): rename quota-row visibility test (path claimed by #7360) * fix(dashboard): restore providerTierField case-collision fix dropped by merge auto-resolve The git merge of origin/release/v3.8.49 auto-resolved providerTierFieldApi.ts (added post-merge-base by commit 3ba3cd145e to fix a case-only filename collision with ProviderTierField.tsx that breaks webpack on case-insensitive filesystems) back down to its pre-fix name providerTierField.ts, silently reverting that base-red fix and dropping its stryker tap.testFiles registration and changelog fragment. Restore all four: the renamed helper file, the import path in ProviderTierField.tsx, the import path in tests/unit/provider-tier-field-helpers.test.ts, and the combo-skip-conn-disable-plugin-block-7806.test.ts stryker registration. --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> --- .../7251-provider-quota-visibility.md | 1 + .../components/ProviderLimits/QuotaCard.tsx | 45 +++++++--- .../ProviderLimits/QuotaCardGrid.tsx | 10 +++ .../usage/components/ProviderLimits/index.tsx | 6 ++ .../parts/QuotaCardExpanded.tsx | 49 +++++++++++ .../ProviderLimits/useQuotaVisibility.ts | 88 +++++++++++++++++++ .../usage/components/ProviderLimits/utils.tsx | 65 ++++++++++++++ src/i18n/messages/ar.json | 6 +- src/i18n/messages/az.json | 6 +- src/i18n/messages/bg.json | 6 +- src/i18n/messages/bn.json | 6 +- src/i18n/messages/cs.json | 6 +- src/i18n/messages/da.json | 6 +- src/i18n/messages/de.json | 6 +- src/i18n/messages/en.json | 4 + src/i18n/messages/es.json | 6 +- src/i18n/messages/fa.json | 6 +- src/i18n/messages/fi.json | 6 +- src/i18n/messages/fr.json | 6 +- src/i18n/messages/gu.json | 6 +- src/i18n/messages/he.json | 6 +- src/i18n/messages/hi.json | 6 +- src/i18n/messages/hu.json | 6 +- src/i18n/messages/id.json | 6 +- src/i18n/messages/in.json | 6 +- src/i18n/messages/it.json | 6 +- src/i18n/messages/ja.json | 6 +- src/i18n/messages/ko.json | 6 +- src/i18n/messages/mr.json | 6 +- src/i18n/messages/ms.json | 6 +- src/i18n/messages/nl.json | 6 +- src/i18n/messages/no.json | 6 +- src/i18n/messages/phi.json | 6 +- src/i18n/messages/pl.json | 6 +- src/i18n/messages/pt-BR.json | 6 +- src/i18n/messages/pt.json | 6 +- src/i18n/messages/ro.json | 6 +- src/i18n/messages/ru.json | 6 +- src/i18n/messages/sk.json | 6 +- src/i18n/messages/sv.json | 6 +- src/i18n/messages/sw.json | 6 +- src/i18n/messages/ta.json | 6 +- src/i18n/messages/te.json | 6 +- src/i18n/messages/th.json | 6 +- src/i18n/messages/tr.json | 6 +- src/i18n/messages/uk-UA.json | 6 +- src/i18n/messages/ur.json | 6 +- src/i18n/messages/vi.json | 4 + src/i18n/messages/zh-CN.json | 6 +- src/i18n/messages/zh-TW.json | 6 +- src/lib/db/settings.ts | 6 ++ src/shared/validation/settingsSchemas.ts | 6 ++ tests/unit/usage-quota-row-visibility.test.ts | 59 +++++++++++++ 53 files changed, 536 insertions(+), 53 deletions(-) create mode 100644 changelog.d/features/7251-provider-quota-visibility.md create mode 100644 src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useQuotaVisibility.ts create mode 100644 tests/unit/usage-quota-row-visibility.test.ts diff --git a/changelog.d/features/7251-provider-quota-visibility.md b/changelog.d/features/7251-provider-quota-visibility.md new file mode 100644 index 0000000000..9969596245 --- /dev/null +++ b/changelog.d/features/7251-provider-quota-visibility.md @@ -0,0 +1 @@ +- **feat(dashboard):** add a per-operator "hide this quota row" toggle to the usage dashboard's provider limit cards, persisted per-provider in settings. (thanks @nguyenha935) diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx index 97dbcfb68d..1f4a4ebcce 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx @@ -3,7 +3,17 @@ import { useMemo, useState } from "react"; import Card from "@/shared/components/Card"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; -import { normalizePlanTier, resolvePlanValue, worstStatus, type CardStatus } from "./utils"; +import { + normalizePlanTier, + resolvePlanValue, + worstStatus, + filterQuotasByVisibility, + getHiddenQuotaRows, + computeCanEditCutoff, + computeCanRedeemResetCredit, + hasQuotaCutoffOverrides, + type CardStatus, +} from "./utils"; import QuotaCardHeader from "./parts/QuotaCardHeader"; import QuotaCardExpanded from "./parts/QuotaCardExpanded"; import ProviderUsdCostModal from "./ProviderUsdCostModal"; @@ -39,6 +49,10 @@ interface QuotaCardProps { togglingActive: boolean; redeemingResetCredit?: boolean; loadingResetCredits?: boolean; + /** Per-operator quota row visibility (upstream 9router#2371 port). */ + quotaVisibility?: Record; + onHideQuota?: (quota: any) => void; + onShowQuota?: (quota: any) => void; } export default function QuotaCard({ @@ -56,10 +70,21 @@ export default function QuotaCard({ togglingActive, redeemingResetCredit = false, loadingResetCredits = false, + quotaVisibility, + onHideQuota, + onShowQuota, }: QuotaCardProps) { const isActive = connection.isActive ?? true; const [costModalOpen, setCostModalOpen] = useState(false); - const quotas = quota?.quotas ?? EMPTY_QUOTAS; + const rawQuotas = quota?.quotas ?? EMPTY_QUOTAS; + const quotas = useMemo( + () => filterQuotasByVisibility(connection.provider, rawQuotas, quotaVisibility), + [connection.provider, rawQuotas, quotaVisibility] + ); + const hiddenQuotaRows = useMemo( + () => getHiddenQuotaRows(connection.provider, rawQuotas, quotaVisibility), + [connection.provider, rawQuotas, quotaVisibility] + ); const cardStatus = useMemo(() => worstStatus(quotas), [quotas]); const tierMeta = useMemo( () => @@ -84,12 +109,11 @@ export default function QuotaCard({ [connection, emailsVisible] ); - const overrides = (connection.quotaWindowThresholds as Record | null) || null; - const hasOverrides = !!overrides && Object.keys(overrides).length > 0; + const hasOverrides = hasQuotaCutoffOverrides(connection); const hasStaleData = !!quota?.stale; const displayRefreshedAt = quota?.stale?.since || refreshedAt; - const canEditCutoff = quotas.some((q: any) => q && typeof q.name === "string" && !q.isCredits); - const canRedeemResetCredit = connection.provider === "codex" && hasAvailableResetCredits(quotas); + const canEditCutoff = computeCanEditCutoff(quotas); + const canRedeemResetCredit = computeCanRedeemResetCredit(connection.provider, quotas); return ( setCostModalOpen(true)} onOpenResetCredits={onOpenResetCredits} + hiddenQuotaRows={hiddenQuotaRows} + onHideQuota={onHideQuota} + onShowQuota={onShowQuota} canEditCutoff={canEditCutoff} hasCutoffOverrides={hasOverrides} canRedeemResetCredit={canRedeemResetCredit} @@ -136,9 +163,3 @@ export default function QuotaCard({ ); } - -function hasAvailableResetCredits(quotas: any[]): boolean { - return quotas.some( - (quota: any) => quota?.isResetCredits && Number(quota.creditCount ?? quota.remaining ?? 0) > 0 - ); -} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx index a1ed6d9d6f..ed524043c8 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx @@ -19,6 +19,10 @@ interface Props { togglingActiveId: string | null; redeemingResetCreditId?: string | null; loadingResetCreditsId?: string | null; + /** Per-operator quota row visibility (upstream 9router#2371 port). */ + quotaVisibility?: Record; + onHideQuota?: (provider: string, quota: any) => void; + onShowQuota?: (provider: string, quota: any) => void; } export default function QuotaCardGrid({ @@ -37,6 +41,9 @@ export default function QuotaCardGrid({ togglingActiveId, redeemingResetCreditId = null, loadingResetCreditsId = null, + quotaVisibility, + onHideQuota, + onShowQuota, }: Props) { if (connections.length === 0) return null; @@ -76,6 +83,9 @@ export default function QuotaCardGrid({ togglingActive={togglingActiveId === conn.id} redeemingResetCredit={redeemingResetCreditId === conn.id} loadingResetCredits={loadingResetCreditsId === conn.id} + quotaVisibility={quotaVisibility} + onHideQuota={onHideQuota ? (q) => onHideQuota(conn.provider, q) : undefined} + onShowQuota={onShowQuota ? (q) => onShowQuota(conn.provider, q) : undefined} /> ))} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx index 2ead860172..84c93e87df 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx @@ -19,6 +19,8 @@ import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; import { useNotificationStore } from "@/store/notificationStore"; + +import { useQuotaVisibility } from "./useQuotaVisibility"; import QuotaCutoffModal from "./QuotaCutoffModal"; import QuotaCardGrid from "./QuotaCardGrid"; import CodexResetCreditsModal from "./CodexResetCreditsModal"; @@ -201,6 +203,7 @@ export default function ProviderLimits({ const [refreshingAll, setRefreshingAll] = useState(false); const [initialLoading, setInitialLoading] = useState(true); const [tierFilter, setTierFilter] = useState("all"); + const { quotaVisibility, handleHideQuota, handleShowQuota } = useQuotaVisibility(tr, notify); const resetCreditRedemption = useCodexResetCreditRedemption( tr, setErrors, @@ -1040,6 +1043,9 @@ export default function ProviderLimits({ onOpenResetCredits={resetCreditRedemption.openCodexResetCredits} onToggleActive={handleToggleActive} togglingActiveId={togglingActiveId} + quotaVisibility={quotaVisibility} + onHideQuota={handleHideQuota} + onShowQuota={handleShowQuota} redeemingResetCreditId={resetCreditRedemption.redeemingResetCreditId} loadingResetCreditsId={resetCreditRedemption.loadingResetCreditsId} /> diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx index ae3885e03e..a51ea35c2a 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx @@ -7,6 +7,7 @@ import { formatQuotaLabel, getBarColor, getQuotaRemainingPercentage, + getQuotaVisibilityKey, shouldShowQuotaUsageCount, } from "../utils"; import QuotaMiniBar from "../QuotaMiniBar"; @@ -83,18 +84,25 @@ interface Props { canRedeemResetCredit?: boolean; redeemingResetCredit?: boolean; loadingResetCredits?: boolean; + /** Per-operator quota row visibility (upstream 9router#2371 port). */ + hiddenQuotaRows?: any[]; + onHideQuota?: (quota: any) => void; + onShowQuota?: (quota: any) => void; } function QuotaDetailRow({ q, + onHideQuota, onOpenResetCredits, loadingResetCredits = false, }: { q: any; + onHideQuota?: (quota: any) => void; onOpenResetCredits?: () => void; loadingResetCredits?: boolean; }) { const t = useTranslations("usage"); + const canHide = typeof onHideQuota === "function" && !q.isCredits && !q.isResetCredits; if (q.isResetCredits) { const count = Number(q.creditCount ?? q.remaining ?? 0); const colors = getBarColor(q.remainingPercentage ?? 100); @@ -186,6 +194,22 @@ function QuotaDetailRow({ > {q.unlimited ? "∞" : translateUsageOrFallback(t, "percentLeft", `${pct}% left`, { pct })} + {canHide && ( + + )} {!q.unlimited && }
@@ -223,6 +247,9 @@ export default function QuotaCardExpanded({ canRedeemResetCredit = false, redeemingResetCredit = false, loadingResetCredits = false, + hiddenQuotaRows = [], + onHideQuota, + onShowQuota, }: Props) { const t = useTranslations("usage"); const tr = (key: string, fallback: string, values?: UsageTranslationValues) => @@ -274,6 +301,7 @@ export default function QuotaCardExpanded({ @@ -281,6 +309,27 @@ export default function QuotaCardExpanded({
)} + {hiddenQuotaRows.length > 0 && ( +
+ visibility_off + {tr("hiddenQuotaRowsLabel", "Hidden:")} + {hiddenQuotaRows.map((q) => ( + + ))} +
+ )} + {!error && sortedQuotas.length > DEFAULT_VISIBLE_ROWS && (