From aac5ebcde5be5abecf9a04b4898fbdc86013c1de Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:13:46 -0300 Subject: [PATCH] feat(dashboard): collapse and sort provider quota rows by remaining (#5977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(dashboard): collapse and sort provider quota rows by remaining Sort the expanded quota list by remaining percentage (highest first) and collapse it to the first 3 rows by default, with a "Show N more" / "Show less" toggle when a connection reports more than 3 quotas. This keeps the most at-risk quotas out of view below a long list of healthy ones. Extracts the sort/slice logic into pure helpers (sortQuotasByRemaining, getVisibleQuotas) exported from QuotaCardExpanded.tsx and unit-tests them directly. Co-authored-by: CườngNH Inspired-by: https://github.com/decolua/9router/pull/1919 * chore(changelog): restore release entries + add quota collapse/sort bullet --------- Co-authored-by: CườngNH --- CHANGELOG.md | 1 + .../parts/QuotaCardExpanded.tsx | 43 ++++++++++++++- src/i18n/messages/en.json | 2 + .../quota-card-expanded-sort-collapse.test.ts | 52 +++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/unit/quota-card-expanded-sort-collapse.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2659810281..18924ab581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - **feat(providers):** support Vercel AI Gateway embeddings and image generation. (thanks @newnol) - **feat(cli-tools):** add Crush CLI tool to the dashboard with one-click configuration. (thanks @dopaemon) - **feat(dashboard):** suggest HuggingFace Hub media models in the media provider view. (thanks @yicone) +- **feat(dashboard):** collapse quota rows and sort by remaining quota in the usage view. (thanks @j2-cuong) ### 🔧 Bug Fixes 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 58b509f078..0457174b36 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx @@ -1,5 +1,6 @@ "use client"; +import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { formatCountdown, @@ -21,6 +22,20 @@ const CURRENCY_SYMBOLS: Record = { INR: "₹", }; +const DEFAULT_VISIBLE_ROWS = 3; + +/** Pure helper — sorts quotas by remaining percentage, highest first. */ +export function sortQuotasByRemaining(quotas: any[]): any[] { + return [...quotas].sort( + (a, b) => getQuotaRemainingPercentage(b) - getQuotaRemainingPercentage(a) + ); +} + +/** Pure helper — slices the sorted quotas down to the visible window. */ +export function getVisibleQuotas(sortedQuotas: any[], expanded: boolean): any[] { + return expanded ? sortedQuotas : sortedQuotas.slice(0, DEFAULT_VISIBLE_ROWS); +} + interface Props { quotas: any[]; loading: boolean; @@ -124,6 +139,14 @@ export default function QuotaCardExpanded({ const tr = (key: string, fallback: string, values?: UsageTranslationValues) => translateUsageOrFallback(t, key, fallback, values); + const [expanded, setExpanded] = useState(false); + const sortedQuotas = useMemo(() => sortQuotasByRemaining(quotas), [quotas]); + const visibleQuotas = useMemo( + () => getVisibleQuotas(sortedQuotas, expanded), + [sortedQuotas, expanded] + ); + const hiddenCount = sortedQuotas.length - visibleQuotas.length; + const refreshedLabel = refreshedAt ? new Date(refreshedAt).toLocaleTimeString([], { hour: "2-digit", @@ -155,12 +178,30 @@ export default function QuotaCardExpanded({
{t("noQuotaData")}
) : (
- {quotas.map((q, i) => ( + {visibleQuotas.map((q, i) => ( ))}
)} + {!loading && !error && sortedQuotas.length > DEFAULT_VISIBLE_ROWS && ( + + )} +
{refreshedLabel && ( { + const quotas = [quota("low", 10), quota("high", 90), quota("mid", 50)]; + const sorted = sortQuotasByRemaining(quotas); + assert.deepEqual( + sorted.map((q) => q.name), + ["high", "mid", "low"] + ); + // original array untouched + assert.deepEqual( + quotas.map((q) => q.name), + ["low", "high", "mid"] + ); +}); + +test("sortQuotasByRemaining treats unlimited quotas as 100% remaining", () => { + const quotas = [quota("capped", 40), { name: "unlimited", unlimited: true }]; + const sorted = sortQuotasByRemaining(quotas); + assert.equal(sorted[0].name, "unlimited"); +}); + +test("getVisibleQuotas collapses to the first 3 rows when not expanded", () => { + const quotas = [1, 2, 3, 4, 5].map((n) => quota(`q${n}`, n)); + const visible = getVisibleQuotas(quotas, false); + assert.equal(visible.length, 3); + assert.deepEqual( + visible.map((q) => q.name), + ["q1", "q2", "q3"] + ); +}); + +test("getVisibleQuotas returns every row when expanded", () => { + const quotas = [1, 2, 3, 4, 5].map((n) => quota(`q${n}`, n)); + const visible = getVisibleQuotas(quotas, true); + assert.equal(visible.length, 5); +}); + +test("getVisibleQuotas returns all rows unchanged when under the default threshold", () => { + const quotas = [quota("a", 10), quota("b", 20)]; + const visible = getVisibleQuotas(quotas, false); + assert.equal(visible.length, 2); +});