Compare commits
15 Commits
v0.92.1
...
fix/extrac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603d6d6fce | ||
|
|
339d81a240 | ||
|
|
2acee366a0 | ||
|
|
76e8672ee5 | ||
|
|
64c0bc1b97 | ||
|
|
9d3733ed72 | ||
|
|
72ec4a1b1f | ||
|
|
f5bf8f9f70 | ||
|
|
365cbdd835 | ||
|
|
d26efd2833 | ||
|
|
0e3ac2a179 | ||
|
|
30bf3a53f5 | ||
|
|
249f8be845 | ||
|
|
0dd085c48e | ||
|
|
531a0a12dd |
@@ -24,7 +24,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- zookeeper
|
- zookeeper
|
||||||
zookeeper:
|
zookeeper:
|
||||||
image: bitnami/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
container_name: zookeeper
|
container_name: zookeeper
|
||||||
volumes:
|
volumes:
|
||||||
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
|
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
hard: 262144
|
hard: 262144
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: bitnami/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
user: root
|
user: root
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
hard: 262144
|
hard: 262144
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: bitnami/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
user: root
|
user: root
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
hard: 262144
|
hard: 262144
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: bitnami/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
user: root
|
user: root
|
||||||
labels:
|
labels:
|
||||||
signoz.io/scrape: "true"
|
signoz.io/scrape: "true"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
hard: 262144
|
hard: 262144
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: bitnami/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
user: root
|
user: root
|
||||||
labels:
|
labels:
|
||||||
signoz.io/scrape: "true"
|
signoz.io/scrape: "true"
|
||||||
|
|||||||
386
frontend/src/components/QueryBuilderV2/utils.test.ts
Normal file
386
frontend/src/components/QueryBuilderV2/utils.test.ts
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||||
|
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||||
|
|
||||||
|
// Now import the function after all mocks are set up
|
||||||
|
import { convertFiltersToExpressionWithExistingQuery } from './utils';
|
||||||
|
|
||||||
|
jest.mock('utils/queryContextUtils', () => ({
|
||||||
|
extractQueryPairs: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Type the mocked functions
|
||||||
|
const mockExtractQueryPairs = extractQueryPairs as jest.MockedFunction<
|
||||||
|
typeof extractQueryPairs
|
||||||
|
>;
|
||||||
|
|
||||||
|
describe('convertFiltersToExpressionWithExistingQuery', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty filters', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toEqual(filters);
|
||||||
|
expect(result.filter.expression).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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'";
|
||||||
|
|
||||||
|
// Mock extractQueryPairs to return query pairs with position information
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toBeDefined();
|
||||||
|
expect(result.filter).toBeDefined();
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||||
|
expect(mockExtractQueryPairs).toHaveBeenCalledWith(
|
||||||
|
"service.name = 'old-service'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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']";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: 'IN',
|
||||||
|
value: "['old-service']",
|
||||||
|
valueList: ["'old-service'"],
|
||||||
|
valuesPosition: [
|
||||||
|
{
|
||||||
|
start: 17,
|
||||||
|
end: 29,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 14,
|
||||||
|
valueStart: 16,
|
||||||
|
valueEnd: 30,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toBeDefined();
|
||||||
|
expect(result.filter).toBeDefined();
|
||||||
|
// The function is currently returning the new value but with extra characters
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name IN ['service1', 'service2']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
// The function is currently returning the new value but with extra characters
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name IN ['service1', 'service2'] ",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['!='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 14,
|
||||||
|
valueStart: 16,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
// The function is currently returning the new value but with extra characters
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name NOT IN ['service1', 'service2'] ",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
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'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'success'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 6,
|
||||||
|
operatorStart: 8,
|
||||||
|
operatorEnd: 8,
|
||||||
|
valueStart: 10,
|
||||||
|
valueEnd: 19,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
// The function is currently returning the original expression (until we fix the replacement logic)
|
||||||
|
expect(result.filter.expression).toBe("status = 'success'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(2); // Original + new filter (even though it has no key)
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
import { NON_VALUE_OPERATORS, OPERATORS } 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 {
|
||||||
@@ -87,10 +87,15 @@ export const convertFiltersToExpression = (
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sanitizedOperator = op.trim().toUpperCase();
|
||||||
if (isFunctionOperator(op)) {
|
if (isFunctionOperator(op)) {
|
||||||
return `${op}(${key.key}, ${value})`;
|
return `${op}(${key.key}, ${value})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (NON_VALUE_OPERATORS.includes(sanitizedOperator)) {
|
||||||
|
return `${key.key} ${op}`;
|
||||||
|
}
|
||||||
|
|
||||||
const formattedValue = formatValueForExpression(value, op);
|
const formattedValue = formatValueForExpression(value, op);
|
||||||
return `${key.key} ${op} ${formattedValue}`;
|
return `${key.key} ${op} ${formattedValue}`;
|
||||||
})
|
})
|
||||||
@@ -201,6 +206,31 @@ 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 +
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ function TimeSeriesViewContainer({
|
|||||||
dataSource = DataSource.TRACES,
|
dataSource = DataSource.TRACES,
|
||||||
isFilterApplied,
|
isFilterApplied,
|
||||||
setWarning,
|
setWarning,
|
||||||
|
setIsLoadingQueries,
|
||||||
}: TimeSeriesViewProps): JSX.Element {
|
}: TimeSeriesViewProps): JSX.Element {
|
||||||
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
@@ -83,6 +84,14 @@ function TimeSeriesViewContainer({
|
|||||||
[data, isValidToConvertToMs],
|
[data, isValidToConvertToMs],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading || isFetching) {
|
||||||
|
setIsLoadingQueries(true);
|
||||||
|
} else {
|
||||||
|
setIsLoadingQueries(false);
|
||||||
|
}
|
||||||
|
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeSeriesView
|
<TimeSeriesView
|
||||||
isFilterApplied={isFilterApplied}
|
isFilterApplied={isFilterApplied}
|
||||||
@@ -101,6 +110,7 @@ interface TimeSeriesViewProps {
|
|||||||
dataSource?: DataSource;
|
dataSource?: DataSource;
|
||||||
isFilterApplied: boolean;
|
isFilterApplied: boolean;
|
||||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||||
|
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeSeriesViewContainer.defaultProps = {
|
TimeSeriesViewContainer.defaultProps = {
|
||||||
|
|||||||
@@ -49,9 +49,14 @@ import { getListColumns, transformDataWithDate } from './utils';
|
|||||||
interface ListViewProps {
|
interface ListViewProps {
|
||||||
isFilterApplied: boolean;
|
isFilterApplied: boolean;
|
||||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||||
|
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListView({ isFilterApplied, setWarning }: ListViewProps): JSX.Element {
|
function ListView({
|
||||||
|
isFilterApplied,
|
||||||
|
setWarning,
|
||||||
|
setIsLoadingQueries,
|
||||||
|
}: ListViewProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
panelType: panelTypeFromQueryBuilder,
|
panelType: panelTypeFromQueryBuilder,
|
||||||
@@ -162,6 +167,14 @@ function ListView({ isFilterApplied, setWarning }: ListViewProps): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data?.payload, data?.warning]);
|
}, [data?.payload, data?.warning]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading || isFetching) {
|
||||||
|
setIsLoadingQueries(true);
|
||||||
|
} else {
|
||||||
|
setIsLoadingQueries(false);
|
||||||
|
}
|
||||||
|
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||||
|
|
||||||
const dataLength =
|
const dataLength =
|
||||||
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
||||||
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
|
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
function TableView({
|
function TableView({
|
||||||
setWarning,
|
setWarning,
|
||||||
|
setIsLoadingQueries,
|
||||||
}: {
|
}: {
|
||||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||||
|
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { stagedQuery, panelType } = useQueryBuilder();
|
const { stagedQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ function TableView({
|
|||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useGetQueryRange(
|
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||||
{
|
{
|
||||||
query: stagedQuery || initialQueriesMap.traces,
|
query: stagedQuery || initialQueriesMap.traces,
|
||||||
graphType: panelType || PANEL_TYPES.TABLE,
|
graphType: panelType || PANEL_TYPES.TABLE,
|
||||||
@@ -49,6 +51,14 @@ function TableView({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading || isFetching) {
|
||||||
|
setIsLoadingQueries(true);
|
||||||
|
} else {
|
||||||
|
setIsLoadingQueries(false);
|
||||||
|
}
|
||||||
|
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||||
|
|
||||||
const queryTableData = useMemo(
|
const queryTableData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
data?.payload?.data?.newResult?.data?.result ||
|
data?.payload?.data?.newResult?.data?.result ||
|
||||||
|
|||||||
@@ -40,11 +40,13 @@ import { ActionsContainer, Container } from './styles';
|
|||||||
interface TracesViewProps {
|
interface TracesViewProps {
|
||||||
isFilterApplied: boolean;
|
isFilterApplied: boolean;
|
||||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||||
|
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TracesView({
|
function TracesView({
|
||||||
isFilterApplied,
|
isFilterApplied,
|
||||||
setWarning,
|
setWarning,
|
||||||
|
setIsLoadingQueries,
|
||||||
}: TracesViewProps): JSX.Element {
|
}: TracesViewProps): JSX.Element {
|
||||||
const { stagedQuery, panelType } = useQueryBuilder();
|
const { stagedQuery, panelType } = useQueryBuilder();
|
||||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||||
@@ -117,6 +119,14 @@ function TracesView({
|
|||||||
[responseData],
|
[responseData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading || isFetching) {
|
||||||
|
setIsLoadingQueries(true);
|
||||||
|
} else {
|
||||||
|
setIsLoadingQueries(false);
|
||||||
|
}
|
||||||
|
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !isFetching && !isError && (tableData || []).length !== 0) {
|
if (!isLoading && !isFetching && !isError && (tableData || []).length !== 0) {
|
||||||
logEvent('Traces Explorer: Data present', {
|
logEvent('Traces Explorer: Data present', {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import cx from 'classnames';
|
|||||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
@@ -129,23 +129,22 @@ function MetricPage(): JSX.Element {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const [renderedGraphCount, setRenderedGraphCount] = useState(0);
|
const renderedGraphCountRef = useRef(0);
|
||||||
const hasLoggedRef = useRef(false);
|
const hasLoggedRef = useRef(false);
|
||||||
|
|
||||||
const checkIfDataExists = (isDataAvailable: boolean): void => {
|
const checkIfDataExists = useCallback((isDataAvailable: boolean): void => {
|
||||||
if (isDataAvailable) {
|
if (isDataAvailable) {
|
||||||
const newCount = renderedGraphCount + 1;
|
renderedGraphCountRef.current += 1;
|
||||||
setRenderedGraphCount(newCount);
|
|
||||||
|
|
||||||
// Only log when first graph has rendered and we haven't logged yet
|
// Only log when first graph has rendered and we haven't logged yet
|
||||||
if (newCount === 1 && !hasLoggedRef.current) {
|
if (renderedGraphCountRef.current === 1 && !hasLoggedRef.current) {
|
||||||
logEvent('MQ Kafka: Metric view', {
|
logEvent('MQ Kafka: Metric view', {
|
||||||
graphRendered: true,
|
graphRendered: true,
|
||||||
});
|
});
|
||||||
hasLoggedRef.current = true;
|
hasLoggedRef.current = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metric-page">
|
<div className="metric-page">
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ function TracesExplorer(): JSX.Element {
|
|||||||
|
|
||||||
// Get panel type from URL
|
// Get panel type from URL
|
||||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
|
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
|
||||||
|
|
||||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||||
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||||
@@ -323,6 +324,7 @@ function TracesExplorer(): JSX.Element {
|
|||||||
rightActions={
|
rightActions={
|
||||||
<RightToolbarActions
|
<RightToolbarActions
|
||||||
onStageRunQuery={(): void => handleRunQuery(true, true)}
|
onStageRunQuery={(): void => handleRunQuery(true, true)}
|
||||||
|
isLoadingQueries={isLoadingQueries}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -344,13 +346,21 @@ function TracesExplorer(): JSX.Element {
|
|||||||
|
|
||||||
{selectedView === ExplorerViews.LIST && (
|
{selectedView === ExplorerViews.LIST && (
|
||||||
<div className="trace-explorer-list-view">
|
<div className="trace-explorer-list-view">
|
||||||
<ListView isFilterApplied={isFilterApplied} setWarning={setWarning} />
|
<ListView
|
||||||
|
isFilterApplied={isFilterApplied}
|
||||||
|
setWarning={setWarning}
|
||||||
|
setIsLoadingQueries={setIsLoadingQueries}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedView === ExplorerViews.TRACE && (
|
{selectedView === ExplorerViews.TRACE && (
|
||||||
<div className="trace-explorer-traces-view">
|
<div className="trace-explorer-traces-view">
|
||||||
<TracesView isFilterApplied={isFilterApplied} setWarning={setWarning} />
|
<TracesView
|
||||||
|
isFilterApplied={isFilterApplied}
|
||||||
|
setWarning={setWarning}
|
||||||
|
setIsLoadingQueries={setIsLoadingQueries}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -360,13 +370,17 @@ function TracesExplorer(): JSX.Element {
|
|||||||
dataSource={DataSource.TRACES}
|
dataSource={DataSource.TRACES}
|
||||||
isFilterApplied={isFilterApplied}
|
isFilterApplied={isFilterApplied}
|
||||||
setWarning={setWarning}
|
setWarning={setWarning}
|
||||||
|
setIsLoadingQueries={setIsLoadingQueries}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedView === ExplorerViews.TABLE && (
|
{selectedView === ExplorerViews.TABLE && (
|
||||||
<div className="trace-explorer-table-view">
|
<div className="trace-explorer-table-view">
|
||||||
<TableView setWarning={setWarning} />
|
<TableView
|
||||||
|
setWarning={setWarning}
|
||||||
|
setIsLoadingQueries={setIsLoadingQueries}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
627
frontend/src/utils/__tests__/queryContextUtils.test.ts
Normal file
627
frontend/src/utils/__tests__/queryContextUtils.test.ts
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// Mock all dependencies before importing the function
|
||||||
|
// Global variable to store the current test input
|
||||||
|
let currentTestInput = '';
|
||||||
|
|
||||||
|
// Now import the function after all mocks are set up
|
||||||
|
// Import the mocked antlr4 to access CharStreams
|
||||||
|
import * as antlr4 from 'antlr4';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
extractQueryPairs,
|
||||||
|
getCurrentQueryPair,
|
||||||
|
getCurrentValueIndexAtCursor,
|
||||||
|
} from '../queryContextUtils';
|
||||||
|
|
||||||
|
jest.mock('antlr4', () => ({
|
||||||
|
CharStreams: {
|
||||||
|
fromString: jest.fn().mockImplementation((input: string) => {
|
||||||
|
currentTestInput = input;
|
||||||
|
return {
|
||||||
|
inputSource: { strdata: input },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
CommonTokenStream: jest.fn().mockImplementation(() => {
|
||||||
|
// Use the dynamically captured input string from the current test
|
||||||
|
const input = currentTestInput;
|
||||||
|
|
||||||
|
// Generate tokens dynamically based on the input
|
||||||
|
const tokens = [];
|
||||||
|
let currentPos = 0;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < input.length) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (i < input.length && /\s/.test(input[i])) {
|
||||||
|
i++;
|
||||||
|
currentPos++;
|
||||||
|
}
|
||||||
|
if (i >= input.length) break;
|
||||||
|
|
||||||
|
// Handle array brackets
|
||||||
|
if (input[i] === '[') {
|
||||||
|
tokens.push({
|
||||||
|
type: 3, // LBRACK
|
||||||
|
text: '[',
|
||||||
|
start: currentPos,
|
||||||
|
stop: currentPos,
|
||||||
|
channel: 0,
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
currentPos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input[i] === ']') {
|
||||||
|
tokens.push({
|
||||||
|
type: 4, // RBRACK
|
||||||
|
text: ']',
|
||||||
|
start: currentPos,
|
||||||
|
stop: currentPos,
|
||||||
|
channel: 0,
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
currentPos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input[i] === ',') {
|
||||||
|
tokens.push({
|
||||||
|
type: 5, // COMMA
|
||||||
|
text: ',',
|
||||||
|
start: currentPos,
|
||||||
|
stop: currentPos,
|
||||||
|
channel: 0,
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
currentPos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the end of the current token
|
||||||
|
let tokenEnd = i;
|
||||||
|
let inQuotes = false;
|
||||||
|
let quoteChar = '';
|
||||||
|
|
||||||
|
while (tokenEnd < input.length) {
|
||||||
|
const char = input[tokenEnd];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!inQuotes &&
|
||||||
|
(char === ' ' || char === '[' || char === ']' || char === ',')
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((char === '"' || char === "'") && !inQuotes) {
|
||||||
|
inQuotes = true;
|
||||||
|
quoteChar = char;
|
||||||
|
} else if (char === quoteChar && inQuotes) {
|
||||||
|
inQuotes = false;
|
||||||
|
quoteChar = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenText = input.substring(i, tokenEnd);
|
||||||
|
|
||||||
|
// Determine token type
|
||||||
|
let tokenType = 28; // Default to QUOTED_TEXT
|
||||||
|
|
||||||
|
if (tokenText === 'IN') {
|
||||||
|
tokenType = 19;
|
||||||
|
} else if (tokenText === 'AND') {
|
||||||
|
tokenType = 21;
|
||||||
|
} else if (tokenText === '=') {
|
||||||
|
tokenType = 6;
|
||||||
|
} else if (tokenText === '<') {
|
||||||
|
tokenType = 9;
|
||||||
|
} else if (tokenText === '>') {
|
||||||
|
tokenType = 10;
|
||||||
|
} else if (tokenText === '!=') {
|
||||||
|
tokenType = 7;
|
||||||
|
} else if (tokenText.includes('.')) {
|
||||||
|
tokenType = 29; // KEY
|
||||||
|
} else if (/^\d+$/.test(tokenText)) {
|
||||||
|
tokenType = 27; // NUMBER
|
||||||
|
} else if (
|
||||||
|
(tokenText.startsWith("'") && tokenText.endsWith("'")) ||
|
||||||
|
(tokenText.startsWith('"') && tokenText.endsWith('"'))
|
||||||
|
) {
|
||||||
|
tokenType = 28; // QUOTED_TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push({
|
||||||
|
type: tokenType,
|
||||||
|
text: tokenText,
|
||||||
|
start: currentPos,
|
||||||
|
stop: currentPos + tokenText.length - 1,
|
||||||
|
channel: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentPos += tokenText.length;
|
||||||
|
i = tokenEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fill: jest.fn(),
|
||||||
|
tokens: [
|
||||||
|
...tokens,
|
||||||
|
// EOF
|
||||||
|
{ type: -1, text: '', start: 0, stop: 0, channel: 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
Token: {
|
||||||
|
EOF: -1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('parser/FilterQueryLexer', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: class MockFilterQueryLexer {
|
||||||
|
static readonly KEY = 29;
|
||||||
|
|
||||||
|
static readonly IN = 19;
|
||||||
|
|
||||||
|
static readonly EQUALS = 6;
|
||||||
|
|
||||||
|
static readonly LT = 9;
|
||||||
|
|
||||||
|
static readonly AND = 21;
|
||||||
|
|
||||||
|
static readonly LPAREN = 1;
|
||||||
|
|
||||||
|
static readonly RPAREN = 2;
|
||||||
|
|
||||||
|
static readonly LBRACK = 3;
|
||||||
|
|
||||||
|
static readonly RBRACK = 4;
|
||||||
|
|
||||||
|
static readonly COMMA = 5;
|
||||||
|
|
||||||
|
static readonly NOT = 20;
|
||||||
|
|
||||||
|
static readonly OR = 22;
|
||||||
|
|
||||||
|
static readonly EOF = -1;
|
||||||
|
|
||||||
|
static readonly QUOTED_TEXT = 28;
|
||||||
|
|
||||||
|
static readonly NUMBER = 27;
|
||||||
|
|
||||||
|
static readonly WS = 30;
|
||||||
|
|
||||||
|
static readonly FREETEXT = 31;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('parser/analyzeQuery', () => ({}));
|
||||||
|
|
||||||
|
jest.mock('../tokenUtils', () => ({
|
||||||
|
isOperatorToken: jest.fn((tokenType: number) =>
|
||||||
|
[6, 9, 19, 20].includes(tokenType),
|
||||||
|
),
|
||||||
|
isMultiValueOperator: jest.fn((operator: string) => operator === 'IN'),
|
||||||
|
isValueToken: jest.fn((tokenType: number) => [27, 28, 29].includes(tokenType)),
|
||||||
|
isConjunctionToken: jest.fn((tokenType: number) =>
|
||||||
|
[21, 22].includes(tokenType),
|
||||||
|
),
|
||||||
|
isQueryPairComplete: jest.fn((pair: any) => {
|
||||||
|
if (!pair) return false;
|
||||||
|
if (pair.operator === 'EXISTS') {
|
||||||
|
return !!pair.key && !!pair.operator;
|
||||||
|
}
|
||||||
|
return Boolean(pair.key && pair.operator && pair.value);
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('extractQueryPairs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 error gracefully and return empty array', () => {
|
||||||
|
// Mock console.error to suppress output during test
|
||||||
|
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Mock CharStreams to throw an error
|
||||||
|
jest.mocked(antlr4.CharStreams.fromString).mockImplementation(() => {
|
||||||
|
throw new Error('Mock error');
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = 'some query';
|
||||||
|
const result = extractQueryPairs(input);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
|
||||||
|
// Restore console.error
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1279,6 +1279,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;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function isQueryPairComplete(queryPair: Partial<IQueryPair>): boolean {
|
|||||||
export function isFunctionOperator(operator: string): boolean {
|
export function isFunctionOperator(operator: string): boolean {
|
||||||
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
|
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
|
||||||
|
|
||||||
const sanitizedOperator = operator.trim();
|
const sanitizedOperator = operator.trim().toLowerCase();
|
||||||
// Check if it's a direct function operator
|
// Check if it's a direct function operator
|
||||||
if (functionOperators.includes(sanitizedOperator)) {
|
if (functionOperators.includes(sanitizedOperator)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def zookeeper(
|
|||||||
def create() -> types.TestContainerDocker:
|
def create() -> types.TestContainerDocker:
|
||||||
version = request.config.getoption("--zookeeper-version")
|
version = request.config.getoption("--zookeeper-version")
|
||||||
|
|
||||||
container = DockerContainer(image=f"bitnami/zookeeper:{version}")
|
container = DockerContainer(image=f"signoz/zookeeper:{version}")
|
||||||
container.with_env("ALLOW_ANONYMOUS_LOGIN", "yes")
|
container.with_env("ALLOW_ANONYMOUS_LOGIN", "yes")
|
||||||
container.with_exposed_ports(2181)
|
container.with_exposed_ports(2181)
|
||||||
container.with_network(network=network)
|
container.with_network(network=network)
|
||||||
|
|||||||
Reference in New Issue
Block a user