[refactor] Extract usePaginatedQuery for the Credits usage table - #15602
[refactor] Extract usePaginatedQuery for the Credits usage table#15602claude[bot] wants to merge 3 commits into
Conversation
Illustrative refactor for an architecture discussion, per docs/guidance/state-and-effects.md. Rebased onto main now that #15151 has merged (as 7494f71) and FE-1609 (#15239) added an `embedded` prop to CreditsPanel in the meantime. - UsageLogsTable owned pagination as a mutable {page, limit, total, totalPages} ref written from three places (a click handler, refresh(), and the server response echo), plus a hand-rolled loadToken race guard duplicated per-component. - CreditsPanel, and separately PlanCreditsPanelContent, each drove a reload by reaching into UsageLogsTable via a template ref and calling an exposed refresh() method. usePaginatedQuery now owns page/limit/total/first as computed values derived from the last response (one source of truth, not several fields kept in sync by hand), bakes the request-token guard in once, and resets/refetches through a single reactive `key`. UsageLogsTable loads itself on mount (no external refresh() needed); CreditsPanel passes a plain `refetchKey` prop instead of imperatively calling into the child, and PlanCreditsPanelContent's now-redundant refresh-on-mount is removed. Behavior is unchanged: same initial load, same refresh triggers, same error messages.
e04f809 to
85ded15
Compare
🎭 Playwright: ✅ 1979 passed, 0 failed · 2 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 9.11 MB gzip 🔴 +504 BDetailsSummary
Category Glance App Entry Points — 3.71 kB (baseline 3.71 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.37 MB (baseline 1.37 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 2 added / 2 removed / 1 unchanged Views & Navigation — 124 kB (baseline 124 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 13 added / 13 removed / 4 unchanged Panels & Settings — 592 kB (baseline 591 kB) • 🔴 +937 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 16 unchanged User & Accounts — 27.5 kB (baseline 27.5 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 6 added / 6 removed / 5 unchanged Editors & Dialogs — 125 kB (baseline 125 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 7 added / 7 removed / 1 unchanged UI Components — 67.1 kB (baseline 67.1 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 8 unchanged Data & Services — 3.53 MB (baseline 3.53 MB) • 🔴 +177 BStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 549 kB (baseline 549 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 18 added / 18 removed / 19 unchanged Vendor & Third-Party — 18.1 MB (baseline 18.1 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 18 unchanged Other — 14.1 MB (baseline 14.1 MB) • 🔴 +6 BBundles that do not match a named category
Status: 66 added / 66 removed / 219 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-22T04:21:47.732Z",
"gitSha": "09ad9b78566772036a6ddfed8b187b5b54e69ef1",
"branch": "refactor/usage-table-paginated-query",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2087.2759999999744,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.526000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 588.061,
"heapDeltaBytes": 16898936,
"heapUsedBytes": 78958460,
"domNodes": -278,
"jsHeapTotalBytes": 4710400,
"scriptDurationMs": 6.5230000000000015,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2057.7560000000403,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.970000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 508.90600000000006,
"heapDeltaBytes": 17053436,
"heapUsedBytes": 78419172,
"domNodes": -282,
"jsHeapTotalBytes": 3923968,
"scriptDurationMs": 11.225999999999999,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1840.174999999988,
"styleRecalcs": 71,
"styleRecalcDurationMs": 32.900000000000006,
"layouts": 12,
"layoutDurationMs": 3.3280000000000003,
"taskDurationMs": 838.1429999999999,
"heapDeltaBytes": -5382776,
"heapUsedBytes": 56714116,
"domNodes": -283,
"jsHeapTotalBytes": 4972544,
"scriptDurationMs": 110.751,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1872.1550000000207,
"styleRecalcs": 77,
"styleRecalcDurationMs": 40.195,
"layouts": 12,
"layoutDurationMs": 3.3600000000000003,
"taskDurationMs": 864.425,
"heapDeltaBytes": -3588780,
"heapUsedBytes": 58477212,
"domNodes": -278,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 109.185,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1717.5770000000057,
"styleRecalcs": 30,
"styleRecalcDurationMs": 16.742,
"layouts": 6,
"layoutDurationMs": 0.7130000000000001,
"taskDurationMs": 349.74799999999993,
"heapDeltaBytes": 3113968,
"heapUsedBytes": 64692924,
"domNodes": 75,
"jsHeapTotalBytes": 4456448,
"scriptDurationMs": 8.939000000000002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1686.249000000089,
"styleRecalcs": 30,
"styleRecalcDurationMs": 18.996,
"layouts": 6,
"layoutDurationMs": 0.67,
"taskDurationMs": 350.60999999999996,
"heapDeltaBytes": 3034100,
"heapUsedBytes": 64744572,
"domNodes": 77,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 9.747,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 561.725000000024,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.483000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 350.383,
"heapDeltaBytes": 10804456,
"heapUsedBytes": 73601460,
"domNodes": 18,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 54.092,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 553.4260000000586,
"styleRecalcs": 9,
"styleRecalcDurationMs": 6.002000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 354.015,
"heapDeltaBytes": 10681220,
"heapUsedBytes": 72088356,
"domNodes": 14,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 55.641000000000005,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2056.2679999999887,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.408000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 615.0649999999999,
"heapDeltaBytes": -3922520,
"heapUsedBytes": 72908784,
"domNodes": -275,
"jsHeapTotalBytes": -1839104,
"scriptDurationMs": 14.130999999999998,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2072.7470000000494,
"styleRecalcs": 9,
"styleRecalcDurationMs": 10.561999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 595.4760000000001,
"heapDeltaBytes": -356748,
"heapUsedBytes": 76197508,
"domNodes": -267,
"jsHeapTotalBytes": -1314816,
"scriptDurationMs": 16.433,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2149.5350000000144,
"styleRecalcs": 70,
"styleRecalcDurationMs": 18.043999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1207.8950000000002,
"heapDeltaBytes": -5772400,
"heapUsedBytes": 71875392,
"domNodes": -275,
"jsHeapTotalBytes": -40960,
"scriptDurationMs": 343.13000000000005,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2198.7830000000486,
"styleRecalcs": 69,
"styleRecalcDurationMs": 16.773999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1196.596,
"heapDeltaBytes": -4347960,
"heapUsedBytes": 73080548,
"domNodes": -240,
"jsHeapTotalBytes": 221184,
"scriptDurationMs": 345.683,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3183.4570000000326,
"styleRecalcs": 62,
"styleRecalcDurationMs": 12.248000000000001,
"layouts": 60,
"layoutDurationMs": 6.973999999999999,
"taskDurationMs": 1379.131,
"heapDeltaBytes": -1594812,
"heapUsedBytes": 77290060,
"domNodes": -280,
"jsHeapTotalBytes": -1576960,
"scriptDurationMs": 397.27000000000004,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3166.7340000000195,
"styleRecalcs": 65,
"styleRecalcDurationMs": 14.329000000000004,
"layouts": 60,
"layoutDurationMs": 7.357000000000001,
"taskDurationMs": 1363.62,
"heapDeltaBytes": -13756628,
"heapUsedBytes": 65076400,
"domNodes": -289,
"jsHeapTotalBytes": 5500928,
"scriptDurationMs": 385.58699999999993,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "legacy-node-drag",
"durationMs": 2343.570999999997,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.527000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1460.571,
"heapDeltaBytes": -14355300,
"heapUsedBytes": 70708500,
"domNodes": 14,
"jsHeapTotalBytes": 9211904,
"scriptDurationMs": 494.8429999999999,
"eventListeners": 186,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "legacy-node-drag",
"durationMs": 2206.5810000000283,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.097999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1407.126,
"heapDeltaBytes": -15703732,
"heapUsedBytes": 69712776,
"domNodes": 14,
"jsHeapTotalBytes": 8163328,
"scriptDurationMs": 455.845,
"eventListeners": 186,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2052.769000000012,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.28,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 606.119,
"heapDeltaBytes": -12766512,
"heapUsedBytes": 70200192,
"domNodes": -277,
"jsHeapTotalBytes": 4714496,
"scriptDurationMs": 13.134,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2008.1370000000334,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.537999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 589.052,
"heapDeltaBytes": -14205900,
"heapUsedBytes": 63452244,
"domNodes": -283,
"jsHeapTotalBytes": 3403776,
"scriptDurationMs": 13.586000000000004,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 589.5449999999869,
"styleRecalcs": 45,
"styleRecalcDurationMs": 9.697000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 391.6069999999999,
"heapDeltaBytes": 11719204,
"heapUsedBytes": 74012924,
"domNodes": 16,
"jsHeapTotalBytes": 5767168,
"scriptDurationMs": 116.81800000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 580.693999999994,
"styleRecalcs": 45,
"styleRecalcDurationMs": 8.645,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 376.705,
"heapDeltaBytes": 11698412,
"heapUsedBytes": 73618924,
"domNodes": 16,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 117.06,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2039.5829999999933,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.994999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 480.005,
"heapDeltaBytes": 6015420,
"heapUsedBytes": 68742128,
"domNodes": -281,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 6.295999999999999,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2007.0259999999962,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.120000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 471.687,
"heapDeltaBytes": 18920644,
"heapUsedBytes": 81564880,
"domNodes": -281,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 5.918000000000001,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1757.520999999997,
"styleRecalcs": 77,
"styleRecalcDurationMs": 37.483000000000004,
"layouts": 16,
"layoutDurationMs": 4.598,
"taskDurationMs": 816.5720000000001,
"heapDeltaBytes": -6441840,
"heapUsedBytes": 55879220,
"domNodes": -282,
"jsHeapTotalBytes": 5758976,
"scriptDurationMs": 84.09400000000001,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1706.963000000087,
"styleRecalcs": 74,
"styleRecalcDurationMs": 34.728,
"layouts": 16,
"layoutDurationMs": 4.351,
"taskDurationMs": 782.177,
"heapDeltaBytes": 3710720,
"heapUsedBytes": 66400188,
"domNodes": -283,
"jsHeapTotalBytes": 4972544,
"scriptDurationMs": 82.979,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1399.314000000004,
"styleRecalcs": 18,
"styleRecalcDurationMs": 31.626999999999995,
"layouts": 13,
"layoutDurationMs": 14.547,
"taskDurationMs": 896.99,
"heapDeltaBytes": -7799396,
"heapUsedBytes": 86508076,
"domNodes": 13673,
"jsHeapTotalBytes": 11534336,
"scriptDurationMs": 16.401000000000007,
"eventListeners": 2373,
"totalBlockingTimeMs": 141,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8332.878999999992,
"styleRecalcs": 250,
"styleRecalcDurationMs": 36.543000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4330.206,
"heapDeltaBytes": 2699204,
"heapUsedBytes": 79484820,
"domNodes": -275,
"jsHeapTotalBytes": 745472,
"scriptDurationMs": 1177.6480000000001,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8207.458999999972,
"styleRecalcs": 248,
"styleRecalcDurationMs": 35.314,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4077.9519999999998,
"heapDeltaBytes": 5440372,
"heapUsedBytes": 82054432,
"domNodes": -244,
"jsHeapTotalBytes": 221184,
"scriptDurationMs": 1049.3560000000002,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 16805.196999999964,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16265.434,
"heapDeltaBytes": -34068104,
"heapUsedBytes": 180647856,
"domNodes": -8312,
"jsHeapTotalBytes": -12238848,
"scriptDurationMs": 113.23000000000002,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 17212.301000000025,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16525.824,
"heapDeltaBytes": -30592112,
"heapUsedBytes": 181338520,
"domNodes": -8312,
"jsHeapTotalBytes": -10047488,
"scriptDurationMs": 105.386,
"eventListeners": -16393,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.333333333333332,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 21054.520999999964,
"styleRecalcs": 177,
"styleRecalcDurationMs": 17.08399999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20395.249999999996,
"heapDeltaBytes": -19100204,
"heapUsedBytes": 193116660,
"domNodes": -8312,
"jsHeapTotalBytes": -18460672,
"scriptDurationMs": 403.24600000000004,
"eventListeners": -16385,
"totalBlockingTimeMs": 23,
"frameDurationMs": 17.780000000000047,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 20962.850000000002,
"styleRecalcs": 179,
"styleRecalcDurationMs": 20.060999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20261.231000000003,
"heapDeltaBytes": -22358312,
"heapUsedBytes": 192226272,
"domNodes": -8312,
"jsHeapTotalBytes": -19648512,
"scriptDurationMs": 429.604,
"eventListeners": -16387,
"totalBlockingTimeMs": 48,
"frameDurationMs": 17.776666666666763,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 464.40499999994245,
"styleRecalcs": 17,
"styleRecalcDurationMs": 21.612000000000002,
"layouts": 3,
"layoutDurationMs": 1.383,
"taskDurationMs": 107.64,
"heapDeltaBytes": 5075540,
"heapUsedBytes": 66569020,
"domNodes": 148,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 7.120000000000001,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "workflow-execution",
"durationMs": 114.15099999999256,
"styleRecalcs": 7,
"styleRecalcDurationMs": 14.586,
"layouts": 2,
"layoutDurationMs": 1.281,
"taskDurationMs": 78.318,
"heapDeltaBytes": 3001952,
"heapUsedBytes": 65253748,
"domNodes": 125,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 5.402,
"eventListeners": 27,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #15602 +/- ##
==========================================
+ Coverage 79.39% 81.86% +2.46%
==========================================
Files 2217 1888 -329
Lines 112188 107186 -5002
Branches 35091 34371 -720
==========================================
- Hits 89069 87745 -1324
+ Misses 22640 19102 -3538
+ Partials 479 339 -140
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 339 files with indirect coverage changes 🚀 New features to boost your workflow:
|
dante01yoon
left a comment
There was a problem hiding this comment.
One blocking contract/complexity issue: the new generic composable recreates backend-owned pagination state that its only caller does not use. Please remove that derived surface or source it from the API contract.
| const page = computed(() => lastResponse.value?.page ?? 1) | ||
| const limit = computed(() => lastResponse.value?.limit ?? initialLimit) | ||
| const total = computed(() => lastResponse.value?.total ?? 0) | ||
| const totalPages = computed(() => |
There was a problem hiding this comment.
issue: totalPages is already returned deterministically by both billing APIs, but this generic layer drops that field and recomputes it from total / limit. That creates a second frontend definition of backend pagination semantics, and the only caller does not consume totalPages at all. Could we remove totalPages from this composable’s public return instead of introducing unused, potentially drifting state?
There was a problem hiding this comment.
Good catch — fixed in c42f753. Removed totalPages from the composable's public return (and the now-invalid test assertion); the only caller never consumed it, and this avoids re-deriving a value the billing APIs already return deterministically.
Generated by Claude Code
totalPages was recomputed from total/limit, duplicating a value both billing APIs already return deterministically, and the only caller (UsageLogsTable) never consumed it. Removed it from the composable's public return instead of keeping unused, potentially drifting state.
dante01yoon
left a comment
There was a problem hiding this comment.
Verified the follow-up: totalPages and its test assertion were removed, so the composable no longer recreates that unused backend-owned pagination state. No further findings.
…eBillingContext
Per review feedback: defineExpose('refresh') on UsageLogsTable was a
legitimate stateless imperative command (no state to desync), so the
refetchKey-prop replacement was the wrong fix for the wrong problem.
Reverted UsageLogsTable to expose refresh() again via a template ref,
matching this repo's own defineExpose convention
(docs/guidance/vue-components.md, src/components/AGENTS.md,
.agents/checks/vue-patterns.md).
What still needed fixing was where the "should credits UI refresh"
decision lived: CreditsPanel had its own local watch(balance) +
revision counter, and PlanCreditsPanelContent had none at all (so a
top-up while on its Activity tab never refreshed that table). Moved
the skip-first-hydration balance-watch into useBillingContext as a new
exposed `usageLogsRefreshSignal`, the shared composable that already
owns `balance`. Both CreditsPanel and PlanCreditsPanelContent now just
watch that one signal and call their own table ref's refresh() -
the "when" lives once in the data layer; the "how" (an imperative
command) stays where Vue's defineExpose convention says it belongs.
usePaginatedQuery's page/limit/total derivation is unchanged - that
part was never in dispute.
|
Updated per feedback (Christian): reverted What actually needed fixing was where the "should credits UI refresh" decision lived — it was duplicated (and in Pushed as Generated by Claude Code |
Requested by Christian Byrne · Slack thread
Summary
Illustrative refactor for an architecture discussion in Slack, demonstrating an alternative pattern for
UsageLogsTable's pagination/refresh state perdocs/guidance/state-and-effects.md(raised as a side discussion on #15151, which already carries its own approval — this is a separate, standalone PR and does not touch that one).Revised after review pushback (Christian): the first version of this PR also replaced
UsageLogsTable'sdefineExpose({ refresh })with arefetchKeyprop threaded down from each parent. That was wrong —refresh()is a stateless imperative command with no state to desync, exactly the case this repo's own convention saysdefineExposeis for (docs/guidance/vue-components.md:21,src/components/AGENTS.md:6,.agents/checks/vue-patterns.md:17).docs/guidance/state-and-effects.mdnever mentionsdefineExpose/imperative commands/child refs at all, so nothing there justified the change. Reverted that part; see "What changed since the last push" below for what's kept instead.Before / After, in plain language
Before
UsageLogsTable.vuekept pagination in one mutable ref,{ page, limit, total, totalPages }, and wrotepagefrom three different places: a page-click handler,refresh()resetting it to1, and the server response echoing it back. Nothing made those three writers agree with each other by construction — they just had to be kept in sync by hand.latestLoadTokencounter guarded against a billing-rail flip superseding an in-flight fetch. That guard is generic pagination-fetch plumbing, but it lived inline in this one component.UsageLogsTablevia a template ref and calling its exposedrefresh()method:CreditsPanel.vue(watchingbalancelocally) andPlanCreditsPanelContent.vue(not watching balance at all — a top-up while on its Activity tab never refreshed that table).After
usePaginatedQuery(src/composables/usePaginatedQuery.ts), owns the query:page,limit,total, andfirstare allcomputedfrom the last server response — one source of truth, not several fields a caller has to remember to keep in sync ("derive everything derivable"). The request-token race guard is baked into the composable once, instead of duplicated per call site.UsageLogsTablestill exposesrefresh()viadefineExpose— unchanged frommain, per review feedback.useBillingContext(the shared composable that already ownsbalance) as a newusageLogsRefreshSignal, instead of each panel re-deriving the same skip-first-hydration balance-watch.CreditsPanelandPlanCreditsPanelContenteach justwatch(usageLogsRefreshSignal, ...)and call their own table ref'srefresh()— the "when" lives once in the data layer; the "how" stays an imperative command on the child, where it belongs. This also fixes thePlanCreditsPanelContentgap above: it now reacts to balance changes too.Why
Per
docs/guidance/state-and-effects.md:computedcan name it, do not put it in arefand keep it in sync by hand — the sync is where the bugs live." (§4) —page/limit/totalarecomputedfrom the response instead of three independently-writable fields.defineExpose/parent-child method calls — that convention comes fromdocs/guidance/vue-components.mdand.agents/checks/vue-patterns.mdinstead, and both call this exact pattern (a stateless command likerefresh()) correct usage.Changes
src/composables/usePaginatedQuery.ts+ test. RefactoredUsageLogsTable.vueto use it instead of a hand-rolled pagination ref andloadTokenguard (still exposesrefresh()). AddedusageLogsRefreshSignaltouseBillingContext/BillingContext(src/composables/billing/types.ts,useBillingContext.ts), owning the balance-change-detection watch once.CreditsPanel.vueandPlanCreditsPanelContent.vuewatch that signal and call their own table ref'srefresh().PlanCreditsPanelContent's Activity tab now refreshes on a genuine balance change, which it never did before (previously onlyCreditsPanelwatched balance at all).Review Focus
usageLogsRefreshSignal(a plain incrementing counter) is the right shape for this signal, versus e.g. a callback-registration/event-bus approach — went with the simpler reactive-counter since it needs no subscribe/unsubscribe lifecycle management.Testing
pnpm test:unit— full suite passes (1201 test files, 16499 tests, 7 expected fail, 13 skipped).pnpm typecheck— clean.pnpm lint— clean (0 errors; pre-existing warnings elsewhere untouched by this PR).