import { QueryBuilderOperation, QueryBuilderOperationDefinition } from '@grafana/plugin-ui'; import { createAggregationOperation, createAggregationOperationWithParam, createRangeOperation, createRangeOperationWithGrouping, getLineFilterRenderer, isConflictingFilter, labelFilterRenderer, pipelineRenderer, } from './operationUtils'; import { operationDefinitions } from './operations'; import { LokiOperationId, LokiVisualQueryOperationCategory } from './types'; describe('createRangeOperation', () => { it('should create basic range operation without possible grouping', () => { expect(createRangeOperation('test_range_operation')).toMatchObject({ id: 'test_range_operation', name: 'Test range operation', params: [{ name: 'Range', type: 'string' }], defaultParams: ['$__auto'], alternativesKey: 'range function', category: LokiVisualQueryOperationCategory.RangeFunctions, }); }); it('should create basic range operation with possible grouping', () => { expect(createRangeOperation('test_range_operation', true)).toMatchObject({ id: 'test_range_operation', name: 'Test range operation', params: [ { name: 'Range', type: 'string' }, { name: 'By label', type: 'string', restParam: true, optional: true, }, ], defaultParams: ['$__auto'], alternativesKey: 'range function', category: LokiVisualQueryOperationCategory.RangeFunctions, }); }); it('should create range operation for quantile_over_time', () => { expect(createRangeOperation('quantile_over_time', true)).toMatchObject({ id: 'quantile_over_time', name: 'Quantile over time', params: [ { name: 'Range', type: 'string' }, { name: 'Quantile', type: 'number' }, { name: 'By label', type: 'string', restParam: true, optional: true }, ], defaultParams: ['$__auto', '0.95'], alternativesKey: 'range function', category: LokiVisualQueryOperationCategory.RangeFunctions, }); }); }); describe('createRangeOperationWithGrouping', () => { it('returns correct operation definitions with overrides and params', () => { const operations = createRangeOperationWithGrouping('quantile_over_time'); expect(operations).toHaveLength(3); expect(operations[0]).toMatchObject({ id: 'quantile_over_time', name: 'Quantile over time', params: [ { name: 'Range', type: 'string' }, { name: 'Quantile', type: 'number' }, { name: 'By label', type: 'string', restParam: true, optional: true }, ], defaultParams: ['$__auto', '0.95'], alternativesKey: 'range function', category: LokiVisualQueryOperationCategory.RangeFunctions, }); expect(operations[1]).toMatchObject({ id: '__quantile_over_time_by', name: 'Quantile over time by', params: [ { name: 'Range', type: 'string' }, { name: 'Quantile', type: 'number' }, { name: 'Label', type: 'string', restParam: true, optional: true }, ], defaultParams: ['$__auto', '0.95', ''], alternativesKey: 'range function with grouping', category: LokiVisualQueryOperationCategory.RangeFunctions, }); expect(operations[2]).toMatchObject({ id: '__quantile_over_time_without', name: 'Quantile over time without', params: [ { name: 'Range', type: 'string' }, { name: 'Quantile', type: 'number' }, { name: 'Label', type: 'string', restParam: true, optional: true }, ], defaultParams: ['$__auto', '0.95', ''], alternativesKey: 'range function with grouping', category: LokiVisualQueryOperationCategory.RangeFunctions, }); }); it('returns correct query string using range operation definitions for quantile_over_time with by grouping', () => { const operations = createRangeOperationWithGrouping('quantile_over_time'); const query = operations[1].renderer( { id: '__quantile_over_time_by', params: ['[5m]', '0.95', 'source', 'place'] }, operations[1], '{job="grafana"}' ); expect(query).toBe('quantile_over_time(0.95, {job="grafana"} [[5m]]) by (source, place)'); }); it('returns correct query string using range operation definitions for quantile_over_time with without grouping', () => { const operations = createRangeOperationWithGrouping('quantile_over_time'); const query = operations[2].renderer( { id: '__quantile_over_time_without', params: ['[$__interval]', '0.91', 'source', 'place'] }, operations[2], '{job="grafana"}' ); expect(query).toBe('quantile_over_time(0.91, {job="grafana"} [[$__interval]]) without (source, place)'); }); it('returns correct query string using range operation definitions for avg_over_time with without grouping', () => { const operations = createRangeOperationWithGrouping('avg_over_time'); const query = operations[2].renderer( { id: '__avg_over_time_without', params: ['[$__interval]', 'source'] }, operations[2], '{job="grafana"}' ); expect(query).toBe('avg_over_time({job="grafana"} [[$__interval]]) without (source)'); }); }); describe('getLineFilterRenderer', () => { const MOCK_MODEL = { id: '__line_contains', params: ['error'], }; const MOCK_MODEL_INSENSITIVE = { id: '__line_contains_case_insensitive', params: ['ERrOR'], }; const MOCK_MODEL_BACKTICKS = { id: '__line_contains', params: ['`error`'], }; const MOCK_DEF = undefined as unknown as QueryBuilderOperationDefinition; const MOCK_INNER_EXPR = '{job="grafana"}'; it('getLineFilterRenderer returns a function', () => { const lineFilterRenderer = getLineFilterRenderer('!~'); expect(typeof lineFilterRenderer).toBe('function'); }); it('lineFilterRenderer returns the correct query for line contains', () => { const lineFilterRenderer = getLineFilterRenderer('!~'); expect(lineFilterRenderer(MOCK_MODEL, MOCK_DEF, MOCK_INNER_EXPR)).toBe('{job="grafana"} !~ `error`'); }); it('lineFilterRenderer returns the correct query for line contains, containing backticks', () => { const lineFilterRenderer = getLineFilterRenderer('!~'); expect(lineFilterRenderer(MOCK_MODEL_BACKTICKS, MOCK_DEF, MOCK_INNER_EXPR)).toBe('{job="grafana"} !~ "`error`"'); }); it('lineFilterRenderer returns the correct query for line contains case insensitive', () => { const lineFilterRenderer = getLineFilterRenderer('!~', true); expect(lineFilterRenderer(MOCK_MODEL_INSENSITIVE, MOCK_DEF, MOCK_INNER_EXPR)).toBe( '{job="grafana"} !~ `(?i)ERrOR`' ); }); }); describe('labelFilterRenderer', () => { const MOCK_MODEL = { id: '__label_filter', params: ['label', '', 'value'] }; const MOCK_DEF = undefined as unknown as QueryBuilderOperationDefinition; const MOCK_INNER_EXPR = '{job="grafana"}'; it.each` operator | type | expected ${'='} | ${'string'} | ${'`value`'} ${'!='} | ${'string'} | ${'`value`'} ${'=~'} | ${'string'} | ${'`value`'} ${'!~'} | ${'string'} | ${'`value`'} ${'>'} | ${'number'} | ${'value'} ${'>='} | ${'number'} | ${'value'} ${'<'} | ${'number'} | ${'value'} ${'<='} | ${'number'} | ${'value'} `("value should be of type '$type' when operator is: $operator", ({ operator, expected }) => { MOCK_MODEL.params[1] = operator; expect(labelFilterRenderer(MOCK_MODEL, MOCK_DEF, MOCK_INNER_EXPR)).toBe( `{job="grafana"} | label ${operator} ${expected}` ); }); }); describe('isConflictingFilter', () => { it('should return true if the operation conflict with another label filter', () => { const operation = { id: '__label_filter', params: ['abc', '!=', '123'] }; const queryOperations = [ { id: '__label_filter', params: ['abc', '=', '123'] }, { id: '__label_filter', params: ['abc', '!=', '123'] }, ]; expect(isConflictingFilter(operation, queryOperations)).toBe(true); }); it("should return false if the operation doesn't conflict with another label filter", () => { const operation = { id: '__label_filter', params: ['abc', '=', '123'] }; const queryOperations = [ { id: '__label_filter', params: ['abc', '=', '123'] }, { id: '__label_filter', params: ['abc', '=', '123'] }, ]; expect(isConflictingFilter(operation, queryOperations)).toBe(false); }); }); describe('pipelineRenderer', () => { it('correctly renders unpack expressions', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Unpack, params: [], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Unpack); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | unpack'); }); it('correctly renders unpack expressions', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Unpack, params: [], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Unpack); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | unpack'); }); it('correctly renders empty logfmt expression', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Logfmt, params: [], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt'); }); it('correctly renders logfmt expression', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Logfmt, params: [true, false, 'foo', ''], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt --strict foo'); }); it('correctly renders logfmt expression with multiple params', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Logfmt, params: [true, false, 'foo', 'bar', 'baz'], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt --strict foo, bar, baz'); }); it('correctly renders empty json expression', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Json, params: [], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json'); }); it('correctly renders json expression', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Json, params: ['foo', ''], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json foo'); }); it('correctly renders json expression with multiple params', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Json, params: ['foo', 'bar', 'baz'], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json foo, bar, baz'); }); it('correctly renders keep expression', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Keep, params: ['foo', ''], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Keep); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | keep foo'); }); it('correctly renders keep expression with multiple params', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Keep, params: ['foo', 'bar', 'baz'], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Keep); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | keep foo, bar, baz'); }); it('correctly renders drop expression', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Drop, params: ['foo', ''], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Drop); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | drop foo'); }); it('correctly renders drop expression with multiple params', () => { const model: QueryBuilderOperation = { id: LokiOperationId.Drop, params: ['foo', 'bar', 'baz'], }; const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Drop); expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | drop foo, bar, baz'); }); }); describe('createAggregationOperation', () => { it('returns correct aggregation definitions with overrides', () => { expect(createAggregationOperation('test_aggregation', { category: 'test_category' })).toMatchObject([ { addOperationHandler: {}, alternativesKey: 'plain aggregations', category: 'test_category', defaultParams: [], explainHandler: {}, id: 'test_aggregation', name: 'Test aggregation', paramChangedHandler: {}, params: [ { name: 'By label', optional: true, restParam: true, type: 'string', }, ], renderer: {}, }, { alternativesKey: 'aggregations by', category: 'test_category', defaultParams: [''], explainHandler: {}, hideFromList: true, id: '__test_aggregation_by', name: 'Test aggregation by', paramChangedHandler: {}, params: [ { editor: {}, name: 'Label', optional: true, restParam: true, type: 'string', }, ], renderer: {}, }, { alternativesKey: 'aggregations by', category: 'test_category', defaultParams: [''], explainHandler: {}, hideFromList: true, id: '__test_aggregation_without', name: 'Test aggregation without', paramChangedHandler: {}, params: [ { name: 'Label', optional: true, restParam: true, type: 'string', }, ], renderer: {}, }, ]); }); }); describe('createAggregationOperationWithParams', () => { it('returns correct aggregation definitions with overrides and params', () => { expect( createAggregationOperationWithParam( 'test_aggregation', { params: [{ name: 'K-value', type: 'number' }], defaultParams: [5], }, { category: 'test_category' } ) ).toMatchObject([ { addOperationHandler: {}, alternativesKey: 'plain aggregations', category: 'test_category', defaultParams: [5], explainHandler: {}, id: 'test_aggregation', name: 'Test aggregation', paramChangedHandler: {}, params: [ { name: 'K-value', type: 'number' }, { name: 'By label', optional: true, restParam: true, type: 'string' }, ], renderer: {}, }, { alternativesKey: 'aggregations by', category: 'test_category', defaultParams: [5, ''], explainHandler: {}, hideFromList: true, id: '__test_aggregation_by', name: 'Test aggregation by', paramChangedHandler: {}, params: [ { name: 'K-value', type: 'number' }, { editor: {}, name: 'Label', optional: true, restParam: true, type: 'string' }, ], renderer: {}, }, { alternativesKey: 'aggregations by', category: 'test_category', defaultParams: [5, ''], explainHandler: {}, hideFromList: true, id: '__test_aggregation_without', name: 'Test aggregation without', paramChangedHandler: {}, params: [ { name: 'K-value', type: 'number' }, { name: 'Label', optional: true, restParam: true, type: 'string' }, ], renderer: {}, }, ]); }); it('returns correct query string using aggregation definitions with overrides and number type param', () => { const def = createAggregationOperationWithParam( 'test_aggregation', { params: [{ name: 'K-value', type: 'number' }], defaultParams: [5], }, { category: 'test_category' } ); const topKByDefinition = def[1]; expect( topKByDefinition.renderer( { id: '__topk_by', params: ['5', 'source', 'place'] }, def[1], 'rate({place="luna"} |= `` [5m])' ) ).toBe('test_aggregation by(source, place) (5, rate({place="luna"} |= `` [5m]))'); }); it('returns correct query string using aggregation definitions with overrides and string type param', () => { const def = createAggregationOperationWithParam( 'test_aggregation', { params: [{ name: 'Identifier', type: 'string' }], defaultParams: ['count'], }, { category: 'test_category' } ); const countValueDefinition = def[1]; expect( countValueDefinition.renderer( { id: 'count_values', params: ['5', 'source', 'place'] }, def[1], 'rate({place="luna"} |= `` [5m])' ) ).toBe('test_aggregation by(source, place) ("5", rate({place="luna"} |= `` [5m]))'); }); });