diff --git a/src/pages/UsageRecordsPage.tsx b/src/pages/UsageRecordsPage.tsx index 9d95108..1c7650b 100644 --- a/src/pages/UsageRecordsPage.tsx +++ b/src/pages/UsageRecordsPage.tsx @@ -39,6 +39,7 @@ import { type UsageTimelinePoint, } from '../services/usageTrend'; import { createRefreshScheduler } from '../services/refreshScheduler'; +import { usageViewScopeKey } from '../services/usageViewScope'; type UsageTab = 'overview' | 'analysis' | 'events' | 'pricing' | 'data-management'; type UsageRange = '4h' | '24h' | 'today' | '7d' | '30d' | 'all' | 'custom'; @@ -309,6 +310,7 @@ export function UsageRecordsPage() { const [pricing, setPricing] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const [loadedScopeKey, setLoadedScopeKey] = useState(''); const requestIdRef = useRef(0); const schedulerRef = useRef | null>(null); if (!schedulerRef.current) schedulerRef.current = createRefreshScheduler(250); @@ -343,6 +345,32 @@ export function UsageRecordsPage() { }; }, [apiKeyHash, customEnd, customStart, model, provider, range, result, source]); + const scopeKey = useMemo(() => usageViewScopeKey({ + tab: activeTab, + range, + customStart, + customEnd, + model, + provider, + source, + apiKeyHash, + result, + page, + pageSize, + }), [ + activeTab, + apiKeyHash, + customEnd, + customStart, + model, + page, + pageSize, + provider, + range, + result, + source, + ]); + const executeLoadData = useCallback( async (quiet = false) => { const requestId = ++requestIdRef.current; @@ -405,6 +433,7 @@ export function UsageRecordsPage() { setStatus(nextStatus); setOptionsAnalysis(nextOptions); } + setLoadedScopeKey(scopeKey); setError(''); } catch (requestError) { if (requestId === requestIdRef.current) setError(String(requestError)); @@ -412,7 +441,7 @@ export function UsageRecordsPage() { if (requestId === requestIdRef.current) setLoading(false); } }, - [activeTab, buildQueries, page, pageSize, model, provider, source, apiKeyHash, result] + [activeTab, buildQueries, page, pageSize, model, provider, source, apiKeyHash, result, scopeKey] ); const loadData = useCallback( @@ -481,12 +510,16 @@ export function UsageRecordsPage() { }; const collectorTone = status?.state === 'error' ? 'error' : status?.state === 'collecting' ? 'success' : ''; + const hasCurrentSnapshot = loadedScopeKey === scopeKey; const showInitialLoading = - loading && - ((activeTab === 'overview' && !overview) || - (activeTab === 'analysis' && !overview) || - (activeTab === 'events' && !events) || - (activeTab === 'pricing' && !pricing)); + activeTab !== 'data-management' && + !error && + (!hasCurrentSnapshot || + (loading && + ((activeTab === 'overview' && !overview) || + (activeTab === 'analysis' && !overview) || + (activeTab === 'events' && !events) || + (activeTab === 'pricing' && !pricing)))); return (
@@ -702,9 +735,9 @@ export function UsageRecordsPage() { ) : null} - {activeTab === 'overview' && overview ? : null} - {activeTab === 'analysis' ? : null} - {activeTab === 'events' && events ? ( + {hasCurrentSnapshot && activeTab === 'overview' && overview ? : null} + {hasCurrentSnapshot && activeTab === 'analysis' ? : null} + {hasCurrentSnapshot && activeTab === 'events' && events ? ( ) : null} - {activeTab === 'pricing' && pricing ? ( + {hasCurrentSnapshot && activeTab === 'pricing' && pricing ? ( loadData(true)} /> ) : null} {activeTab === 'data-management' ? : null} diff --git a/src/services/usageViewScope.ts b/src/services/usageViewScope.ts new file mode 100644 index 0000000..0c4671e --- /dev/null +++ b/src/services/usageViewScope.ts @@ -0,0 +1,29 @@ +export type UsageViewScope = { + tab: string; + range: string; + customStart: string; + customEnd: string; + model: string; + provider: string; + source: string; + apiKeyHash: string; + result: string; + page: number; + pageSize: number; +}; + +export function usageViewScopeKey(scope: UsageViewScope): string { + return JSON.stringify([ + scope.tab, + scope.range, + scope.customStart, + scope.customEnd, + scope.model, + scope.provider, + scope.source, + scope.apiKeyHash, + scope.result, + scope.page, + scope.pageSize, + ]); +} diff --git a/tests/usageViewScope.test.ts b/tests/usageViewScope.test.ts new file mode 100644 index 0000000..ef557c2 --- /dev/null +++ b/tests/usageViewScope.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'bun:test'; +import { usageViewScopeKey, type UsageViewScope } from '../src/services/usageViewScope'; + +const allModelsScope: UsageViewScope = { + tab: 'overview', + range: '24h', + customStart: '', + customEnd: '', + model: '', + provider: '', + source: '', + apiKeyHash: '', + result: 'all', + page: 1, + pageSize: 50, +}; + +describe('usage view scope', () => { + test('invalidates a filtered snapshot when switching back to all models', () => { + const grokScope = usageViewScopeKey({ ...allModelsScope, model: 'grok-4' }); + const currentScope = usageViewScopeKey(allModelsScope); + + expect(currentScope).not.toBe(grokScope); + }); + + test('stays stable when the represented view and filters do not change', () => { + expect(usageViewScopeKey({ ...allModelsScope })).toBe(usageViewScopeKey({ ...allModelsScope })); + }); + + test('invalidates snapshots for pagination and tab changes', () => { + const base = usageViewScopeKey(allModelsScope); + + expect(usageViewScopeKey({ ...allModelsScope, page: 2 })).not.toBe(base); + expect(usageViewScopeKey({ ...allModelsScope, tab: 'events' })).not.toBe(base); + }); +});