From dca0453c2e6692b6b09d7ee247f01ebedab611a0 Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Wed, 1 Jun 2022 10:23:31 -0700 Subject: [PATCH] CloudWatch: Add multi-value template variable support for log group names in logs query builder (#49737) * Add multi-value template variable support for log group names * add test for multi-value template variable for log group names * add test --- .betterer.results | 4 +- .../__mocks__/CloudWatchDataSource.ts | 42 +++++++++++++++++++ .../components/LogsQueryField.test.tsx | 33 +++++++++++++++ .../cloudwatch/components/LogsQueryField.tsx | 3 +- .../datasource/cloudwatch/datasource.test.ts | 29 +++++++++++++ .../datasource/cloudwatch/datasource.ts | 10 +++-- .../cloudwatch/utils/datalinks.test.ts | 1 + .../datasource/cloudwatch/utils/datalinks.ts | 9 ++-- 8 files changed, 122 insertions(+), 9 deletions(-) diff --git a/.betterer.results b/.betterer.results index 95c4515507eab..cf4faad28b692 100644 --- a/.betterer.results +++ b/.betterer.results @@ -179,8 +179,8 @@ exports[`no enzyme tests`] = { "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:1224072551": [ [0, 19, 13, "RegExp match", "2409514259"] ], - "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:2097436158": [ - [1, 19, 13, "RegExp match", "2409514259"] + "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:1501504663": [ + [2, 19, 13, "RegExp match", "2409514259"] ], "public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [ [0, 26, 13, "RegExp match", "2409514259"] diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index 5b90705521598..bcb8d3a7584f1 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -149,3 +149,45 @@ export const dimensionVariable: CustomVariableModel = { ], multi: false, }; + +export const logGroupNamesVariable: CustomVariableModel = { + ...initialCustomVariableModelState, + id: 'groups', + name: 'groups', + current: { + value: ['templatedGroup-1', 'templatedGroup-2'], + text: ['templatedGroup-1', 'templatedGroup-2'], + selected: true, + }, + options: [ + { value: 'templatedGroup-1', text: 'templatedGroup-1', selected: true }, + { value: 'templatedGroup-2', text: 'templatedGroup-2', selected: true }, + ], + multi: true, +}; + +export const regionVariable: CustomVariableModel = { + ...initialCustomVariableModelState, + id: 'region', + name: 'region', + current: { + value: 'templatedRegion', + text: 'templatedRegion', + selected: true, + }, + options: [{ value: 'templatedRegion', text: 'templatedRegion', selected: true }], + multi: false, +}; + +export const expressionVariable: CustomVariableModel = { + ...initialCustomVariableModelState, + id: 'fields', + name: 'fields', + current: { + value: 'templatedField', + text: 'templatedField', + selected: true, + }, + options: [{ value: 'templatedField', text: 'templatedField', selected: true }], + multi: false, +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx index 0aed8959533e9..b179c1bd260ea 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx @@ -1,8 +1,10 @@ import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { shallow } from 'enzyme'; import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope import React from 'react'; import { act } from 'react-dom/test-utils'; +import { openMenu, select } from 'react-select-event'; import { SelectableValue } from '@grafana/data'; @@ -69,6 +71,7 @@ describe('CloudWatchLogsQueryField', () => { return Promise.resolve(['log_group_2']); } }, + getVariables: jest.fn().mockReturnValue([]), } as any } query={{} as any} @@ -201,6 +204,7 @@ describe('CloudWatchLogsQueryField', () => { .slice(0, Math.max(params.limit ?? 50, 50)); return Promise.resolve(theLogGroups); }, + getVariables: jest.fn().mockReturnValue([]), } as any } query={{} as any} @@ -235,4 +239,33 @@ describe('CloudWatchLogsQueryField', () => { .concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3', 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3']) ); }); + + it('should render template variables a selectable option', async () => { + const { datasource } = setupMockedDataSource(); + const onChange = jest.fn(); + + render( + {}} + onChange={onChange} + /> + ); + + const logGroupSelector = await screen.findByLabelText('Log Groups'); + expect(logGroupSelector).toBeInTheDocument(); + + await openMenu(logGroupSelector); + const templateVariableSelector = await screen.findByText('Template Variables'); + expect(templateVariableSelector).toBeInTheDocument(); + + userEvent.click(templateVariableSelector); + await select(await screen.findByLabelText('Select option'), 'test'); + + expect(await screen.findByText('test')).toBeInTheDocument(); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index c981310dd346f..8d2ef81e7c647 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -27,6 +27,7 @@ import { CloudWatchLanguageProvider } from '../language_provider'; import syntax from '../syntax'; import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types'; import { getStatsGroups } from '../utils/query/getStatsGroups'; +import { appendTemplateVariables } from '../utils/utils'; import QueryHeader from './QueryHeader'; @@ -310,7 +311,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent { this.setSelectedLogGroups(v); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 964a17a061dc4..494a54492c6f8 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -6,11 +6,14 @@ import { setDataSourceSrv } from '@grafana/runtime'; import { dimensionVariable, + expressionVariable, labelsVariable, limitVariable, + logGroupNamesVariable, metricVariable, namespaceVariable, setupMockedDataSource, + regionVariable, } from './__mocks__/CloudWatchDataSource'; import { CloudWatchLogsQuery, @@ -65,6 +68,32 @@ describe('datasource', () => { }); }); + it('should interpolate multi-value template variable for log group names in the query', async () => { + const { datasource, fetchMock } = setupMockedDataSource({ + variables: [expressionVariable, logGroupNamesVariable, regionVariable], + mockGetVariableName: false, + }); + await lastValueFrom( + datasource + .query({ + targets: [ + { + queryMode: 'Logs', + region: '$region', + expression: 'fields $fields', + logGroupNames: ['$groups'], + }, + ], + } as any) + .pipe(toArray()) + ); + expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({ + queryString: 'fields templatedField', + logGroupNames: ['templatedGroup-1', 'templatedGroup-2'], + region: 'templatedRegion', + }); + }); + it('should add links to log queries', async () => { const { datasource } = setupForLogs(); const observable = datasource.query({ diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 582d3016959f9..0c80835037a66 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -233,6 +233,7 @@ export class CloudWatchDatasource options, this.timeSrv.timeRange(), this.replace.bind(this), + this.getVariableValue.bind(this), this.getActualRegion.bind(this), this.tracingDataSourceUid ); @@ -648,9 +649,12 @@ export class CloudWatchDatasource for (const fieldName of fieldsToReplace) { if (query.hasOwnProperty(fieldName)) { if (Array.isArray(anyQuery[fieldName])) { - anyQuery[fieldName] = anyQuery[fieldName].map((val: string) => - this.replace(val, options.scopedVars, true, fieldName) - ); + anyQuery[fieldName] = anyQuery[fieldName].flatMap((val: string) => { + if (fieldName === 'logGroupNames') { + return this.getVariableValue(val, options.scopedVars || {}); + } + return this.replace(val, options.scopedVars, true, fieldName); + }); } else { anyQuery[fieldName] = this.replace(anyQuery[fieldName], options.scopedVars, true, fieldName); } diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts index 25edfd33b167b..bc0fa5e73807a 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts @@ -52,6 +52,7 @@ describe('addDataLinksToLogsResponse', () => { mockOptions, { ...time, raw: time }, (s) => s ?? '', + (v) => [v] ?? [], (r) => r, 'xrayUid' ); diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts index 05f19e6192587..e0f8f05c6a758 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts @@ -16,10 +16,12 @@ export async function addDataLinksToLogsResponse( request: DataQueryRequest, range: TimeRange, replaceFn: ReplaceFn, + getVariableValueFn: (value: string, scopedVars: ScopedVars) => string[], getRegion: (region: string) => string, tracingDatasourceUid?: string ): Promise { const replace = (target: string, fieldName?: string) => replaceFn(target, request.scopedVars, true, fieldName); + const getVariableValue = (target: string) => getVariableValueFn(target, request.scopedVars); for (const dataFrame of response.data as DataFrame[]) { const curTarget = request.targets.find((target) => target.refId === dataFrame.refId) as CloudWatchLogsQuery; @@ -35,7 +37,7 @@ export async function addDataLinksToLogsResponse( } else { // Right now we add generic link to open the query in xray console to every field so it shows in the logs row // details. Unfortunately this also creates link for all values inside table which look weird. - field.config.links = [createAwsConsoleLink(curTarget, range, interpolatedRegion, replace)]; + field.config.links = [createAwsConsoleLink(curTarget, range, interpolatedRegion, replace, getVariableValue)]; } } } @@ -65,10 +67,11 @@ function createAwsConsoleLink( target: CloudWatchLogsQuery, range: TimeRange, region: string, - replace: (target: string, fieldName?: string) => string + replace: (target: string, fieldName?: string) => string, + getVariableValue: (value: string) => string[] ) { const interpolatedExpression = target.expression ? replace(target.expression) : ''; - const interpolatedGroups = target.logGroupNames?.map((logGroup: string) => replace(logGroup, 'log groups')) ?? []; + const interpolatedGroups = target.logGroupNames?.flatMap(getVariableValue) ?? []; const urlProps: AwsUrl = { end: range.to.toISOString(),