feat(dashboard): collapse and sort provider quota rows by remaining (#5977)

* 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 <j2.cuong@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/1919

* chore(changelog): restore release entries + add quota collapse/sort bullet

---------

Co-authored-by: CườngNH <j2.cuong@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-03 00:13:46 -03:00
committed by GitHub
parent 9838acc741
commit aac5ebcde5
4 changed files with 97 additions and 1 deletions

View File

@@ -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

View File

@@ -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<string, string> = {
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({
<div className="text-[11px] text-text-muted italic">{t("noQuotaData")}</div>
) : (
<div className="flex flex-col divide-y divide-border/40">
{quotas.map((q, i) => (
{visibleQuotas.map((q, i) => (
<QuotaDetailRow key={`${q.name}-${q.modelKey ?? ""}-${i}`} q={q} />
))}
</div>
)}
{!loading && !error && sortedQuotas.length > DEFAULT_VISIBLE_ROWS && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setExpanded((prev) => !prev);
}}
className="inline-flex items-center justify-center gap-1 text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-bg-subtle hover:bg-black/[0.04] dark:hover:bg-white/[0.04] cursor-pointer"
>
<span className="material-symbols-outlined text-[12px]">
{expanded ? "expand_less" : "expand_more"}
</span>
{expanded
? tr("showLessQuotas", "Show less")
: tr("showMoreQuotas", `Show ${hiddenCount} more`, { count: hiddenCount })}
</button>
)}
<div className="flex items-center justify-between gap-2 pt-1.5 border-t border-border/40">
{refreshedLabel && (
<span

View File

@@ -6703,6 +6703,8 @@
"autoRefresh": "Auto-refresh",
"refreshAll": "Refresh All",
"loadingQuotas": "Loading...",
"showMoreQuotas": "Show {count} more",
"showLessQuotas": "Show less",
"account": "Account",
"modelQuotas": "Model Quotas",
"lastUsed": "Last Refreshed",

View File

@@ -0,0 +1,52 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
sortQuotasByRemaining,
getVisibleQuotas,
} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded";
function quota(name: string, remainingPercentage: number) {
return { name, remainingPercentage };
}
test("sortQuotasByRemaining orders quotas by remaining percentage descending", () => {
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);
});