Compare commits

...

9 Commits

6 changed files with 391 additions and 31 deletions

View File

@@ -1,10 +1,16 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
Having,
IBuilderQuery,
Query,
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogAggregation,
@@ -13,6 +19,8 @@ import {
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { v4 as uuid } from 'uuid';
/**
* Check if an operator requires array values (like IN, NOT IN)
@@ -20,7 +28,7 @@ import { DataSource } from 'types/common/queryBuilder';
* @returns True if the operator requires array values
*/
const isArrayOperator = (operator: string): boolean => {
const arrayOperators = ['in', 'nin', 'IN', 'NOT IN'];
const arrayOperators = ['in', 'not in', 'IN', 'NOT IN'];
return arrayOperators.includes(operator);
};
@@ -87,6 +95,330 @@ export const convertFiltersToExpression = (
};
};
function unquote(str: string): string {
if (typeof str !== 'string') return str;
const startsWithQuote = str.startsWith('"') || str.startsWith("'");
const endsWithSameQuote =
(str.endsWith('"') && str[0] === '"') ||
(str.endsWith("'") && str[0] === "'");
if (startsWithQuote && endsWithSameQuote && str.length >= 2) {
return str.slice(1, -1);
}
return str;
}
const formatValuesForFilter = (value: string | string[]): string | string[] => {
if (Array.isArray(value)) {
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
}
if (typeof value === 'string') {
return unquote(value);
}
return String(value);
};
export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
if (!existingQuery) {
// If no existing query, return filters with a newly generated expression
return {
filters,
filter: convertFiltersToExpression(filters),
};
}
// Extract query pairs from the existing query
const queryPairs = extractQueryPairs(existingQuery.trim());
let queryPairsMap: Map<string, IQueryPair> = new Map();
const updatedFilters = cloneDeep(filters); // Clone filters to avoid direct mutation
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
// Map extracted query pairs to key-specific pair information for faster access
if (queryPairs.length > 0) {
queryPairsMap = new Map(
queryPairs.map((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
return [key, pair];
}),
);
}
filters.items.forEach((filter) => {
const { key, op, value } = filter;
// Skip invalid filters with no key
if (!key) return;
let shouldAddToNonExisting = true; // Flag to decide if the filter should be added to non-existing filters
const sanitizedOperator = op.trim().toUpperCase();
// Check if the operator is IN or NOT IN
if (
[OPERATORS.IN, `${OPERATORS.NOT} ${OPERATORS.IN}`].includes(
sanitizedOperator,
)
) {
const existingPair = queryPairsMap.get(
`${key.key}-${op}`.trim().toLowerCase(),
);
const formattedValue = formatValueForExpression(value, op);
// If a matching query pair exists, modify the query
if (
existingPair &&
existingPair.position?.valueStart &&
existingPair.position?.valueEnd
) {
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
return;
}
// Handle the different cases for IN operator
switch (sanitizedOperator) {
case OPERATORS.IN:
// If there's a NOT IN or equal operator, merge the filter
if (
queryPairsMap.has(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
)
) {
const notInPair = queryPairsMap.get(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
visitedPairs.add(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
if (notInPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notInPair.position.negationStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notInPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase())
) {
const equalsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase());
if (equalsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
equalsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
equalsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break;
case `${OPERATORS.NOT} ${OPERATORS.IN}`:
if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.NOT} ${
OPERATORS.IN
} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break; // No operation needed for NOT IN case
default:
break;
}
}
if (
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
}
// Add filters that don't have an existing pair to non-existing filters
if (
shouldAddToNonExisting &&
!queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
nonExistingFilters.push(filter);
}
});
// Create new filters from non-visited query pairs
const newFilterItems: TagFilterItem[] = [];
queryPairsMap.forEach((pair, key) => {
if (!visitedPairs.has(key)) {
const operator = pair.hasNegation
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
: getOperatorValue(pair.operator.toUpperCase());
newFilterItems.push({
id: uuid(),
op: operator,
key: {
id: pair.key,
key: pair.key,
type: '',
},
value: pair.isMultiValue
? formatValuesForFilter(pair.valueList as string[]) ?? ''
: formatValuesForFilter(pair.value as string) ?? '',
});
}
});
// Merge new filter items with existing ones
if (newFilterItems.length > 0) {
updatedFilters.items = [...updatedFilters.items, ...newFilterItems];
}
// If no non-existing filters, return the modified query directly
if (nonExistingFilters.length === 0) {
return {
filters: updatedFilters,
filter: { expression: modifiedQuery },
};
}
// Convert non-existing filters to an expression and append to the modified query
const nonExistingFilterExpression = convertFiltersToExpression({
items: nonExistingFilters,
op: filters.op || 'AND',
});
if (nonExistingFilterExpression.expression) {
return {
filters: updatedFilters,
filter: {
expression: `${modifiedQuery.trim()} ${
nonExistingFilterExpression.expression
}`,
},
};
}
// Return the final result with the modified query
return {
filters: updatedFilters,
filter: { expression: modifiedQuery || '' },
};
};
/**
* Removes specified key-value pairs from a logical query expression string.
*
* This function parses the given query expression and removes any query pairs
* whose keys match those in the `keysToRemove` array. It also removes any trailing
* logical conjunctions (e.g., `AND`, `OR`) and whitespace that follow the matched pairs,
* ensuring that the resulting expression remains valid and clean.
*
* @param expression - The full query string.
* @param keysToRemove - An array of keys (case-insensitive) that should be removed from the expression.
* @returns A new expression string with the specified keys and their associated clauses removed.
*/
export const removeKeysFromExpression = (
expression: string,
keysToRemove: string[],
): string => {
if (!keysToRemove || keysToRemove.length === 0) {
return expression;
}
let updatedExpression = expression;
if (updatedExpression) {
keysToRemove.forEach((key) => {
// Extract key-value query pairs from the expression
const existingQueryPairs = extractQueryPairs(updatedExpression);
let queryPairsMap: Map<string, IQueryPair>;
if (existingQueryPairs.length > 0) {
// Build a map for quick lookup of query pairs by their lowercase trimmed keys
queryPairsMap = new Map(
existingQueryPairs.map((pair) => {
const key = pair.key.trim().toLowerCase();
return [key, pair];
}),
);
// Lookup the current query pair using the attribute key (case-insensitive)
const currentQueryPair = queryPairsMap.get(`${key}`.trim().toLowerCase());
if (currentQueryPair && currentQueryPair.isComplete) {
// Determine the start index of the query pair (fallback order: key → operator → value)
const queryPairStart =
currentQueryPair.position.keyStart ??
currentQueryPair.position.operatorStart ??
currentQueryPair.position.valueStart;
// Determine the end index of the query pair (fallback order: value → operator → key)
let queryPairEnd =
currentQueryPair.position.valueEnd ??
currentQueryPair.position.operatorEnd ??
currentQueryPair.position.keyEnd;
// Get the part of the expression that comes after the current query pair
const expressionAfterPair = `${expression.slice(queryPairEnd + 1)}`;
// Match optional spaces and an optional conjunction (AND/OR), case-insensitive
const conjunctionOrSpacesRegex = /^(\s*((AND|OR)\s+)?)/i;
const match = expressionAfterPair.match(conjunctionOrSpacesRegex);
if (match && match.length > 0) {
// If match is found, extend the queryPairEnd to include the matched part
queryPairEnd += match[0].length;
}
// Remove the full query pair (including any conjunction/whitespace) from the expression
updatedExpression = `${expression.slice(
0,
queryPairStart,
)}${expression.slice(queryPairEnd + 1)}`.trim();
}
}
});
}
return updatedExpression;
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value

View File

@@ -6,14 +6,13 @@ import './Checkbox.styles.scss';
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
OPERATORS,
} from 'constants/queryBuilder';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
@@ -30,7 +29,7 @@ import { v4 as uuid } from 'uuid';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in'];
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
@@ -168,6 +167,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filter: {
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
filter.attributeKey.key,
]),
},
filters: {
...item.filters,
items:
@@ -213,6 +217,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
query.filters.items = query.filters.items.filter(
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
if (isOnlyOrAll === 'Only') {
const newFilterItem: TagFilterItem = {
id: uuid(),
@@ -293,7 +305,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}
}
break;
case 'nin':
case 'not in':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
@@ -372,7 +384,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (!checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue(OPERATORS.NIN),
op: getOperatorValue('NOT_IN'),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
@@ -395,7 +407,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.NIN),
op: getOperatorValue('NOT_IN'),
key: filter.attributeKey,
value,
};

View File

@@ -29,7 +29,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'In',
},
{
label: 'nin',
label: 'not in',
metricValue: '!~',
traceValue: 'NotIn',
},
@@ -49,7 +49,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'Exists',
},
{
label: 'nexists',
label: 'not exists',
metricValue: '!~',
traceValue: 'NotExists',
},
@@ -59,7 +59,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'Contains',
},
{
label: 'ncontains',
label: 'not contains',
metricValue: '!~',
traceValue: 'NotContains',
},

View File

@@ -58,27 +58,31 @@ export function getOperatorValue(op: string): string {
case 'IN':
return 'in';
case 'NOT_IN':
return 'nin';
return 'not in';
case OPERATORS.REGEX:
return 'regex';
case OPERATORS.HAS:
return 'has';
case OPERATORS.NHAS:
return 'nhas';
return 'not has';
case OPERATORS.NREGEX:
return 'nregex';
return 'not regex';
case 'LIKE':
return 'like';
case 'ILIKE':
return 'ilike';
case 'NOT_LIKE':
return 'nlike';
return 'not like';
case 'NOT_ILIKE':
return 'not ilike';
case 'EXISTS':
return 'exists';
case 'NOT_EXISTS':
return 'nexists';
return 'not exists';
case 'CONTAINS':
return 'contains';
case 'NOT_CONTAINS':
return 'ncontains';
return 'not contains';
default:
return op;
}
@@ -88,27 +92,27 @@ export function getOperatorFromValue(op: string): string {
switch (op) {
case 'in':
return 'IN';
case 'nin':
case 'not in':
return 'NOT_IN';
case 'like':
return 'LIKE';
case 'regex':
return OPERATORS.REGEX;
case 'nregex':
case 'not regex':
return OPERATORS.NREGEX;
case 'nlike':
case 'not like':
return 'NOT_LIKE';
case 'exists':
return 'EXISTS';
case 'nexists':
case 'not exists':
return 'NOT_EXISTS';
case 'contains':
return 'CONTAINS';
case 'ncontains':
case 'not contains':
return 'NOT_CONTAINS';
case 'has':
return OPERATORS.HAS;
case 'nhas':
case 'not has':
return OPERATORS.NHAS;
default:
return op;

View File

@@ -1,4 +1,5 @@
import { Select } from 'antd';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep } from 'lodash-es';
import { useEffect, useState } from 'react';
@@ -119,8 +120,18 @@ function SpanScopeSelector({
return [...nonScopeFilters, ...newScopeFilter];
};
const keysToRemove = Object.values(SPAN_FILTER_CONFIG)
.map((config) => config?.key)
.filter((key): key is string => typeof key === 'string');
newQuery.builder.queryData = newQuery.builder.queryData.map((item) => ({
...item,
filter: {
expression: removeKeysFromExpression(
item.filter?.expression ?? '',
keysToRemove,
),
},
filters: {
...item.filters,
items: getUpdatedFilters(

View File

@@ -1,6 +1,6 @@
import {
convertAggregationToExpression,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
convertHavingToExpression,
} from 'components/QueryBuilderV2/utils';
import { QueryParams } from 'constants/query';
@@ -28,13 +28,15 @@ export const useGetCompositeQueryParam = (): Query | null => {
if (parsedCompositeQuery?.builder?.queryData) {
parsedCompositeQuery.builder.queryData = parsedCompositeQuery.builder.queryData.map(
(query) => {
const existingExpression = query.filter?.expression || '';
const convertedQuery = { ...query };
// Convert filters if needed
if (query.filters?.items?.length > 0 && !query.filter?.expression) {
const convertedFilter = convertFiltersToExpression(query.filters);
convertedQuery.filter = convertedFilter;
}
const convertedFilter = convertFiltersToExpressionWithExistingQuery(
query.filters,
existingExpression,
);
convertedQuery.filter = convertedFilter.filter;
convertedQuery.filters = convertedFilter.filters;
// Convert having if needed
if (query.having?.length > 0 && !query.havingExpression?.expression) {
@@ -53,7 +55,6 @@ export const useGetCompositeQueryParam = (): Query | null => {
) as any; // Type assertion to handle union type
convertedQuery.aggregations = convertedAggregation;
}
return convertedQuery;
},
);