fix(quota-share): guard usage.dimensions to stop "reading 'length'" ISE

The pool usage snapshot can come back without a dimensions array (e.g. when the
plan resolves to empty for catalog-only providers). PoolCard.computeStatus and
hasDimensions read usage.dimensions.length directly, crashing the whole page
("Cannot read properties of undefined (reading 'length')"). Normalize to [] in
PoolCard and in usePoolsUsageAggregate (dimensions/perKey).
This commit is contained in:
diegosouzapw
2026-05-30 16:50:47 -03:00
parent f1d0416d72
commit 6095842ef0
3 changed files with 31 additions and 7 deletions

View File

@@ -24,8 +24,9 @@ export interface PoolCardProps {
}
function computeStatus(usage: PoolUsageSnapshot | null): "green" | "amber" | "red" {
if (!usage || usage.dimensions.length === 0) return "green";
const utilizations = usage.dimensions.map((d) =>
const dims = usage?.dimensions ?? [];
if (dims.length === 0) return "green";
const utilizations = dims.map((d) =>
d.limit > 0 ? (d.consumedTotal / d.limit) * 100 : 0
);
const avg = utilizations.reduce((s, u) => s + u, 0) / utilizations.length;
@@ -54,7 +55,7 @@ export default function PoolCard({
const { icon: statusIcon, cls: statusCls } = STATUS_ICONS[status];
// Check for plan dimensions from usage
const hasDimensions = usage && usage.dimensions.length > 0;
const hasDimensions = !!usage?.dimensions?.length;
return (
<Card padding="md">
@@ -103,10 +104,10 @@ export default function PoolCard({
<div
className="grid gap-3 mb-3"
style={{
gridTemplateColumns: `repeat(${Math.min(usage.dimensions.length, 3)}, 1fr)`,
gridTemplateColumns: `repeat(${Math.min(usage?.dimensions?.length ?? 0, 3)}, 1fr)`,
}}
>
{usage.dimensions.map((dim, i) => (
{(usage?.dimensions ?? []).map((dim, i) => (
<DimensionBar
key={`${dim.unit}-${dim.window}-${i}`}
dimension={{ unit: dim.unit, window: dim.window, limit: dim.limit }}

View File

@@ -40,12 +40,12 @@ export function usePoolsUsageAggregate(pools: QuotaPool[]): PoolsUsageAggregate
let utilCount = 0;
let borrowing = 0;
for (const { usage } of valid) {
for (const dim of usage.dimensions) {
for (const dim of usage.dimensions ?? []) {
if (dim.limit > 0) {
totalUtil += (dim.consumedTotal / dim.limit) * 100;
utilCount += 1;
}
for (const key of dim.perKey) {
for (const key of dim.perKey ?? []) {
if (key.borrowing) borrowing += 1;
}
}

View File

@@ -0,0 +1,23 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
// Regression guard: quota-share crashed with "Cannot read properties of undefined
// (reading 'length')" because PoolCard/aggregate read usage.dimensions without a
// guard when the usage snapshot came back without a dimensions array.
const root = join(import.meta.dirname, "../..");
const read = (p: string) => readFileSync(join(root, p), "utf8");
test("quota-share PoolCard guards usage.dimensions", () => {
const pc = read("src/app/(dashboard)/dashboard/costs/quota-share/components/PoolCard.tsx");
assert.ok(pc.includes("usage?.dimensions ?? []"), "computeStatus normalizes dimensions to []");
assert.ok(pc.includes("!!usage?.dimensions?.length"), "hasDimensions guards dimensions");
assert.equal(/[^?.]usage\.dimensions\.length/.test(pc), false, "no unguarded usage.dimensions.length");
});
test("quota-share aggregate hook guards dimensions/perKey", () => {
const agg = read("src/app/(dashboard)/dashboard/costs/quota-share/hooks/usePoolsUsageAggregate.ts");
assert.ok(agg.includes("usage.dimensions ?? []"), "iterates dimensions with ?? []");
assert.ok(agg.includes("dim.perKey ?? []"), "iterates perKey with ?? []");
});