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 && (