mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
fix: added fix for query builder filters (#8830)
* fix: added fix for query builder filters * fix: added fix for multivalue operator without brackets * test: added tests for querycontextUtils + querybuilderv2 utils * fix: added fix for replacing filter with the new value * fix: added fix for replacing filters + datetimepicker composite query * test: fixed querybuilderv2 utils test * chore: added changes for jest to use es6 * test: fixed tests for querycontextutils + querybuilderv2 utils * test: fixed failing tests
This commit is contained in:
parent
08323e4dfd
commit
deddf47e84
@ -16,6 +16,7 @@ const config: Config.InitialOptions = {
|
|||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
useESM: true,
|
useESM: true,
|
||||||
isolatedModules: true,
|
isolatedModules: true,
|
||||||
|
tsconfig: '<rootDir>/tsconfig.jest.json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import getLocal from '../../../api/browser/localstorage/get';
|
||||||
import AppLoading from '../AppLoading';
|
import AppLoading from '../AppLoading';
|
||||||
|
|
||||||
// Mock the localStorage API
|
jest.mock('../../../api/browser/localstorage/get', () => ({
|
||||||
const mockGet = jest.fn();
|
|
||||||
jest.mock('api/browser/localstorage/get', () => ({
|
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: mockGet,
|
default: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Access the mocked function
|
||||||
|
const mockGet = (getLocal as unknown) as jest.Mock;
|
||||||
|
|
||||||
describe('AppLoading', () => {
|
describe('AppLoading', () => {
|
||||||
const SIGNOZ_TEXT = 'SigNoz';
|
const SIGNOZ_TEXT = 'SigNoz';
|
||||||
const TAGLINE_TEXT =
|
const TAGLINE_TEXT =
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
/* eslint-disable import/no-unresolved */
|
||||||
|
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||||
|
|
||||||
import { convertFiltersToExpression } from '../utils';
|
import {
|
||||||
|
convertFiltersToExpression,
|
||||||
|
convertFiltersToExpressionWithExistingQuery,
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
describe('convertFiltersToExpression', () => {
|
describe('convertFiltersToExpression', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle empty, null, and undefined inputs', () => {
|
it('should handle empty, null, and undefined inputs', () => {
|
||||||
// Test null and undefined
|
// Test null and undefined
|
||||||
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
|
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
|
||||||
@ -533,4 +543,229 @@ describe('convertFiltersToExpression', () => {
|
|||||||
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
|
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return filters with new expression when no existing query', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'test-service',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toEqual(filters);
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'test-service'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty filters', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toEqual(filters);
|
||||||
|
expect(result.filter.expression).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle existing query with matching filters', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'updated-service',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toBeDefined();
|
||||||
|
expect(result.filter).toBeDefined();
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'updated-service'");
|
||||||
|
// Ensure parser can parse the existing query
|
||||||
|
expect(extractQueryPairs(existingQuery)).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'service.name',
|
||||||
|
operator: '=',
|
||||||
|
value: "'old-service'",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle IN operator with existing query', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS.IN,
|
||||||
|
value: ['service1', 'service2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name IN ['old-service']";
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toBeDefined();
|
||||||
|
expect(result.filter).toBeDefined();
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name IN ['service1', 'service2']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle IN operator conversion from equals', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS.IN,
|
||||||
|
value: ['service1', 'service2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name IN ['service1', 'service2'] ",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle NOT IN operator conversion from not equals', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: negateOperator(OPERATORS.IN),
|
||||||
|
value: ['service1', 'service2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name != 'old-service'";
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name NOT IN ['service1', 'service2'] ",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add new filters when they do not exist in existing query', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'new.key', key: 'new.key', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'new-value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(2); // Original + new filter
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name = 'old-service' new.key = 'new-value'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle simple value replacement', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'status', key: 'status', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "status = 'success'";
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
expect(result.filter.expression).toBe("status = 'error'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle filters with no key gracefully', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: undefined,
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'test-value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(2);
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
QUERY_BUILDER_FUNCTIONS,
|
QUERY_BUILDER_FUNCTIONS,
|
||||||
} from 'constants/antlrQueryConstants';
|
} from 'constants/antlrQueryConstants';
|
||||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
|
||||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import {
|
import {
|
||||||
@ -163,6 +163,19 @@ export const convertExpressionToFilters = (
|
|||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
};
|
};
|
||||||
|
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
|
||||||
|
const queryPairs = extractQueryPairs(query);
|
||||||
|
const queryPairsMap: Map<string, IQueryPair> = new Map();
|
||||||
|
|
||||||
|
queryPairs.forEach((pair) => {
|
||||||
|
const key = pair.hasNegation
|
||||||
|
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
|
||||||
|
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
|
||||||
|
queryPairsMap.set(key, pair);
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryPairsMap;
|
||||||
|
};
|
||||||
|
|
||||||
export const convertFiltersToExpressionWithExistingQuery = (
|
export const convertFiltersToExpressionWithExistingQuery = (
|
||||||
filters: TagFilter,
|
filters: TagFilter,
|
||||||
@ -195,24 +208,12 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract query pairs from the existing query
|
|
||||||
const queryPairs = extractQueryPairs(existingQuery.trim());
|
|
||||||
let queryPairsMap: Map<string, IQueryPair> = new Map();
|
|
||||||
const nonExistingFilters: TagFilterItem[] = [];
|
const nonExistingFilters: TagFilterItem[] = [];
|
||||||
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
|
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
|
||||||
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
|
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
|
||||||
|
|
||||||
// Map extracted query pairs to key-specific pair information for faster access
|
// Map extracted query pairs to key-specific pair information for faster access
|
||||||
if (queryPairs.length > 0) {
|
let queryPairsMap = getQueryPairsMap(existingQuery.trim());
|
||||||
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) => {
|
filters?.items?.forEach((filter) => {
|
||||||
const { key, op, value } = filter;
|
const { key, op, value } = filter;
|
||||||
@ -241,10 +242,37 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
existingPair.position?.valueEnd
|
existingPair.position?.valueEnd
|
||||||
) {
|
) {
|
||||||
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
|
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
|
||||||
|
|
||||||
|
// Check if existing values match current filter values (for array-based operators)
|
||||||
|
if (existingPair.valueList && filter.value && Array.isArray(filter.value)) {
|
||||||
|
// Clean quotes from string values for comparison
|
||||||
|
const cleanValues = (values: any[]): any[] =>
|
||||||
|
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
|
||||||
|
|
||||||
|
const cleanExistingValues = cleanValues(existingPair.valueList);
|
||||||
|
const cleanFilterValues = cleanValues(filter.value);
|
||||||
|
|
||||||
|
// Compare arrays (order-independent) - if identical, keep existing value
|
||||||
|
const isSameValues =
|
||||||
|
cleanExistingValues.length === cleanFilterValues.length &&
|
||||||
|
isEqual(sortBy(cleanExistingValues), sortBy(cleanFilterValues));
|
||||||
|
|
||||||
|
if (isSameValues) {
|
||||||
|
// Values are identical, preserve existing formatting
|
||||||
|
modifiedQuery =
|
||||||
|
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||||
|
existingPair.value +
|
||||||
|
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
modifiedQuery =
|
modifiedQuery =
|
||||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||||
formattedValue +
|
formattedValue +
|
||||||
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||||
|
|
||||||
|
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,6 +298,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||||
notInPair.position.valueEnd + 1,
|
notInPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
|
queryPairsMap = getQueryPairsMap(modifiedQuery.trim());
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
} else if (
|
} else if (
|
||||||
@ -286,6 +315,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||||
equalsPair.position.valueEnd + 1,
|
equalsPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
|
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
} else if (
|
} else if (
|
||||||
@ -302,6 +332,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||||
notEqualsPair.position.valueEnd + 1,
|
notEqualsPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
|
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
}
|
}
|
||||||
@ -323,6 +354,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
} ${formattedValue} ${modifiedQuery.slice(
|
} ${formattedValue} ${modifiedQuery.slice(
|
||||||
notEqualsPair.position.valueEnd + 1,
|
notEqualsPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
|
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
}
|
}
|
||||||
@ -335,6 +367,23 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
if (
|
if (
|
||||||
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
|
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
|
||||||
) {
|
) {
|
||||||
|
const existingPair = queryPairsMap.get(
|
||||||
|
`${filter.key?.key}-${filter.op}`.trim().toLowerCase(),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
existingPair &&
|
||||||
|
existingPair.position?.valueStart &&
|
||||||
|
existingPair.position?.valueEnd
|
||||||
|
) {
|
||||||
|
const formattedValue = formatValueForExpression(value, op);
|
||||||
|
// replace the value with the new value
|
||||||
|
modifiedQuery =
|
||||||
|
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||||
|
formattedValue +
|
||||||
|
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||||
|
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
|
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -151,7 +151,7 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
|||||||
|
|
||||||
jest.mock('hooks/logs/useCopyLogLink', () => ({
|
jest.mock('hooks/logs/useCopyLogLink', () => ({
|
||||||
useCopyLogLink: jest.fn().mockReturnValue({
|
useCopyLogLink: jest.fn().mockReturnValue({
|
||||||
activeLogId: ACTIVE_LOG_ID,
|
activeLogId: undefined,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||||
import { useNavigationType } from 'react-router-dom-v5-compat';
|
import { useNavigationType, useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
@ -117,6 +117,8 @@ function DateTimeSelection({
|
|||||||
);
|
);
|
||||||
const [modalEndTime, setModalEndTime] = useState<number>(initialModalEndTime);
|
const [modalEndTime, setModalEndTime] = useState<number>(initialModalEndTime);
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
// Effect to update modal time state when props change
|
// Effect to update modal time state when props change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modalInitialStartTime !== undefined) {
|
if (modalInitialStartTime !== undefined) {
|
||||||
@ -410,8 +412,10 @@ function DateTimeSelection({
|
|||||||
// Remove Hidden Filters from URL query parameters on time change
|
// Remove Hidden Filters from URL query parameters on time change
|
||||||
urlQuery.delete(QueryParams.activeLogId);
|
urlQuery.delete(QueryParams.activeLogId);
|
||||||
|
|
||||||
|
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||||
|
}
|
||||||
|
|
||||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||||
safeNavigate(generatedUrl);
|
safeNavigate(generatedUrl);
|
||||||
@ -428,6 +432,7 @@ function DateTimeSelection({
|
|||||||
updateLocalStorageForRoutes,
|
updateLocalStorageForRoutes,
|
||||||
updateTimeInterval,
|
updateTimeInterval,
|
||||||
urlQuery,
|
urlQuery,
|
||||||
|
searchParams,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -488,8 +493,10 @@ function DateTimeSelection({
|
|||||||
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
|
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
|
||||||
urlQuery.delete(QueryParams.relativeTime);
|
urlQuery.delete(QueryParams.relativeTime);
|
||||||
|
|
||||||
|
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||||
|
}
|
||||||
|
|
||||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||||
safeNavigate(generatedUrl);
|
safeNavigate(generatedUrl);
|
||||||
|
|||||||
501
frontend/src/utils/__tests__/queryContextUtils.test.ts
Normal file
501
frontend/src/utils/__tests__/queryContextUtils.test.ts
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
extractQueryPairs,
|
||||||
|
getCurrentQueryPair,
|
||||||
|
getCurrentValueIndexAtCursor,
|
||||||
|
} from '../queryContextUtils';
|
||||||
|
|
||||||
|
describe('extractQueryPairs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract NOT EXISTS and NOT LIKE correctly', () => {
|
||||||
|
const input = "active NOT EXISTS AND name NOT LIKE '%tmp%'";
|
||||||
|
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
key: 'active',
|
||||||
|
operator: 'EXISTS',
|
||||||
|
value: undefined,
|
||||||
|
valueList: [],
|
||||||
|
valuesPosition: [],
|
||||||
|
hasNegation: true,
|
||||||
|
isMultiValue: false,
|
||||||
|
position: expect.any(Object),
|
||||||
|
isComplete: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
operator: 'LIKE',
|
||||||
|
value: "'%tmp%'",
|
||||||
|
valueList: [],
|
||||||
|
valuesPosition: [],
|
||||||
|
hasNegation: true,
|
||||||
|
isMultiValue: false,
|
||||||
|
position: expect.any(Object),
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract IN with numeric list inside parentheses', () => {
|
||||||
|
const input = 'id IN (1, 2, 3)';
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'id',
|
||||||
|
operator: 'IN',
|
||||||
|
isMultiValue: true,
|
||||||
|
isComplete: true,
|
||||||
|
value: expect.stringMatching(/^\(.*\)$/),
|
||||||
|
valueList: ['1', '2', '3'],
|
||||||
|
valuesPosition: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
start: expect.any(Number),
|
||||||
|
end: expect.any(Number),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle extra whitespace and separators in IN lists', () => {
|
||||||
|
const input = "label IN [ 'a' , 'b' , 'c' ]";
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'label',
|
||||||
|
operator: 'IN',
|
||||||
|
isMultiValue: true,
|
||||||
|
isComplete: true,
|
||||||
|
value: expect.stringMatching(/^\[.*\]$/),
|
||||||
|
valueList: ["'a'", "'b'", "'c'"],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return incomplete pair when value is missing', () => {
|
||||||
|
const input = 'a =';
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'a',
|
||||||
|
operator: '=',
|
||||||
|
value: undefined,
|
||||||
|
isComplete: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse pairs within grouping parentheses with conjunctions', () => {
|
||||||
|
const input = "(name = 'x' AND age > 10) OR active EXISTS";
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'name',
|
||||||
|
operator: '=',
|
||||||
|
value: "'x'",
|
||||||
|
isComplete: true,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'age',
|
||||||
|
operator: '>',
|
||||||
|
value: '10',
|
||||||
|
isComplete: true,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'active',
|
||||||
|
operator: 'EXISTS',
|
||||||
|
value: undefined,
|
||||||
|
isComplete: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract query pairs from complex query with IN operator and multiple conditions', () => {
|
||||||
|
const input =
|
||||||
|
"service.name IN ['adservice', 'consumer-svc-1'] AND cloud.account.id = 'signoz-staging' code.lineno < 172";
|
||||||
|
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: 'IN',
|
||||||
|
value: "['adservice', 'consumer-svc-1']",
|
||||||
|
valueList: ["'adservice'", "'consumer-svc-1'"],
|
||||||
|
valuesPosition: [
|
||||||
|
{
|
||||||
|
start: 17,
|
||||||
|
end: 27,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: 30,
|
||||||
|
end: 45,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 14,
|
||||||
|
valueStart: 16,
|
||||||
|
valueEnd: 46,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cloud.account.id',
|
||||||
|
operator: '=',
|
||||||
|
value: "'signoz-staging'",
|
||||||
|
valueList: [],
|
||||||
|
valuesPosition: [],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
position: {
|
||||||
|
keyStart: 52,
|
||||||
|
keyEnd: 67,
|
||||||
|
operatorStart: 69,
|
||||||
|
operatorEnd: 69,
|
||||||
|
valueStart: 71,
|
||||||
|
valueEnd: 86,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'code.lineno',
|
||||||
|
operator: '<',
|
||||||
|
value: '172',
|
||||||
|
valueList: [],
|
||||||
|
valuesPosition: [],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
position: {
|
||||||
|
keyStart: 88,
|
||||||
|
keyEnd: 98,
|
||||||
|
operatorStart: 100,
|
||||||
|
operatorEnd: 100,
|
||||||
|
valueStart: 102,
|
||||||
|
valueEnd: 104,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract query pairs from complex query with IN operator without brackets', () => {
|
||||||
|
const input =
|
||||||
|
"service.name IN 'adservice' AND cloud.account.id = 'signoz-staging' code.lineno < 172";
|
||||||
|
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: 'IN',
|
||||||
|
value: "'adservice'",
|
||||||
|
valueList: ["'adservice'"],
|
||||||
|
valuesPosition: [
|
||||||
|
{
|
||||||
|
start: 16,
|
||||||
|
end: 26,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 14,
|
||||||
|
valueStart: 16,
|
||||||
|
valueEnd: 26,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cloud.account.id',
|
||||||
|
operator: '=',
|
||||||
|
value: "'signoz-staging'",
|
||||||
|
valueList: [],
|
||||||
|
valuesPosition: [],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
position: {
|
||||||
|
keyStart: 32,
|
||||||
|
keyEnd: 47,
|
||||||
|
operatorStart: 49,
|
||||||
|
operatorEnd: 49,
|
||||||
|
valueStart: 51,
|
||||||
|
valueEnd: 66,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'code.lineno',
|
||||||
|
operator: '<',
|
||||||
|
value: '172',
|
||||||
|
valueList: [],
|
||||||
|
valuesPosition: [],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
position: {
|
||||||
|
keyStart: 68,
|
||||||
|
keyEnd: 78,
|
||||||
|
operatorStart: 80,
|
||||||
|
operatorEnd: 80,
|
||||||
|
valueStart: 82,
|
||||||
|
valueEnd: 84,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle recursion guard', () => {
|
||||||
|
// This test verifies the recursion protection in the function
|
||||||
|
// We'll mock the function to simulate recursion
|
||||||
|
|
||||||
|
// Mock console.warn to capture the warning
|
||||||
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Call the function multiple times to trigger recursion guard
|
||||||
|
// Note: This is a simplified test since we can't easily trigger the actual recursion
|
||||||
|
const result = extractQueryPairs('test');
|
||||||
|
|
||||||
|
// The function should still work normally
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createContext', () => {
|
||||||
|
test('should create a context object with all parameters', () => {
|
||||||
|
const mockToken = {
|
||||||
|
type: 29,
|
||||||
|
text: 'test',
|
||||||
|
start: 0,
|
||||||
|
stop: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createContext(
|
||||||
|
mockToken as any,
|
||||||
|
true, // isInKey
|
||||||
|
false, // isInNegation
|
||||||
|
false, // isInOperator
|
||||||
|
false, // isInValue
|
||||||
|
'testKey', // keyToken
|
||||||
|
'=', // operatorToken
|
||||||
|
'testValue', // valueToken
|
||||||
|
[], // queryPairs
|
||||||
|
null, // currentPair
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
tokenType: 29,
|
||||||
|
text: 'test',
|
||||||
|
start: 0,
|
||||||
|
stop: 3,
|
||||||
|
currentToken: 'test',
|
||||||
|
isInKey: true,
|
||||||
|
isInNegation: false,
|
||||||
|
isInOperator: false,
|
||||||
|
isInValue: false,
|
||||||
|
isInFunction: false,
|
||||||
|
isInConjunction: false,
|
||||||
|
isInParenthesis: false,
|
||||||
|
keyToken: 'testKey',
|
||||||
|
operatorToken: '=',
|
||||||
|
valueToken: 'testValue',
|
||||||
|
queryPairs: [],
|
||||||
|
currentPair: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create a context object with minimal parameters', () => {
|
||||||
|
const mockToken = {
|
||||||
|
type: 29,
|
||||||
|
text: 'test',
|
||||||
|
start: 0,
|
||||||
|
stop: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createContext(mockToken as any, false, false, false, false);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
tokenType: 29,
|
||||||
|
text: 'test',
|
||||||
|
start: 0,
|
||||||
|
stop: 3,
|
||||||
|
currentToken: 'test',
|
||||||
|
isInKey: false,
|
||||||
|
isInNegation: false,
|
||||||
|
isInOperator: false,
|
||||||
|
isInValue: false,
|
||||||
|
isInFunction: false,
|
||||||
|
isInConjunction: false,
|
||||||
|
isInParenthesis: false,
|
||||||
|
keyToken: undefined,
|
||||||
|
operatorToken: undefined,
|
||||||
|
valueToken: undefined,
|
||||||
|
queryPairs: [],
|
||||||
|
currentPair: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCurrentValueIndexAtCursor', () => {
|
||||||
|
test('should return correct value index when cursor is within a value range', () => {
|
||||||
|
const valuesPosition = [
|
||||||
|
{ start: 0, end: 10 },
|
||||||
|
{ start: 15, end: 25 },
|
||||||
|
{ start: 30, end: 40 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getCurrentValueIndexAtCursor(valuesPosition, 20);
|
||||||
|
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null when cursor is not within any value range', () => {
|
||||||
|
const valuesPosition = [
|
||||||
|
{ start: 0, end: 10 },
|
||||||
|
{ start: 15, end: 25 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getCurrentValueIndexAtCursor(valuesPosition, 12);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct index when cursor is at the boundary', () => {
|
||||||
|
const valuesPosition = [
|
||||||
|
{ start: 0, end: 10 },
|
||||||
|
{ start: 15, end: 25 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getCurrentValueIndexAtCursor(valuesPosition, 10);
|
||||||
|
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for empty valuesPosition array', () => {
|
||||||
|
const result = getCurrentValueIndexAtCursor([], 5);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCurrentQueryPair', () => {
|
||||||
|
test('should return the correct query pair at cursor position', () => {
|
||||||
|
const queryPairs = [
|
||||||
|
{
|
||||||
|
key: 'a',
|
||||||
|
operator: '=',
|
||||||
|
value: '1',
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 0,
|
||||||
|
operatorStart: 2,
|
||||||
|
operatorEnd: 2,
|
||||||
|
valueStart: 4,
|
||||||
|
valueEnd: 4,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
key: 'b',
|
||||||
|
operator: '=',
|
||||||
|
value: '2',
|
||||||
|
position: {
|
||||||
|
keyStart: 10,
|
||||||
|
keyEnd: 10,
|
||||||
|
operatorStart: 12,
|
||||||
|
operatorEnd: 12,
|
||||||
|
valueStart: 14,
|
||||||
|
valueEnd: 14,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
} as any,
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = 'a = 1 AND b = 2';
|
||||||
|
const result = getCurrentQueryPair(queryPairs, query, 15);
|
||||||
|
|
||||||
|
expect(result).toEqual(queryPairs[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null when no pairs match cursor position', () => {
|
||||||
|
const queryPairs = [
|
||||||
|
{
|
||||||
|
key: 'a',
|
||||||
|
operator: '=',
|
||||||
|
value: '1',
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 0,
|
||||||
|
operatorStart: 2,
|
||||||
|
operatorEnd: 2,
|
||||||
|
valueStart: 4,
|
||||||
|
valueEnd: 4,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
} as any,
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = 'a = 1';
|
||||||
|
// Test with cursor position that's before any pair starts
|
||||||
|
const result = getCurrentQueryPair(queryPairs, query, -1);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for empty queryPairs array', () => {
|
||||||
|
const result = getCurrentQueryPair([], 'test query', 5);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return last pair when cursor is at the end', () => {
|
||||||
|
const queryPairs = [
|
||||||
|
{
|
||||||
|
key: 'a',
|
||||||
|
operator: '=',
|
||||||
|
value: '1',
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 0,
|
||||||
|
operatorStart: 2,
|
||||||
|
operatorEnd: 2,
|
||||||
|
valueStart: 4,
|
||||||
|
valueEnd: 4,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
} as any,
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = 'a = 1';
|
||||||
|
const result = getCurrentQueryPair(queryPairs, query, 5);
|
||||||
|
|
||||||
|
expect(result).toEqual(queryPairs[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1255,10 +1255,12 @@ export function extractQueryPairs(query: string): IQueryPair[] {
|
|||||||
allTokens[iterator].type,
|
allTokens[iterator].type,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
// Capture opening token type before advancing
|
||||||
|
const openingTokenType = allTokens[iterator].type;
|
||||||
multiValueStart = allTokens[iterator].start;
|
multiValueStart = allTokens[iterator].start;
|
||||||
iterator += 1;
|
iterator += 1;
|
||||||
const closingToken =
|
const closingToken =
|
||||||
allTokens[iterator].type === FilterQueryLexer.LPAREN
|
openingTokenType === FilterQueryLexer.LPAREN
|
||||||
? FilterQueryLexer.RPAREN
|
? FilterQueryLexer.RPAREN
|
||||||
: FilterQueryLexer.RBRACK;
|
: FilterQueryLexer.RBRACK;
|
||||||
|
|
||||||
@ -1279,6 +1281,15 @@ export function extractQueryPairs(query: string): IQueryPair[] {
|
|||||||
if (allTokens[iterator].type === closingToken) {
|
if (allTokens[iterator].type === closingToken) {
|
||||||
multiValueEnd = allTokens[iterator].stop;
|
multiValueEnd = allTokens[iterator].stop;
|
||||||
}
|
}
|
||||||
|
} else if (isValueToken(allTokens[iterator].type)) {
|
||||||
|
valueList.push(allTokens[iterator].text);
|
||||||
|
valuesPosition.push({
|
||||||
|
start: allTokens[iterator].start,
|
||||||
|
end: allTokens[iterator].stop,
|
||||||
|
});
|
||||||
|
multiValueStart = allTokens[iterator].start;
|
||||||
|
multiValueEnd = allTokens[iterator].stop;
|
||||||
|
iterator += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPair.valuesPosition = valuesPosition;
|
currentPair.valuesPosition = valuesPosition;
|
||||||
|
|||||||
7
frontend/tsconfig.jest.json
Normal file
7
frontend/tsconfig.jest.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"noEmit": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user