fix(usage): prevent stale data after clearing model filter
This commit is contained in:
1 parent
deb7ad71de
commit
a8fa7fb1f2
3 files changed
+108
-10
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user