diff --git a/src/core/query-builder.utils.test.ts b/src/core/query-builder.utils.test.ts index 52de6bf8..5d0bf9bd 100644 --- a/src/core/query-builder.utils.test.ts +++ b/src/core/query-builder.utils.test.ts @@ -1,5 +1,5 @@ import { QueryBuilderOperations } from "./query-builder.constants"; -import { buildExpressionFromTemplate, expressionBuilderCallback, expressionBuilderCallbackWithRef, expressionReaderCallback, expressionReaderCallbackWithRef, ExpressionTransformFunction, multipleValuesQuery, timeFieldsQuery, transformComputedFieldsQuery } from "./query-builder.utils" +import { buildExpressionFromTemplate, expressionBuilderCallback, expressionBuilderCallbackWithRef, expressionReaderCallback, expressionReaderCallbackWithRef, ExpressionTransformFunction, getConcatOperatorForMultiExpression, multipleValuesQuery, timeFieldsQuery, transformComputedFieldsQuery } from "./query-builder.utils" describe('QueryBuilderUtils', () => { describe('transformComputedFieldsQuery', () => { @@ -328,4 +328,29 @@ describe('QueryBuilderUtils', () => { expect(result).toBe('(field like "value1")'); }); }); + + describe('getConcatOperatorForMultiExpression', () => { + [ + { + name: 'equals', + operator: '=', + }, + { + name: 'is not blank', + operator: 'isnotblank', + }, + ].forEach(testCase => { + it(`should return OR for ${testCase.name} operator`, () => { + const result = getConcatOperatorForMultiExpression(testCase.operator); + + expect(result).toBe('||'); + }); + }); + + it('should return AND as the default logical operator', () => { + const result = getConcatOperatorForMultiExpression('>'); + + expect(result).toBe('&&'); + }); + }); }) diff --git a/src/core/query-builder.utils.ts b/src/core/query-builder.utils.ts index 9529a9a7..3e78000c 100644 --- a/src/core/query-builder.utils.ts +++ b/src/core/query-builder.utils.ts @@ -180,7 +180,7 @@ export function multipleValuesQuery(field: string): ExpressionTransformFunction return (value: string, operation: string, _options?: any) => { const isMultiSelect = isMultiValueExpression(value); const valuesArray = getMultipleValuesArray(value); - const logicalOperator = getLogicalOperator(operation); + const logicalOperator = getConcatOperatorForMultiExpression(operation); return isMultiSelect ? `(${valuesArray.map(val => buildExpression(field, val, operation)).join(` ${logicalOperator} `)})` @@ -188,6 +188,26 @@ export function multipleValuesQuery(field: string): ExpressionTransformFunction }; } +/** + * Gets the logical operator for a given query operation when building multi-value expressions + * or combining multiple properties. + * + * Use Cases: + * 1. Multi-value fields: Combines expressions when using multi-value variables + * Example: status = "{active,pending}" → (status = "active" || status = "pending") + * + * 2. Multi property fields: Combines expressions when a field corresponds to multiple properties + * Example: source != "sys1" → (system != "sys1" && minionId != "sys1") + * + * @param operation The operation to be checked. + * @returns The logical operator as a string. + */ +export function getConcatOperatorForMultiExpression(operation: string): string { + return operation === QueryBuilderOperations.EQUALS.name || operation === QueryBuilderOperations.IS_NOT_BLANK.name + ? '||' + : '&&'; +} + /** * Builds a query expression for a specific field, value, and operation. * @param field - The name of the field to be queried. @@ -224,16 +244,6 @@ function getMultipleValuesArray(value: string): string[] { return value.replace(/({|})/g, '').split(','); } -/** - * Gets the logical operator for a given query operation when building multi-value expressions - * or combining multiple conditions. - * @param operation The operation to be checked. - * @returns The logical operator as a string. - */ -function getLogicalOperator(operation: string): string { - return operation === QueryBuilderOperations.EQUALS.name ? '||' : '&&'; -} - /** * Returns the value of returnKey from the matching entry in the options array. * @param matchKey - The property name to match on. diff --git a/src/datasources/alarms/AlarmsDataSourceCore.test.ts b/src/datasources/alarms/AlarmsDataSourceCore.test.ts index a75ccdb2..6dd238a9 100644 --- a/src/datasources/alarms/AlarmsDataSourceCore.test.ts +++ b/src/datasources/alarms/AlarmsDataSourceCore.test.ts @@ -221,6 +221,83 @@ describe('AlarmsDataSourceCore', () => { expect(datastore.templateSrv.replace).toHaveBeenCalledWith('channel != "${query0}"', {}); expect(transformQuery).toBe('(channel != "channel1" && channel != "channel2")'); }); + + describe('transformSourceFilter', () => { + [ + { + name: 'source equals', + input: 'source = "test-source"', + expected: '(properties.system = "test-source" || properties.minionId = "test-source")', + }, + { + name: 'source does not equal', + input: 'source != "test-source"', + expected: '(properties.system != "test-source" && properties.minionId != "test-source")', + }, + { + name: 'source is blank', + input: 'string.IsNullOrEmpty(source)', + expected: '(string.IsNullOrEmpty(properties.system) && string.IsNullOrEmpty(properties.minionId))', + }, + { + name: 'source is not blank', + input: '!string.IsNullOrEmpty(source)', + expected: '(!string.IsNullOrEmpty(properties.system) || !string.IsNullOrEmpty(properties.minionId))', + }, + ].forEach(({ name, input, expected }) => { + it(`should transform ${name} filter`, () => { + const result = datastore.transformAlarmsQueryWrapper({}, input); + + expect(result).toBe(expected); + }); + }); + + [ + { + name: 'source equals', + input: 'source = "${query0}"', + replacedInput: 'source = "{source1,source2}"', + expected: + '((properties.system = "source1" || properties.system = "source2") || (properties.minionId = "source1" || properties.minionId = "source2"))', + }, + { + name: 'source does not equal', + input: 'source != "${query0}"', + replacedInput: 'source != "{source1,source2}"', + expected: + '((properties.system != "source1" && properties.system != "source2") && (properties.minionId != "source1" && properties.minionId != "source2"))', + }, + ].forEach(({ name, input, replacedInput, expected }) => { + it(`should transform ${name} for mutiple value variable filter`, () => { + jest.spyOn(datastore.templateSrv, 'replace').mockReturnValue(replacedInput); + + const transformQuery = datastore.transformAlarmsQueryWrapper({}, input); + + expect(datastore.templateSrv.replace).toHaveBeenCalledWith(input, {}); + expect(transformQuery).toBe(expected); + }); + }); + + it('should replace single value variable in the source filter', () => { + const mockQueryBy = 'source != "${query0}"'; + jest.spyOn(datastore.templateSrv, 'replace').mockReturnValue('source != "test-source"'); + + const transformQuery = datastore.transformAlarmsQueryWrapper({}, mockQueryBy); + + expect(datastore.templateSrv.replace).toHaveBeenCalledWith('source != "${query0}"', {}); + expect(transformQuery).toBe('(properties.system != "test-source" && properties.minionId != "test-source")'); + }); + + it('should handle transformation for multiple source filters in a query', () => { + const mockFilter = 'source = "source1" || string.IsNullOrEmpty(source)'; + + const result = datastore.transformAlarmsQueryWrapper({}, mockFilter); + + expect(result).toBe( + '(properties.system = "source1" || properties.minionId = "source1") || (string.IsNullOrEmpty(properties.system) && string.IsNullOrEmpty(properties.minionId))' + ); + }); + }); }); }); diff --git a/src/datasources/alarms/AlarmsDataSourceCore.ts b/src/datasources/alarms/AlarmsDataSourceCore.ts index b75538e0..2daf3c51 100644 --- a/src/datasources/alarms/AlarmsDataSourceCore.ts +++ b/src/datasources/alarms/AlarmsDataSourceCore.ts @@ -3,12 +3,13 @@ import { DataQueryRequest, DataFrameDTO, TestDataSourceResponse, AppEvents, Scop import { AlarmsQuery, QueryAlarmsRequest, QueryAlarmsResponse } from "./types/types"; import { extractErrorInfo } from "core/errors"; import { QUERY_ALARMS_RELATIVE_PATH } from "./constants/QueryAlarms.constants"; -import { ExpressionTransformFunction, multipleValuesQuery, timeFieldsQuery, transformComputedFieldsQuery } from "core/query-builder.utils"; +import { ExpressionTransformFunction, getConcatOperatorForMultiExpression, multipleValuesQuery, timeFieldsQuery, transformComputedFieldsQuery } from "core/query-builder.utils"; import { ALARMS_TIME_FIELDS, AlarmsQueryBuilderFields } from "./constants/AlarmsQueryBuilder.constants"; import { QueryBuilderOption, Workspace } from "core/types"; import { WorkspaceUtils } from "shared/workspace.utils"; import { getVariableOptions } from "core/utils"; import { BackendSrv, getBackendSrv, getTemplateSrv, TemplateSrv } from "@grafana/runtime"; +import { MINION_ID_CUSTOM_PROPERTY, SYSTEM_CUSTOM_PROPERTY } from "./constants/SourceProperties.constants"; export abstract class AlarmsDataSourceCore extends DataSourceBase { public errorTitle?: string; @@ -69,13 +70,17 @@ export abstract class AlarmsDataSourceCore extends DataSourceBase { private readonly computedDataFields = new Map( Object.values(AlarmsQueryBuilderFields).map(field => { const dataField = field.dataField as string; + let callback; + + if (dataField === AlarmsQueryBuilderFields.SOURCE.dataField) { + callback = this.getSourceTransformation(); + } else if (this.isTimeField(dataField)) { + callback = timeFieldsQuery(dataField); + } else { + callback = multipleValuesQuery(dataField); + } - return [ - dataField, - this.isTimeField(dataField) - ? timeFieldsQuery(dataField) - : multipleValuesQuery(dataField), - ]; + return [dataField, callback]; }) ); @@ -103,6 +108,16 @@ export abstract class AlarmsDataSourceCore extends DataSourceBase { return ALARMS_TIME_FIELDS.includes(field); } + private getSourceTransformation(): ExpressionTransformFunction { + return (value: string, operation: string) => { + const systemExpression = multipleValuesQuery(`properties.${SYSTEM_CUSTOM_PROPERTY}`)(value, operation); + const minionExpression = multipleValuesQuery(`properties.${MINION_ID_CUSTOM_PROPERTY}`)(value, operation); + const logicalOperator = getConcatOperatorForMultiExpression(operation); + + return `(${systemExpression} ${logicalOperator} ${minionExpression})`; + }; + } + private getStatusCodeErrorMessage(errorDetails: { statusCode: string; message: string }): string { let errorMessage: string; switch (errorDetails.statusCode) { diff --git a/src/datasources/alarms/components/query-builder/AlarmsQueryBuilder.test.tsx b/src/datasources/alarms/components/query-builder/AlarmsQueryBuilder.test.tsx index a718eab3..069711c6 100644 --- a/src/datasources/alarms/components/query-builder/AlarmsQueryBuilder.test.tsx +++ b/src/datasources/alarms/components/query-builder/AlarmsQueryBuilder.test.tsx @@ -86,6 +86,16 @@ describe('AlarmsQueryBuilder', () => { expect(conditionsContainer.item(0)?.textContent).toContain('Workspace Name'); }); + it('should select source in query builder', () => { + const { conditionsContainer } = renderElement('source = "test-source"', [], [workspace]); + + expect(conditionsContainer?.length).toBe(1); + const conditionText = conditionsContainer.item(0)?.textContent; + expect(conditionText).toContain('Source'); + expect(conditionText).toContain('equals'); + expect(conditionText).toContain('test-source'); + }); + it('should select value for alarm ID in query builder', () => { const { conditionsContainer } = renderElement('alarmId = "test-alarm-123"'); diff --git a/src/datasources/alarms/constants/AlarmsQueryBuilder.constants.ts b/src/datasources/alarms/constants/AlarmsQueryBuilder.constants.ts index ef31b974..4a5b2eab 100644 --- a/src/datasources/alarms/constants/AlarmsQueryBuilder.constants.ts +++ b/src/datasources/alarms/constants/AlarmsQueryBuilder.constants.ts @@ -145,6 +145,19 @@ export const AlarmsQueryBuilderFields: Record = { dataField: 'resourceType', filterOperations: BASIC_STRING_FILTER_OPERATIONS, }, + SOURCE: { + label: 'Source', + dataField: 'source', + filterOperations: [ + QueryBuilderOperations.EQUALS.name, + QueryBuilderOperations.DOES_NOT_EQUAL.name, + QueryBuilderOperations.IS_BLANK.name, + QueryBuilderOperations.IS_NOT_BLANK.name, + /* #AB#3422087 - Switch to BASIC_STRING_FILTER_OPERATIONS + once transformation support for "contains" and "does not contain" + is implemented */ + ], + }, WORKSPACE: { label: 'Workspace', dataField: 'workspace', @@ -171,4 +184,5 @@ export const AlarmsQueryBuilderStaticFields: QBField[] = [ AlarmsQueryBuilderFields.KEYWORD, AlarmsQueryBuilderFields.PROPERTIES, AlarmsQueryBuilderFields.RESOURCE_TYPE, + AlarmsQueryBuilderFields.SOURCE, ]; diff --git a/src/datasources/alarms/constants/SourceProperties.constants.ts b/src/datasources/alarms/constants/SourceProperties.constants.ts new file mode 100644 index 00000000..a9082348 --- /dev/null +++ b/src/datasources/alarms/constants/SourceProperties.constants.ts @@ -0,0 +1,2 @@ +export const SYSTEM_CUSTOM_PROPERTY = 'system'; +export const MINION_ID_CUSTOM_PROPERTY = 'minionId';