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
This commit is contained in:
Kevin Yu
2022-06-01 10:23:31 -07:00
committed by GitHub
parent 4783db59c7
commit dca0453c2e
8 changed files with 122 additions and 9 deletions

View File

@ -179,8 +179,8 @@ exports[`no enzyme tests`] = {
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:1224072551": [ "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:1224072551": [
[0, 19, 13, "RegExp match", "2409514259"] [0, 19, 13, "RegExp match", "2409514259"]
], ],
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:2097436158": [ "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:1501504663": [
[1, 19, 13, "RegExp match", "2409514259"] [2, 19, 13, "RegExp match", "2409514259"]
], ],
"public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [ "public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [
[0, 26, 13, "RegExp match", "2409514259"] [0, 26, 13, "RegExp match", "2409514259"]

View File

@ -149,3 +149,45 @@ export const dimensionVariable: CustomVariableModel = {
], ],
multi: false, 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,
};

View File

@ -1,8 +1,10 @@
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { openMenu, select } from 'react-select-event';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
@ -69,6 +71,7 @@ describe('CloudWatchLogsQueryField', () => {
return Promise.resolve(['log_group_2']); return Promise.resolve(['log_group_2']);
} }
}, },
getVariables: jest.fn().mockReturnValue([]),
} as any } as any
} }
query={{} as any} query={{} as any}
@ -201,6 +204,7 @@ describe('CloudWatchLogsQueryField', () => {
.slice(0, Math.max(params.limit ?? 50, 50)); .slice(0, Math.max(params.limit ?? 50, 50));
return Promise.resolve(theLogGroups); return Promise.resolve(theLogGroups);
}, },
getVariables: jest.fn().mockReturnValue([]),
} as any } as any
} }
query={{} as any} query={{} as any}
@ -235,4 +239,33 @@ describe('CloudWatchLogsQueryField', () => {
.concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3', 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3']) .concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3', 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3'])
); );
}); });
it('should render template variables a selectable option', async () => {
const { datasource } = setupMockedDataSource();
const onChange = jest.fn();
render(
<CloudWatchLogsQueryField
history={[]}
absoluteRange={{ from: 1, to: 10 }}
exploreId={ExploreId.left}
datasource={datasource}
query={{} as any}
onRunQuery={() => {}}
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();
});
}); });

View File

@ -27,6 +27,7 @@ import { CloudWatchLanguageProvider } from '../language_provider';
import syntax from '../syntax'; import syntax from '../syntax';
import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types'; import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types';
import { getStatsGroups } from '../utils/query/getStatsGroups'; import { getStatsGroups } from '../utils/query/getStatsGroups';
import { appendTemplateVariables } from '../utils/utils';
import QueryHeader from './QueryHeader'; import QueryHeader from './QueryHeader';
@ -310,7 +311,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
<MultiSelect <MultiSelect
aria-label="Log Groups" aria-label="Log Groups"
allowCustomValue={allowCustomValue} allowCustomValue={allowCustomValue}
options={unionBy(availableLogGroups, selectedLogGroups, 'value')} options={appendTemplateVariables(datasource, unionBy(availableLogGroups, selectedLogGroups, 'value'))}
value={selectedLogGroups} value={selectedLogGroups}
onChange={(v) => { onChange={(v) => {
this.setSelectedLogGroups(v); this.setSelectedLogGroups(v);

View File

@ -6,11 +6,14 @@ import { setDataSourceSrv } from '@grafana/runtime';
import { import {
dimensionVariable, dimensionVariable,
expressionVariable,
labelsVariable, labelsVariable,
limitVariable, limitVariable,
logGroupNamesVariable,
metricVariable, metricVariable,
namespaceVariable, namespaceVariable,
setupMockedDataSource, setupMockedDataSource,
regionVariable,
} from './__mocks__/CloudWatchDataSource'; } from './__mocks__/CloudWatchDataSource';
import { import {
CloudWatchLogsQuery, 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 () => { it('should add links to log queries', async () => {
const { datasource } = setupForLogs(); const { datasource } = setupForLogs();
const observable = datasource.query({ const observable = datasource.query({

View File

@ -233,6 +233,7 @@ export class CloudWatchDatasource
options, options,
this.timeSrv.timeRange(), this.timeSrv.timeRange(),
this.replace.bind(this), this.replace.bind(this),
this.getVariableValue.bind(this),
this.getActualRegion.bind(this), this.getActualRegion.bind(this),
this.tracingDataSourceUid this.tracingDataSourceUid
); );
@ -648,9 +649,12 @@ export class CloudWatchDatasource
for (const fieldName of fieldsToReplace) { for (const fieldName of fieldsToReplace) {
if (query.hasOwnProperty(fieldName)) { if (query.hasOwnProperty(fieldName)) {
if (Array.isArray(anyQuery[fieldName])) { if (Array.isArray(anyQuery[fieldName])) {
anyQuery[fieldName] = anyQuery[fieldName].map((val: string) => anyQuery[fieldName] = anyQuery[fieldName].flatMap((val: string) => {
this.replace(val, options.scopedVars, true, fieldName) if (fieldName === 'logGroupNames') {
); return this.getVariableValue(val, options.scopedVars || {});
}
return this.replace(val, options.scopedVars, true, fieldName);
});
} else { } else {
anyQuery[fieldName] = this.replace(anyQuery[fieldName], options.scopedVars, true, fieldName); anyQuery[fieldName] = this.replace(anyQuery[fieldName], options.scopedVars, true, fieldName);
} }

View File

@ -52,6 +52,7 @@ describe('addDataLinksToLogsResponse', () => {
mockOptions, mockOptions,
{ ...time, raw: time }, { ...time, raw: time },
(s) => s ?? '', (s) => s ?? '',
(v) => [v] ?? [],
(r) => r, (r) => r,
'xrayUid' 'xrayUid'
); );

View File

@ -16,10 +16,12 @@ export async function addDataLinksToLogsResponse(
request: DataQueryRequest<CloudWatchQuery>, request: DataQueryRequest<CloudWatchQuery>,
range: TimeRange, range: TimeRange,
replaceFn: ReplaceFn, replaceFn: ReplaceFn,
getVariableValueFn: (value: string, scopedVars: ScopedVars) => string[],
getRegion: (region: string) => string, getRegion: (region: string) => string,
tracingDatasourceUid?: string tracingDatasourceUid?: string
): Promise<void> { ): Promise<void> {
const replace = (target: string, fieldName?: string) => replaceFn(target, request.scopedVars, true, fieldName); 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[]) { for (const dataFrame of response.data as DataFrame[]) {
const curTarget = request.targets.find((target) => target.refId === dataFrame.refId) as CloudWatchLogsQuery; const curTarget = request.targets.find((target) => target.refId === dataFrame.refId) as CloudWatchLogsQuery;
@ -35,7 +37,7 @@ export async function addDataLinksToLogsResponse(
} else { } else {
// Right now we add generic link to open the query in xray console to every field so it shows in the logs row // 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. // 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, target: CloudWatchLogsQuery,
range: TimeRange, range: TimeRange,
region: string, 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 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 = { const urlProps: AwsUrl = {
end: range.to.toISOString(), end: range.to.toISOString(),