mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
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:
committed by
GitHub
parent
9838acc741
commit
aac5ebcde5
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
52
tests/unit/quota-card-expanded-sort-collapse.test.ts
Normal file
52
tests/unit/quota-card-expanded-sort-collapse.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user