mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 04:32:08 +08:00
Tempo: Fix TraceQL Search escaping regex queries (#106693)
* Fix for TraceQL Search incorrectly escaping regex queries * Update test * Keep existing filter values when adding custom
This commit is contained in:
@ -125,6 +125,10 @@ export interface TraceqlFilter {
|
||||
* Uniquely identify the filter, will not be used in the query generation
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Whether the value is a custom value typed by the user
|
||||
*/
|
||||
isCustomValue?: boolean;
|
||||
/**
|
||||
* The operator that connects the tag to the value, for example: =, >, !=, =~
|
||||
*/
|
||||
|
@ -84,6 +84,8 @@ type TraceqlFilter struct {
|
||||
ValueType *string `json:"valueType,omitempty"`
|
||||
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
||||
Scope *TraceqlSearchScope `json:"scope,omitempty"`
|
||||
// Whether the value is a custom value typed by the user
|
||||
IsCustomValue *bool `json:"isCustomValue,omitempty"`
|
||||
}
|
||||
|
||||
// NewTraceqlFilter creates a new TraceqlFilter object.
|
||||
|
@ -74,6 +74,7 @@ describe('SearchField', () => {
|
||||
});
|
||||
const filter: TraceqlFilter = {
|
||||
id: 'test1',
|
||||
isCustomValue: false,
|
||||
valueType: 'string',
|
||||
tag: 'test-tag',
|
||||
};
|
||||
@ -260,6 +261,66 @@ describe('SearchField', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should create custom option with single value when filter value is not an array', async () => {
|
||||
const updateFilter = jest.fn((val) => {
|
||||
return val;
|
||||
});
|
||||
const filter: TraceqlFilter = {
|
||||
id: 'test1',
|
||||
valueType: 'string',
|
||||
tag: 'test-tag',
|
||||
value: 'existing-value',
|
||||
};
|
||||
|
||||
const { container } = renderSearchField(updateFilter, filter, [], false, undefined, false);
|
||||
|
||||
const select = container.querySelector(`input[aria-label="select test1 value"]`);
|
||||
expect(select).not.toBeNull();
|
||||
expect(select).toBeInTheDocument();
|
||||
|
||||
if (select) {
|
||||
await user.type(select, 'custom-value');
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
expect(updateFilter).toHaveBeenCalledWith({
|
||||
...filter,
|
||||
value: 'custom-value',
|
||||
valueType: 'string',
|
||||
isCustomValue: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should create custom option with array value when filter value is an array', async () => {
|
||||
const updateFilter = jest.fn((val) => {
|
||||
return val;
|
||||
});
|
||||
const filter: TraceqlFilter = {
|
||||
id: 'test1',
|
||||
valueType: 'string',
|
||||
tag: 'test-tag',
|
||||
value: ['existing-value1', 'existing-value2'],
|
||||
};
|
||||
|
||||
const { container } = renderSearchField(updateFilter, filter, [], false, undefined, true);
|
||||
|
||||
const select = container.querySelector(`input[aria-label="select test1 value"]`);
|
||||
expect(select).not.toBeNull();
|
||||
expect(select).toBeInTheDocument();
|
||||
|
||||
if (select) {
|
||||
await user.type(select, 'custom-value');
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
expect(updateFilter).toHaveBeenCalledWith({
|
||||
...filter,
|
||||
value: ['existing-value1', 'existing-value2', 'custom-value'],
|
||||
valueType: 'string',
|
||||
isCustomValue: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const renderSearchField = (
|
||||
@ -267,7 +328,8 @@ const renderSearchField = (
|
||||
filter: TraceqlFilter,
|
||||
tags?: string[],
|
||||
hideTag?: boolean,
|
||||
lp?: LanguageProvider
|
||||
lp?: LanguageProvider,
|
||||
isMulti?: boolean
|
||||
) => {
|
||||
const languageProvider =
|
||||
lp ||
|
||||
@ -313,6 +375,7 @@ const renderSearchField = (
|
||||
hideTag={hideTag}
|
||||
query={'{}'}
|
||||
addVariablesToOptions={true}
|
||||
isMulti={isMulti}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -127,12 +127,27 @@ const SearchField = ({
|
||||
return;
|
||||
}
|
||||
|
||||
let currentOptions = options;
|
||||
|
||||
// Add custom value if it exists and isn't already in options
|
||||
if (filter.isCustomValue && filter.value) {
|
||||
const customValue = Array.isArray(filter.value) ? filter.value : [filter.value];
|
||||
|
||||
const newCustomOptions = customValue
|
||||
.filter((val) => !options.some((opt) => opt.value === val))
|
||||
.map((val) => ({ label: val, value: val, type: filter.valueType }));
|
||||
|
||||
if (newCustomOptions.length > 0) {
|
||||
currentOptions = [...options, ...newCustomOptions];
|
||||
}
|
||||
}
|
||||
|
||||
if (tagValuesQuery.length === 0) {
|
||||
return options.slice(0, OPTIONS_LIMIT);
|
||||
return currentOptions.slice(0, OPTIONS_LIMIT);
|
||||
}
|
||||
|
||||
const queryLowerCase = tagValuesQuery.toLowerCase();
|
||||
return options
|
||||
return currentOptions
|
||||
.filter((tag) => {
|
||||
if (tag.value && tag.value.length > 0) {
|
||||
return tag.value.toLowerCase().includes(queryLowerCase);
|
||||
@ -140,7 +155,7 @@ const SearchField = ({
|
||||
return false;
|
||||
})
|
||||
.slice(0, OPTIONS_LIMIT);
|
||||
}, [tagValuesQuery, options]);
|
||||
}, [tagValuesQuery, options, filter.isCustomValue, filter.value, filter.valueType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -218,11 +233,25 @@ const SearchField = ({
|
||||
...filter,
|
||||
value: val.map((v) => v.value),
|
||||
valueType: val[0]?.type || uniqueOptionType,
|
||||
isCustomValue: false,
|
||||
});
|
||||
} else {
|
||||
updateFilter({ ...filter, value: val?.value, valueType: val?.type || uniqueOptionType });
|
||||
updateFilter({
|
||||
...filter,
|
||||
value: val?.value,
|
||||
valueType: val?.type || uniqueOptionType,
|
||||
isCustomValue: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onCreateOption={(val) => {
|
||||
updateFilter({
|
||||
...filter,
|
||||
value: Array.isArray(filter.value) ? filter.value?.concat(val) : val,
|
||||
valueType: uniqueOptionType,
|
||||
isCustomValue: true,
|
||||
});
|
||||
}}
|
||||
placeholder="Select value"
|
||||
isClearable={true}
|
||||
aria-label={`select ${filter.id} value`}
|
||||
|
@ -35,7 +35,8 @@ const isRegExpOperator = (operator: string) => operator === '=~' || operator ===
|
||||
const escapeValues = (values: string[]) => getEscapedSpanNames(values);
|
||||
|
||||
export const valueHelper = (f: TraceqlFilter) => {
|
||||
const value = Array.isArray(f.value) && isRegExpOperator(f.operator!) ? escapeValues(f.value) : f.value;
|
||||
const value =
|
||||
Array.isArray(f.value) && isRegExpOperator(f.operator!) && !f.isCustomValue ? escapeValues(f.value) : f.value;
|
||||
|
||||
if (Array.isArray(value) && value.length > 1) {
|
||||
return `"${value.join('|')}"`;
|
||||
|
@ -84,6 +84,8 @@ composableKinds: DataQuery: {
|
||||
valueType?: string
|
||||
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
||||
scope?: #TraceqlSearchScope
|
||||
// Whether the value is a custom value typed by the user
|
||||
isCustomValue?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
}
|
||||
}]
|
||||
|
@ -123,6 +123,10 @@ export interface TraceqlFilter {
|
||||
* Uniquely identify the filter, will not be used in the query generation
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Whether the value is a custom value typed by the user
|
||||
*/
|
||||
isCustomValue?: boolean;
|
||||
/**
|
||||
* The operator that connects the tag to the value, for example: =, >, !=, =~
|
||||
*/
|
||||
|
Reference in New Issue
Block a user