perf(usage): keep request pages responsive while paging and resizing

Keep the current event rows visible while the next page loads, and refresh filter options only when the time range changes. Resize columns directly in the DOM and commit the width when the drag ends.
This commit is contained in:
lzt404 committed 2026-10-06 01:22:41 +08:00
1 parent 830fc2c1cc
commit ffdc53c629
3 files changed
+89 -46

No files matched your search

+54 -33
View File
@@ -487,6 +487,7 @@ export function EventsView({
}) {
const { t } = useI18n();
const [widths, setWidths] = useState<Record<EventColumnKey, number>>(getInitialColumnWidths);
const widthsRef = useRef(widths);
const [visibleColumnKeys, setVisibleColumnKeys] = useState<EventColumnKey[]>(getInitialVisibleColumns);
const [columnSettingsOpen, setColumnSettingsOpen] = useState(false);
const [draftVisibleColumnKeys, setDraftVisibleColumnKeys] = useState<EventColumnKey[]>(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<EventColumnKey, number>) => {
widthsRef.current = next;
setWidths(next);
};
const resetAllWidths = () => {
const defaults: Record<EventColumnKey, number> = {} 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<HTMLDivElement>) => {
@@ -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<HTMLElement>(`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 (
<section className={`panel usage-events-panel usage-request-log${isCompactDefault ? ' usage-events-compact' : ''}`} aria-label={t('usage.events.title')} aria-busy={loading}>
{loading ? <div className="usage-empty" role="status"><Database size={20} aria-hidden="true" /><span>{t('usage.loading')}</span></div> : events.items.length ? (
{loading && events.items.length === 0 ? <div className="usage-empty" role="status"><Database size={20} aria-hidden="true" /><span>{t('usage.loading')}</span></div> : events.items.length ? (
<div ref={tableWrapRef} className="usage-table-wrap" tabIndex={0} role="region" aria-label={t('usage.events.title')}>
<table
className="usage-events-table"
@@ -686,7 +707,7 @@ export function EventsView({
>
<colgroup>
{visibleColumns.map((col) => (
<col key={col.key} style={{ width: `${widths[col.key]}px` }} />
<col key={col.key} data-column={col.key} style={{ width: `${widthsRef.current[col.key] ?? widths[col.key]}px` }} />
))}
</colgroup>
<thead>
@@ -697,7 +718,7 @@ export function EventsView({
<th
key={col.key}
className={`usage-th-${col.key} align-${col.align}`}
style={{ width: `${widths[col.key]}px` }}
style={{ width: `${widthsRef.current[col.key] ?? widths[col.key]}px` }}
>
<div className="usage-th-content" title={label}>
<span>{label}</span>
@@ -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({
<UsageEmpty />
)}
{!loading && events.items.length > 0 ? <TableTopScrollbar tableWrapRef={tableWrapRef} /> : null}
{events.items.length > 0 ? <TableTopScrollbar tableWrapRef={tableWrapRef} /> : null}
<div className="usage-events-footer">
<span className="usage-pagination-summary">{t('usage.events.rangeSummary', { start: startRecordNum, end: endRecordNum, total: compactNumber(events.total) })}</span>
+30 -11
View File
@@ -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<ReturnType<typeof createRefreshScheduler> | 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<CollectorStatus>('get_usage_collector_status');
const optionsRequest = invoke<UsageAnalysis>('get_usage_analysis', { query: timeQuery });
const filtersChanged = loadedFilterScopeKeyRef.current !== filterScopeKey;
const optionsRequest = filtersChanged
? invoke<UsageAnalysis>('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<UsageOverview>('get_usage_overview', { query }),
model || provider || source || apiKeyHash || result !== 'all'
model || provider || source || apiKeyHash || result !== 'all' || !filtersChanged
? invoke<UsageAnalysis>('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' ? <UsageAnalysisView analysis={analysis} overview={overview} range={overviewRange} /> : null}
{activeTab === 'events' ? (
<EventsView
events={hasCurrentSnapshot && events ? events : { items: [], total: 0, page, pageSize, totalPages: 1 }}
loading={showInitialLoading}
events={events ?? { items: [], total: 0, page, pageSize, totalPages: 1 }}
loading={!events || (loading && !hasCurrentSnapshot)}
pageSize={pageSize}
query={buildQueries().query}
onPage={setPage}
+5 -2
View File
@@ -203,7 +203,11 @@
border: 0;
background: var(--usage-log-surface);
color: var(--usage-log-text);
box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
box-shadow: inset 0 -1px 0 var(--usage-log-line);
}
body.table-col-resizing .usage-records-page .usage-request-log .usage-events-table tbody tr {
pointer-events: none;
}
.usage-records-page .usage-request-log .usage-events-table tbody td:first-child {
@@ -216,7 +220,6 @@
.usage-records-page .usage-request-log .usage-events-table tbody tr:hover td {
background: var(--usage-log-hover) !important;
box-shadow: 0 8px 22px rgb(16 24 40 / 7%);
}
.usage-records-page .usage-request-log .usage-th-total,