fix(usage): prevent stale data after clearing model filter

This commit is contained in:
lzt404 committed 2026-09-18 18:17:31 +08:00
1 parent deb7ad71de
commit a8fa7fb1f2
3 files changed
+108 -10

No files matched your search

+43 -10
View File
@@ -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<UsagePricing | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [loadedScopeKey, setLoadedScopeKey] = useState('');
const requestIdRef = useRef(0);
const schedulerRef = useRef<ReturnType<typeof createRefreshScheduler> | 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 (
<section className="page management-page usage-records-page">
@@ -702,9 +735,9 @@ export function UsageRecordsPage() {
</div>
) : null}
{activeTab === 'overview' && overview ? <OverviewView overview={overview} range={overviewRange} /> : null}
{activeTab === 'analysis' ? <AnalysisView analysis={analysis} overview={overview} /> : null}
{activeTab === 'events' && events ? (
{hasCurrentSnapshot && activeTab === 'overview' && overview ? <OverviewView overview={overview} range={overviewRange} /> : null}
{hasCurrentSnapshot && activeTab === 'analysis' ? <AnalysisView analysis={analysis} overview={overview} /> : null}
{hasCurrentSnapshot && activeTab === 'events' && events ? (
<EventsView
events={events}
pageSize={pageSize}
@@ -715,7 +748,7 @@ export function UsageRecordsPage() {
}}
/>
) : null}
{activeTab === 'pricing' && pricing ? (
{hasCurrentSnapshot && activeTab === 'pricing' && pricing ? (
<PricingView pricing={pricing} query={buildQueries().query} onChanged={() => loadData(true)} />
) : null}
{activeTab === 'data-management' ? <UsageDataManagementView /> : null}
+29
View File
@@ -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,
]);
}
+36
View File
@@ -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);
});
});