diff --git a/.changeset/nasty-baboons-listen.md b/.changeset/nasty-baboons-listen.md new file mode 100644 index 0000000..3714443 --- /dev/null +++ b/.changeset/nasty-baboons-listen.md @@ -0,0 +1,5 @@ +--- +'grafana-zabbix': patch +--- + +Fix: alias functions in Services query type diff --git a/src/datasource/dataProcessor.ts b/src/datasource/dataProcessor.ts index c183cf6..0f843aa 100644 --- a/src/datasource/dataProcessor.ts +++ b/src/datasource/dataProcessor.ts @@ -1,11 +1,11 @@ +import { DataFrame, FieldType, TIME_SERIES_TIME_FIELD_NAME } from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; import _ from 'lodash'; import * as utils from './utils'; -import { getTemplateSrv } from '@grafana/runtime'; -import { DataFrame, FieldType, TIME_SERIES_VALUE_FIELD_NAME } from '@grafana/data'; function setAlias(alias: string, frame: DataFrame) { if (frame.fields?.length <= 2) { - const valueField = frame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME); + const valueField = frame.fields.find((f) => f.name !== TIME_SERIES_TIME_FIELD_NAME); if (valueField?.config?.custom?.scopedVars) { alias = getTemplateSrv().replace(alias, valueField?.config?.custom?.scopedVars); } @@ -38,7 +38,7 @@ function replaceAlias(regexp: string, newAlias: string, frame: DataFrame) { if (frame.fields?.length <= 2) { let alias = frame.name.replace(pattern, newAlias); - const valueField = frame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME); + const valueField = frame.fields.find((f) => f.name !== TIME_SERIES_TIME_FIELD_NAME); if (valueField?.state?.scopedVars) { alias = getTemplateSrv().replace(alias, valueField?.state?.scopedVars); } @@ -63,7 +63,7 @@ function replaceAlias(regexp: string, newAlias: string, frame: DataFrame) { function setAliasByRegex(alias: string, frame: DataFrame) { if (frame.fields?.length <= 2) { - const valueField = frame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME); + const valueField = frame.fields.find((f) => f.name !== TIME_SERIES_TIME_FIELD_NAME); try { if (valueField) { valueField.config.displayNameFromDS = extractText(valueField.config?.displayNameFromDS, alias); diff --git a/src/datasource/datasource.ts b/src/datasource/datasource.ts index 9bf0e5e..1487d65 100644 --- a/src/datasource/datasource.ts +++ b/src/datasource/datasource.ts @@ -526,6 +526,9 @@ export class ZabbixDatasource extends DataSourceApi { - return _.includes(aggregationFunctions, func.def.name); + return _.includes(aggregationFunctions, func.def.name) && func.params.length > 0; }); return _.map(aggFuncDefs, (func) => { diff --git a/src/datasource/specs/dataProcessor.spec.ts b/src/datasource/specs/dataProcessor.spec.ts new file mode 100644 index 0000000..11c5b0a --- /dev/null +++ b/src/datasource/specs/dataProcessor.spec.ts @@ -0,0 +1,478 @@ +import { DataFrame, FieldType, TIME_SERIES_TIME_FIELD_NAME } from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; +import dataProcessor from '../dataProcessor'; + +// Mock the template service +jest.mock('@grafana/runtime', () => ({ + getTemplateSrv: jest.fn(), +})); + +const mockTemplateSrv = getTemplateSrv as jest.MockedFunction; + +describe('DataProcessor', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockTemplateSrv.mockReturnValue({ + replace: jest.fn((text: string) => text), + } as any); + }); + + describe('setAlias', () => { + it('should set alias for a simple time series DataFrame', () => { + const frame: DataFrame = { + name: 'original_name', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000, 3000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10, 20, 30], + config: {}, + }, + ], + length: 3, + }; + + const result = dataProcessor.metricFunctions.setAlias('new_alias', frame); + + expect(result.name).toBe('new_alias'); + expect(result.fields[1].config.displayNameFromDS).toBe('new_alias'); + }); + + it('should set alias with template variable replacement', () => { + const mockReplace = jest.fn((text: string) => text.replace('$host', 'server1')); + mockTemplateSrv.mockReturnValue({ + replace: mockReplace, + } as any); + + const frame: DataFrame = { + name: 'original_name', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10, 20], + config: { + custom: { + scopedVars: { host: { value: 'server1' } }, + }, + }, + }, + ], + length: 2, + }; + + const result = dataProcessor.metricFunctions.setAlias('CPU usage on $host', frame); + + expect(mockReplace).toHaveBeenCalledWith('CPU usage on $host', { host: { value: 'server1' } }); + expect(result.fields[1].config.displayNameFromDS).toBe('CPU usage on server1'); + }); + + it('should handle DataFrame with multiple value fields', () => { + const frame: DataFrame = { + name: 'original_name', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'cpu_user', + type: FieldType.number, + values: [10, 20], + config: {}, + }, + { + name: 'cpu_system', + type: FieldType.number, + values: [5, 10], + config: {}, + }, + ], + length: 2, + }; + + const result = dataProcessor.metricFunctions.setAlias('CPU metrics', frame); + + expect(result.fields[1].config.displayNameFromDS).toBe('CPU metrics'); + expect(result.fields[2].config.displayNameFromDS).toBe('CPU metrics'); + }); + + it('should handle empty DataFrame gracefully', () => { + const frame: DataFrame = { + name: 'empty_frame', + fields: [], + length: 0, + }; + + const result = dataProcessor.metricFunctions.setAlias('test_alias', frame); + + expect(result.name).toBe('test_alias'); + }); + }); + + describe('replaceAlias', () => { + it('should replace alias using string pattern', () => { + const frame: DataFrame = { + name: 'cpu.usage.server1', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10, 20], + config: { + displayNameFromDS: 'cpu.usage.server1', + }, + }, + ], + length: 2, + }; + + const result = dataProcessor.metricFunctions.replaceAlias('server1', 'production', frame); + + expect(result.name).toBe('cpu.usage.production'); + expect(result.fields[1].config.displayNameFromDS).toBe('cpu.usage.production'); + }); + + it('should replace alias using regex pattern', () => { + const frame: DataFrame = { + name: 'metric.host123.value', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10, 20], + config: { + displayNameFromDS: 'metric.host123.value', + }, + }, + ], + length: 2, + }; + + const result = dataProcessor.metricFunctions.replaceAlias('/host\\d+/', 'server', frame); + + expect(result.name).toBe('metric.server.value'); + expect(result.fields[1].config.displayNameFromDS).toBe('metric.server.value'); + }); + + it('should handle template variable replacement in replaced alias', () => { + const mockReplace = jest.fn((text: string) => text.replace('$env', 'production')); + mockTemplateSrv.mockReturnValue({ + replace: mockReplace, + } as any); + + const frame: DataFrame = { + name: 'cpu.usage.dev', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10, 20], + config: {}, + state: { + scopedVars: { env: { value: 'production' } }, + }, + }, + ], + length: 2, + }; + + dataProcessor.metricFunctions.replaceAlias('dev', '$env', frame); + + expect(mockReplace).toHaveBeenCalledWith('cpu.usage.$env', { env: { value: 'production' } }); + }); + + it('should handle multiple value fields', () => { + const frame: DataFrame = { + name: 'original_name', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'cpu_user', + type: FieldType.number, + values: [10, 20], + config: { + displayNameFromDS: 'cpu.user.server1', + }, + }, + { + name: 'cpu_system', + type: FieldType.number, + values: [5, 10], + config: { + displayNameFromDS: 'cpu.system.server1', + }, + }, + ], + length: 2, + }; + + const result = dataProcessor.metricFunctions.replaceAlias('server1', 'production', frame); + + expect(result.fields[1].name).toBe('cpu.user.production'); + expect(result.fields[2].name).toBe('cpu.system.production'); + }); + }); + + describe('setAliasByRegex', () => { + it('should extract text using regex pattern', () => { + const frame: DataFrame = { + name: 'system.cpu.util[,user,avg1] on server1', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10, 20], + config: { + displayNameFromDS: 'system.cpu.util[,user,avg1] on server1', + }, + }, + ], + length: 2, + }; + + const result = dataProcessor.metricFunctions.setAliasByRegex('server\\d+', frame); + + expect(result.name).toBe('server1'); + expect(result.fields[1].config.displayNameFromDS).toBe('server1'); + }); + + it('should handle regex extraction for multiple fields', () => { + const frame: DataFrame = { + name: 'original_name', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'cpu_user', + type: FieldType.number, + values: [10, 20], + config: { + displayNameFromDS: 'cpu.user on host123', + }, + }, + { + name: 'cpu_system', + type: FieldType.number, + values: [5, 10], + config: { + displayNameFromDS: 'cpu.system on host456', + }, + }, + ], + length: 2, + }; + + const result = dataProcessor.metricFunctions.setAliasByRegex('host\\d+', frame); + + expect(result.fields[1].config.displayNameFromDS).toBe('host123'); + expect(result.fields[2].config.displayNameFromDS).toBe('host456'); + }); + + it('should handle invalid regex gracefully', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + const frame: DataFrame = { + name: 'test.metric', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10], + config: { + displayNameFromDS: 'test.metric', + }, + }, + ], + length: 1, + }; + + // Invalid regex pattern - unmatched brackets + dataProcessor.metricFunctions.setAliasByRegex('[invalid', frame); + + expect(consoleSpy).toHaveBeenCalledWith('Failed to apply RegExp:', expect.any(String)); + + consoleSpy.mockRestore(); + }); + + it('should handle case when regex does not match', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + const frame: DataFrame = { + name: 'metric.without.numbers', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10], + config: { + displayNameFromDS: 'metric.without.numbers', + }, + }, + ], + length: 1, + }; + + // This should log an error because the regex doesn't match (returns null) + const result = dataProcessor.metricFunctions.setAliasByRegex('\\d+', frame); + + expect(consoleSpy).toHaveBeenCalledWith('Failed to apply RegExp:', expect.any(String)); + + // Frame should be returned unchanged + expect(result.name).toBe('metric.without.numbers'); + + consoleSpy.mockRestore(); + }); + }); + + describe('timeShift', () => { + it('should shift time range by specified interval', () => { + const timeRange = [1609459200, 1609462800]; // 2021-01-01 00:00:00 to 2021-01-01 01:00:00 UTC + const interval = '1h'; + + const result = dataProcessor.metricFunctions.timeShift(interval, timeRange); + + // Should shift back by 1 hour (3600 seconds) + expect(result).toEqual([1609455600, 1609459200]); + }); + + it('should handle negative time shift', () => { + const timeRange = [1609459200, 1609462800]; + const interval = '-30m'; + + const result = dataProcessor.metricFunctions.timeShift(interval, timeRange); + + // Negative interval shifts back in time by 30 minutes (1800 seconds) + expect(result).toEqual([1609457400, 1609461000]); + }); + + it('should handle different time units', () => { + const timeRange = [1609459200]; + + // Test minutes + let result = dataProcessor.metricFunctions.timeShift('15m', timeRange); + expect(result).toEqual([1609458300]); + + // Test seconds + result = dataProcessor.metricFunctions.timeShift('30s', timeRange); + expect(result).toEqual([1609459170]); + + // Test days + result = dataProcessor.metricFunctions.timeShift('1d', timeRange); + expect(result).toEqual([1609372800]); + }); + + it('should handle empty time range', () => { + const timeRange: number[] = []; + const interval = '1h'; + + const result = dataProcessor.metricFunctions.timeShift(interval, timeRange); + + expect(result).toEqual([]); + }); + }); + + describe('metricFunctions integration', () => { + it('should expose all expected functions', () => { + const functions = dataProcessor.metricFunctions; + + expect(functions).toHaveProperty('setAlias'); + expect(functions).toHaveProperty('setAliasByRegex'); + expect(functions).toHaveProperty('replaceAlias'); + expect(functions).toHaveProperty('timeShift'); + + expect(typeof functions.setAlias).toBe('function'); + expect(typeof functions.setAliasByRegex).toBe('function'); + expect(typeof functions.replaceAlias).toBe('function'); + expect(typeof functions.timeShift).toBe('function'); + }); + + it('should maintain function context when used in sequence', () => { + // This tests the integration with the sequence function from utils + const frame: DataFrame = { + name: 'cpu.usage.server1', + fields: [ + { + name: TIME_SERIES_TIME_FIELD_NAME, + type: FieldType.time, + values: [1000, 2000], + config: {}, + }, + { + name: 'value', + type: FieldType.number, + values: [10, 20], + config: {}, + }, + ], + length: 2, + }; + + // Test chaining multiple operations + let result = dataProcessor.metricFunctions.replaceAlias('server1', 'production', frame); + result = dataProcessor.metricFunctions.setAlias('Final Alias', result); + + expect(result.name).toBe('Final Alias'); + expect(result.fields[1].config.displayNameFromDS).toBe('Final Alias'); + }); + }); +});