mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-23 12:50:29 +00:00
Compare commits
2 Commits
main
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a40670ab21 | ||
|
|
10e6a50e2c |
@@ -1191,6 +1191,172 @@ describe('removeKeysFromExpression', () => {
|
||||
expect(pairs).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parenthesised expressions', () => {
|
||||
it('should not leave a dangling AND when removing the last filter inside parens', () => {
|
||||
const expression =
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND operation IN $top_level_operation)';
|
||||
const result = removeKeysFromExpression(expression, ['operation'], true);
|
||||
|
||||
expect(result).toBe(
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not leave a dangling AND when removing the first filter inside parens', () => {
|
||||
const expression =
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND operation IN $top_level_operation)';
|
||||
const result = removeKeysFromExpression(
|
||||
expression,
|
||||
['deployment.environment'],
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toBe(
|
||||
'(service.name = $service.name AND operation IN $top_level_operation)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not leave a dangling AND when removing a middle filter inside parens', () => {
|
||||
const expression =
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND operation IN $top_level_operation)';
|
||||
const result = removeKeysFromExpression(expression, ['service.name'], true);
|
||||
|
||||
expect(result).toBe(
|
||||
'(deployment.environment = $deployment.environment AND operation IN $top_level_operation)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty parens when removing the only filter inside parens', () => {
|
||||
const expression = '(operation IN $top_level_operation)';
|
||||
const result = removeKeysFromExpression(expression, ['operation'], true);
|
||||
|
||||
expect(result).toBe('()');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertFiltersToExpressionWithExistingQuery — appending new filters', () => {
|
||||
it('should insert a new filter inside parens with AND when expression is a single parenthesised group', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
id: 'deployment.environment',
|
||||
key: 'deployment.environment',
|
||||
type: 'string',
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: '$deployment.environment',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: '$service.name',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
key: { id: 'host.id', key: 'host.id', type: 'string' },
|
||||
op: 'IN',
|
||||
value: '$host.id',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery =
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name)';
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filter.expression).toBe(
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND host.id in $host.id)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should append a new filter with a space when expression is not parenthesised', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: '$service.name',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: { id: 'host.id', key: 'host.id', type: 'string' },
|
||||
op: 'IN',
|
||||
value: '$host.id',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = 'service.name = $service.name';
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filter.expression).toBe(
|
||||
'service.name = $service.name host.id in $host.id',
|
||||
);
|
||||
});
|
||||
|
||||
it('should insert inside parens without double-closing for a multi-filter group', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
id: 'deployment.environment',
|
||||
key: 'deployment.environment',
|
||||
type: 'string',
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: '$deployment.environment',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: '$service.name',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
key: { id: 'operation', key: 'operation', type: 'string' },
|
||||
op: 'IN',
|
||||
value: '$top_level_operation',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
key: { id: 'host.id', key: 'host.id', type: 'string' },
|
||||
op: 'IN',
|
||||
value: '$host.id',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery =
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND operation IN $top_level_operation)';
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filter.expression).toBe(
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND operation IN $top_level_operation AND host.id in $host.id)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatValueForExpression', () => {
|
||||
|
||||
@@ -217,6 +217,29 @@ export const convertExpressionToFilters = (
|
||||
|
||||
return filters;
|
||||
};
|
||||
/**
|
||||
* Returns true if the expression is enclosed by a single matching outer parenthesis pair,
|
||||
* e.g. "(a AND b AND c)". Used to decide whether to insert new filters inside or append.
|
||||
*/
|
||||
const isWrappedInParens = (expr: string): boolean => {
|
||||
const trimmed = expr.trim();
|
||||
if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) {
|
||||
return false;
|
||||
}
|
||||
let depth = 0;
|
||||
for (let i = 0; i < trimmed.length - 1; i++) {
|
||||
if (trimmed[i] === '(') {
|
||||
depth += 1;
|
||||
} else if (trimmed[i] === ')') {
|
||||
depth -= 1;
|
||||
}
|
||||
if (depth === 0) {
|
||||
return false;
|
||||
} // outer paren closed before the end
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
|
||||
const queryPairs = extractQueryPairs(query);
|
||||
const queryPairsMap: Map<string, IQueryPair> = new Map();
|
||||
@@ -495,13 +518,18 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
});
|
||||
|
||||
if (nonExistingFilterExpression.expression) {
|
||||
const trimmedQuery = modifiedQuery.trim();
|
||||
// If the existing expression is a single parenthesised group, insert the new
|
||||
// filter inside the closing paren so the group stays intact.
|
||||
// e.g. "(a AND b)" + "c" => "(a AND b AND c)" not "(a AND b) c"
|
||||
const expression = isWrappedInParens(trimmedQuery)
|
||||
? `${trimmedQuery.slice(0, -1)} AND ${
|
||||
nonExistingFilterExpression.expression
|
||||
})`
|
||||
: `${trimmedQuery} ${nonExistingFilterExpression.expression}`;
|
||||
return {
|
||||
filters: updatedFilters,
|
||||
filter: {
|
||||
expression: `${modifiedQuery.trim()} ${
|
||||
nonExistingFilterExpression.expression
|
||||
}`,
|
||||
},
|
||||
filter: { expression },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -569,7 +597,7 @@ export const removeKeysFromExpression = (
|
||||
const currentQueryPair = queryPairsMap.get(`${key}`.trim().toLowerCase());
|
||||
if (currentQueryPair && currentQueryPair.isComplete) {
|
||||
// Determine the start index of the query pair (fallback order: key → operator → value)
|
||||
const queryPairStart =
|
||||
let queryPairStart =
|
||||
currentQueryPair.position.keyStart ??
|
||||
currentQueryPair.position.operatorStart ??
|
||||
currentQueryPair.position.valueStart;
|
||||
@@ -587,6 +615,15 @@ export const removeKeysFromExpression = (
|
||||
// If match is found, extend the queryPairEnd to include the matched part
|
||||
queryPairEnd += match[0].length;
|
||||
}
|
||||
// If no following conjunction was absorbed (e.g. removed pair is last in expression),
|
||||
// absorb the preceding AND/OR instead to avoid leaving a dangling conjunction
|
||||
if (!match?.[3]) {
|
||||
const beforePair = updatedExpression.slice(0, queryPairStart);
|
||||
const precedingConjunctionMatch = beforePair.match(/\s+(AND|OR)\s+$/i);
|
||||
if (precedingConjunctionMatch) {
|
||||
queryPairStart -= precedingConjunctionMatch[0].length;
|
||||
}
|
||||
}
|
||||
// Remove the full query pair (including any conjunction/whitespace) from the expression
|
||||
updatedExpression = `${updatedExpression.slice(
|
||||
0,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
@@ -44,6 +45,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
|
||||
const addDynamicVariableToPanels = useAddDynamicVariableToPanels();
|
||||
const updateMutation = useUpdateDashboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onValueUpdate = useCallback(
|
||||
(
|
||||
@@ -171,6 +173,15 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
// Get current dashboard variables
|
||||
const currentVariables = selectedDashboard.data.variables || {};
|
||||
|
||||
// Prevent duplicate variable names
|
||||
const nameExists = Object.values(currentVariables).some(
|
||||
(v) => v.name === name,
|
||||
);
|
||||
if (nameExists) {
|
||||
notifications.error({ message: `Variable "${name}" already exists` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create tableRowData like Dashboard Settings does
|
||||
const tableRowData = [];
|
||||
const variableOrderArr = [];
|
||||
@@ -223,7 +234,8 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
|
||||
// Convert to dashboard format and update
|
||||
const updatedVariables = convertVariablesToDbFormat(tableRowData);
|
||||
updateVariables(updatedVariables, newVariable.id, [], false);
|
||||
// Don't pass currentRequestedId — variable creation should not modify widget filters.
|
||||
updateVariables(updatedVariables);
|
||||
},
|
||||
[selectedDashboard, updateVariables],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user