Skip to content

Commit 8c95b9e

Browse files
authored
feat: add search history for search page (#749)
- save search history for search page - record number limit to 10 - add search history relate function - support both sql and lucene search - test https://github.com/user-attachments/assets/67bbc4ce-999d-494d-9fa4-1f085c2fbf8d ref: hdx-1565
1 parent b16c8e1 commit 8c95b9e

File tree

7 files changed

+250
-12
lines changed

7 files changed

+250
-12
lines changed

.changeset/sweet-kiwis-cheer.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
Add search history

packages/app/src/AutocompleteInput.tsx

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import Fuse from 'fuse.js';
33
import { OverlayTrigger } from 'react-bootstrap';
4-
import { TextInput } from '@mantine/core';
4+
import { TextInput, UnstyledButton } from '@mantine/core';
5+
6+
import { useQueryHistory } from '@/utils';
57

68
import InputLanguageSwitch from './components/InputLanguageSwitch';
79
import { useDebounce } from './utils';
@@ -22,6 +24,7 @@ export default function AutocompleteInput({
2224
language,
2325
showHotkey,
2426
onSubmit,
27+
queryHistoryType,
2528
}: {
2629
inputRef: React.RefObject<HTMLInputElement>;
2730
value: string;
@@ -38,21 +41,46 @@ export default function AutocompleteInput({
3841
onLanguageChange?: (language: 'sql' | 'lucene') => void;
3942
language?: 'sql' | 'lucene';
4043
showHotkey?: boolean;
44+
queryHistoryType?: string;
4145
}) {
4246
const suggestionsLimit = 10;
4347

4448
const [isSearchInputFocused, setIsSearchInputFocused] = useState(false);
4549
const [isInputDropdownOpen, setIsInputDropdownOpen] = useState(false);
50+
const [showSearchHistory, setShowSearchHistory] = useState(false);
4651

4752
const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] =
4853
useState(-1);
4954

55+
const [selectedQueryHistoryIndex, setSelectedQueryHistoryIndex] =
56+
useState(-1);
57+
// query search history
58+
const [queryHistory, setQueryHistory] = useQueryHistory(queryHistoryType);
59+
const queryHistoryList = useMemo(() => {
60+
if (!queryHistoryType || !queryHistory) return [];
61+
return queryHistory.map(q => {
62+
return {
63+
value: q,
64+
label: q,
65+
};
66+
});
67+
}, [queryHistory]);
68+
5069
useEffect(() => {
5170
if (isSearchInputFocused) {
5271
setIsInputDropdownOpen(true);
5372
}
5473
}, [isSearchInputFocused]);
5574

75+
useEffect(() => {
76+
// only show search history when: 1.no input, 2.has search type, 3.has history list
77+
if (value.length === 0 && queryHistoryList.length > 0 && queryHistoryType) {
78+
setShowSearchHistory(true);
79+
} else {
80+
setShowSearchHistory(false);
81+
}
82+
}, [value, queryHistoryType, queryHistoryList]);
83+
5684
const fuse = useMemo(
5785
() =>
5886
new Fuse(autocompleteOptions ?? [], {
@@ -74,6 +102,14 @@ export default function AutocompleteInput({
74102
return fuse.search(lastToken).map(result => result.item);
75103
}, [debouncedValue, fuse, autocompleteOptions, showSuggestionsOnEmpty]);
76104

105+
const onSelectSearchHistory = (query: string) => {
106+
setSelectedQueryHistoryIndex(-1);
107+
onChange(query); // update inputText bar
108+
setQueryHistory(query); // update history order
109+
setIsInputDropdownOpen(false); // close dropdown since we execute search
110+
onSubmit?.(); // search
111+
};
112+
77113
const onAcceptSuggestion = (suggestion: string) => {
78114
setSelectedAutocompleteIndex(-1);
79115

@@ -154,6 +190,29 @@ export default function AutocompleteInput({
154190
{belowSuggestions}
155191
</div>
156192
)}
193+
<div>
194+
{showSearchHistory && (
195+
<div className="border-top border-dark fs-8 py-2">
196+
<div className="text-muted fs-8 fw-bold me-1 px-3">
197+
Search History:
198+
</div>
199+
{queryHistoryList.map(({ value, label }, i) => {
200+
return (
201+
<UnstyledButton
202+
className={`d-block w-100 text-start text-muted fw-normal px-3 py-2 fs-8 ${
203+
selectedQueryHistoryIndex === i ? 'bg-hdx-dark' : ''
204+
}`}
205+
key={value}
206+
onMouseOver={() => setSelectedQueryHistoryIndex(i)}
207+
onClick={() => onSelectSearchHistory(value)}
208+
>
209+
<span className="me-1 text-truncate">{label}</span>
210+
</UnstyledButton>
211+
);
212+
})}
213+
</div>
214+
)}
215+
</div>
157216
</div>
158217
)}
159218
popperConfig={{
@@ -179,10 +238,12 @@ export default function AutocompleteInput({
179238
onChange={e => onChange(e.target.value)}
180239
onFocus={() => {
181240
setSelectedAutocompleteIndex(-1);
241+
setSelectedQueryHistoryIndex(-1);
182242
setIsSearchInputFocused(true);
183243
}}
184244
onBlur={() => {
185245
setSelectedAutocompleteIndex(-1);
246+
setSelectedQueryHistoryIndex(-1);
186247
setIsSearchInputFocused(false);
187248
}}
188249
onKeyDown={e => {
@@ -213,6 +274,9 @@ export default function AutocompleteInput({
213274
suggestedProperties[selectedAutocompleteIndex].value,
214275
);
215276
} else {
277+
if (queryHistoryType) {
278+
setQueryHistory(value);
279+
}
216280
onSubmit?.();
217281
}
218282
}

packages/app/src/DBSearchPage.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ import {
8787
useSources,
8888
} from '@/source';
8989
import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery';
90-
import { usePrevious } from '@/utils';
90+
import { QUERY_LOCAL_STORAGE, usePrevious } from '@/utils';
9191

9292
import { SQLPreview } from './components/ChartSQLPreview';
9393
import { useSqlSuggestions } from './hooks/useSqlSuggestions';
@@ -1146,6 +1146,7 @@ function DBSearchPage() {
11461146
language="sql"
11471147
onSubmit={onSubmit}
11481148
label="WHERE"
1149+
queryHistoryType={QUERY_LOCAL_STORAGE.SEARCH_SQL}
11491150
enableHotkey
11501151
/>
11511152
}
@@ -1159,8 +1160,10 @@ function DBSearchPage() {
11591160
shouldDirty: true,
11601161
})
11611162
}
1163+
onSubmit={onSubmit}
11621164
language="lucene"
11631165
placeholder="Search your events w/ Lucene ex. column:foo"
1166+
queryHistoryType={QUERY_LOCAL_STORAGE.SEARCH_LUCENE}
11641167
enableHotkey
11651168
/>
11661169
}

packages/app/src/SearchInputV2.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export default function SearchInputV2({
3333
enableHotkey,
3434
onSubmit,
3535
additionalSuggestions,
36+
queryHistoryType,
3637
...props
3738
}: {
3839
tableConnections?: TableConnection | TableConnection[];
@@ -44,6 +45,7 @@ export default function SearchInputV2({
4445
enableHotkey?: boolean;
4546
onSubmit?: () => void;
4647
additionalSuggestions?: string[];
48+
queryHistoryType?: string;
4749
} & UseControllerProps<any>) {
4850
const {
4951
field: { onChange, value },
@@ -91,6 +93,7 @@ export default function SearchInputV2({
9193
showHotkey={enableHotkey}
9294
onLanguageChange={onLanguageChange}
9395
onSubmit={onSubmit}
96+
queryHistoryType={queryHistoryType}
9497
aboveSuggestions={
9598
<>
9699
<div className="text-muted fs-8 fw-bold me-1">Searching for:</div>

packages/app/src/__tests__/utils.test.ts

+65
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
formatDate,
99
formatNumber,
1010
getMetricTableName,
11+
useQueryHistory,
1112
} from '../utils';
1213

1314
describe('utils', () => {
@@ -471,3 +472,67 @@ describe('useLocalStorage', () => {
471472
expect(localStorageMock.getItem).not.toHaveBeenCalled();
472473
});
473474
});
475+
476+
describe('useQueryHistory', () => {
477+
const mockGetItem = jest.fn();
478+
const mockSetItem = jest.fn();
479+
const mockRemoveItem = jest.fn();
480+
const originalLocalStorage = window.localStorage;
481+
482+
beforeEach(() => {
483+
mockGetItem.mockClear();
484+
mockSetItem.mockClear();
485+
mockRemoveItem.mockClear();
486+
mockGetItem.mockReturnValue('["service = test3","service = test1"]');
487+
Object.defineProperty(window, 'localStorage', {
488+
value: {
489+
getItem: (...args: string[]) => mockGetItem(...args),
490+
setItem: (...args: string[]) => mockSetItem(...args),
491+
removeItem: (...args: string[]) => mockRemoveItem(...args),
492+
},
493+
});
494+
});
495+
496+
afterEach(() => {
497+
jest.restoreAllMocks();
498+
Object.defineProperty(window, 'localStorage', {
499+
value: originalLocalStorage,
500+
configurable: true,
501+
});
502+
});
503+
504+
it('adds new query', () => {
505+
const { result } = renderHook(() => useQueryHistory('searchSQL'));
506+
const setQueryHistory = result.current[1];
507+
act(() => {
508+
setQueryHistory('service = test2');
509+
});
510+
511+
expect(mockSetItem).toHaveBeenCalledWith(
512+
'QuerySearchHistory.searchSQL',
513+
'["service = test2","service = test3","service = test1"]',
514+
);
515+
});
516+
517+
it('does not add duplicate query, but change the order to front', () => {
518+
const { result } = renderHook(() => useQueryHistory('searchSQL'));
519+
const setQueryHistory = result.current[1];
520+
act(() => {
521+
setQueryHistory('service = test1');
522+
});
523+
524+
expect(mockSetItem).toHaveBeenCalledWith(
525+
'QuerySearchHistory.searchSQL',
526+
'["service = test1","service = test3"]',
527+
);
528+
});
529+
530+
it('does not add empty query', () => {
531+
const { result } = renderHook(() => useQueryHistory('searchSQL'));
532+
const setQueryHistory = result.current[1];
533+
act(() => {
534+
setQueryHistory(' '); // empty after trim
535+
});
536+
expect(mockSetItem).not.toBeCalled();
537+
});
538+
});

0 commit comments

Comments
 (0)