mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 13:02:29 +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
|
* Uniquely identify the filter, will not be used in the query generation
|
||||||
*/
|
*/
|
||||||
id: string;
|
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: =, >, !=, =~
|
* The operator that connects the tag to the value, for example: =, >, !=, =~
|
||||||
*/
|
*/
|
||||||
|
@ -84,6 +84,8 @@ type TraceqlFilter struct {
|
|||||||
ValueType *string `json:"valueType,omitempty"`
|
ValueType *string `json:"valueType,omitempty"`
|
||||||
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
||||||
Scope *TraceqlSearchScope `json:"scope,omitempty"`
|
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.
|
// NewTraceqlFilter creates a new TraceqlFilter object.
|
||||||
|
@ -74,6 +74,7 @@ describe('SearchField', () => {
|
|||||||
});
|
});
|
||||||
const filter: TraceqlFilter = {
|
const filter: TraceqlFilter = {
|
||||||
id: 'test1',
|
id: 'test1',
|
||||||
|
isCustomValue: false,
|
||||||
valueType: 'string',
|
valueType: 'string',
|
||||||
tag: 'test-tag',
|
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 = (
|
const renderSearchField = (
|
||||||
@ -267,7 +328,8 @@ const renderSearchField = (
|
|||||||
filter: TraceqlFilter,
|
filter: TraceqlFilter,
|
||||||
tags?: string[],
|
tags?: string[],
|
||||||
hideTag?: boolean,
|
hideTag?: boolean,
|
||||||
lp?: LanguageProvider
|
lp?: LanguageProvider,
|
||||||
|
isMulti?: boolean
|
||||||
) => {
|
) => {
|
||||||
const languageProvider =
|
const languageProvider =
|
||||||
lp ||
|
lp ||
|
||||||
@ -313,6 +375,7 @@ const renderSearchField = (
|
|||||||
hideTag={hideTag}
|
hideTag={hideTag}
|
||||||
query={'{}'}
|
query={'{}'}
|
||||||
addVariablesToOptions={true}
|
addVariablesToOptions={true}
|
||||||
|
isMulti={isMulti}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -127,12 +127,27 @@ const SearchField = ({
|
|||||||
return;
|
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) {
|
if (tagValuesQuery.length === 0) {
|
||||||
return options.slice(0, OPTIONS_LIMIT);
|
return currentOptions.slice(0, OPTIONS_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryLowerCase = tagValuesQuery.toLowerCase();
|
const queryLowerCase = tagValuesQuery.toLowerCase();
|
||||||
return options
|
return currentOptions
|
||||||
.filter((tag) => {
|
.filter((tag) => {
|
||||||
if (tag.value && tag.value.length > 0) {
|
if (tag.value && tag.value.length > 0) {
|
||||||
return tag.value.toLowerCase().includes(queryLowerCase);
|
return tag.value.toLowerCase().includes(queryLowerCase);
|
||||||
@ -140,7 +155,7 @@ const SearchField = ({
|
|||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.slice(0, OPTIONS_LIMIT);
|
.slice(0, OPTIONS_LIMIT);
|
||||||
}, [tagValuesQuery, options]);
|
}, [tagValuesQuery, options, filter.isCustomValue, filter.value, filter.valueType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -218,11 +233,25 @@ const SearchField = ({
|
|||||||
...filter,
|
...filter,
|
||||||
value: val.map((v) => v.value),
|
value: val.map((v) => v.value),
|
||||||
valueType: val[0]?.type || uniqueOptionType,
|
valueType: val[0]?.type || uniqueOptionType,
|
||||||
|
isCustomValue: false,
|
||||||
});
|
});
|
||||||
} else {
|
} 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"
|
placeholder="Select value"
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
aria-label={`select ${filter.id} value`}
|
aria-label={`select ${filter.id} value`}
|
||||||
|
@ -35,7 +35,8 @@ const isRegExpOperator = (operator: string) => operator === '=~' || operator ===
|
|||||||
const escapeValues = (values: string[]) => getEscapedSpanNames(values);
|
const escapeValues = (values: string[]) => getEscapedSpanNames(values);
|
||||||
|
|
||||||
export const valueHelper = (f: TraceqlFilter) => {
|
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) {
|
if (Array.isArray(value) && value.length > 1) {
|
||||||
return `"${value.join('|')}"`;
|
return `"${value.join('|')}"`;
|
||||||
|
@ -84,6 +84,8 @@ composableKinds: DataQuery: {
|
|||||||
valueType?: string
|
valueType?: string
|
||||||
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
||||||
scope?: #TraceqlSearchScope
|
scope?: #TraceqlSearchScope
|
||||||
|
// Whether the value is a custom value typed by the user
|
||||||
|
isCustomValue?: bool
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
@ -123,6 +123,10 @@ export interface TraceqlFilter {
|
|||||||
* Uniquely identify the filter, will not be used in the query generation
|
* Uniquely identify the filter, will not be used in the query generation
|
||||||
*/
|
*/
|
||||||
id: string;
|
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: =, >, !=, =~
|
* The operator that connects the tag to the value, for example: =, >, !=, =~
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user