Skip to content

Commit f714083

Browse files
authored
Fix - Handling for resource. prefix in quick filters (#10497)
* fix: resource added in checkbox * fix: resource name handling * fix: added testcases * fix: updated for other types as well * fix: updated testcases * fix: pr comments
1 parent 8c5ff10 commit f714083

File tree

3 files changed

+116
-22
lines changed

3 files changed

+116
-22
lines changed

frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,58 @@ describe('CheckboxFilter - User Flows', () => {
361361
expect(filtersForServiceName).toHaveLength(0);
362362
});
363363

364+
it('should match filter when query uses resource. prefix (resource.service.name matches service.name)', async () => {
365+
// Filter config uses unprefixed key (service.name)
366+
// Query has filter with resource. prefix (resource.service.name)
367+
// Checkbox should recognize the match and show checked state
368+
mockUseQueryBuilder.mockReturnValue({
369+
lastUsedQuery: 0,
370+
currentQuery: {
371+
builder: {
372+
queryData: [
373+
{
374+
filters: {
375+
items: [
376+
{
377+
key: {
378+
key: 'resource.service.name',
379+
dataType: DataTypes.String,
380+
type: 'resource',
381+
},
382+
op: 'in',
383+
value: [OTEL_DEMO],
384+
},
385+
],
386+
op: 'AND',
387+
},
388+
},
389+
],
390+
},
391+
},
392+
redirectWithQueryBuilderData: jest.fn(),
393+
} as any);
394+
395+
const mockFilter = createMockFilter({ defaultOpen: false });
396+
397+
render(
398+
<CheckboxFilter
399+
filter={mockFilter}
400+
source={QuickFiltersSource.LOGS_EXPLORER}
401+
/>,
402+
);
403+
404+
// Filter should auto-open because it has active filters (key match via prefix stripping)
405+
await waitFor(() => {
406+
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
407+
});
408+
409+
// otel-demo should be checked (filter uses resource.service.name IN [otel-demo])
410+
// Checked items are sorted to the top, so otel-demo is first
411+
const checkboxes = screen.getAllByRole('checkbox');
412+
expect(checkboxes[0]).toBeChecked();
413+
expect(screen.getByText(OTEL_DEMO)).toBeInTheDocument();
414+
});
415+
364416
it('should extend an existing IN filter when checking an additional value', async () => {
365417
const redirectWithQueryBuilderData = jest.fn();
366418

frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'
1818
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
1919
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
2020
import useDebouncedFn from 'hooks/useDebouncedFunction';
21-
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
21+
import { cloneDeep, isArray, isFunction } from 'lodash-es';
2222
import { ChevronDown, ChevronRight } from 'lucide-react';
2323
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
2424
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
2525
import { DataSource } from 'types/common/queryBuilder';
2626
import { v4 as uuid } from 'uuid';
2727

2828
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
29+
import { isKeyMatch } from './utils';
2930

3031
import './Checkbox.styles.scss';
3132

@@ -84,7 +85,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
8485
currentQuery.builder.queryData?.[
8586
activeQueryIndex
8687
]?.filters?.items?.some((item) =>
87-
isEqual(item.key?.key, filter.attributeKey.key),
88+
isKeyMatch(item.key?.key, filter.attributeKey.key),
8889
),
8990
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
9091
);
@@ -189,7 +190,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
189190
const filterSync = currentQuery?.builder.queryData?.[
190191
activeQueryIndex
191192
]?.filters?.items.find((item) =>
192-
isEqual(item.key?.key, filter.attributeKey.key),
193+
isKeyMatch(item.key?.key, filter.attributeKey.key),
193194
);
194195

195196
if (filterSync) {
@@ -236,7 +237,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
236237
(currentQuery?.builder?.queryData?.[
237238
activeQueryIndex
238239
]?.filters?.items?.filter((item) =>
239-
isEqual(item.key?.key, filter.attributeKey.key),
240+
isKeyMatch(item.key?.key, filter.attributeKey.key),
240241
)?.length || 0) > 1,
241242

242243
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
@@ -280,7 +281,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
280281
items:
281282
idx === activeQueryIndex
282283
? item.filters?.items?.filter(
283-
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
284+
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
284285
) || []
285286
: [...(item.filters?.items || [])],
286287
op: item.filters?.op || 'AND',
@@ -313,7 +314,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
313314
: 'Only'
314315
: 'Only';
315316
query.filters.items = query.filters.items.filter(
316-
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
317+
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
317318
);
318319

319320
if (query.filter?.expression) {
@@ -335,13 +336,13 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
335336
} else if (query?.filters?.items) {
336337
if (
337338
query.filters?.items?.some((item) =>
338-
isEqual(item.key?.key, filter.attributeKey.key),
339+
isKeyMatch(item.key?.key, filter.attributeKey.key),
339340
)
340341
) {
341342
// if there is already a running filter for the current attribute key then
342343
// we split the cases by which particular operator is present right now!
343344
const currentFilter = query.filters?.items?.find((q) =>
344-
isEqual(q.key?.key, filter.attributeKey.key),
345+
isKeyMatch(q.key?.key, filter.attributeKey.key),
345346
);
346347
if (currentFilter) {
347348
const runningOperator = currentFilter?.op;
@@ -356,7 +357,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
356357
value: [...currentFilter.value, value],
357358
};
358359
query.filters.items = query.filters.items.map((item) => {
359-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
360+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
360361
return newFilter;
361362
}
362363
return item;
@@ -368,7 +369,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
368369
value: [currentFilter.value as string, value],
369370
};
370371
query.filters.items = query.filters.items.map((item) => {
371-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
372+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
372373
return newFilter;
373374
}
374375
return item;
@@ -385,11 +386,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
385386

386387
if (newFilter.value.length === 0) {
387388
query.filters.items = query.filters.items.filter(
388-
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
389+
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
389390
);
390391
} else {
391392
query.filters.items = query.filters.items.map((item) => {
392-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
393+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
393394
return newFilter;
394395
}
395396
return item;
@@ -398,7 +399,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
398399
} else {
399400
// if not an array remove the whole thing altogether!
400401
query.filters.items = query.filters.items.filter(
401-
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
402+
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
402403
);
403404
}
404405
}
@@ -414,7 +415,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
414415
value: [...currentFilter.value, value],
415416
};
416417
query.filters.items = query.filters.items.map((item) => {
417-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
418+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
418419
return newFilter;
419420
}
420421
return item;
@@ -426,7 +427,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
426427
value: [currentFilter.value as string, value],
427428
};
428429
query.filters.items = query.filters.items.map((item) => {
429-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
430+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
430431
return newFilter;
431432
}
432433
return item;
@@ -441,7 +442,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
441442
};
442443
if (newFilter.value.length === 0) {
443444
query.filters.items = query.filters.items.filter(
444-
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
445+
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
445446
);
446447
if (query.filter?.expression) {
447448
query.filter.expression = removeKeysFromExpression(
@@ -451,7 +452,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
451452
}
452453
} else {
453454
query.filters.items = query.filters.items.map((item) => {
454-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
455+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
455456
return newFilter;
456457
}
457458
return item;
@@ -469,7 +470,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
469470
);
470471
}
471472
query.filters.items = query.filters.items.filter(
472-
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
473+
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
473474
);
474475
}
475476
}
@@ -482,14 +483,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
482483
value: [currentFilter.value as string, value],
483484
};
484485
query.filters.items = query.filters.items.map((item) => {
485-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
486+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
486487
return newFilter;
487488
}
488489
return item;
489490
});
490491
} else if (!checked) {
491492
query.filters.items = query.filters.items.filter(
492-
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
493+
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
493494
);
494495
}
495496
break;
@@ -501,14 +502,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
501502
value: [currentFilter.value as string, value],
502503
};
503504
query.filters.items = query.filters.items.map((item) => {
504-
if (isEqual(item.key?.key, filter.attributeKey.key)) {
505+
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
505506
return newFilter;
506507
}
507508
return item;
508509
});
509510
} else if (checked) {
510511
query.filters.items = query.filters.items.filter(
511-
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
512+
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
512513
);
513514
}
514515
break;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* These prefixes are added to attribute keys based on their context.
3+
*/
4+
export const FIELD_CONTEXT_PREFIXES = [
5+
'metric',
6+
'log',
7+
'span',
8+
'trace',
9+
'resource',
10+
'scope',
11+
'attribute',
12+
'event',
13+
'body',
14+
];
15+
16+
/**
17+
* Removes the field context prefix from a key to get the base key name.
18+
* Example: 'resource.service.name' -> 'service.name'
19+
* Example: 'attribute.http.method' -> 'http.method'
20+
*/
21+
export function getKeyWithoutPrefix(key: string | undefined): string {
22+
if (!key) {
23+
return '';
24+
}
25+
const prefixPattern = new RegExp(
26+
`^(${FIELD_CONTEXT_PREFIXES.join('|')})\\.`,
27+
'i',
28+
);
29+
return key.replace(prefixPattern, '').trim();
30+
}
31+
32+
/**
33+
* Compares two keys by their base name (without prefix).
34+
* This ensures that 'service.name' matches 'resource.service.name'
35+
*/
36+
export function isKeyMatch(
37+
itemKey: string | undefined,
38+
filterKey: string | undefined,
39+
): boolean {
40+
return getKeyWithoutPrefix(itemKey) === getKeyWithoutPrefix(filterKey);
41+
}

0 commit comments

Comments
 (0)