From 601470b89494955b189e34f5fe92fa29d2d8b60d Mon Sep 17 00:00:00 2001
From: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Date: Mon, 20 Jul 2026 21:07:42 +0800
Subject: [PATCH] fix(dashboard): make quota cards container responsive (#7027)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(dashboard): make quota cards container responsive
* test(dashboard): assert #7072 mobile guard behaviorally, not by literal token
The #7072 regression guard asserted the literal `grid-cols-1` class token,
which only fits a breakpoint-driven grid. This PR switched the per-group
card grid to a container-driven `auto-fit`/`minmax()` template, which the
guard doesn't recognize even though it also guarantees a single column on
mobile-width viewports.
Rewrite the guard to assert the underlying behavior — single column on
mobile — accepting either the breakpoint model (unprefixed grid-cols-1) or
the auto-fit model (a minmax() track wide enough that two columns can't
fit on a phone viewport). Reverting to the pre-#7072 forced grid-cols-2
still fails the guard.
* fix(dashboard): keep stale quota rows during refresh
Refreshing a quota card replaced its entire quota section with a loading
placeholder. The card height collapsed and then grew back when data
arrived, and each height change rebalanced the outer 2xl CSS multi-column
layout, making provider groups visibly jump between columns (flash).
Show the loading placeholder only on the initial load (nothing to display
yet). During a refresh the stale rows stay rendered while the refresh
button icon spins, and the UI swaps in new data once it arrives. Also
keep the expand/collapse button visible during refresh so its row does
not add another height change.
---
.../ProviderLimits/QuotaCardGrid.tsx | 2 +-
.../parts/QuotaCardExpanded.tsx | 21 +++++-
...-card-expanded-loading-placeholder.test.ts | 28 +++++++
.../quota-card-grid-horizontal-layout.test.ts | 31 ++++----
.../unit/quota-card-grid-mobile-7072.test.ts | 74 ++++++++++++++++---
5 files changed, 128 insertions(+), 28 deletions(-)
create mode 100644 tests/unit/quota-card-expanded-loading-placeholder.test.ts
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx
index dfdc46a9c7..a1ed6d9d6f 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCardGrid.tsx
@@ -58,7 +58,7 @@ export default function QuotaCardGrid({
({conns.length} account{conns.length !== 1 ? "s" : ""})
-
+
{conns.map((conn) => (
- {loading ? (
+ {shouldShowLoadingPlaceholder(loading, sortedQuotas.length, message) ? (
progress_activity
@@ -264,7 +281,7 @@ export default function QuotaCardExpanded({
)}
- {!loading && !error && sortedQuotas.length > DEFAULT_VISIBLE_ROWS && (
+ {!error && sortedQuotas.length > DEFAULT_VISIBLE_ROWS && (
{
diff --git a/tests/unit/quota-card-expanded-loading-placeholder.test.ts b/tests/unit/quota-card-expanded-loading-placeholder.test.ts
new file mode 100644
index 0000000000..bbe5698b4d
--- /dev/null
+++ b/tests/unit/quota-card-expanded-loading-placeholder.test.ts
@@ -0,0 +1,28 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { shouldShowLoadingPlaceholder } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded";
+
+// The loading placeholder replaces the entire quota section with a spinner.
+// Swapping rows for a spinner mid-refresh collapses the card height, which
+// rebalances the outer CSS multi-column layout (provider groups visibly jump
+// between columns). During a refresh the existing rows must stay rendered —
+// the refresh button icon already spins — so only the initial load (nothing
+// to display yet) may show the placeholder.
+
+test("initial load with no quota data shows the placeholder", () => {
+ assert.equal(shouldShowLoadingPlaceholder(true, 0), true);
+});
+
+test("refresh with existing quota rows keeps them rendered", () => {
+ assert.equal(shouldShowLoadingPlaceholder(true, 3), false);
+ assert.equal(shouldShowLoadingPlaceholder(true, 1), false);
+});
+
+test("refresh with only a provider message keeps it rendered", () => {
+ assert.equal(shouldShowLoadingPlaceholder(true, 0, "re-authenticate this account."), false);
+});
+
+test("not loading never shows the placeholder", () => {
+ assert.equal(shouldShowLoadingPlaceholder(false, 0), false);
+ assert.equal(shouldShowLoadingPlaceholder(false, 3), false);
+});
diff --git a/tests/unit/quota-card-grid-horizontal-layout.test.ts b/tests/unit/quota-card-grid-horizontal-layout.test.ts
index 5e9fcd52ba..447d6ddc33 100644
--- a/tests/unit/quota-card-grid-horizontal-layout.test.ts
+++ b/tests/unit/quota-card-grid-horizontal-layout.test.ts
@@ -1,16 +1,14 @@
// #3520 — Provider Quota page should use horizontal whitespace better.
//
// QuotaCardGrid previously stacked provider groups vertically via a single
-// `flex flex-col` container and kept cards to a conservative 1/2/3/4-column
-// breakpoint ladder starting at `grid-cols-1`. This regression guard asserts
-// the shipped JSX structure and grouping logic directly:
+// `flex flex-col` container and sized card columns from fixed viewport
+// breakpoints. This regression guard asserts the shipped JSX structure and
+// grouping logic directly:
// 1. Grouping still produces one header per distinct provider with the
// correct account count ("N account(s)").
-// 2. The per-group card grid keeps a single-column mobile fallback
-// (`grid-cols-1`) below the `sm:` breakpoint (restored by #7072 after
-// PR #6815 dropped it and clipped labels on phone-width viewports),
-// while still going multi-column (`sm:grid-cols-2`) from `sm:` up so
-// cards fill horizontal space sooner on larger screens.
+// 2. Each provider group's cards auto-fit into as many 280px columns as that
+// group's actual width supports, while a card can shrink to the group's
+// width when its container is narrower than 280px.
// 3. Provider groups themselves flow into multiple columns on wide screens
// (`columns-*`) instead of an unconditional vertical `flex flex-col`
// stack.
@@ -107,14 +105,19 @@ test("QuotaCardGrid (#3520) — outer container flows groups into multiple colum
assert.notEqual(outerClassName, "flex flex-col gap-6");
});
-test("QuotaCardGrid (#3520/#7072) — per-group card grid keeps a mobile grid-cols-1 fallback and goes multi-column from sm: up", () => {
+test("QuotaCardGrid (#3520) — cards follow actual group width with a narrow-container fallback", () => {
const classNames = extractDivClassNames(COMPONENT_PATH);
- const cardGridClassName = classNames.find(
- (c) => /\bgrid\b/.test(c) && /grid-cols-/.test(c)
+ const cardGridClassName = classNames.find((c) => /\bgrid\b/.test(c) && /grid-cols-/.test(c));
+ assert.ok(cardGridClassName, "expected to find the actual-width per-group card grid's className");
+ assert.match(
+ cardGridClassName,
+ /(?:^|\s)grid-cols-\[repeat\(auto-fit,minmax\(min\(100%,280px\),1fr\)\)\](?:\s|$)/,
+ "expected 280px auto-fit columns that can shrink to 100% in a narrower group container"
+ );
+ assert.doesNotMatch(
+ cardGridClassName,
+ /(?:^|\s)(?:grid-cols-2|md:grid-cols-3|xl:grid-cols-4)(?:\s|$)/
);
- assert.ok(cardGridClassName, "expected to find the per-group card grid's className");
- assert.match(cardGridClassName!, /\bgrid-cols-1\b/);
- assert.match(cardGridClassName!, /\bsm:grid-cols-2\b/);
});
test("QuotaCardGrid (#3520) — early-returns null when there are no connections", () => {
diff --git a/tests/unit/quota-card-grid-mobile-7072.test.ts b/tests/unit/quota-card-grid-mobile-7072.test.ts
index aeede949a8..eb10aa4cff 100644
--- a/tests/unit/quota-card-grid-mobile-7072.test.ts
+++ b/tests/unit/quota-card-grid-mobile-7072.test.ts
@@ -10,9 +10,17 @@
// the overflowing button/label text is clipped instead of wrapping.
//
// This regression guard parses QuotaCardGrid.tsx's JSX via the TypeScript
-// compiler API and asserts the per-group card grid's className restores an
-// unprefixed `grid-cols-1` mobile fallback, while keeping the #6815 density
-// gains (grid-cols-2 at `sm:` and up).
+// compiler API and asserts the per-group card grid's className guarantees a
+// single column on mobile-width viewports — the BEHAVIOR that fixes #7072 —
+// rather than one specific token. Two implementations satisfy this:
+// 1. Breakpoint-driven (#7194): an unprefixed `grid-cols-1` token forces a
+// single column below `sm:`, widening at larger breakpoints.
+// 2. Container-driven (#7027): an arbitrary-value
+// `grid-cols-[repeat(auto-fit,minmax(min(100%,Npx),1fr))]` template
+// where the minimum track width is wide enough that a second track
+// cannot fit on any realistic phone viewport.
+// Either way, reverting to the pre-#7072 forced `grid-cols-2` (no
+// unprefixed 1-column fallback, no auto-fit) must fail this guard.
import { test } from "node:test";
import assert from "node:assert/strict";
@@ -63,17 +71,61 @@ function extractDivClassNames(sourcePath: string): string[] {
return classNames;
}
+// Minimum track width (px) below which two auto-fit columns could plausibly
+// fit side by side on a real phone viewport (narrowest common width ~320px).
+// Requiring the min track to clear this bar keeps auto-fit implementations
+// honest about the same guarantee the breakpoint model gives explicitly.
+const MIN_MOBILE_SAFE_TRACK_PX = 200;
+
+function assertSingleColumnMobileFallback(cardGridClassName: string): void {
+ const tokens = cardGridClassName.split(/\s+/);
+
+ // Breakpoint-driven layout (#7194 style): an unprefixed `grid-cols-N`
+ // token controls the base (mobile-first, <640px) column count directly.
+ const unprefixedGridCols = tokens.find((t) => /^grid-cols-\d+$/.test(t));
+ if (unprefixedGridCols) {
+ assert.equal(
+ unprefixedGridCols,
+ "grid-cols-1",
+ `breakpoint-driven grid must keep an unprefixed grid-cols-1 mobile fallback, got className="${cardGridClassName}"`
+ );
+ return;
+ }
+
+ // Container-driven layout (#7027 style): an arbitrary-value auto-fit grid
+ // template computes column count from available width, not a breakpoint.
+ const autoFitToken = tokens.find(
+ (t) => t.startsWith("grid-cols-[") && t.includes("auto-fit") && t.includes("minmax(")
+ );
+ assert.ok(
+ autoFitToken,
+ `expected either an unprefixed grid-cols-1 mobile fallback or a repeat(auto-fit, minmax(...)) ` +
+ `container grid, got className="${cardGridClassName}"`
+ );
+
+ const minTrackMatch =
+ autoFitToken!.match(/minmax\(min\(100%,(\d+)px\)/) ?? autoFitToken!.match(/minmax\((\d+)px/);
+ assert.ok(
+ minTrackMatch,
+ `expected the auto-fit grid's minmax() to specify a numeric minimum track width, got "${autoFitToken}"`
+ );
+ const minTrackPx = Number(minTrackMatch![1]);
+
+ // A second card-wide column can only appear once the container is at
+ // least 2x the minimum track width. Requiring the track minimum to clear
+ // MIN_MOBILE_SAFE_TRACK_PX guards against reintroducing #7072's forced
+ // 2-column-on-mobile clipping via an undersized auto-fit track.
+ assert.ok(
+ minTrackPx >= MIN_MOBILE_SAFE_TRACK_PX,
+ `auto-fit min track width too small (${minTrackPx}px) — two columns could fit on a mobile ` +
+ `viewport, reintroducing the #7072 clipping regression`
+ );
+}
+
test("QuotaCardGrid (#7072) — per-group card grid keeps a single-column mobile fallback", () => {
const classNames = extractDivClassNames(COMPONENT_PATH);
const cardGridClassName = classNames.find((c) => /\bgrid\b/.test(c) && /grid-cols-/.test(c));
assert.ok(cardGridClassName, "expected to find the per-group card grid's className");
- const tokens = cardGridClassName!.split(/\s+/);
- const unprefixedGridCols = tokens.find((t) => /^grid-cols-\d+$/.test(t));
- assert.equal(
- unprefixedGridCols,
- "grid-cols-1",
- `expected unprefixed grid-cols-1 (mobile fallback), got className="${cardGridClassName}"`
- );
- assert.match(cardGridClassName!, /\bsm:grid-cols-2\b/, "expected sm:grid-cols-2 to be preserved");
+ assertSingleColumnMobileFallback(cardGridClassName!);
});