Skip to content

Commit 092a292

Browse files
authored
fix: autocomplete for lucene column values (#720)
![image](https://github.com/user-attachments/assets/14048d5c-a88b-46ef-b15b-1412791923de) Added autocomplete for potential search values for lucene where clauses. Added testing for useAutoCompleteOptions and useGetKeyValues. Ref: HDX-1509
1 parent 56e39dc commit 092a292

13 files changed

+672
-45
lines changed

.changeset/fifty-pugs-nail.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
fix: autocomplete for key-values complete for v2 lucene

packages/app/src/NamespaceDetailsSidePanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ export default function NamespaceDetailsSidePanel({
278278

279279
const { data: logServiceNames } = useGetKeyValues(
280280
{
281-
chartConfig: {
281+
chartConfigs: {
282282
from: logSource.from,
283283
where: `${logSource?.resourceAttributesExpression}.k8s.namespace.name:"${namespaceName}"`,
284284
whereLanguage: 'lucene',

packages/app/src/NodeDetailsSidePanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ export default function NodeDetailsSidePanel({
297297

298298
const { data: logServiceNames } = useGetKeyValues(
299299
{
300-
chartConfig: {
300+
chartConfigs: {
301301
from: logSource.from,
302302
where: `${logSource?.resourceAttributesExpression}.k8s.node.name:"${nodeName}"`,
303303
whereLanguage: 'lucene',

packages/app/src/PodDetailsSidePanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export default function PodDetailsSidePanel({
280280

281281
const { data: logServiceNames } = useGetKeyValues(
282282
{
283-
chartConfig: {
283+
chartConfigs: {
284284
from: logSource.from,
285285
where: `${logSource?.resourceAttributesExpression}.k8s.pod.name:"${podName}"`,
286286
whereLanguage: 'lucene',

packages/app/src/SearchInputV2.tsx

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1-
import { useEffect, useMemo, useRef, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22
import { useController, UseControllerProps } from 'react-hook-form';
33
import { useHotkeys } from 'react-hotkeys-hook';
4-
import { TableConnection } from '@hyperdx/common-utils/dist/metadata';
4+
import { Field, TableConnection } from '@hyperdx/common-utils/dist/metadata';
55
import { genEnglishExplanation } from '@hyperdx/common-utils/dist/queryParser';
66

77
import AutocompleteInput from '@/AutocompleteInput';
8-
import { useAllFields } from '@/hooks/useMetadata';
8+
9+
import {
10+
ILanguageFormatter,
11+
useAutoCompleteOptions,
12+
} from './hooks/useAutoCompleteOptions';
13+
14+
export class LuceneLanguageFormatter implements ILanguageFormatter {
15+
formatFieldValue(f: Field): string {
16+
return f.path.join('.');
17+
}
18+
formatFieldLabel(f: Field): string {
19+
return `${f.path.join('.')} (${f.jsType})`;
20+
}
21+
formatKeyValPair(key: string, value: string): string {
22+
return `${key}:"${value}"`;
23+
}
24+
}
925

1026
export default function SearchInputV2({
1127
tableConnections,
@@ -34,31 +50,17 @@ export default function SearchInputV2({
3450
} = useController(props);
3551

3652
const ref = useRef<HTMLInputElement>(null);
37-
38-
const { data: fields } = useAllFields(tableConnections ?? [], {
39-
enabled:
40-
!!tableConnections &&
41-
(Array.isArray(tableConnections) ? tableConnections.length > 0 : true),
42-
});
43-
44-
const autoCompleteOptions = useMemo(() => {
45-
const _columns = (fields ?? []).filter(c => c.jsType !== null);
46-
const baseOptions = _columns.map(c => ({
47-
value: c.path.join('.'),
48-
label: `${c.path.join('.')} (${c.jsType})`,
49-
}));
50-
51-
const suggestionOptions =
52-
additionalSuggestions?.map(column => ({
53-
value: column,
54-
label: column,
55-
})) ?? [];
56-
57-
return [...baseOptions, ...suggestionOptions];
58-
}, [fields, additionalSuggestions]);
59-
6053
const [parsedEnglishQuery, setParsedEnglishQuery] = useState<string>('');
6154

55+
const autoCompleteOptions = useAutoCompleteOptions(
56+
new LuceneLanguageFormatter(),
57+
value,
58+
{
59+
tableConnections,
60+
additionalSuggestions,
61+
},
62+
);
63+
6264
useEffect(() => {
6365
genEnglishExplanation(value).then(q => {
6466
setParsedEnglishQuery(q);

packages/app/src/components/DBSearchPageFilters.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ export const DBSearchPageFilters = ({
380380
isLoading: isFacetsLoading,
381381
isFetching: isFacetsFetching,
382382
} = useGetKeyValues({
383-
chartConfig: { ...chartConfig, dateRange },
383+
chartConfigs: { ...chartConfig, dateRange },
384384
keys: datum,
385385
});
386386

packages/app/src/components/MetricNameSelect.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,19 @@ function useMetricNames(metricSource: TSource) {
4242
}, [metricSource]);
4343

4444
const { data: gaugeMetrics } = useGetKeyValues({
45-
chartConfig: gaugeConfig,
45+
chartConfigs: gaugeConfig,
4646
keys: ['MetricName'],
4747
limit: MAX_METRIC_NAME_OPTIONS,
4848
disableRowLimit: true,
4949
});
5050
// const { data: histogramMetrics } = useGetKeyValues({
51-
// chartConfig: histogramConfig,
51+
// chartConfigs: histogramConfig,
5252
// keys: ['MetricName'],
5353
// limit: MAX_METRIC_NAME_OPTIONS,
5454
// disableRowLimit: true,
5555
// });
5656
const { data: sumMetrics } = useGetKeyValues({
57-
chartConfig: sumConfig,
57+
chartConfigs: sumConfig,
5858
keys: ['MetricName'],
5959
limit: MAX_METRIC_NAME_OPTIONS,
6060
disableRowLimit: true,
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { JSDataType } from '@hyperdx/common-utils/dist/clickhouse';
2+
import { Field } from '@hyperdx/common-utils/dist/metadata';
3+
import { renderHook } from '@testing-library/react';
4+
5+
import { LuceneLanguageFormatter } from '../../SearchInputV2';
6+
import { useAutoCompleteOptions } from '../useAutoCompleteOptions';
7+
import { useAllFields, useGetKeyValues } from '../useMetadata';
8+
9+
if (!globalThis.structuredClone) {
10+
globalThis.structuredClone = (obj: any) => {
11+
return JSON.parse(JSON.stringify(obj));
12+
};
13+
}
14+
15+
// Mock dependencies
16+
jest.mock('../useMetadata', () => ({
17+
...jest.requireActual('../useMetadata.tsx'),
18+
useAllFields: jest.fn(),
19+
useGetKeyValues: jest.fn(),
20+
}));
21+
22+
const luceneFormatter = new LuceneLanguageFormatter();
23+
24+
const mockFields: Field[] = [
25+
{
26+
path: ['ResourceAttributes'],
27+
jsType: JSDataType.Map,
28+
type: 'map',
29+
},
30+
{
31+
path: ['ResourceAttributes', 'service.name'],
32+
jsType: JSDataType.String,
33+
type: 'string',
34+
},
35+
{
36+
path: ['TraceAttributes', 'trace.id'],
37+
jsType: JSDataType.String,
38+
type: 'string',
39+
},
40+
];
41+
42+
const mockTableConnections = [
43+
{
44+
databaseName: 'test_db',
45+
tableName: 'traces',
46+
connectionId: 'conn1',
47+
},
48+
];
49+
50+
describe('useAutoCompleteOptions', () => {
51+
beforeEach(() => {
52+
// Reset mocks before each test
53+
jest.clearAllMocks();
54+
55+
// Setup default mock implementations
56+
(useAllFields as jest.Mock).mockReturnValue({
57+
data: mockFields,
58+
});
59+
60+
(useGetKeyValues as jest.Mock).mockReturnValue({
61+
data: null,
62+
});
63+
});
64+
65+
it('should return field options with correct lucene formatting', () => {
66+
const { result } = renderHook(() =>
67+
useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', {
68+
tableConnections: mockTableConnections,
69+
}),
70+
);
71+
72+
expect(result.current).toEqual([
73+
{
74+
value: 'ResourceAttributes',
75+
label: 'ResourceAttributes (map)',
76+
},
77+
{
78+
value: 'ResourceAttributes.service.name',
79+
label: 'ResourceAttributes.service.name (string)',
80+
},
81+
{
82+
value: 'TraceAttributes.trace.id',
83+
label: 'TraceAttributes.trace.id (string)',
84+
},
85+
]);
86+
});
87+
88+
it('should return key value options with correct lucene formatting', () => {
89+
const mockKeyValues = [
90+
{
91+
key: 'ResourceAttributes.service.name',
92+
value: ['frontend', 'backend'],
93+
},
94+
];
95+
96+
(useGetKeyValues as jest.Mock).mockReturnValue({
97+
data: mockKeyValues,
98+
});
99+
100+
const { result } = renderHook(() =>
101+
useAutoCompleteOptions(
102+
luceneFormatter,
103+
'ResourceAttributes.service.name',
104+
{
105+
tableConnections: mockTableConnections,
106+
},
107+
),
108+
);
109+
110+
expect(result.current).toEqual([
111+
{
112+
value: 'ResourceAttributes',
113+
label: 'ResourceAttributes (map)',
114+
},
115+
{
116+
value: 'ResourceAttributes.service.name',
117+
label: 'ResourceAttributes.service.name (string)',
118+
},
119+
{
120+
value: 'TraceAttributes.trace.id',
121+
label: 'TraceAttributes.trace.id (string)',
122+
},
123+
{
124+
value: 'ResourceAttributes.service.name:"frontend"',
125+
label: 'ResourceAttributes.service.name:"frontend"',
126+
},
127+
{
128+
value: 'ResourceAttributes.service.name:"backend"',
129+
label: 'ResourceAttributes.service.name:"backend"',
130+
},
131+
]);
132+
});
133+
134+
// TODO: Does this test case need to be removed after HDX-1548?
135+
it('should handle nested key value options', () => {
136+
const mockKeyValues = [
137+
{
138+
key: 'ResourceAttributes',
139+
value: [
140+
{
141+
'service.name': 'frontend',
142+
'deployment.environment': 'production',
143+
},
144+
],
145+
},
146+
];
147+
148+
(useGetKeyValues as jest.Mock).mockReturnValue({
149+
data: mockKeyValues,
150+
});
151+
152+
const { result } = renderHook(() =>
153+
useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', {
154+
tableConnections: mockTableConnections,
155+
}),
156+
);
157+
158+
//console.log(result.current);
159+
expect(result.current).toEqual([
160+
{
161+
value: 'ResourceAttributes',
162+
label: 'ResourceAttributes (map)',
163+
},
164+
{
165+
value: 'ResourceAttributes.service.name',
166+
label: 'ResourceAttributes.service.name (string)',
167+
},
168+
{
169+
value: 'TraceAttributes.trace.id',
170+
label: 'TraceAttributes.trace.id (string)',
171+
},
172+
{
173+
value: 'ResourceAttributes.service.name:"frontend"',
174+
label: 'ResourceAttributes.service.name:"frontend"',
175+
},
176+
{
177+
value: 'ResourceAttributes.deployment.environment:"production"',
178+
label: 'ResourceAttributes.deployment.environment:"production"',
179+
},
180+
]);
181+
});
182+
183+
it('should handle additional suggestions', () => {
184+
const { result } = renderHook(() =>
185+
useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', {
186+
tableConnections: mockTableConnections,
187+
additionalSuggestions: ['custom.field'],
188+
}),
189+
);
190+
191+
expect(result.current).toEqual([
192+
{
193+
value: 'ResourceAttributes',
194+
label: 'ResourceAttributes (map)',
195+
},
196+
{
197+
value: 'ResourceAttributes.service.name',
198+
label: 'ResourceAttributes.service.name (string)',
199+
},
200+
{
201+
value: 'TraceAttributes.trace.id',
202+
label: 'TraceAttributes.trace.id (string)',
203+
},
204+
{
205+
value: 'custom.field',
206+
label: 'custom.field',
207+
},
208+
]);
209+
});
210+
});

0 commit comments

Comments
 (0)