diff --git a/src/pages/UsageEventsView.tsx b/src/pages/UsageEventsView.tsx index 60a7dbc..a03a4e1 100644 --- a/src/pages/UsageEventsView.tsx +++ b/src/pages/UsageEventsView.tsx @@ -487,6 +487,7 @@ export function EventsView({ }) { const { t } = useI18n(); const [widths, setWidths] = useState>(getInitialColumnWidths); + const widthsRef = useRef(widths); const [visibleColumnKeys, setVisibleColumnKeys] = useState(getInitialVisibleColumns); const [columnSettingsOpen, setColumnSettingsOpen] = useState(false); const [draftVisibleColumnKeys, setDraftVisibleColumnKeys] = useState(visibleColumnKeys); @@ -508,12 +509,17 @@ export function EventsView({ const isCustomized = EVENT_COLUMNS.some((col) => widths[col.key] !== col.defaultWidth); const noRemarkLabel = t('usage.key.noRemark'); + const commitWidths = (next: Record) => { + widthsRef.current = next; + setWidths(next); + }; + const resetAllWidths = () => { const defaults: Record = {} as any; for (const col of EVENT_COLUMNS) { defaults[col.key] = col.defaultWidth; } - setWidths(defaults); + commitWidths(defaults); try { localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(defaults)); } catch {} @@ -554,23 +560,19 @@ export function EventsView({ e.stopPropagation(); const colDef = EVENT_COLUMNS.find((c) => c.key === key); if (!colDef) return; - setWidths((prev) => { - const next = { ...prev, [key]: colDef.defaultWidth }; - try { - localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); - } catch {} - return next; - }); + const next = { ...widthsRef.current, [key]: colDef.defaultWidth }; + commitWidths(next); + try { + localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); + } catch {} }; const persistColumnWidth = (key: EventColumnKey, width: number) => { - setWidths((current) => { - const next = { ...current, [key]: width }; - try { - localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); - } catch {} - return next; - }); + const next = { ...widthsRef.current, [key]: width }; + commitWidths(next); + try { + localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); + } catch {} }; const handleResizeKeyDown = (key: EventColumnKey, event: KeyboardEvent) => { @@ -597,41 +599,60 @@ export function EventsView({ resizeCleanupRef.current?.(); const startX = e.clientX; const startWidth = - widths[key] ?? EVENT_COLUMNS.find((c) => c.key === key)?.defaultWidth ?? 100; + widthsRef.current[key] ?? EVENT_COLUMNS.find((c) => c.key === key)?.defaultWidth ?? 100; const colDef = EVENT_COLUMNS.find((c) => c.key === key); const minWidth = colDef?.minWidth ?? 50; + const table = e.currentTarget.closest('table'); + const column = table?.querySelector(`col[data-column="${key}"]`) ?? null; + const header = e.currentTarget.closest('th'); setResizingCol(key); document.body.classList.add('table-col-resizing'); let currentWidth = startWidth; + let frame = 0; + + const paintWidth = (nextWidth: number) => { + widthsRef.current = { ...widthsRef.current, [key]: nextWidth }; + if (column) column.style.width = `${nextWidth}px`; + if (header) header.style.width = `${nextWidth}px`; + if (table) { + const total = visibleColumns.reduce( + (sum, item) => sum + (widthsRef.current[item.key] ?? item.defaultWidth), + 0, + ); + table.style.width = `${total}px`; + } + }; const onPointerMove = (moveEvent: PointerEvent) => { const delta = moveEvent.clientX - startX; - const nextWidth = Math.min(800, Math.max(minWidth, Math.round(startWidth + delta))); - currentWidth = nextWidth; - setWidths((prev) => ({ ...prev, [key]: nextWidth })); + currentWidth = Math.min(800, Math.max(minWidth, Math.round(startWidth + delta))); + if (frame) return; + frame = window.requestAnimationFrame(() => { + frame = 0; + paintWidth(currentWidth); + }); }; const cleanup = () => { window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', onPointerUp); window.removeEventListener('pointercancel', onPointerUp); + if (frame) window.cancelAnimationFrame(frame); document.body.classList.remove('table-col-resizing'); resizeCleanupRef.current = null; }; const onPointerUp = () => { + paintWidth(currentWidth); cleanup(); setResizingCol(null); - - setWidths((prev) => { - const next = { ...prev, [key]: currentWidth }; - try { - localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); - } catch {} - return next; - }); + const next = widthsRef.current; + setWidths(next); + try { + localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); + } catch {} }; window.addEventListener('pointermove', onPointerMove); @@ -641,7 +662,7 @@ export function EventsView({ }; const totalTableWidth = visibleColumns.reduce( - (sum, col) => sum + (widths[col.key] ?? col.defaultWidth), + (sum, col) => sum + (widthsRef.current[col.key] ?? widths[col.key] ?? col.defaultWidth), 0 ); @@ -678,7 +699,7 @@ export function EventsView({ return (
- {loading ?
: events.items.length ? ( + {loading && events.items.length === 0 ?
: events.items.length ? (
{visibleColumns.map((col) => ( - + ))} @@ -697,7 +718,7 @@ export function EventsView({
{label} @@ -710,7 +731,7 @@ export function EventsView({ aria-orientation="vertical" aria-valuemin={col.minWidth} aria-valuemax={800} - aria-valuenow={widths[col.key]} + aria-valuenow={widthsRef.current[col.key] ?? widths[col.key]} onPointerDown={(e) => handleResizeStart(col.key, e)} onDoubleClick={(e) => resetSingleColumn(col.key, e)} onKeyDown={(event) => handleResizeKeyDown(col.key, event)} @@ -749,7 +770,7 @@ export function EventsView({ )} - {!loading && events.items.length > 0 ? : null} + {events.items.length > 0 ? : null}
{t('usage.events.rangeSummary', { start: startRecordNum, end: endRecordNum, total: compactNumber(events.total) })} diff --git a/src/pages/UsageRecordsPage.tsx b/src/pages/UsageRecordsPage.tsx index d341ec0..32c3cef 100644 --- a/src/pages/UsageRecordsPage.tsx +++ b/src/pages/UsageRecordsPage.tsx @@ -270,6 +270,7 @@ export function UsageRecordsPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [loadedScopeKey, setLoadedScopeKey] = useState(''); + const loadedFilterScopeKeyRef = useRef(''); const requestIdRef = useRef(0); const schedulerRef = useRef | null>(null); if (!schedulerRef.current) schedulerRef.current = createRefreshScheduler(250); @@ -311,6 +312,20 @@ export function UsageRecordsPage() { }; }, [apiKeyHash, customEnd, customStart, model, provider, range, result, source]); + const filterScopeKey = useMemo(() => usageViewScopeKey({ + tab: 'overview', + range, + customStart, + customEnd, + model: '', + provider: '', + source: '', + apiKeyHash: '', + result: 'all', + page: 1, + pageSize: 1, + }), [customEnd, customStart, range]); + const scopeKey = useMemo(() => usageViewScopeKey({ tab: activeTab, range, @@ -344,7 +359,10 @@ export function UsageRecordsPage() { if (!quiet) setLoading(true); try { const statusRequest = invoke('get_usage_collector_status'); - const optionsRequest = invoke('get_usage_analysis', { query: timeQuery }); + const filtersChanged = loadedFilterScopeKeyRef.current !== filterScopeKey; + const optionsRequest = filtersChanged + ? invoke('get_usage_analysis', { query: timeQuery }) + : Promise.resolve(null); if (activeTab === 'overview') { const [nextStatus, nextOptions, nextOverview] = await Promise.all([ statusRequest, @@ -353,7 +371,7 @@ export function UsageRecordsPage() { ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setOverview(nextOverview); setOverviewRange(timeQuery); } else if (activeTab === 'analysis') { @@ -361,13 +379,13 @@ export function UsageRecordsPage() { statusRequest, optionsRequest, invoke('get_usage_overview', { query }), - model || provider || source || apiKeyHash || result !== 'all' + model || provider || source || apiKeyHash || result !== 'all' || !filtersChanged ? invoke('get_usage_analysis', { query }) - : optionsRequest, + : optionsRequest.then((analysis) => analysis ?? emptyAnalysis), ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setOverview(nextOverview); setOverviewRange(timeQuery); setAnalysis(nextAnalysis); @@ -381,7 +399,7 @@ export function UsageRecordsPage() { ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setEvents(nextEvents); } else if (activeTab === 'pricing') { const [nextStatus, nextOptions, nextPricing] = await Promise.all([ @@ -391,15 +409,16 @@ export function UsageRecordsPage() { ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setPricing(nextPricing); } else { const [nextStatus, nextOptions] = await Promise.all([statusRequest, optionsRequest]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); } setLoadedScopeKey(scopeKey); + if (filtersChanged) loadedFilterScopeKeyRef.current = filterScopeKey; setError(''); } catch (requestError) { if (requestId === requestIdRef.current) setError(String(requestError)); @@ -407,7 +426,7 @@ export function UsageRecordsPage() { if (requestId === requestIdRef.current) setLoading(false); } }, - [activeTab, buildQueries, page, pageSize, model, provider, source, apiKeyHash, result, scopeKey] + [activeTab, buildQueries, filterScopeKey, page, pageSize, model, provider, source, apiKeyHash, result, scopeKey] ); const loadData = useCallback( @@ -720,8 +739,8 @@ export function UsageRecordsPage() { {hasCurrentSnapshot && activeTab === 'analysis' ? : null} {activeTab === 'events' ? (