From de74cb083a436c74415d85274ca710fb30c4ef70 Mon Sep 17 00:00:00 2001 From: yincong Date: Wed, 4 Sep 2024 15:39:22 +0800 Subject: [PATCH 1/5] add replace value --- pkg/plugin/query.go | 4 ++++ pkg/utils/utils.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/pkg/plugin/query.go b/pkg/plugin/query.go index fd3a3ad..f50c4e8 100644 --- a/pkg/plugin/query.go +++ b/pkg/plugin/query.go @@ -6,6 +6,8 @@ import ( "path" "strconv" "time" + + "github.com/VictoriaMetrics/victorialogs-datasource/pkg/utils" ) const ( @@ -20,6 +22,7 @@ type Query struct { LegendFormat string `json:"legendFormat"` MaxLines int `json:"maxLines"` TimeRange TimeRange + IntervalMs int `json:"intervalMs"` url *url.URL } @@ -72,6 +75,7 @@ func (q *Query) queryInstantURL(queryParams url.Values) string { q.TimeRange.To = now } + q.Expr = utils.ReplaceTemplateVariable(q.Expr, q.IntervalMs) values.Set("query", q.Expr) values.Set("limit", strconv.Itoa(q.MaxLines)) values.Set("start", strconv.FormatInt(q.TimeRange.From.Unix(), 10)) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index bfe24b0..094f157 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -9,6 +9,16 @@ import ( "github.com/VictoriaMetrics/metricsql" ) +const ( + varInterval = "$__interval" +) + +var ( + defaultResolution int64 = 1500 + year = time.Hour * 24 * 365 + day = time.Hour * 24 +) + const ( // These values prevent from overflow when storing msec-precision time in int64. minTimeMsecs = 0 // use 0 instead of `int64(-1<<63) / 1e6` because the storage engine doesn't actually support negative time @@ -172,3 +182,28 @@ func ParseDuration(s string) (time.Duration, error) { } return time.Duration(ms) * time.Millisecond, nil } + +// ReplaceTemplateVariable get query and use it expression to remove grafana template variables with +func ReplaceTemplateVariable(expr string, interval int) string { + expr = strings.ReplaceAll(expr, varInterval, formatDuration(time.Duration(interval)*time.Millisecond)) + return expr +} + +func formatDuration(inter time.Duration) string { + switch { + case inter >= year: + return fmt.Sprintf("%dy", inter/year) + case inter >= day: + return fmt.Sprintf("%dd", inter/day) + case inter >= time.Hour: + return fmt.Sprintf("%dh", inter/time.Hour) + case inter >= time.Minute: + return fmt.Sprintf("%dm", inter/time.Minute) + case inter >= time.Second: + return fmt.Sprintf("%ds", inter/time.Second) + case inter >= time.Millisecond: + return fmt.Sprintf("%dms", inter/time.Millisecond) + default: + return "1ms" + } +} From 47e45210d4438ebed73778c155847edd0d188a8d Mon Sep 17 00:00:00 2001 From: yincong Date: Fri, 11 Oct 2024 19:17:56 +0800 Subject: [PATCH 2/5] update defaultUrl --- src/configuration/ConfigEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/configuration/ConfigEditor.tsx b/src/configuration/ConfigEditor.tsx index 36b242c..618c1c0 100644 --- a/src/configuration/ConfigEditor.tsx +++ b/src/configuration/ConfigEditor.tsx @@ -30,7 +30,7 @@ const ConfigEditor = (props: Props) => { <> Date: Tue, 15 Oct 2024 10:51:54 +0800 Subject: [PATCH 3/5] explore auto adaptive issue --- src/components/QueryEditor/QueryEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/QueryEditor/QueryEditor.tsx b/src/components/QueryEditor/QueryEditor.tsx index a01f035..6c7777d 100644 --- a/src/components/QueryEditor/QueryEditor.tsx +++ b/src/components/QueryEditor/QueryEditor.tsx @@ -97,8 +97,8 @@ const QueryEditor = React.memo((props) => { const getStyles = (theme: GrafanaTheme2) => { return { wrapper: css` - display: grid; - align-items: flex-start; + display: flex; + flex-direction: column; gap: ${theme.spacing(0.5)}; ` }; From 5bb8717404dc741ee3399a0c86854bfa8e29f4d8 Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Wed, 16 Oct 2024 11:05:03 +0200 Subject: [PATCH 4/5] add limit param for /field_values (#78) * feature: add limit param for /field_values * add input for configuring the field values limit in query builder --- CHANGELOG.md | 2 + .../QueryBuilderFieldFilter.tsx | 27 +++--- .../VariableQueryEditor.tsx | 42 ++++++-- src/configuration/ConfigEditor.tsx | 2 + src/configuration/LimitSettings.tsx | 97 +++++++++++++++++++ src/configuration/QuerySettings.tsx | 2 +- src/datasource.ts | 11 ++- src/language_provider.ts | 16 ++- src/types.ts | 11 ++- 9 files changed, 180 insertions(+), 30 deletions(-) create mode 100644 src/configuration/LimitSettings.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index c564cfc..e5daad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## tip +* FEATURE: add `limit` param for the `/field_values` request. See [this issue](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/75). + * BUGFIX: fix variable substitution in queries. See [this issue](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/77). * BUGFIX: fixed health path for case, when url ends with trailing slash. diff --git a/src/components/QueryEditor/QueryBuilder/components/QueryBuilderFilters/QueryBuilderFieldFilter.tsx b/src/components/QueryEditor/QueryBuilder/components/QueryBuilderFilters/QueryBuilderFieldFilter.tsx index 33eb8e6..54cdd90 100644 --- a/src/components/QueryEditor/QueryBuilder/components/QueryBuilderFilters/QueryBuilderFieldFilter.tsx +++ b/src/components/QueryEditor/QueryBuilder/components/QueryBuilderFilters/QueryBuilderFieldFilter.tsx @@ -49,7 +49,7 @@ const QueryBuilderFieldFilter = ({ datasource, filter, query, indexPath, timeRan } const handleSelect = (type: FilterFieldType) => ({ value: selected }: SelectableValue) => { - const fullFilter = type === FilterFieldType.Name + const fullFilter = type === FilterFieldType.FieldName ? `${selected}: ${fieldValue || ''}` : `${field || ''}: ${field === '_stream' ? selected : `"${escapeLabelValueInExactSelector(selected || "")}"`} ` @@ -64,11 +64,12 @@ const QueryBuilderFieldFilter = ({ datasource, filter, query, indexPath, timeRan } const handleOpenMenu = (type: FilterFieldType) => async () => { - const setterLoading = type === FilterFieldType.Name ? setIsLoadingFieldNames : setIsLoadingFieldValues - const setterValues = type === FilterFieldType.Name ? setFieldNames : setFieldValues + const setterLoading = type === FilterFieldType.FieldName ? setIsLoadingFieldNames : setIsLoadingFieldValues + const setterValues = type === FilterFieldType.FieldName ? setFieldNames : setFieldValues setterLoading(true) - const list = await datasource.languageProvider?.getFieldList({ type, timeRange, field, }) + const limit = datasource.getQueryBuilderLimits(type) + const list = await datasource.languageProvider?.getFieldList({ type, timeRange, field, limit }) const result = list ? list.map(({ value, hits }) => ({ value, label: value || " ", @@ -91,30 +92,30 @@ const QueryBuilderFieldFilter = ({ datasource, filter, query, indexPath, timeRan
diff --git a/src/components/VariableQueryEditor/VariableQueryEditor.tsx b/src/components/VariableQueryEditor/VariableQueryEditor.tsx index 1522385..379ae9c 100644 --- a/src/components/VariableQueryEditor/VariableQueryEditor.tsx +++ b/src/components/VariableQueryEditor/VariableQueryEditor.tsx @@ -1,15 +1,15 @@ import React, { FormEvent, useEffect, useState } from 'react'; import { usePrevious } from 'react-use'; -import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { DEFAULT_FIELD_DISPLAY_VALUES_LIMIT, QueryEditorProps, SelectableValue } from '@grafana/data'; import { InlineField, InlineFieldRow, Input, Select } from "@grafana/ui"; import { VictoriaLogsDatasource } from "../../datasource"; import { FilterFieldType, Options, Query, VariableQuery } from '../../types'; const variableOptions = [ - { label: 'Field names', value: FilterFieldType.Name }, - { label: 'Field values', value: FilterFieldType.Value }, + { label: 'Field names', value: FilterFieldType.FieldName }, + { label: 'Field values', value: FilterFieldType.FieldValue }, ]; const refId = 'VictoriaLogsVariableQueryEditor-VariableQuery' @@ -20,6 +20,7 @@ export const VariableQueryEditor = ({ onChange, query, datasource, range }: Prop const [type, setType] = useState(); const [queryFilter, setQueryFilter] = useState(''); const [field, setField] = useState(''); + const [limit, setLimit] = useState(DEFAULT_FIELD_DISPLAY_VALUES_LIMIT); const [fieldNames, setFieldNames] = useState[]>([]) const previousType = usePrevious(type); @@ -40,13 +41,18 @@ export const VariableQueryEditor = ({ onChange, query, datasource, range }: Prop if (!type) { return; } - onChange({ refId, type, field, query: queryFilter }); + onChange({ refId, type, field, query: queryFilter, limit }); }; const handleQueryFilterChange = (e: FormEvent) => { setQueryFilter(e.currentTarget.value); }; + const handleLimitChange = (e: FormEvent) => { + const value = Number(e.currentTarget.value) + setLimit(isNaN(value) ? DEFAULT_FIELD_DISPLAY_VALUES_LIMIT : value); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { e.key === 'Enter' && handleBlur() } @@ -59,15 +65,20 @@ export const VariableQueryEditor = ({ onChange, query, datasource, range }: Prop setType(query.type); setField(query.field || ''); setQueryFilter(query.query || ''); + setLimit(query.limit ?? DEFAULT_FIELD_DISPLAY_VALUES_LIMIT); }, [query]); useEffect(() => { - if (type !== FilterFieldType.Value || previousType === type) { + if (type !== FilterFieldType.FieldValue || previousType === type) { return; } const getFiledNames = async () => { - const list = await datasource.languageProvider?.getFieldList({ type: FilterFieldType.Name, timeRange: range }) + const list = await datasource.languageProvider?.getFieldList({ + type: FilterFieldType.FieldName, + timeRange: range, + limit, + }) const result = list ? list.map(({ value, hits }) => ({ value, label: value || " ", @@ -77,7 +88,7 @@ export const VariableQueryEditor = ({ onChange, query, datasource, range }: Prop } getFiledNames().catch(console.error) - }, [datasource, type, range, previousType]); + }, [datasource, type, range, previousType, limit]); return (
@@ -92,7 +103,7 @@ export const VariableQueryEditor = ({ onChange, query, datasource, range }: Prop width={20} /> - {type === FilterFieldType.Value && ( + {type === FilterFieldType.FieldValue && ( + ; @@ -40,6 +41,7 @@ const ConfigEditor = (props: Props) => { maxLines={options.jsonData.maxLines || ''} onMaxLinedChange={(value) => onOptionsChange(setMaxLines(options, value))} /> + ); }; diff --git a/src/configuration/LimitSettings.tsx b/src/configuration/LimitSettings.tsx new file mode 100644 index 0000000..f998ea6 --- /dev/null +++ b/src/configuration/LimitSettings.tsx @@ -0,0 +1,97 @@ +import React, { SyntheticEvent } from 'react'; + +import { + DataSourcePluginOptionsEditorProps, + SelectableValue, +} from '@grafana/data'; +import { + InlineField, + regexValidation, + Input, validate +} from '@grafana/ui'; + +import { FilterFieldType, Options } from "../types"; + +const validationRule = regexValidation( + /^$|^\d+$/, + 'Value is not valid, you can use number' +) + +const limitFields = [ + { + label: "Field values", + tooltip: <>In the Query Builder, the /select/logsql/field_values endpoint allows an optional limit=N parameter to restrict the number of returned values to N. For more details, see the documentation here., + placeholder: "", + key: FilterFieldType.FieldValue + } +] + +type Props = Pick, 'options' | 'onOptionsChange'>; + +export const LimitsSettings = (props: Props) => { + const { options, onOptionsChange } = props; + + const [error, setError] = React.useState(null) + + const handleBlur = (event: SyntheticEvent) => { + const errors = validate(event.currentTarget.value, [validationRule]) + setError(errors?.[0] || null) + } + + return ( + <> +

Limits

+

Leave the field blank or set the value to 0 to remove the limit

+
+ {limitFields.map((field) => ( +
+ + + +
+ ))} +
+ + ) +}; + +const getValueFromEventItem = (eventItem: SyntheticEvent | SelectableValue) => { + if (!eventItem) { + return ''; + } + + if (eventItem.hasOwnProperty('currentTarget')) { + return eventItem.currentTarget.value; + } + + return (eventItem as SelectableValue).value; +}; + +const onChangeHandler = + (key: string, options: Props['options'], onOptionsChange: Props['onOptionsChange']) => + (eventItem: SyntheticEvent | SelectableValue) => { + onOptionsChange({ + ...options, + jsonData: { + ...options.jsonData, + queryBuilderLimits: { + ...options.jsonData.queryBuilderLimits, + [key]: getValueFromEventItem(eventItem), + } + }, + }); + }; diff --git a/src/configuration/QuerySettings.tsx b/src/configuration/QuerySettings.tsx index 1078b65..19d0d69 100644 --- a/src/configuration/QuerySettings.tsx +++ b/src/configuration/QuerySettings.tsx @@ -10,7 +10,7 @@ type Props = { export const QuerySettings = (props: Props) => { const { maxLines, onMaxLinedChange } = props; return ( -
+
, @@ -70,6 +73,7 @@ export class VictoriaLogsDatasource QueryEditor: QueryEditor, }; this.variables = new VariableSupport(this); + this.queryBuilderLimits = instanceSettings.jsonData.queryBuilderLimits; } query(request: DataQueryRequest): Observable { @@ -248,8 +252,13 @@ export class VictoriaLogsDatasource type: query.type, timeRange, field: query.field, - query: query.query + query: query.query, + limit: query.limit, }); return (list ? list.map(({ value }) => ({ text: value })) : []) } + + getQueryBuilderLimits(key: FilterFieldType): number { + return this.queryBuilderLimits?.[key] || 0; + } } diff --git a/src/language_provider.ts b/src/language_provider.ts index 75525eb..fbfdf85 100644 --- a/src/language_provider.ts +++ b/src/language_provider.ts @@ -9,12 +9,14 @@ interface FetchFieldsOptions { query?: string; field?: string; timeRange?: TimeRange; + limit?: number; } interface FieldsRequestParams { query: string; - from: number; - to: number; + start: number; + end: number; + limit?: number; field?: string; } @@ -50,7 +52,7 @@ export default class LogsQlLanguageProvider extends LanguageProvider { }; async getFieldList(options: FetchFieldsOptions): Promise { - if (options.type === FilterFieldType.Value && !options.field) { + if (options.type === FilterFieldType.FieldValue && !options.field) { return []; } @@ -58,11 +60,15 @@ export default class LogsQlLanguageProvider extends LanguageProvider { query: options.query || "*", ...this.getTimeRangeParams(options.timeRange), }; - if (options.type === FilterFieldType.Value) { + if (options.type === FilterFieldType.FieldValue) { params.field = options.field; } - const url = options.type === FilterFieldType.Name ? 'select/logsql/field_names' : `select/logsql/field_values`; + if (options.limit && (options.limit > 0) && (options.type === FilterFieldType.FieldValue)) { + params.limit = options.limit; + } + + const url = options.type === FilterFieldType.FieldName ? 'select/logsql/field_names' : `select/logsql/field_values`; const key = `${url}/${Object.values(params).join('/')}`; if (this.cacheValues.has(key)) { diff --git a/src/types.ts b/src/types.ts index f579b29..5ce3f14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,7 @@ export interface Options extends DataSourceJsonData { maxLines?: string; httpMethod?: string; customQueryParameters?: string; + queryBuilderLimits?: QueryBuilderLimits; // derivedFields?: DerivedFieldConfig[]; // alertmanager?: string; // keepCookies?: string[]; @@ -102,12 +103,18 @@ export interface FiledHits { } export enum FilterFieldType { - Name = 'name', - Value = 'value' + FieldName = 'fieldName', + FieldValue = 'fieldValue' } export interface VariableQuery extends DataQuery { type: FilterFieldType; query?: string; field?: string; + limit?: number; } + +export type QueryBuilderLimits = { + [FilterFieldType.FieldValue]?: number; + [FilterFieldType.FieldName]?: number; +}; From 6ba3ee3782b1d6ec0cc1faab04e46f3ed05b35bf Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Wed, 16 Oct 2024 11:08:41 +0200 Subject: [PATCH 5/5] fix: correct filter application in queries (#90) --- CHANGELOG.md | 1 + .../QueryBuilder/utils/parseFromString.ts | 5 +- .../QueryBuilder/utils/parseToString.ts | 4 +- src/modifyQuery.test.ts | 49 +++++++++++++++ src/modifyQuery.ts | 60 +++++++++++++------ 5 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 src/modifyQuery.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e5daad1..25dfc4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * BUGFIX: fix variable substitution in queries. See [this issue](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/77). * BUGFIX: fixed health path for case, when url ends with trailing slash. +* BUGFIX: fix the application of filtering in queries. See [this issue](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/81). ## v0.5.0 diff --git a/src/components/QueryEditor/QueryBuilder/utils/parseFromString.ts b/src/components/QueryEditor/QueryBuilder/utils/parseFromString.ts index cd0f4a5..a63adfa 100644 --- a/src/components/QueryEditor/QueryBuilder/utils/parseFromString.ts +++ b/src/components/QueryEditor/QueryBuilder/utils/parseFromString.ts @@ -10,7 +10,6 @@ interface Context { type ParsedExpression = string | ParsedExpression[]; export const buildVisualQueryFromString = (expr: string): Context => { - // This will be modified in the handleExpression const visQuery: VisualQuery = { filters: { operators: [], values: [] }, pipes: [] @@ -46,6 +45,10 @@ const handleExpression = (expr: string) => { return { filters, pipes: pipeParts }; } +export const splitExpression = (expr: string): string[] => { + return expr.split('|').map(part => part.trim()); +}; + const parseStringToFilterVisualQuery = (expression: string): FilterVisualQuery => { const parsedExpressions = parseExpression(expression) diff --git a/src/components/QueryEditor/QueryBuilder/utils/parseToString.ts b/src/components/QueryEditor/QueryBuilder/utils/parseToString.ts index 936c310..9ea1abb 100644 --- a/src/components/QueryEditor/QueryBuilder/utils/parseToString.ts +++ b/src/components/QueryEditor/QueryBuilder/utils/parseToString.ts @@ -24,6 +24,6 @@ const filterVisualQueryToString = (query: FilterVisualQuery): string => { } export const parseVisualQueryToString = (query: VisualQuery): string => { - // TODO add parse pipes - return filterVisualQueryToString(query.filters); + const pipesPart = query.pipes?.length ? ` | ${query.pipes.join(' | ')}` : '' + return filterVisualQueryToString(query.filters) + pipesPart; } diff --git a/src/modifyQuery.test.ts b/src/modifyQuery.test.ts new file mode 100644 index 0000000..156afeb --- /dev/null +++ b/src/modifyQuery.test.ts @@ -0,0 +1,49 @@ +import { addLabelToQuery, removeLabelFromQuery } from './modifyQuery'; + +describe('modifyQuery', () => { + describe('addLabelToQuery', () => { + it('should add a label to the query with the specified operator', () => { + const query = 'foo: bar'; + const key = 'baz'; + const value = 'qux'; + const operator = 'AND'; + const result = addLabelToQuery(query, key, value, operator); + expect(result).toBe('foo: bar AND baz:"qux"'); + }); + + it('should add a label to the query and retain pipes', () => { + const query = 'foo: bar | pipe1 | pipe2'; + const key = 'baz'; + const value = 'qux'; + const operator = 'AND'; + const result = addLabelToQuery(query, key, value, operator); + expect(result).toBe('foo: bar AND baz:"qux" | pipe1 | pipe2'); + }); + }); + + describe('removeLabelFromQuery', () => { + it('should remove a label from the query', () => { + const query = 'foo: bar AND baz:"qux"'; + const key = 'baz'; + const value = 'qux'; + const result = removeLabelFromQuery(query, key, value); + expect(result).toBe('foo: bar'); + }); + + it('should remove a label from the query and retain pipes', () => { + const query = 'foo: bar AND baz:"qux" | pipe1 | pipe2'; + const key = 'baz'; + const value = 'qux'; + const result = removeLabelFromQuery(query, key, value); + expect(result).toBe('foo: bar | pipe1 | pipe2'); + }); + + it('should handle nested filters correctly', () => { + const query = 'foo: bar AND (baz:"qux" OR quux:"corge")'; + const key = 'baz'; + const value = 'qux'; + const result = removeLabelFromQuery(query, key, value); + expect(result).toBe('foo: bar AND (quux:"corge")'); + }); + }); +}); diff --git a/src/modifyQuery.ts b/src/modifyQuery.ts index 3ca56a3..b80569d 100644 --- a/src/modifyQuery.ts +++ b/src/modifyQuery.ts @@ -1,30 +1,54 @@ -export function queryHasFilter(query: string, key: string, value: string): boolean { - return query.includes(`${key}:${value}`) +import { buildVisualQueryFromString, splitExpression } from "./components/QueryEditor/QueryBuilder/utils/parseFromString"; +import { parseVisualQueryToString } from "./components/QueryEditor/QueryBuilder/utils/parseToString"; +import { FilterVisualQuery } from "./types"; + +const getKeyValue = (key: string, value: string): string => { + return `${key}:"${value}"` } -export const removeLabelFromQuery = (query: string, key: string, value: string): string => { - const operators = ['AND', 'NOT'] - const parts = query.split(' ') - const index = parts.findIndex((part) => part.includes(`${key}:${value}`)) - const newParts = removeAtIndexAndBefore(parts, index, operators.includes(parts[index - 1])) - return newParts.join(' ') +export function queryHasFilter(query: string, key: string, value: string): boolean { + return query.includes(getKeyValue(key, value)) } export const addLabelToQuery = (query: string, key: string, value: string, operator: string): string => { - return `${query} ${operator} ${key}:${value}` + const [filters, ...pipes] = splitExpression(query) + const insertPart = `${operator} ${getKeyValue(key, value)}` + const pipesPart = pipes?.length ? `| ${pipes.join(' | ')}` : '' + return (`${filters} ${insertPart} ${pipesPart}`).trim() } -const removeAtIndexAndBefore = (arr: string[], index: number, removeBefore: boolean): string[] => { - if (index < 0 || index >= arr.length) { - return arr; +export const removeLabelFromQuery = (query: string, key: string, value: string): string => { + const { query: { filters, pipes }, errors } = buildVisualQueryFromString(query); + if (errors.length) { + console.error(errors.join('\n')); + return query; } - if (removeBefore) { - const isStart = index === 0; - arr.splice(isStart ? index : index - 1, isStart ? 1 : 2); - } else { - arr.splice(index, 1); + const keyValue = getKeyValue(key, value); + recursiveRemove(filters, keyValue) + return parseVisualQueryToString({ filters, pipes }) +} + +const recursiveRemove = (filters: FilterVisualQuery, keyValue: string): boolean => { + const { values, operators } = filters; + let removed = false; + + for (let i = values.length - 1; i >= 0; i--) { + const val = values[i]; + const isString = typeof val === 'string' + const isFilterObject = typeof val === 'object' && 'values' in val + + if (isString && val === keyValue) { + // If the string matches keyValue, delete it and the operator + values.splice(i, 1); + (i > 0 && i - 1 < operators.length) && operators.splice(i - 1, 1); + removed = true; + } else if (isFilterObject) { + // If it is an object of type FilterVisualQuery, recursively check it + const wasRemoved = recursiveRemove(val, keyValue); + removed = wasRemoved || removed; + } } - return arr; + return removed; }