Skip to content

Commit b6df994

Browse files
enhancement row actions.
1 parent df25939 commit b6df994

File tree

1 file changed

+216
-35
lines changed

1 file changed

+216
-35
lines changed

packages/app/src/components/DBRowJsonViewer.tsx

Lines changed: 216 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useCallback, useContext, useMemo, useState } from 'react';
2+
import { useSearchParams } from 'next/navigation';
23
import router from 'next/router';
34
import { useAtom, useAtomValue } from 'jotai';
45
import { atomWithStorage } from 'jotai/utils';
56
import get from 'lodash/get';
7+
import lucene from '@hyperdx/lucene';
68
import {
7-
ActionIcon,
89
Box,
910
Button,
1011
Group,
@@ -118,6 +119,97 @@ function HyperJsonMenu() {
118119
);
119120
}
120121

122+
// if keep is true, remove node which matched the match condition.
123+
// if value is '', match condition is key equal.
124+
// if value is not '', match condition is key and value equal.
125+
function rangeNodesWithKey(
126+
ast: any,
127+
key: string,
128+
value: string,
129+
keep: boolean,
130+
): { result: any; modified: boolean } {
131+
if (!ast) return { result: null, modified: false };
132+
133+
if (ast.term) {
134+
let matched = false;
135+
if (ast.field === key || ast.field === `-${key}`) {
136+
if (value !== '') {
137+
if (ast.term === value) {
138+
matched = true;
139+
}
140+
} else {
141+
matched = true;
142+
}
143+
}
144+
if ((matched && keep) || (!matched && !keep)) {
145+
return { result: ast, modified: false };
146+
} else {
147+
return { result: null, modified: true };
148+
}
149+
}
150+
151+
const leftResult = rangeNodesWithKey(ast.left, key, value, keep);
152+
const rightResult = rangeNodesWithKey(ast.right, key, value, keep);
153+
const left = leftResult.result;
154+
const right = rightResult.result;
155+
const modified = leftResult.modified || rightResult.modified;
156+
157+
if (!left && !right) {
158+
return { result: null, modified: modified };
159+
}
160+
161+
if (!left && right) {
162+
return { result: right, modified: modified };
163+
}
164+
165+
if (left && !right) {
166+
return { result: left, modified: modified };
167+
}
168+
169+
return {
170+
result: {
171+
...ast,
172+
left,
173+
right,
174+
},
175+
modified,
176+
};
177+
}
178+
179+
// removeLuceneField remove field in a lucene search query.
180+
// modified meaning if it is removed success.
181+
// example:
182+
// removeLuceneField('(ServiceName:a OR ServiceName:b) AND SeverityText:ERROR', 'ServiceName', 'a')
183+
// ServiceName:b AND SeverityText:ERROR
184+
export function removeLuceneField(
185+
query: string,
186+
key: string,
187+
value: string,
188+
): { result: string; modified: boolean } {
189+
if (typeof query !== 'string') return { result: query, modified: false };
190+
191+
try {
192+
// delete matched node
193+
const ast = lucene.parse(query);
194+
const { result: modifiedAst, modified } = rangeNodesWithKey(
195+
ast,
196+
key,
197+
value,
198+
false,
199+
);
200+
201+
if (!modifiedAst) {
202+
return { result: '', modified: modified };
203+
}
204+
205+
const modifiedString = lucene.toString(modifiedAst);
206+
return { result: modifiedString, modified };
207+
} catch (error) {
208+
console.warn('Failed to parse Lucene query', error);
209+
210+
return { result: query, modified: false };
211+
}
212+
}
121213
export function DBRowJsonViewer({
122214
data,
123215
jsonColumns = [],
@@ -151,67 +243,146 @@ export function DBRowJsonViewer({
151243
return filterObjectRecursively(data, debouncedFilter);
152244
}, [data, debouncedFilter]);
153245

246+
const searchParams = useSearchParams();
247+
154248
const getLineActions = useCallback<GetLineActions>(
155249
({ keyPath, value }) => {
156250
const actions: LineAction[] = [];
157251
const fieldPath = mergePath(keyPath, jsonColumns);
158252
const isJsonColumn =
159253
keyPath.length > 0 && jsonColumns?.includes(keyPath[0]);
160254

161-
// Add to Filters action (strings only)
162-
// FIXME: TOTAL HACK To disallow adding timestamp to filters
163-
if (
164-
onPropertyAddClick != null &&
165-
typeof value === 'string' &&
166-
value &&
167-
fieldPath != 'Timestamp' &&
168-
fieldPath != 'TimestampTime'
169-
) {
255+
let where = searchParams.get('where') || '';
256+
let whereLanguage = searchParams.get('whereLanguage');
257+
if (whereLanguage == '') {
258+
whereLanguage = 'lucene';
259+
}
260+
261+
let luceneFieldPath = '';
262+
if (whereLanguage === 'lucene') {
263+
luceneFieldPath = keyPath.join('.');
264+
}
265+
266+
let removedFilterWhere = ''; // filter which already removed value.
267+
let hadFilter = false;
268+
if (where !== '') {
269+
// if it is lucene, we support remove-filter.
270+
if (whereLanguage === 'lucene') {
271+
const { result, modified } = removeLuceneField(
272+
where,
273+
luceneFieldPath,
274+
value,
275+
);
276+
removedFilterWhere = result;
277+
hadFilter = modified;
278+
where += ' ';
279+
} else {
280+
where += ' AND ';
281+
}
282+
}
283+
284+
if (generateSearchUrl && typeof value !== 'object' && hadFilter) {
170285
actions.push({
171-
key: 'add-to-search',
286+
key: 'remove-filter',
172287
label: (
173288
<>
174-
<i className="bi bi-funnel-fill me-1" />
175-
Add to Filters
289+
<i className="bi bi-x-circle me-1" />
290+
Remove Filter
176291
</>
177292
),
178-
title: 'Add to Filters',
179293
onClick: () => {
180-
onPropertyAddClick(
181-
isJsonColumn ? `toString(${fieldPath})` : fieldPath,
182-
value,
294+
router.push(
295+
generateSearchUrl({
296+
where: removedFilterWhere,
297+
whereLanguage: whereLanguage as 'sql' | 'lucene',
298+
}),
183299
);
184-
notifications.show({
185-
color: 'green',
186-
message: `Added "${fieldPath} = ${value}" to filters`,
187-
});
188300
},
189301
});
190302
}
191303

192-
if (generateSearchUrl && typeof value !== 'object') {
304+
if (generateSearchUrl && typeof value !== 'object' && !hadFilter) {
193305
actions.push({
194-
key: 'search',
306+
key: 'filter',
195307
label: (
196308
<>
197309
<i className="bi bi-search me-1" />
198-
Search
310+
Filter
199311
</>
200312
),
201-
title: 'Search for this value only',
313+
title: 'Add to Filters',
202314
onClick: () => {
203-
let defaultWhere = `${fieldPath} = ${
204-
typeof value === 'string' ? `'${value}'` : value
205-
}`;
315+
if (whereLanguage === 'lucene') {
316+
where += `${luceneFieldPath}:"${value}"`;
317+
} else {
318+
where += `${fieldPath} = ${
319+
typeof value === 'string' ? `'${value}'` : value
320+
}`;
321+
}
206322

207-
// FIXME: TOTAL HACK
208-
if (fieldPath == 'Timestamp' || fieldPath == 'TimestampTime') {
209-
defaultWhere = `${fieldPath} = parseDateTime64BestEffort('${value}', 9)`;
323+
router.push(
324+
generateSearchUrl({
325+
where: where,
326+
whereLanguage: whereLanguage as 'sql' | 'lucene',
327+
}),
328+
);
329+
},
330+
});
331+
}
332+
333+
if (generateSearchUrl && typeof value !== 'object' && !hadFilter) {
334+
actions.push({
335+
key: 'exclude',
336+
label: (
337+
<>
338+
<i className="bi bi-dash-circle me-1" />
339+
Exclude
340+
</>
341+
),
342+
title: 'Exclude from Filters',
343+
onClick: () => {
344+
if (whereLanguage === 'lucene') {
345+
where += `-${luceneFieldPath}:"${value}"`;
346+
} else {
347+
where += `${fieldPath} != ${
348+
typeof value === 'string' ? `'${value}'` : value
349+
}`;
210350
}
351+
211352
router.push(
212353
generateSearchUrl({
213-
where: defaultWhere,
214-
whereLanguage: 'sql',
354+
where: where,
355+
whereLanguage: whereLanguage as 'sql' | 'lucene',
356+
}),
357+
);
358+
},
359+
});
360+
}
361+
362+
if (generateSearchUrl && typeof value !== 'object' && !hadFilter) {
363+
actions.push({
364+
key: 'replace-filter',
365+
label: (
366+
<>
367+
<i className="bi bi-arrow-counterclockwise me-1" />
368+
Replace Filter
369+
</>
370+
),
371+
title: 'Search for this value only',
372+
onClick: () => {
373+
where = '';
374+
if (whereLanguage === 'lucene') {
375+
where = `${luceneFieldPath}:"${value}"`;
376+
} else {
377+
where = `${fieldPath} = ${
378+
typeof value === 'string' ? `'${value}'` : value
379+
}`;
380+
}
381+
382+
router.push(
383+
generateSearchUrl({
384+
where: where,
385+
whereLanguage: whereLanguage as 'sql' | 'lucene',
215386
}),
216387
);
217388
},
@@ -282,13 +453,23 @@ export function DBRowJsonViewer({
282453
if (typeof value === 'object') {
283454
actions.push({
284455
key: 'copy-object',
285-
label: 'Copy Object',
456+
label: (
457+
<>
458+
<i className="bi bi-clipboard me-1" />
459+
Copy Object
460+
</>
461+
),
286462
onClick: handleCopyObject,
287463
});
288464
} else {
289465
actions.push({
290466
key: 'copy-value',
291-
label: 'Copy Value',
467+
label: (
468+
<>
469+
<i className="bi bi-copy me-1" />
470+
Copy Value
471+
</>
472+
),
292473
onClick: () => {
293474
window.navigator.clipboard.writeText(
294475
typeof value === 'string'

0 commit comments

Comments
 (0)