fix laggy performance for search page (#1423)

Fixes HDX-2896
This commit is contained in:
Aaron Knudtson 2025-12-02 15:08:43 -05:00 committed by GitHub
parent ea25cc5d43
commit 2f25ce6fa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 178 additions and 128 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: laggy performance across app

View file

@ -123,6 +123,7 @@ import { SearchConfig } from './types';
import searchPageStyles from '../styles/SearchPage.module.scss';
const ALLOWED_SOURCE_KINDS = [SourceKind.Log, SourceKind.Trace];
const SearchConfigSchema = z.object({
select: z.string(),
source: z.string(),
@ -1075,19 +1076,26 @@ function DBSearchPage() {
};
}, [chartConfig, searchedTimeRange]);
const displayedColumns = splitAndTrimWithBracket(
dbSqlRowTableConfig?.select ??
searchedSource?.defaultTableSelectExpression ??
'',
const displayedColumns = useMemo(
() =>
splitAndTrimWithBracket(
dbSqlRowTableConfig?.select ??
searchedSource?.defaultTableSelectExpression ??
'',
),
[dbSqlRowTableConfig?.select, searchedSource?.defaultTableSelectExpression],
);
const toggleColumn = (column: string) => {
const newSelectArray = displayedColumns.includes(column)
? displayedColumns.filter(s => s !== column)
: [...displayedColumns, column];
setValue('select', newSelectArray.join(', '));
onSubmit();
};
const toggleColumn = useCallback(
(column: string) => {
const newSelectArray = displayedColumns.includes(column)
? displayedColumns.filter(s => s !== column)
: [...displayedColumns, column];
setValue('select', newSelectArray.join(', '));
onSubmit();
},
[displayedColumns, setValue, onSubmit],
);
const generateSearchUrl = useCallback(
({
@ -1277,6 +1285,38 @@ function DBSearchPage() {
const [isDrawerChildModalOpen, setDrawerChildModalOpen] = useState(false);
const rowTableContext = useMemo(
() => ({
onPropertyAddClick: searchFilters.setFilterValue,
displayedColumns,
toggleColumn,
generateSearchUrl,
dbSqlRowTableConfig,
isChildModalOpen: isDrawerChildModalOpen,
setChildModalOpen: setDrawerChildModalOpen,
source: searchedSource,
}),
[
searchFilters.setFilterValue,
searchedSource,
dbSqlRowTableConfig,
displayedColumns,
toggleColumn,
generateSearchUrl,
isDrawerChildModalOpen,
],
);
const inputSourceTableConnection = useMemo(
() => tcFromSource(inputSourceObj),
[inputSourceObj],
);
const sourceSchemaPreview = useMemo(
() => <SourceSchemaPreview source={inputSourceObj} variant="text" />,
[inputSourceObj],
);
return (
<Flex direction="column" h="100vh" style={{ overflow: 'hidden' }}>
<Head>
@ -1307,11 +1347,9 @@ function DBSearchPage() {
control={control}
name="source"
onCreate={openNewSourceModal}
allowedSourceKinds={[SourceKind.Log, SourceKind.Trace]}
allowedSourceKinds={ALLOWED_SOURCE_KINDS}
data-testid="source-selector"
sourceSchemaPreview={
<SourceSchemaPreview source={inputSourceObj} variant="text" />
}
sourceSchemaPreview={sourceSchemaPreview}
/>
<Menu withArrow position="bottom-start">
<Menu.Target>
@ -1358,7 +1396,7 @@ function DBSearchPage() {
</Group>
<Box style={{ minWidth: 100, flexGrow: 1 }}>
<SQLInlineEditorControlled
tableConnection={tcFromSource(inputSourceObj)}
tableConnection={inputSourceTableConnection}
control={control}
name="select"
defaultValue={inputSourceObj?.defaultTableSelectExpression}
@ -1372,7 +1410,7 @@ function DBSearchPage() {
</Box>
<Box style={{ maxWidth: 400, width: '20%' }}>
<SQLInlineEditorControlled
tableConnection={tcFromSource(inputSourceObj)}
tableConnection={inputSourceTableConnection}
control={control}
name="orderBy"
defaultValue={defaultOrderBy}
@ -1834,16 +1872,7 @@ function DBSearchPage() {
dbSqlRowTableConfig &&
analysisMode === 'results' && (
<DBSqlRowTableWithSideBar
context={{
onPropertyAddClick: searchFilters.setFilterValue,
displayedColumns,
toggleColumn,
generateSearchUrl,
dbSqlRowTableConfig,
isChildModalOpen: isDrawerChildModalOpen,
setChildModalOpen: setDrawerChildModalOpen,
source: searchedSource,
}}
context={rowTableContext}
config={dbSqlRowTableConfig}
sourceId={searchedConfig.source}
onSidebarOpen={onSidebarOpen}

View file

@ -618,7 +618,10 @@ export const RawLogTable = memo(
const rowVirtualizer = useVirtualizer({
count: _rows.length,
// count: hasNextPage ? allRows.length + 1 : allRows.length,
getScrollElement: () => tableContainerRef.current,
getScrollElement: useCallback(
() => tableContainerRef.current,
[tableContainerRef],
),
estimateSize: useCallback(() => 23, []),
overscan: 30,
paddingEnd: 20,
@ -897,6 +900,7 @@ export const RawLogTable = memo(
>
<div className={styles.fieldTextContainer}>
<DBRowTableFieldWithPopover
key={cell.id}
cellValue={cellValue}
wrapLinesEnabled={wrapLinesEnabled}
tableContainerRef={tableContainerRef}

View file

@ -192,6 +192,13 @@ const createStyleTheme = (allowMultiline: boolean = false) =>
},
});
const cmBasicSetup = {
lineNumbers: false,
foldGutter: false,
highlightActiveLine: false,
highlightActiveLineGutter: false,
};
export default function SQLInlineEditor({
tableConnection,
tableConnections,
@ -355,6 +362,54 @@ export default function SQLInlineEditor({
];
}, [parentRef]);
const cmExtensions = useMemo(
() => [
...tooltipExt,
createStyleTheme(allowMultiline),
...(allowMultiline ? [EditorView.lineWrapping] : []),
compartmentRef.current.of(
sql({
upperCaseKeywords: true,
}),
),
Prec.highest(
keymap.of([
{
key: 'Enter',
run: view => {
if (onSubmit == null) {
return false;
}
if (queryHistoryType && ref?.current?.view) {
setQueryHistory(ref?.current?.view.state.doc.toString());
}
onSubmit();
return true;
},
},
...(allowMultiline
? [
{
key: 'Shift-Enter',
run: () => {
// Allow default behavior (insert new line)
return false;
},
},
]
: []),
]),
),
keymap.of([
{
key: 'Tab',
run: acceptCompletion,
},
]),
],
[allowMultiline, onSubmit, queryHistoryType, setQueryHistory, tooltipExt],
);
return (
<Paper
flex="auto"
@ -393,65 +448,15 @@ export default function SQLInlineEditor({
value={value}
onChange={onChange}
theme={colorScheme === 'dark' ? 'dark' : 'light'}
onFocus={() => {
onFocus={useCallback(() => {
setIsFocused(true);
}}
onBlur={() => {
}, [setIsFocused])}
onBlur={useCallback(() => {
setIsFocused(false);
}}
extensions={[
...tooltipExt,
createStyleTheme(allowMultiline),
...(allowMultiline ? [EditorView.lineWrapping] : []),
compartmentRef.current.of(
sql({
upperCaseKeywords: true,
}),
),
Prec.highest(
keymap.of([
{
key: 'Enter',
run: view => {
if (onSubmit == null) {
return false;
}
if (queryHistoryType && ref?.current?.view) {
setQueryHistory(ref?.current?.view.state.doc.toString());
}
onSubmit();
return true;
},
},
...(allowMultiline
? [
{
key: 'Shift-Enter',
run: () => {
// Allow default behavior (insert new line)
return false;
},
},
]
: []),
]),
),
keymap.of([
{
key: 'Tab',
run: acceptCompletion,
},
]),
]}
onCreateEditor={view => {
updateAutocompleteColumns(view);
}}
basicSetup={{
lineNumbers: false,
foldGutter: false,
highlightActiveLine: false,
highlightActiveLineGutter: false,
}}
}, [setIsFocused])}
extensions={cmExtensions}
onCreateEditor={updateAutocompleteColumns}
basicSetup={cmBasicSetup}
placeholder={placeholder}
onClick={() => {
if (ref?.current?.view) {

View file

@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import { formatDistanceToNowStrict } from 'date-fns';
import numbro from 'numbro';
import type { MutableRefObject } from 'react';
import type { MutableRefObject, SetStateAction } from 'react';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { dateRangeToString } from './timeQuery';
@ -260,55 +260,62 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: T | ((prevState: T) => T)) => {
if (typeof window === 'undefined') {
return;
}
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
// Fire off event so other localStorage hooks listening with the same key
// will update
const event = new CustomEvent<CustomStorageChangeDetail>(
'customStorage',
{
detail: {
key,
instanceId,
const setValue = useCallback(
(value: SetStateAction<T>) => {
if (typeof window === 'undefined') {
return;
}
try {
// Allow value to be a function so we have same API as useState
// Save state
setStoredValue(prev => {
const newValue = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(newValue));
return newValue;
});
// Fire off event so other localStorage hooks listening with the same key
// will update
const event = new CustomEvent<CustomStorageChangeDetail>(
'customStorage',
{
detail: {
key,
instanceId,
},
},
},
);
window.dispatchEvent(event);
} catch (error) {
// A more advanced implementation would handle the error case
// eslint-disable-next-line no-console
console.log(error);
}
};
);
window.dispatchEvent(event);
} catch (error) {
// A more advanced implementation would handle the error case
// eslint-disable-next-line no-console
console.log(error);
}
},
[instanceId, key],
);
return [storedValue, setValue] as const;
}
export function useQueryHistory<T>(type: string | undefined) {
const key = `${QUERY_LOCAL_STORAGE.KEY}.${type}`;
const [queryHistory, _setQueryHistory] = useLocalStorage<string[]>(key, []);
const setQueryHistory = (query: string) => {
// do not set up anything if there is no type or empty query
try {
const trimmed = query.trim();
if (!type || !trimmed) return null;
const deduped = [trimmed, ...queryHistory.filter(q => q !== trimmed)];
const limited = deduped.slice(0, QUERY_LOCAL_STORAGE.LIMIT);
_setQueryHistory(limited);
} catch (e) {
// eslint-disable-next-line no-console
console.log(`Failed to cache query history, error ${e.message}`);
}
};
const setQueryHistory = useCallback(
(query: string) => {
// do not set up anything if there is no type or empty query
try {
const trimmed = query.trim();
if (!type || !trimmed) return null;
const deduped = [trimmed, ...queryHistory.filter(q => q !== trimmed)];
const limited = deduped.slice(0, QUERY_LOCAL_STORAGE.LIMIT);
_setQueryHistory(limited);
} catch (e) {
// eslint-disable-next-line no-console
console.log(`Failed to cache query history, error ${e.message}`);
}
},
[_setQueryHistory, queryHistory, type],
);
return [queryHistory, setQueryHistory] as const;
}