From 4fa6bad7c03948cd648f784283bc60d7b2199ad3 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Tue, 9 Jan 2024 16:52:37 -0500 Subject: [PATCH] CloudWatch: Remove dependency on local TemplateSrv (#79283) --- .../__mocks__/AnnotationQueryRunner.ts | 6 +- .../__mocks__/CloudWatchDataSource.ts | 70 +++++++++++++++---- .../cloudwatch/__mocks__/LogsQueryRunner.ts | 9 +-- .../__mocks__/MetricsQueryRunner.ts | 11 +-- .../cloudwatch/__mocks__/ResourcesAPI.ts | 3 +- .../MetricsQueryEditor.test.tsx | 5 +- .../shared/LogGroups/LogGroupsField.test.tsx | 14 +++- .../datasource/cloudwatch/datasource.test.ts | 10 +-- .../datasource/cloudwatch/datasource.ts | 3 +- .../datasource/cloudwatch/hooks.test.ts | 1 - .../CloudWatchLogsLanguageProvider.test.ts | 3 +- .../CloudWatchLogsLanguageProvider.ts | 9 +-- .../cloudwatch-sql/SQLGenerator.test.ts | 48 +++++++------ .../language/cloudwatch-sql/SQLGenerator.ts | 2 +- .../CloudWatchAnnotationQueryRunner.ts | 2 +- .../CloudWatchLogsQueryRunner.test.ts | 2 +- .../query-runner/CloudWatchLogsQueryRunner.ts | 3 +- .../CloudWatchMetricsQueryRunner.test.ts | 11 ++- .../CloudWatchMetricsQueryRunner.ts | 2 +- .../query-runner/CloudWatchRequest.ts | 8 +-- .../cloudwatch/resources/ResourcesAPI.ts | 3 +- .../cloudwatch/utils/templateVariableUtils.ts | 30 +++++++- 22 files changed, 154 insertions(+), 101 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/AnnotationQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/AnnotationQueryRunner.ts index bae51a44dec..919059465d4 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/AnnotationQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/AnnotationQueryRunner.ts @@ -1,7 +1,6 @@ import { of } from 'rxjs'; import { CustomVariableModel, DataQueryRequest } from '@grafana/data'; -import { TemplateSrv } from 'app/features/templating/template_srv'; import { CloudWatchAnnotationQueryRunner } from '../query-runner/CloudWatchAnnotationQueryRunner'; import { CloudWatchQuery } from '../types'; @@ -10,10 +9,7 @@ import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchData import { TimeRangeMock } from './timeRange'; export function setupMockedAnnotationQueryRunner({ variables }: { variables?: CustomVariableModel[] }) { - let templateService = new TemplateSrv(); - if (variables) { - templateService = setupMockedTemplateService(variables); - } + const templateService = setupMockedTemplateService(variables); const queryMock = jest.fn().mockReturnValue(of({})); const runner = new CloudWatchAnnotationQueryRunner(CloudWatchSettings, templateService); diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index 8ad1ce877db..e0c868baa2e 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -6,22 +6,70 @@ import { DataSourcePluginMeta, PluginMetaInfo, PluginType, + ScopedVars, VariableHide, } from '@grafana/data'; -import { getBackendSrv, setBackendSrv, DataSourceWithBackend } from '@grafana/runtime'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { getBackendSrv, setBackendSrv, DataSourceWithBackend, TemplateSrv } from '@grafana/runtime'; import { initialCustomVariableModelState } from '../__mocks__/variables'; import { CloudWatchDatasource } from '../datasource'; import { CloudWatchJsonData } from '../types'; +import { getVariableName } from '../utils/templateVariableUtils'; const queryMock = jest.fn().mockReturnValue(of({ data: [] })); jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation((args) => queryMock(args)); +const separatorMap = new Map([ + ['pipe', '|'], + ['raw', ','], + ['text', ' + '], +]); -export function setupMockedTemplateService(variables: CustomVariableModel[]) { - const templateService = new TemplateSrv(); - templateService.init(variables); - templateService.getVariables = jest.fn().mockReturnValue(variables); +export function setupMockedTemplateService(variables?: CustomVariableModel[]): TemplateSrv { + const templateService = { + replace: jest.fn().mockImplementation((input: string, scopedVars?: ScopedVars, format?: string) => { + if (!input) { + return ''; + } + let output = input; + ['datasource', 'dimension'].forEach((name) => { + const variable = scopedVars ? scopedVars[name] : undefined; + if (variable) { + output = output.replace('$' + name, variable.value); + } + }); + + if (variables) { + variables.forEach((variable) => { + let repVal = ''; + let value = format === 'text' ? variable.current.text : variable.current.value; + let separator = separatorMap.get(format ?? 'raw'); + if (Array.isArray(value)) { + repVal = value.join(separator); + } else { + repVal = value; + } + output = output.replace('$' + variable.name, repVal); + output = output.replace('[[' + variable.name + ']]', repVal); + }); + } + return output; + }), + getVariables: jest.fn().mockReturnValue(variables ?? []), + containsTemplate: jest.fn().mockImplementation((name) => { + const varName = getVariableName(name); + if (!varName || !variables) { + return false; + } + let found = false; + variables.forEach((variable) => { + if (varName === variable.name) { + found = true; + } + }); + return found; + }), + updateTimeRange: jest.fn(), + }; return templateService; } @@ -62,22 +110,14 @@ export const CloudWatchSettings: DataSourceInstanceSettings export function setupMockedDataSource({ variables, - mockGetVariableName = true, getMock = jest.fn(), customInstanceSettings = CloudWatchSettings, }: { getMock?: jest.Func; variables?: CustomVariableModel[]; - mockGetVariableName?: boolean; customInstanceSettings?: DataSourceInstanceSettings; } = {}) { - let templateService = new TemplateSrv(); - if (variables) { - templateService = setupMockedTemplateService(variables); - if (mockGetVariableName) { - templateService.getVariableName = (name: string) => name.replace('$', ''); - } - } + const templateService = setupMockedTemplateService(variables); const datasource = new CloudWatchDatasource(customInstanceSettings, templateService); datasource.getVariables = () => ['test']; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/LogsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/LogsQueryRunner.ts index 94ff8a5f3ff..9c689d1a900 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/LogsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/LogsQueryRunner.ts @@ -2,7 +2,6 @@ import { of } from 'rxjs'; import { CustomVariableModel, DataFrame, DataSourceInstanceSettings } from '@grafana/data'; import { BackendDataSourceResponse, toDataQueryResponse } from '@grafana/runtime'; -import { TemplateSrv } from 'app/features/templating/template_srv'; import { CloudWatchLogsQueryRunner } from '../query-runner/CloudWatchLogsQueryRunner'; import { CloudWatchJsonData, CloudWatchLogsQueryStatus, CloudWatchLogsRequest } from '../types'; @@ -22,13 +21,7 @@ export function setupMockedLogsQueryRunner({ mockGetVariableName?: boolean; settings?: DataSourceInstanceSettings; } = {}) { - let templateService = new TemplateSrv(); - if (variables) { - templateService = setupMockedTemplateService(variables); - if (mockGetVariableName) { - templateService.getVariableName = (name: string) => name; - } - } + let templateService = setupMockedTemplateService(variables); const queryMock = jest.fn().mockReturnValue(of(toDataQueryResponse({ data }))); const runner = new CloudWatchLogsQueryRunner(settings, templateService); diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/MetricsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/MetricsQueryRunner.ts index 362983b0a2c..5025d723d49 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/MetricsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/MetricsQueryRunner.ts @@ -2,7 +2,6 @@ import { of, throwError } from 'rxjs'; import { CustomVariableModel, DataQueryError, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data'; import { BackendDataSourceResponse, toDataQueryResponse } from '@grafana/runtime'; -import { TemplateSrv } from 'app/features/templating/template_srv'; import { CloudWatchMetricsQueryRunner } from '../query-runner/CloudWatchMetricsQueryRunner'; import { CloudWatchJsonData, CloudWatchQuery } from '../types'; @@ -15,23 +14,15 @@ export function setupMockedMetricsQueryRunner({ results: {}, }, variables, - mockGetVariableName = true, errorResponse, instanceSettings = CloudWatchSettings, }: { data?: BackendDataSourceResponse; variables?: CustomVariableModel[]; - mockGetVariableName?: boolean; errorResponse?: DataQueryError; instanceSettings?: DataSourceInstanceSettings; } = {}) { - let templateService = new TemplateSrv(); - if (variables) { - templateService = setupMockedTemplateService(variables); - if (mockGetVariableName) { - templateService.getVariableName = (name: string) => name.replace('$', ''); - } - } + const templateService = setupMockedTemplateService(variables); const queryMock = errorResponse ? jest.fn().mockImplementation(() => throwError(errorResponse)) diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts index 920cea52cf7..6da47f3c676 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts @@ -1,6 +1,5 @@ import { CustomVariableModel } from '@grafana/data'; import { getBackendSrv, setBackendSrv } from '@grafana/runtime'; -import { TemplateSrv } from 'app/features/templating/template_srv'; import { ResourcesAPI } from '../resources/ResourcesAPI'; @@ -16,7 +15,7 @@ export function setupMockedResourcesAPI({ variables?: CustomVariableModel[]; mockGetVariableName?: boolean; } = {}) { - let templateService = variables ? setupMockedTemplateService(variables) : new TemplateSrv(); + let templateService = setupMockedTemplateService(variables); const api = new ResourcesAPI(CloudWatchSettings, templateService); let resourceRequestMock = getMock ? getMock : jest.fn().mockReturnValue(response); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx index d5519dbbc2f..7f0f149b67c 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -4,8 +4,8 @@ import selectEvent from 'react-select-event'; import { CustomVariableModel, DataSourceInstanceSettings } from '@grafana/data'; import * as ui from '@grafana/ui'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { setupMockedTemplateService } from '../../../__mocks__/CloudWatchDataSource'; import { initialVariableModelState } from '../../../__mocks__/variables'; import { CloudWatchDatasource } from '../../../datasource'; import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../../../types'; @@ -24,7 +24,6 @@ const setup = () => { jsonData: { defaultRegion: 'us-east-1' }, } as DataSourceInstanceSettings; - const templateSrv = new TemplateSrv(); const variable: CustomVariableModel = { ...initialVariableModelState, id: 'var3', @@ -41,7 +40,7 @@ const setup = () => { query: '', type: 'custom', }; - templateSrv.init([variable]); + const templateSrv = setupMockedTemplateService([variable]); const datasource = new CloudWatchDatasource(instanceSettings, templateSrv); datasource.metricFindQuery = async () => [{ value: 'test', label: 'test', text: 'test' }]; diff --git a/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsField.test.tsx index 9425f9b0d92..b3cf4e2a489 100644 --- a/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LogGroupsField.test.tsx @@ -5,7 +5,11 @@ import React from 'react'; import { config } from '@grafana/runtime'; -import { logGroupNamesVariable, setupMockedDataSource } from '../../../__mocks__/CloudWatchDataSource'; +import { + logGroupNamesVariable, + setupMockedDataSource, + setupMockedTemplateService, +} from '../../../__mocks__/CloudWatchDataSource'; import { LogGroupsField } from './LogGroupsField'; @@ -17,6 +21,7 @@ const defaultProps = { region: '', onChange: jest.fn(), }; + describe('LogGroupSelection', () => { beforeEach(() => { jest.resetAllMocks(); @@ -35,6 +40,7 @@ describe('LogGroupSelection', () => { defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); + defaultProps.datasource.resources.templateSrv = setupMockedTemplateService(); render(); await waitFor(async () => expect(screen.getByText('Select log groups')).toBeInTheDocument()); @@ -51,7 +57,8 @@ describe('LogGroupSelection', () => { defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); - render(); + const varName = '$' + logGroupNamesVariable.name; + render(); await waitFor(async () => expect(screen.getByText('Select log groups')).toBeInTheDocument()); expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledTimes(1); @@ -61,7 +68,7 @@ describe('LogGroupSelection', () => { }); expect(defaultProps.onChange).toHaveBeenCalledWith([ { arn: 'arn', name: 'loggroupname' }, - { arn: logGroupNamesVariable.name, name: logGroupNamesVariable.name }, + { arn: varName, name: varName }, ]); }); @@ -70,6 +77,7 @@ describe('LogGroupSelection', () => { defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); + defaultProps.datasource.resources.templateSrv = setupMockedTemplateService(); render(); await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); expect(defaultProps.datasource.resources.getLogGroups).not.toHaveBeenCalled(); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index edcab78c137..096c3ff5c5b 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -21,6 +21,7 @@ import { MetricEditorMode, MetricQueryType, } from './types'; +import * as templateUtils from './utils/templateVariableUtils'; describe('datasource', () => { beforeEach(() => { @@ -178,7 +179,6 @@ describe('datasource', () => { it('should interpolate multi-value template variable for log group names in the query', async () => { const { datasource, queryMock } = setupMockedDataSource({ variables: [fieldsVariable, logGroupNamesVariable, regionVariable], - mockGetVariableName: false, }); await lastValueFrom( datasource @@ -304,7 +304,9 @@ describe('datasource', () => { it('should replace correct variables in CloudWatchMetricsQuery', () => { const { datasource, templateService } = setupMockedDataSource(); templateService.replace = jest.fn(); - templateService.getVariableName = jest.fn(); + const mockGetVariableName = jest + .spyOn(templateUtils, 'getVariableName') + .mockImplementation((name: string) => name.replace('$', '')); const variableName = 'someVar'; const metricsQuery: CloudWatchMetricsQuery = { queryMode: 'Metrics', @@ -330,8 +332,8 @@ describe('datasource', () => { expect(templateService.replace).toHaveBeenCalledWith(`$${variableName}`, {}); expect(templateService.replace).toHaveBeenCalledTimes(8); - expect(templateService.getVariableName).toHaveBeenCalledWith(`$${variableName}`); - expect(templateService.getVariableName).toHaveBeenCalledTimes(1); + expect(mockGetVariableName).toHaveBeenCalledWith(`$${variableName}`); + expect(mockGetVariableName).toHaveBeenCalledTimes(1); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 01783555ebf..9864f161123 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -13,8 +13,7 @@ import { LogRowModel, ScopedVars, } from '@grafana/data'; -import { DataSourceWithBackend } from '@grafana/runtime'; -import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; +import { DataSourceWithBackend, TemplateSrv, getTemplateSrv } from '@grafana/runtime'; import { CloudWatchAnnotationSupport } from './annotationSupport'; import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries'; diff --git a/public/app/plugins/datasource/cloudwatch/hooks.test.ts b/public/app/plugins/datasource/cloudwatch/hooks.test.ts index 79a8a49cf05..65ab0f6c1e4 100644 --- a/public/app/plugins/datasource/cloudwatch/hooks.test.ts +++ b/public/app/plugins/datasource/cloudwatch/hooks.test.ts @@ -64,7 +64,6 @@ describe('hooks', () => { describe('useDimensionKeys', () => { it('should interpolate variables before calling api', async () => { const { datasource } = setupMockedDataSource({ - mockGetVariableName: true, variables: [regionVariable, namespaceVariable, accountIdVariable, metricVariable, dimensionVariable], }); const getDimensionKeysMock = jest.fn().mockResolvedValue([]); diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.test.ts index 620ac3575e4..77a59d1fa21 100644 --- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.test.ts @@ -3,6 +3,7 @@ import { Value } from 'slate'; import { TypeaheadOutput } from '@grafana/ui'; +import { setupMockedTemplateService } from '../../__mocks__/CloudWatchDataSource'; import { CloudWatchDatasource } from '../../datasource'; import { ResourceResponse } from '../../resources/types'; import { LogGroupField } from '../../types'; @@ -124,7 +125,7 @@ function makeDatasource(): CloudWatchDatasource { * Get suggestion items based on query. Use `^` to mark position of the cursor. */ function getProvideCompletionItems(query: string): Promise { - const provider = new CloudWatchLogsLanguageProvider(makeDatasource()); + const provider = new CloudWatchLogsLanguageProvider(makeDatasource(), setupMockedTemplateService()); const cursorOffset = query.indexOf('^'); const queryWithoutCursor = query.replace('^', ''); let tokens: Token[] = Prism.tokenize(queryWithoutCursor, provider.getSyntax()) as any; diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.ts index f2dfa350b8e..f2e889f9c22 100644 --- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-logs/CloudWatchLogsLanguageProvider.ts @@ -2,9 +2,8 @@ import Prism, { Grammar } from 'prismjs'; import { lastValueFrom } from 'rxjs'; import { AbsoluteTimeRange, HistoryItem, LanguageProvider } from '@grafana/data'; -import { BackendDataSourceResponse, FetchResponse } from '@grafana/runtime'; +import { BackendDataSourceResponse, FetchResponse, TemplateSrv, getTemplateSrv } from '@grafana/runtime'; import { CompletionItemGroup, SearchFunctionType, Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui'; -import { getTemplateSrv } from 'app/features/templating/template_srv'; import { CloudWatchDatasource } from '../../datasource'; import { CloudWatchQuery, LogGroup } from '../../types'; @@ -34,11 +33,13 @@ export class CloudWatchLogsLanguageProvider extends LanguageProvider { started = false; declare initialRange: AbsoluteTimeRange; datasource: CloudWatchDatasource; + templateSrv: TemplateSrv; - constructor(datasource: CloudWatchDatasource, initialValues?: any) { + constructor(datasource: CloudWatchDatasource, templateSrv?: TemplateSrv, initialValues?: any) { super(); this.datasource = datasource; + this.templateSrv = templateSrv ?? getTemplateSrv(); Object.assign(this, initialValues); } @@ -132,7 +133,7 @@ export class CloudWatchLogsLanguageProvider extends LanguageProvider { private fetchFields = async (logGroups: LogGroup[], region: string): Promise => { const interpolatedLogGroups = interpolateStringArrayUsingSingleOrMultiValuedVariable( - getTemplateSrv(), + this.templateSrv, logGroups.map((lg) => lg.name), {}, 'text' diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.test.ts index 93c68c2bf0b..9a63cee6bd8 100644 --- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.test.ts @@ -1,10 +1,9 @@ -import { TemplateSrv } from 'app/features/templating/template_srv'; - import { aggregationvariable, labelsVariable, metricVariable, namespaceVariable, + setupMockedTemplateService, } from '../../__mocks__/CloudWatchDataSource'; import { createFunctionWithParameter, @@ -25,11 +24,12 @@ describe('SQLGenerator', () => { from: createFunctionWithParameter('SCHEMA', ['AWS/EC2']), orderByDirection: 'DESC', }; + let mockTemplateSrv = setupMockedTemplateService(); describe('mandatory fields check', () => { it('should return undefined if metric and aggregation is missing', () => { expect( - new SQLGenerator().expressionToSqlQuery({ + new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ from: createFunctionWithParameter('SCHEMA', ['AWS/EC2']), }) ).toBeUndefined(); @@ -37,7 +37,7 @@ describe('SQLGenerator', () => { it('should return undefined if aggregation is missing', () => { expect( - new SQLGenerator().expressionToSqlQuery({ + new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ from: createFunctionWithParameter('SCHEMA', []), }) ).toBeUndefined(); @@ -45,27 +45,27 @@ describe('SQLGenerator', () => { }); it('should return query if mandatory fields are provided', () => { - expect(new SQLGenerator().expressionToSqlQuery(baseQuery)).not.toBeUndefined(); + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery(baseQuery)).not.toBeUndefined(); }); describe('select', () => { it('should use statistic and metric name', () => { const select = createFunctionWithParameter('COUNT', ['BytesPerSecond']); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, select })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, select })).toEqual( `SELECT COUNT(BytesPerSecond) FROM SCHEMA("AWS/EC2")` ); }); it('should wrap in double quotes if metric name contains illegal characters ', () => { const select = createFunctionWithParameter('COUNT', ['Bytes-Per-Second']); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, select })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, select })).toEqual( `SELECT COUNT("Bytes-Per-Second") FROM SCHEMA("AWS/EC2")` ); }); it('should wrap in double quotes if metric name starts with a number ', () => { const select = createFunctionWithParameter('COUNT', ['4xxErrorRate']); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, select })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, select })).toEqual( `SELECT COUNT("4xxErrorRate") FROM SCHEMA("AWS/EC2")` ); }); @@ -75,14 +75,14 @@ describe('SQLGenerator', () => { describe('with schema contraint', () => { it('should handle schema without dimensions', () => { const from = createFunctionWithParameter('SCHEMA', ['AWS/MQ']); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, from })).toEqual( `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/MQ")` ); }); it('should handle schema with dimensions', () => { const from = createFunctionWithParameter('SCHEMA', ['AWS/MQ', 'InstanceId', 'InstanceType']); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, from })).toEqual( `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/MQ", InstanceId, InstanceType)` ); }); @@ -94,7 +94,7 @@ describe('SQLGenerator', () => { 'Instance.Type', 'Instance-Group', ]); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, from })).toEqual( `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/MQ", "Instance Id", "Instance.Type", "Instance-Group")` ); }); @@ -103,7 +103,7 @@ describe('SQLGenerator', () => { describe('without schema', () => { it('should use the specified namespace', () => { const from = createProperty('AWS/MQ'); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, from })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, from })).toEqual( `SELECT SUM(CPUUtilization) FROM "AWS/MQ"` ); }); @@ -111,25 +111,25 @@ describe('SQLGenerator', () => { }); function assertQueryEndsWith(rest: Partial, expectedFilter: string) { - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, ...rest })).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, ...rest })).toEqual( `SELECT SUM(CPUUtilization) FROM SCHEMA("AWS/EC2") ${expectedFilter}` ); } describe('filter', () => { it('should not add WHERE clause in case its empty', () => { - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('WHERE'); + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery })).not.toContain('WHERE'); }); it('should not add WHERE clause when there is no filter conditions', () => { const where = createArray([]); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, where })).not.toContain('WHERE'); + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, where })).not.toContain('WHERE'); }); // TODO: We should handle this scenario it.skip('should not add WHERE clause when the operator is incomplete', () => { const where = createArray([createOperator('Instance-Id', '=')]); - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery, where })).not.toContain('WHERE'); + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery, where })).not.toContain('WHERE'); }); it('should handle one top level filter with AND', () => { @@ -280,7 +280,7 @@ describe('SQLGenerator', () => { describe('group by', () => { it('should not add GROUP BY clause in case its empty', () => { - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('GROUP BY'); + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery })).not.toContain('GROUP BY'); }); it('should handle single label', () => { const groupBy = createArray([createGroupBy('InstanceId')], QueryEditorExpressionType.And); @@ -297,7 +297,7 @@ describe('SQLGenerator', () => { describe('order by', () => { it('should not add ORDER BY clause in case its empty', () => { - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('ORDER BY'); + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery })).not.toContain('ORDER BY'); }); it('should handle SUM ASC', () => { const orderBy = createFunction('SUM'); @@ -315,7 +315,7 @@ describe('SQLGenerator', () => { }); describe('limit', () => { it('should not add LIMIT clause in case its empty', () => { - expect(new SQLGenerator().expressionToSqlQuery({ ...baseQuery })).not.toContain('LIMIT'); + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery({ ...baseQuery })).not.toContain('LIMIT'); }); it('should be added in case its specified', () => { @@ -346,15 +346,19 @@ describe('SQLGenerator', () => { orderByDirection: 'DESC', limit: 100, }; - expect(new SQLGenerator().expressionToSqlQuery(query)).toEqual( + expect(new SQLGenerator(mockTemplateSrv).expressionToSqlQuery(query)).toEqual( `SELECT COUNT(DroppedBytes) FROM SCHEMA("AWS/MQ", InstanceId, "Instance-Group") WHERE (InstanceId = 'I-123' OR Type != 'some-type') AND (InstanceId != 'I-456' OR Type != 'some-type') GROUP BY InstanceId, InstanceType ORDER BY COUNT() DESC LIMIT 100` ); }); }); describe('using variables', () => { - const templateService = new TemplateSrv(); - templateService.init([metricVariable, namespaceVariable, labelsVariable, aggregationvariable]); + const templateService = setupMockedTemplateService([ + metricVariable, + namespaceVariable, + labelsVariable, + aggregationvariable, + ]); it('should interpolate variables correctly', () => { let query: SQLExpression = { diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.ts index b98136e1f89..105dd5a3da6 100644 --- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.ts +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/SQLGenerator.ts @@ -1,4 +1,4 @@ -import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; +import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { QueryEditorArrayExpression, diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchAnnotationQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchAnnotationQueryRunner.ts index ec01b0f3aa1..013d14bbd6f 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchAnnotationQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchAnnotationQueryRunner.ts @@ -1,7 +1,7 @@ import { Observable } from 'rxjs'; import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { TemplateSrv } from '@grafana/runtime'; import { CloudWatchAnnotationQuery, CloudWatchJsonData, CloudWatchQuery } from '../types'; diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts index dd098e59ddc..5bb8230e81d 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts @@ -244,7 +244,7 @@ describe('CloudWatchLogsQueryRunner', () => { id: '', region: '$' + regionVariable.name, refId: 'A', - expression: `stats count(*) by queryType, bin($__interval)`, + expression: `stats count(*) by queryType, bin(20s)`, }; describe('handleLogQueries', () => { diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts index a9b28b58765..2107bc02269 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts @@ -31,8 +31,7 @@ import { getDefaultTimeRange, rangeUtil, } from '@grafana/data'; -import { config, FetchError } from '@grafana/runtime'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { config, FetchError, TemplateSrv } from '@grafana/runtime'; import { CloudWatchJsonData, diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts index 3b73fa18fdd..13c1bb8cd5d 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts @@ -358,7 +358,7 @@ describe('CloudWatchMetricsQueryRunner', () => { it('interpolates variables correctly', async () => { const { runner, queryMock, request } = setupMockedMetricsQueryRunner({ - variables: [namespaceVariable, metricVariable, labelsVariable, limitVariable], + variables: [namespaceVariable, metricVariable, limitVariable], }); runner.handleMetricQueries( [ @@ -376,7 +376,7 @@ describe('CloudWatchMetricsQueryRunner', () => { expression: '', metricQueryType: MetricQueryType.Query, metricEditorMode: MetricEditorMode.Code, - sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit', + sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY InstanceId,InstanceType LIMIT $limit', }, ], request, @@ -666,8 +666,8 @@ describe('CloudWatchMetricsQueryRunner', () => { queryMode: 'Metrics', id: '', region: 'us-east-2', - namespace: namespaceVariable.id, - metricName: metricVariable.id, + namespace: '$' + namespaceVariable.name, + metricName: '$' + metricVariable.name, period: '', alias: '', dimensions: {}, @@ -712,7 +712,7 @@ describe('CloudWatchMetricsQueryRunner', () => { sqlExpression: 'select SUM(CPUUtilization) from $datasource', dimensions: { InstanceId: '$dimension' }, }; - const { runner } = setupMockedMetricsQueryRunner({ variables: [dimensionVariable], mockGetVariableName: false }); + const { runner } = setupMockedMetricsQueryRunner({ variables: [dimensionVariable] }); const result = runner.interpolateMetricsQueryVariables(testQuery, { datasource: { text: 'foo', value: 'foo' }, dimension: { text: 'foo', value: 'foo' }, @@ -732,7 +732,6 @@ describe('CloudWatchMetricsQueryRunner', () => { describe('convertMultiFiltersFormat', () => { const { runner } = setupMockedMetricsQueryRunner({ variables: [labelsVariable, dimensionVariable], - mockGetVariableName: false, }); it('converts keys and values correctly', () => { const filters = { $dimension: ['b'], a: ['$labels', 'bar'] }; diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts index c6966eadc91..58cea66694e 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.ts @@ -12,9 +12,9 @@ import { rangeUtil, ScopedVars, } from '@grafana/data'; +import { TemplateSrv } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; -import { TemplateSrv } from 'app/features/templating/template_srv'; import { store } from 'app/store/store'; import { AppNotificationTimeout } from 'app/types'; diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts index 8570f18393d..c20ca70b19b 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts @@ -1,15 +1,15 @@ import { Observable } from 'rxjs'; import { DataSourceInstanceSettings, DataSourceRef, getDataSourceRef, ScopedVars } from '@grafana/data'; -import { BackendDataSourceResponse, FetchResponse, getBackendSrv } from '@grafana/runtime'; +import { BackendDataSourceResponse, FetchResponse, getBackendSrv, TemplateSrv } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; -import { TemplateSrv } from 'app/features/templating/template_srv'; import { store } from 'app/store/store'; import { AppNotificationTimeout } from 'app/types'; import memoizedDebounce from '../memoizedDebounce'; import { CloudWatchJsonData, Dimensions, MetricRequest, MultiFilters } from '../types'; +import { getVariableName } from '../utils/templateVariableUtils'; export abstract class CloudWatchRequest { templateSrv: TemplateSrv; @@ -62,7 +62,7 @@ export abstract class CloudWatchRequest { // get the value for a given template variable expandVariableToArray(value: string, scopedVars: ScopedVars): string[] { - const variableName = this.templateSrv.getVariableName(value); + const variableName = getVariableName(value); const valueVar = this.templateSrv.getVariables().find(({ name }) => { return name === variableName; }); @@ -101,7 +101,7 @@ export abstract class CloudWatchRequest { ) { if (displayErrorIfIsMultiTemplateVariable && !!target) { const variables = this.templateSrv.getVariables(); - const variable = variables.find(({ name }) => name === this.templateSrv.getVariableName(target)); + const variable = variables.find(({ name }) => name === getVariableName(target)); const isMultiVariable = variable?.type === 'custom' || variable?.type === 'query' || variable?.type === 'datasource'; if (isMultiVariable && variable.multi) { diff --git a/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts index d3b6fa0979a..0836b115187 100644 --- a/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts +++ b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts @@ -1,8 +1,7 @@ import { memoize } from 'lodash'; import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data'; -import { getBackendSrv, config } from '@grafana/runtime'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { getBackendSrv, config, TemplateSrv } from '@grafana/runtime'; import { CloudWatchRequest } from '../query-runner/CloudWatchRequest'; import { CloudWatchJsonData, LogGroupField, MultiFilters } from '../types'; diff --git a/public/app/plugins/datasource/cloudwatch/utils/templateVariableUtils.ts b/public/app/plugins/datasource/cloudwatch/utils/templateVariableUtils.ts index be1e2043754..1cad2dfdb7a 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/templateVariableUtils.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/templateVariableUtils.ts @@ -1,5 +1,29 @@ import { VariableOption, UserProps, OrgProps, DashboardProps, ScopedVars } from '@grafana/data'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { TemplateSrv } from '@grafana/runtime'; + +/* + * This regex matches 3 types of variable reference with an optional format specifier + * There are 6 capture groups that replace will return + * \$(\w+) $var1 + * \[\[(\w+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] + * \${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?} ${var3} or ${var3.fieldPath} or ${var3:fmt3} (or ${var3.fieldPath:fmt3} but that is not a separate capture group) + */ +const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; + +// Helper function since lastIndex is not reset +const variableRegexExec = (variableString: string) => { + variableRegex.lastIndex = 0; + return variableRegex.exec(variableString); +}; + +export const getVariableName = (expression: string) => { + const match = variableRegexExec(expression); + if (!match) { + return null; + } + const variableName = match.slice(1).find((match) => match !== undefined); + return variableName; +}; /** * @remarks @@ -22,7 +46,7 @@ export const interpolateStringArrayUsingSingleOrMultiValuedVariable = ( const format = key === 'value' ? 'pipe' : 'text'; let result: string[] = []; for (const string of strings) { - const variableName = templateSrv.getVariableName(string); + const variableName = getVariableName(string); const valueVar = templateSrv.getVariables().find(({ name }) => name === variableName); if (valueVar && 'current' in valueVar && isVariableOption(valueVar.current)) { @@ -43,7 +67,7 @@ export const interpolateStringArrayUsingSingleOrMultiValuedVariable = ( }; export const isTemplateVariable = (templateSrv: TemplateSrv, string: string) => { - const variableName = templateSrv.getVariableName(string); + const variableName = getVariableName(string); return templateSrv.getVariables().some(({ name }) => name === variableName); };