diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx index 47f84744da..23b1b48be8 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx @@ -12,7 +12,6 @@ import { calculatePercentage, matchesProviderFilter, buildProviderOptions, - getQuotaVisibilityKey, } from "./utils"; import Card from "@/shared/components/Card"; import { CardSkeleton } from "@/shared/components/Loading"; @@ -20,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 { useVisibleQuotaData } from "./useVisibleQuotaData"; @@ -200,12 +201,7 @@ export default function ProviderLimits({ const [refreshingAll, setRefreshingAll] = useState(false); const [initialLoading, setInitialLoading] = useState(true); const [tierFilter, setTierFilter] = useState("all"); - // Per-operator quota row visibility (upstream 9router#2371 port). Keyed by - // provider id → { hidden: [] }, persisted server-side - // via PATCH /api/settings so it survives refresh/reload. - const [quotaVisibility, setQuotaVisibility] = useState>( - {} - ); + const { quotaVisibility, handleHideQuota, handleShowQuota } = useQuotaVisibility(tr, notify); const resetCreditRedemption = useCodexResetCreditRedemption( tr, setErrors, @@ -328,76 +324,6 @@ export default function ProviderLimits({ [notify, tr] ); - // Load persisted per-operator quota row visibility once on mount. Best - // effort — a fetch failure just leaves every row visible (safe default). - useEffect(() => { - let alive = true; - fetch("/api/settings", { cache: "no-store" }) - .then((r) => (r.ok ? r.json() : {})) - .then((s) => { - if (alive) setQuotaVisibility(s?.quotaVisibility || {}); - }) - .catch(() => {}); - return () => { - alive = false; - }; - }, []); - - const persistQuotaVisibility = useCallback( - async ( - next: Record, - previous: Record - ) => { - setQuotaVisibility(next); - try { - const response = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ quotaVisibility: next }), - }); - if (!response.ok) throw new Error("Failed to update quota visibility"); - } catch { - setQuotaVisibility(previous); - notify.error(tr("quotaVisibilityUpdateFailed", "Failed to update quota visibility")); - } - }, - [notify, tr] - ); - - const handleHideQuota = useCallback( - (provider: string, quotaRow: any) => { - const key = getQuotaVisibilityKey(quotaRow); - 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] }, - }; - persistQuotaVisibility(next, previous); - }, - [quotaVisibility, persistQuotaVisibility] - ); - - const handleShowQuota = useCallback( - (provider: string, quotaRow: any) => { - const key = getQuotaVisibilityKey(quotaRow); - 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] }, - }; - persistQuotaVisibility(next, previous); - }, - [quotaVisibility, persistQuotaVisibility] - ); - const applyCachedQuotaState = useCallback( (connectionList: any[], caches: Record) => { const nextQuotaData: Record = {}; diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useQuotaVisibility.ts b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useQuotaVisibility.ts new file mode 100644 index 0000000000..d81ca635c9 --- /dev/null +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useQuotaVisibility.ts @@ -0,0 +1,88 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { getQuotaVisibilityKey } from "./utils"; + +type QuotaVisibilityMap = Record; + +type Translate = (key: string, fallback: string) => string; + +interface NotifyLike { + error: (message: string) => void; +} + +/** + * Per-operator quota row visibility (upstream 9router#2371 port). Keyed by + * provider id → { hidden: [] }, persisted server-side + * via PATCH /api/settings so it survives refresh/reload. + * + * Extracted from ProviderLimits/index.tsx to keep the container under the + * frozen file-size budget — same pattern as useCodexResetCreditRedemption. + */ +export function useQuotaVisibility(tr: Translate, notify: NotifyLike) { + const [quotaVisibility, setQuotaVisibility] = useState({}); + + // Load persisted per-operator quota row visibility once on mount. Best + // effort — a fetch failure just leaves every row visible (safe default). + useEffect(() => { + let alive = true; + fetch("/api/settings", { cache: "no-store" }) + .then((r) => (r.ok ? r.json() : {})) + .then((s) => { + if (alive) setQuotaVisibility(s?.quotaVisibility || {}); + }) + .catch(() => {}); + return () => { + alive = false; + }; + }, []); + + const persistQuotaVisibility = useCallback( + async (next: QuotaVisibilityMap, previous: QuotaVisibilityMap) => { + setQuotaVisibility(next); + try { + const response = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ quotaVisibility: next }), + }); + if (!response.ok) throw new Error("Failed to update quota visibility"); + } catch { + setQuotaVisibility(previous); + notify.error(tr("quotaVisibilityUpdateFailed", "Failed to update quota visibility")); + } + }, + [notify, tr] + ); + + const toggleQuotaKey = useCallback( + (provider: string, quotaRow: any, hide: boolean) => { + const key = getQuotaVisibilityKey(quotaRow); + if (!provider || !key) return; + const previous = quotaVisibility; + const providerVisibility = previous[provider] || {}; + const hidden = new Set(providerVisibility.hidden || []); + if (hide) hidden.add(key); + else hidden.delete(key); + const next = { + ...previous, + [provider]: { ...providerVisibility, hidden: [...hidden] }, + }; + persistQuotaVisibility(next, previous); + }, + [quotaVisibility, persistQuotaVisibility] + ); + + const handleHideQuota = useCallback( + (provider: string, quotaRow: any) => toggleQuotaKey(provider, quotaRow, true), + [toggleQuotaKey] + ); + + const handleShowQuota = useCallback( + (provider: string, quotaRow: any) => toggleQuotaKey(provider, quotaRow, false), + [toggleQuotaKey] + ); + + return { quotaVisibility, handleHideQuota, handleShowQuota }; +}