Datasource/CloudWatch: More robust handling of different query modes (#25691)

* Datasource/CloudWatch: More robust handling of different query modes
A small refactor which changes how the CloudWatch datasource handles
multiple queries with different query modes. Groundwork for future
Logs/Metrics unification work.
This commit is contained in:
kay delaney 2020-07-09 13:11:13 +01:00 committed by GitHub
parent ddcc0c3c80
commit 2ac1bfcc79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 259 additions and 176 deletions

View File

@ -46,9 +46,9 @@ export default class CloudWatchLink extends Component<Props, State> {
start,
timeType: 'ABSOLUTE',
tz: 'UTC',
editorString: query.expression,
editorString: query.expression ?? '',
isLiveTail: false,
source: query.logGroupNames,
source: query.logGroupNames ?? [],
};
return encodeUrl(urlProps, datasource.getActualRegion(query.region));

View File

@ -220,18 +220,12 @@ export default class LogsCheatSheet extends PureComponent<ExploreStartPageProps,
switchToMetrics = (query: CloudWatchLogsQuery) => {
const { onClickExample, exploreId } = this.props;
const nextQuery: CloudWatchLogsQuery = {
...(query as CloudWatchLogsQuery),
apiMode: 'Logs',
queryMode: 'Logs',
};
dispatch(changeModeAction({ exploreId, mode: ExploreMode.Metrics }));
onClickExample(nextQuery);
onClickExample(query);
};
onClickExample(query: CloudWatchLogsQuery) {
if (query.expression.includes('stats')) {
if (query.expression?.includes('stats')) {
this.switchToMetrics(query);
} else {
this.props.onClickExample(query);
@ -243,7 +237,9 @@ export default class LogsCheatSheet extends PureComponent<ExploreStartPageProps,
<div
className="cheat-sheet-item__example"
key={expr}
onClick={e => this.onClickExample({ refId: 'A', expression: expr } as CloudWatchLogsQuery)}
onClick={e =>
this.onClickExample({ refId: 'A', expression: expr, queryMode: 'Logs', region: 'default', id: 'A' })
}
>
<pre>{renderHighlightedMarkup(expr, keyPrefix)}</pre>
</div>

View File

@ -275,16 +275,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
};
switchToMetrics = () => {
const { query, onChange, exploreId } = this.props;
if (onChange) {
const nextQuery: CloudWatchLogsQuery = {
...(query as CloudWatchLogsQuery),
apiMode: 'Logs',
};
onChange(nextQuery);
}
const { exploreId } = this.props;
dispatch(changeModeAction({ exploreId, mode: ExploreMode.Metrics }));
};
@ -409,7 +400,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
<div className="gf-form gf-form--grow flex-shrink-1">
<QueryField
additionalPlugins={this.plugins}
query={query.expression}
query={query.expression ?? ''}
onChange={this.onChangeQuery}
onBlur={this.props.onBlur}
onClick={this.onQueryFieldClick}

View File

@ -41,7 +41,6 @@ const setup = () => {
const props: Props = {
query: {
queryMode: 'Metrics',
apiMode: 'Metrics',
refId: '',
id: '',
region: 'us-east-1',

View File

@ -1,15 +1,15 @@
import React, { useState, useEffect } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment, SegmentAsync } from '@grafana/ui';
import { CloudWatchQuery, SelectableStrings, CloudWatchMetricsQuery } from '../types';
import { SelectableStrings, CloudWatchMetricsQuery } from '../types';
import { CloudWatchDatasource } from '../datasource';
import { Stats, Dimensions, QueryInlineField } from '.';
export type Props = {
query: CloudWatchQuery;
query: CloudWatchMetricsQuery;
datasource: CloudWatchDatasource;
onRunQuery?: () => void;
onChange: (value: CloudWatchQuery) => void;
onChange: (value: CloudWatchMetricsQuery) => void;
};
interface State {
@ -66,7 +66,7 @@ export function MetricsQueryFieldsEditor({
const toOption = (value: any) => ({ label: value, value });
const onQueryChange = (query: CloudWatchQuery) => {
const onQueryChange = (query: CloudWatchMetricsQuery) => {
onChange(query);
onRunQuery();
};
@ -98,7 +98,7 @@ export function MetricsQueryFieldsEditor({
/>
</QueryInlineField>
{query.expression.length === 0 && (
{query.expression?.length === 0 && (
<>
<QueryInlineField label="Namespace">
<Segment

View File

@ -1,4 +1,5 @@
import React, { PureComponent } from 'react';
import pick from 'lodash/pick';
import { ExploreQueryFieldProps, ExploreMode } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { CloudWatchQuery } from '../types';
@ -17,7 +18,7 @@ const apiModes = {
export class PanelQueryEditor extends PureComponent<Props> {
render() {
const { query } = this.props;
const apiMode = query.apiMode ?? query.queryMode ?? 'Metrics';
const apiMode = query.queryMode ?? 'Metrics';
return (
<>
@ -25,9 +26,27 @@ export class PanelQueryEditor extends PureComponent<Props> {
<Segment
value={apiModes[apiMode]}
options={Object.values(apiModes)}
onChange={({ value }) =>
this.props.onChange({ ...query, apiMode: (value as 'Metrics' | 'Logs') ?? 'Metrics' })
}
onChange={({ value }) => {
const newMode = (value as 'Metrics' | 'Logs') ?? 'Metrics';
if (newMode !== apiModes[apiMode].value) {
const commonProps = pick(
query,
'id',
'region',
'namespace',
'refId',
'hide',
'key',
'queryType',
'datasource'
);
this.props.onChange({
...commonProps,
queryMode: newMode,
} as CloudWatchQuery);
}
}}
/>
</QueryInlineField>
{apiMode === ExploreMode.Logs ? (

View File

@ -7,26 +7,30 @@ describe('datasource', () => {
describe('query', () => {
it('should return error if log query and log groups is not specified', async () => {
const { datasource } = setup();
const response: DataQueryResponse = (await datasource.query({
targets: [
{
queryMode: 'Logs' as 'Logs',
},
],
} as any)) as any;
const response: DataQueryResponse = (await datasource
.query({
targets: [
{
queryMode: 'Logs' as 'Logs',
},
],
} as any)
.toPromise()) as any;
expect(response.error?.message).toBe('Log group is required');
});
it('should return empty response if queries are hidden', async () => {
const { datasource } = setup();
const response: DataQueryResponse = (await datasource.query({
targets: [
{
queryMode: 'Logs' as 'Logs',
hide: true,
},
],
} as any)) as any;
const response: DataQueryResponse = (await datasource
.query({
targets: [
{
queryMode: 'Logs' as 'Logs',
hide: true,
},
],
} as any)
.toPromise()) as any;
expect(response.data).toEqual([]);
});
});

View File

@ -39,7 +39,7 @@ import {
MetricRequest,
TSDBResponse,
} from './types';
import { empty, from, Observable } from 'rxjs';
import { empty, from, Observable, of, merge } from 'rxjs';
import { catchError, delay, expand, finalize, map, mergeMap, tap } from 'rxjs/operators';
import { CloudWatchLanguageProvider } from './language_provider';
@ -101,55 +101,78 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
this.languageProvider = new CloudWatchLanguageProvider(this);
}
query(options: DataQueryRequest<CloudWatchQuery>): Promise<DataQueryResponse> | Observable<DataQueryResponse> {
query(options: DataQueryRequest<CloudWatchQuery>): Observable<DataQueryResponse> {
options = angular.copy(options);
const firstTarget = options.targets[0];
let queries = options.targets.filter(item => item.id !== '' || item.hide !== true);
const { logQueries, metricsQueries } = this.getTargetsByQueryMode(queries);
if (firstTarget.queryMode === 'Logs') {
const logQueries: CloudWatchLogsQuery[] = queries.filter(item => item.queryMode === 'Logs') as any;
const validLogQueries = logQueries.filter(item => item.logGroupNames?.length);
if (logQueries.length > validLogQueries.length) {
return Promise.resolve({ data: [], error: { message: 'Log group is required' } });
}
// No valid targets, return the empty result to save a round trip.
if (_.isEmpty(validLogQueries)) {
return Promise.resolve({ data: [] });
}
const queryParams = validLogQueries.map((target: CloudWatchLogsQuery) => ({
queryString: target.expression,
refId: target.refId,
logGroupNames: target.logGroupNames,
region: this.replace(this.getActualRegion(target.region), options.scopedVars, true, 'region'),
}));
return this.makeLogActionRequest('StartQuery', queryParams, options.scopedVars).pipe(
mergeMap(dataFrames =>
this.logsQuery(
dataFrames.map(dataFrame => ({
queryId: dataFrame.fields[0].values.get(0),
region: dataFrame.meta?.custom?.['Region'] ?? 'default',
refId: dataFrame.refId!,
statsGroups: (options.targets.find(target => target.refId === dataFrame.refId)! as CloudWatchLogsQuery)
.statsGroups,
}))
)
),
map(response => this.addDataLinksToLogsResponse(response, options))
);
const dataQueryResponses: Array<Observable<DataQueryResponse>> = [];
if (logQueries.length > 0) {
dataQueryResponses.push(this.handleLogQueries(logQueries, options));
}
const metricQueries: MetricQuery[] = options.targets
if (metricsQueries.length > 0) {
dataQueryResponses.push(this.handleMetricQueries(metricsQueries, options));
}
// No valid targets, return the empty result to save a round trip.
if (_.isEmpty(dataQueryResponses)) {
return of({
data: [],
state: LoadingState.Done,
});
}
return merge(...dataQueryResponses);
}
handleLogQueries = (
logQueries: CloudWatchLogsQuery[],
options: DataQueryRequest<CloudWatchQuery>
): Observable<DataQueryResponse> => {
const validLogQueries = logQueries.filter(item => item.logGroupNames?.length);
if (logQueries.length > validLogQueries.length) {
return of({ data: [], error: { message: 'Log group is required' } });
}
// No valid targets, return the empty result to save a round trip.
if (_.isEmpty(validLogQueries)) {
return of({ data: [], state: LoadingState.Done });
}
const queryParams = validLogQueries.map((target: CloudWatchLogsQuery) => ({
queryString: target.expression,
refId: target.refId,
logGroupNames: target.logGroupNames,
region: this.replace(this.getActualRegion(target.region), options.scopedVars, true, 'region'),
}));
return this.makeLogActionRequest('StartQuery', queryParams, options.scopedVars).pipe(
mergeMap(dataFrames =>
this.logsQuery(
dataFrames.map(dataFrame => ({
queryId: dataFrame.fields[0].values.get(0),
region: dataFrame.meta?.custom?.['Region'] ?? 'default',
refId: dataFrame.refId!,
statsGroups: (logQueries.find(target => target.refId === dataFrame.refId)! as CloudWatchLogsQuery)
.statsGroups,
}))
)
),
map(response => this.addDataLinksToLogsResponse(response, options))
);
};
handleMetricQueries = (
metricQueries: CloudWatchMetricsQuery[],
options: DataQueryRequest<CloudWatchQuery>
): Observable<DataQueryResponse> => {
const validMetricsQueries = metricQueries
.filter(
item =>
item.queryMode !== 'Logs' &&
((!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) ||
item.expression?.length > 0)
(!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) ||
item.expression?.length > 0
)
.map(
(item: CloudWatchMetricsQuery): MetricQuery => {
@ -187,18 +210,18 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
);
// No valid targets, return the empty result to save a round trip.
if (_.isEmpty(metricQueries)) {
return Promise.resolve({ data: [] });
if (_.isEmpty(validMetricsQueries)) {
return of({ data: [] });
}
const request = {
from: options?.range?.from.valueOf().toString(),
to: options?.range?.to.valueOf().toString(),
queries: metricQueries,
queries: validMetricsQueries,
};
return this.performTimeSeriesQuery(request, options.range);
}
return from(this.performTimeSeriesQuery(request, options.range));
};
logsQuery(
queryParams: Array<{ queryId: string; refId: string; limit?: number; region: string; statsGroups?: string[] }>
@ -279,9 +302,9 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
start,
timeType: 'ABSOLUTE',
tz: 'UTC',
editorString: curTarget.expression,
editorString: curTarget.expression ?? '',
isLiveTail: false,
source: curTarget.logGroupNames,
source: curTarget.logGroupNames ?? [],
};
const encodedUrl = encodeUrl(
@ -902,11 +925,30 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
getQueryDisplayText(query: CloudWatchQuery) {
if (query.queryMode === 'Logs') {
return query.expression;
return query.expression ?? '';
} else {
return JSON.stringify(query);
}
}
getTargetsByQueryMode = (targets: CloudWatchQuery[]) => {
const logQueries: CloudWatchLogsQuery[] = [];
const metricsQueries: CloudWatchMetricsQuery[] = [];
targets.forEach(query => {
const mode = query.queryMode ?? 'Metrics';
if (mode === 'Logs') {
logQueries.push(query as CloudWatchLogsQuery);
} else {
metricsQueries.push(query as CloudWatchMetricsQuery);
}
});
return {
logQueries,
metricsQueries,
};
};
}
function withTeardown<T = any>(observable: Observable<T>, onUnsubscribe: () => void): Observable<T> {

View File

@ -314,7 +314,7 @@ describe('CloudWatchDatasource', () => {
});
it('should generate the correct query', async () => {
await ctx.ds.query(query);
await ctx.ds.query(query).toPromise();
expect(datasourceRequestMock.mock.calls[0][0].data.queries).toMatchObject(
expect.arrayContaining([
expect.objectContaining({
@ -365,7 +365,7 @@ describe('CloudWatchDatasource', () => {
],
};
await ctx.ds.query(query);
await ctx.ds.query(query).toPromise();
expect(datasourceRequestMock.mock.calls[0][0].data.queries[0].period).toEqual('600');
});
@ -392,11 +392,14 @@ describe('CloudWatchDatasource', () => {
});
it('should return series list', done => {
ctx.ds.query(query).then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});
});
describe('a correct cloudwatch url should be built for each time series in the response', () => {
@ -408,14 +411,17 @@ describe('CloudWatchDatasource', () => {
it('should be built correctly if theres one search expressions returned in meta for a given query row', done => {
response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }];
ctx.ds.query(query).then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}`
);
done();
});
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}`
);
done();
});
});
it('should be built correctly if theres two search expressions returned in meta for a given query row', done => {
@ -423,35 +429,44 @@ describe('CloudWatchDatasource', () => {
{ Expression: `REMOVE_EMPTY(SEARCH('first expression'))` },
{ Expression: `REMOVE_EMPTY(SEARCH('second expression'))` },
];
ctx.ds.query(query).then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}`
);
done();
});
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}`
);
done();
});
});
it('should be built correctly if the query is a metric stat query', done => {
response.results['A'].meta.gmdMeta = [{ Period: '300' }];
ctx.ds.query(query).then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}`
);
done();
});
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}`
);
done();
});
});
it('should not be added at all if query is a math expression', done => {
query.targets[0].expression = 'a * 2';
response.results['A'].meta.searchExpressions = [];
ctx.ds.query(query).then((result: any) => {
expect(result.data[0].fields[1].config.links).toBeUndefined();
done();
});
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(result.data[0].fields[1].config.links).toBeUndefined();
done();
});
});
});
@ -525,13 +540,16 @@ describe('CloudWatchDatasource', () => {
it('should display one alert error message per region+datasource combination', done => {
const memoizedDebounceSpy = jest.spyOn(ctx.ds, 'debouncedAlert');
ctx.ds.query(query).catch(() => {
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-1');
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-2');
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'eu-north-1');
expect(memoizedDebounceSpy).toBeCalledTimes(3);
done();
});
ctx.ds
.query(query)
.toPromise()
.catch(() => {
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-1');
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-2');
expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'eu-north-1');
expect(memoizedDebounceSpy).toBeCalledTimes(3);
done();
});
});
});
@ -610,10 +628,13 @@ describe('CloudWatchDatasource', () => {
],
};
ctx.ds.query(query).then((result: any) => {
expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion);
done();
});
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion);
done();
});
});
});
@ -676,11 +697,14 @@ describe('CloudWatchDatasource', () => {
});
it('should return series list', done => {
ctx.ds.query(query).then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});
ctx.ds
.query(query)
.toPromise()
.then((result: any) => {
expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});
});
});
@ -787,10 +811,13 @@ describe('CloudWatchDatasource', () => {
],
};
ctx.ds.query(query).then(() => {
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
done();
});
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
done();
});
});
it('should generate the correct query in the case of one multilple template variables', done => {
@ -819,12 +846,15 @@ describe('CloudWatchDatasource', () => {
},
};
ctx.ds.query(query).then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
done();
});
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
done();
});
});
it('should generate the correct query in the case of multilple multi template variables', done => {
@ -849,12 +879,15 @@ describe('CloudWatchDatasource', () => {
],
};
ctx.ds.query(query).then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
expect(requestParams.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']);
done();
});
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
expect(requestParams.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']);
done();
});
});
it('should generate the correct query for multilple template variables, lack scopedVars', done => {
@ -882,12 +915,15 @@ describe('CloudWatchDatasource', () => {
},
};
ctx.ds.query(query).then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
done();
});
ctx.ds
.query(query)
.toPromise()
.then(() => {
expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']);
expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']);
expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']);
done();
});
});
});

View File

@ -3,8 +3,6 @@ import { DataQuery, SelectableValue, DataSourceJsonData } from '@grafana/data';
export interface CloudWatchMetricsQuery extends DataQuery {
queryMode: 'Metrics';
apiMode: 'Logs' | 'Metrics'; // TEMP: Remove when logs/metrics unification is done
id: string;
region: string;
namespace: string;
@ -37,12 +35,10 @@ export enum CloudWatchLogsQueryStatus {
export interface CloudWatchLogsQuery extends DataQuery {
queryMode: 'Logs';
apiMode: 'Logs' | 'Metrics'; // TEMP: Remove when logs/metrics unification is done
id: string;
region: string;
namespace: string;
expression: string;
logGroupNames: string[];
expression?: string;
logGroupNames?: string[];
statsGroups?: string[];
}