mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
Merge branch 'fix/query-builder-filters' of github.com:SigNoz/signoz into feat/custom-destinations
This commit is contained in:
commit
6cb1ffdbc2
@ -1,9 +1,27 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import { convertFiltersToExpression } from '../utils';
|
||||
import {
|
||||
convertFiltersToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from '../utils';
|
||||
|
||||
jest.mock('utils/queryContextUtils', () => ({
|
||||
extractQueryPairs: jest.fn(),
|
||||
}));
|
||||
|
||||
// Type the mocked functions
|
||||
const mockExtractQueryPairs = extractQueryPairs as jest.MockedFunction<
|
||||
typeof extractQueryPairs
|
||||
>;
|
||||
|
||||
describe('convertFiltersToExpression', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle empty, null, and undefined inputs', () => {
|
||||
// Test null and undefined
|
||||
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
|
||||
@ -533,4 +551,364 @@ describe('convertFiltersToExpression', () => {
|
||||
"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'";
|
||||
|
||||
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 = 'updated-service'");
|
||||
expect(mockExtractQueryPairs).toHaveBeenCalledWith(
|
||||
"service.name = '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']";
|
||||
|
||||
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();
|
||||
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'";
|
||||
|
||||
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);
|
||||
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'";
|
||||
|
||||
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);
|
||||
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'";
|
||||
|
||||
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'",
|
||||
);
|
||||
});
|
||||
|
||||
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'";
|
||||
|
||||
mockExtractQueryPairs.mockReturnValue([
|
||||
{
|
||||
key: 'status',
|
||||
operator: OPERATORS['='],
|
||||
value: "'success'",
|
||||
hasNegation: false,
|
||||
isMultiValue: false,
|
||||
isComplete: true,
|
||||
position: {
|
||||
keyStart: 0,
|
||||
keyEnd: 6,
|
||||
operatorStart: 7,
|
||||
operatorEnd: 7,
|
||||
valueStart: 9,
|
||||
valueEnd: 19,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
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'";
|
||||
|
||||
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);
|
||||
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||
});
|
||||
});
|
||||
|
||||
@ -163,6 +163,19 @@ export const convertExpressionToFilters = (
|
||||
|
||||
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 = (
|
||||
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[] = [];
|
||||
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];
|
||||
}),
|
||||
);
|
||||
}
|
||||
let queryPairsMap = getQueryPairsMap(existingQuery.trim());
|
||||
|
||||
filters?.items?.forEach((filter) => {
|
||||
const { key, op, value } = filter;
|
||||
@ -242,21 +243,22 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
) {
|
||||
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)) {
|
||||
// Remove quotes from values before comparison
|
||||
const cleanExistingValues = existingPair.valueList.map((val) =>
|
||||
typeof val === 'string' ? val.replace(/^['"]|['"]$/g, '') : val,
|
||||
);
|
||||
const cleanFilterValues = filter.value.map((val) =>
|
||||
typeof val === 'string' ? val.replace(/^['"]|['"]$/g, '') : val,
|
||||
);
|
||||
// Clean quotes from string values for comparison
|
||||
const cleanValues = (values: any[]): any[] =>
|
||||
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
|
||||
|
||||
// Check if the value arrays are the same (order-independent)
|
||||
if (
|
||||
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))
|
||||
) {
|
||||
// Use existingPair.value instead of formattedValue
|
||||
isEqual(sortBy(cleanExistingValues), sortBy(cleanFilterValues));
|
||||
|
||||
if (isSameValues) {
|
||||
// Values are identical, preserve existing formatting
|
||||
modifiedQuery =
|
||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
existingPair.value +
|
||||
@ -269,6 +271,8 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
formattedValue +
|
||||
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -294,6 +298,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||
notInPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery.trim());
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
} else if (
|
||||
@ -310,6 +315,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||
equalsPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
} else if (
|
||||
@ -326,6 +332,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||
notEqualsPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
}
|
||||
@ -347,6 +354,7 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
} ${formattedValue} ${modifiedQuery.slice(
|
||||
notEqualsPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
}
|
||||
@ -359,6 +367,23 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
if (
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export const flattenLabels = (labels: Labels): ILabelRecord[] => {
|
||||
if (!hiddenLabels.includes(key)) {
|
||||
recs.push({
|
||||
key,
|
||||
value: labels[key],
|
||||
value: labels[key] || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -272,12 +272,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
width: 80,
|
||||
key: 'severity',
|
||||
sorter: (a, b): number =>
|
||||
(a.labels ? a.labels.severity.length : 0) -
|
||||
(b.labels ? b.labels.severity.length : 0),
|
||||
(a?.labels?.severity?.length || 0) - (b?.labels?.severity?.length || 0),
|
||||
render: (value): JSX.Element => {
|
||||
const objectKeys = Object.keys(value);
|
||||
const objectKeys = value ? Object.keys(value) : [];
|
||||
const withSeverityKey = objectKeys.find((e) => e === 'severity') || '';
|
||||
const severityValue = value[withSeverityKey];
|
||||
const severityValue = withSeverityKey ? value[withSeverityKey] : '-';
|
||||
|
||||
return <Typography>{severityValue}</Typography>;
|
||||
},
|
||||
@ -290,7 +289,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: (value): JSX.Element => {
|
||||
const objectKeys = Object.keys(value);
|
||||
const objectKeys = value ? Object.keys(value) : [];
|
||||
const withOutSeverityKeys = objectKeys.filter((e) => e !== 'severity');
|
||||
|
||||
if (withOutSeverityKeys.length === 0) {
|
||||
|
||||
@ -34,7 +34,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
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 { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
@ -117,6 +117,8 @@ function DateTimeSelection({
|
||||
);
|
||||
const [modalEndTime, setModalEndTime] = useState<number>(initialModalEndTime);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Effect to update modal time state when props change
|
||||
useEffect(() => {
|
||||
if (modalInitialStartTime !== undefined) {
|
||||
@ -410,8 +412,10 @@ function DateTimeSelection({
|
||||
// Remove Hidden Filters from URL query parameters on time change
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
|
||||
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||
}
|
||||
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
@ -428,6 +432,7 @@ function DateTimeSelection({
|
||||
updateLocalStorageForRoutes,
|
||||
updateTimeInterval,
|
||||
urlQuery,
|
||||
searchParams,
|
||||
],
|
||||
);
|
||||
|
||||
@ -488,8 +493,10 @@ function DateTimeSelection({
|
||||
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
|
||||
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||
}
|
||||
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
|
||||
@ -14,7 +14,7 @@ export type AlertHeaderProps = {
|
||||
state: string;
|
||||
alert: string;
|
||||
id: string;
|
||||
labels: Record<string, string>;
|
||||
labels: Record<string, string | undefined> | undefined;
|
||||
disabled: boolean;
|
||||
};
|
||||
};
|
||||
@ -23,13 +23,14 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
const { alertRuleState } = useAlertRule();
|
||||
const [updatedName, setUpdatedName] = useState(alertName);
|
||||
|
||||
const labelsWithoutSeverity = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
const labelsWithoutSeverity = useMemo(() => {
|
||||
if (labels) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(labels).filter(([key]) => key !== 'severity'),
|
||||
),
|
||||
[labels],
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}, [labels]);
|
||||
|
||||
return (
|
||||
<div className="alert-info">
|
||||
@ -43,7 +44,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-section">
|
||||
{labels.severity && <AlertSeverity severity={labels.severity} />}
|
||||
{labels?.severity && <AlertSeverity severity={labels.severity} />}
|
||||
|
||||
{/* // TODO(shaheer): Get actual data when we are able to get alert firing from state from API */}
|
||||
{/* <AlertStatus
|
||||
|
||||
@ -48,7 +48,7 @@ export interface RuleCondition {
|
||||
seasonality?: string;
|
||||
}
|
||||
export interface Labels {
|
||||
[key: string]: string;
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
|
||||
export interface AlertRuleStats {
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
@ -366,7 +366,7 @@ func (mc *migrateCommon) createAggregations(ctx context.Context, queryData map[s
|
||||
aggregation = map[string]any{
|
||||
"metricName": aggregateAttr["key"],
|
||||
"temporality": queryData["temporality"],
|
||||
"timeAggregation": aggregateOp,
|
||||
"timeAggregation": queryData["timeAggregation"],
|
||||
"spaceAggregation": queryData["spaceAggregation"],
|
||||
}
|
||||
if reduceTo, ok := queryData["reduceTo"].(string); ok {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user