mirror of https://github.com/grafana/grafana.git
				
				
				
			CloudWatch: Support request cancellation properly (#28865)
This commit is contained in:
		
							parent
							
								
									f9281742d7
								
							
						
					
					
						commit
						32d4c8c6bc
					
				|  | @ -1,55 +1,65 @@ | |||
| import { DataQueryResponse, dateTime, DefaultTimeRange } from '@grafana/data'; | ||||
| import { of } from 'rxjs'; | ||||
| import { setBackendSrv } from '@grafana/runtime'; | ||||
| import { dateTime, DefaultTimeRange, observableTester } from '@grafana/data'; | ||||
| 
 | ||||
| import { TemplateSrv } from '../../../features/templating/template_srv'; | ||||
| import { CloudWatchDatasource } from './datasource'; | ||||
| 
 | ||||
| describe('datasource', () => { | ||||
|   describe('query', () => { | ||||
|     it('should return error if log query and log groups is not specified', async () => { | ||||
|     it('should return error if log query and log groups is not specified', done => { | ||||
|       const { datasource } = setup(); | ||||
|       const response: DataQueryResponse = (await datasource | ||||
|         .query({ | ||||
| 
 | ||||
|       observableTester().subscribeAndExpectOnNext({ | ||||
|         observable: datasource.query({ | ||||
|           targets: [ | ||||
|             { | ||||
|               queryMode: 'Logs' as 'Logs', | ||||
|             }, | ||||
|           ], | ||||
|         } as any) | ||||
|         .toPromise()) as any; | ||||
|         } as any), | ||||
|         expect: response => { | ||||
|           expect(response.error?.message).toBe('Log group is required'); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return empty response if queries are hidden', async () => { | ||||
|     it('should return empty response if queries are hidden', done => { | ||||
|       const { datasource } = setup(); | ||||
|       const response: DataQueryResponse = (await datasource | ||||
|         .query({ | ||||
| 
 | ||||
|       observableTester().subscribeAndExpectOnNext({ | ||||
|         observable: datasource.query({ | ||||
|           targets: [ | ||||
|             { | ||||
|               queryMode: 'Logs' as 'Logs', | ||||
|               hide: true, | ||||
|             }, | ||||
|           ], | ||||
|         } as any) | ||||
|         .toPromise()) as any; | ||||
|         } as any), | ||||
|         expect: response => { | ||||
|           expect(response.data).toEqual([]); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('performTimeSeriesQuery', () => { | ||||
|     it('should return the same length of data as result', async () => { | ||||
|       const { datasource } = setup(); | ||||
|       const awsRequestMock = jest.spyOn(datasource, 'awsRequest'); | ||||
|       const buildCloudwatchConsoleUrlMock = jest.spyOn(datasource, 'buildCloudwatchConsoleUrl'); | ||||
|       buildCloudwatchConsoleUrlMock.mockImplementation(() => ''); | ||||
|       awsRequestMock.mockImplementation(async () => { | ||||
|         return { | ||||
|     it('should return the same length of data as result', done => { | ||||
|       const { datasource } = setup({ | ||||
|         data: { | ||||
|           results: { | ||||
|             a: { refId: 'a', series: [{ name: 'cpu', points: [1, 1] }], meta: { gmdMeta: '' } }, | ||||
|             b: { refId: 'b', series: [{ name: 'memory', points: [2, 2] }], meta: { gmdMeta: '' } }, | ||||
|           }, | ||||
|         }; | ||||
|         }, | ||||
|       }); | ||||
|       const response: DataQueryResponse = await datasource.performTimeSeriesQuery( | ||||
|       const buildCloudwatchConsoleUrlMock = jest.spyOn(datasource, 'buildCloudwatchConsoleUrl'); | ||||
|       buildCloudwatchConsoleUrlMock.mockImplementation(() => ''); | ||||
| 
 | ||||
|       observableTester().subscribeAndExpectOnNext({ | ||||
|         observable: datasource.performTimeSeriesQuery( | ||||
|           { | ||||
|             queries: [ | ||||
|               { datasourceId: 1, refId: 'a' }, | ||||
|  | @ -57,32 +67,35 @@ describe('datasource', () => { | |||
|             ], | ||||
|           } as any, | ||||
|           { from: dateTime(), to: dateTime() } as any | ||||
|       ); | ||||
|         ), | ||||
|         expect: response => { | ||||
|           expect(response.data.length).toEqual(2); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('describeLogGroup', () => { | ||||
|     it('replaces region correctly in the query', async () => { | ||||
|       const { datasource, datasourceRequestMock } = setup(); | ||||
|       const { datasource, fetchMock } = setup(); | ||||
|       await datasource.describeLogGroups({ region: 'default' }); | ||||
|       expect(datasourceRequestMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1'); | ||||
|       expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1'); | ||||
| 
 | ||||
|       await datasource.describeLogGroups({ region: 'eu-east' }); | ||||
|       expect(datasourceRequestMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east'); | ||||
|       expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| function setup() { | ||||
| function setup({ data = [] }: { data?: any } = {}) { | ||||
|   const datasource = new CloudWatchDatasource({ jsonData: { defaultRegion: 'us-west-1' } } as any, new TemplateSrv(), { | ||||
|     timeRange() { | ||||
|       return DefaultTimeRange; | ||||
|     }, | ||||
|   } as any); | ||||
|   const datasourceRequestMock = jest.fn(); | ||||
|   datasourceRequestMock.mockResolvedValue({ data: [] }); | ||||
|   setBackendSrv({ datasourceRequest: datasourceRequestMock } as any); | ||||
|   const fetchMock = jest.fn().mockReturnValue(of({ data })); | ||||
|   setBackendSrv({ fetch: fetchMock } as any); | ||||
| 
 | ||||
|   return { datasource, datasourceRequestMock }; | ||||
|   return { datasource, fetchMock }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import React from 'react'; | ||||
| import angular from 'angular'; | ||||
| import _ from 'lodash'; | ||||
| import { from, merge, Observable, of, zip } from 'rxjs'; | ||||
| import { merge, Observable, of, throwError, zip } from 'rxjs'; | ||||
| import { | ||||
|   catchError, | ||||
|   concatMap, | ||||
|  | @ -184,7 +184,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|       queries: queryParams, | ||||
|     }; | ||||
| 
 | ||||
|     return from(this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams)).pipe( | ||||
|     return this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams).pipe( | ||||
|       mergeMap((response: TSDBResponse) => { | ||||
|         const channelName: string = response.results['A'].meta.channelName; | ||||
|         const channel = getGrafanaLiveSrv().getChannel({ | ||||
|  | @ -310,11 +310,17 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|       queries: validMetricsQueries, | ||||
|     }; | ||||
| 
 | ||||
|     return from(this.performTimeSeriesQuery(request, options.range)); | ||||
|     return this.performTimeSeriesQuery(request, options.range); | ||||
|   }; | ||||
| 
 | ||||
|   logsQuery( | ||||
|     queryParams: Array<{ queryId: string; refId: string; limit?: number; region: string; statsGroups?: string[] }> | ||||
|     queryParams: Array<{ | ||||
|       queryId: string; | ||||
|       refId: string; | ||||
|       limit?: number; | ||||
|       region: string; | ||||
|       statsGroups?: string[]; | ||||
|     }> | ||||
|   ): Observable<DataQueryResponse> { | ||||
|     this.logQueries = {}; | ||||
|     queryParams.forEach(param => { | ||||
|  | @ -587,9 +593,9 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|     )}`;
 | ||||
|   } | ||||
| 
 | ||||
|   async performTimeSeriesQuery(request: MetricRequest, { from, to }: TimeRange): Promise<any> { | ||||
|     try { | ||||
|       const res: TSDBResponse = await this.awsRequest(TSDB_QUERY_ENDPOINT, request); | ||||
|   performTimeSeriesQuery(request: MetricRequest, { from, to }: TimeRange): Observable<any> { | ||||
|     return this.awsRequest(TSDB_QUERY_ENDPOINT, request).pipe( | ||||
|       map(res => { | ||||
|         const dataframes: DataFrame[] = toDataQueryResponse({ data: res }).data; | ||||
|         if (!dataframes || dataframes.length <= 0) { | ||||
|           return { data: [] }; | ||||
|  | @ -634,7 +640,8 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|               return err || error; | ||||
|             }, null), | ||||
|         }; | ||||
|     } catch (err) { | ||||
|       }), | ||||
|       catchError(err => { | ||||
|         if (/^Throttling:.*/.test(err.data.message)) { | ||||
|           const failedRedIds = Object.keys(err.data.results); | ||||
|           const regionsAffected = Object.values(request.queries).reduce( | ||||
|  | @ -650,11 +657,12 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|           err.data.message = err.data.error; | ||||
|         } | ||||
| 
 | ||||
|       throw err; | ||||
|     } | ||||
|         return throwError(err); | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   transformSuggestDataFromTable(suggestData: TSDBResponse) { | ||||
|   transformSuggestDataFromTable(suggestData: TSDBResponse): Array<{ text: any; label: any; value: any }> { | ||||
|     return suggestData.results['metricFindQuery'].tables[0].rows.map(([text, value]) => ({ | ||||
|       text, | ||||
|       value, | ||||
|  | @ -662,7 +670,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   doMetricQueryRequest(subtype: string, parameters: any) { | ||||
|   doMetricQueryRequest(subtype: string, parameters: any): Promise<Array<{ text: any; label: any; value: any }>> { | ||||
|     const range = this.timeSrv.timeRange(); | ||||
|     return this.awsRequest(TSDB_QUERY_ENDPOINT, { | ||||
|       from: range.from.valueOf().toString(), | ||||
|  | @ -678,9 +686,13 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|           ...parameters, | ||||
|         }, | ||||
|       ], | ||||
|     }).then((r: TSDBResponse) => { | ||||
|     }) | ||||
|       .pipe( | ||||
|         map(r => { | ||||
|           return this.transformSuggestDataFromTable(r); | ||||
|     }); | ||||
|         }) | ||||
|       ) | ||||
|       .toPromise(); | ||||
|   } | ||||
| 
 | ||||
|   makeLogActionRequest( | ||||
|  | @ -724,7 +736,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
| 
 | ||||
|     const resultsToDataFrames = (val: any): DataFrame[] => toDataQueryResponse(val).data || []; | ||||
| 
 | ||||
|     return from(this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams)).pipe( | ||||
|     return this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams).pipe( | ||||
|       map(response => resultsToDataFrames({ data: response })), | ||||
|       catchError(err => { | ||||
|         if (err.data?.error) { | ||||
|  | @ -920,7 +932,9 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|           ...parameters, | ||||
|         }, | ||||
|       ], | ||||
|     }).then((r: TSDBResponse) => { | ||||
|     }) | ||||
|       .pipe( | ||||
|         map(r => { | ||||
|           return r.results['annotationQuery'].tables[0].rows.map(v => ({ | ||||
|             annotation: annotation, | ||||
|             time: Date.parse(v[0]), | ||||
|  | @ -928,7 +942,9 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|             tags: [v[2]], | ||||
|             text: v[3], | ||||
|           })); | ||||
|     }); | ||||
|         }) | ||||
|       ) | ||||
|       .toPromise(); | ||||
|   } | ||||
| 
 | ||||
|   targetContainsTemplate(target: any) { | ||||
|  | @ -955,16 +971,16 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa | |||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   async awsRequest(url: string, data: MetricRequest) { | ||||
|   awsRequest(url: string, data: MetricRequest): Observable<TSDBResponse> { | ||||
|     const options = { | ||||
|       method: 'POST', | ||||
|       url, | ||||
|       data, | ||||
|     }; | ||||
| 
 | ||||
|     const result = await getBackendSrv().datasourceRequest(options); | ||||
| 
 | ||||
|     return result.data; | ||||
|     return getBackendSrv() | ||||
|       .fetch<TSDBResponse>(options) | ||||
|       .pipe(map(result => result.data)); | ||||
|   } | ||||
| 
 | ||||
|   getDefaultRegion() { | ||||
|  |  | |||
|  | @ -3,22 +3,22 @@ import _ from 'lodash'; | |||
| 
 | ||||
| // Services & Utils
 | ||||
| import syntax, { | ||||
|   QUERY_COMMANDS, | ||||
|   AGGREGATION_FUNCTIONS_STATS, | ||||
|   STRING_FUNCTIONS, | ||||
|   DATETIME_FUNCTIONS, | ||||
|   IP_FUNCTIONS, | ||||
|   BOOLEAN_FUNCTIONS, | ||||
|   NUMERIC_OPERATORS, | ||||
|   DATETIME_FUNCTIONS, | ||||
|   FIELD_AND_FILTER_FUNCTIONS, | ||||
|   IP_FUNCTIONS, | ||||
|   NUMERIC_OPERATORS, | ||||
|   QUERY_COMMANDS, | ||||
|   STRING_FUNCTIONS, | ||||
| } from './syntax'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { CloudWatchQuery } from './types'; | ||||
| import { AbsoluteTimeRange, LanguageProvider, HistoryItem } from '@grafana/data'; | ||||
| import { CloudWatchQuery, TSDBResponse } from './types'; | ||||
| import { AbsoluteTimeRange, HistoryItem, LanguageProvider } from '@grafana/data'; | ||||
| 
 | ||||
| import { CloudWatchDatasource } from './datasource'; | ||||
| import { TypeaheadInput, TypeaheadOutput, Token } from '@grafana/ui'; | ||||
| import { Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui'; | ||||
| import Prism, { Grammar } from 'prismjs'; | ||||
| 
 | ||||
| export type CloudWatchHistoryItem = HistoryItem<CloudWatchQuery>; | ||||
|  | @ -49,8 +49,8 @@ export class CloudWatchLanguageProvider extends LanguageProvider { | |||
|     return syntax; | ||||
|   } | ||||
| 
 | ||||
|   request = (url: string, params?: any): Promise<{ data: { data: string[] } }> => { | ||||
|     return this.datasource.awsRequest(url, params); | ||||
|   request = (url: string, params?: any): Promise<TSDBResponse> => { | ||||
|     return this.datasource.awsRequest(url, params).toPromise(); | ||||
|   }; | ||||
| 
 | ||||
|   start = () => { | ||||
|  |  | |||
|  | @ -1,6 +1,4 @@ | |||
| import '../datasource'; | ||||
| import { CloudWatchDatasource, MAX_ATTEMPTS } from '../datasource'; | ||||
| import * as redux from 'app/store/store'; | ||||
| import { interval, of, throwError } from 'rxjs'; | ||||
| import { | ||||
|   DataFrame, | ||||
|   DataQueryErrorType, | ||||
|  | @ -8,22 +6,20 @@ import { | |||
|   DataSourceInstanceSettings, | ||||
|   dateMath, | ||||
|   getFrameDisplayName, | ||||
|   observableTester, | ||||
| } from '@grafana/data'; | ||||
| import { FetchResponse } from '@grafana/runtime'; | ||||
| 
 | ||||
| import * as redux from 'app/store/store'; | ||||
| import '../datasource'; | ||||
| import { CloudWatchDatasource, MAX_ATTEMPTS } from '../datasource'; | ||||
| import { TemplateSrv } from 'app/features/templating/template_srv'; | ||||
| import { | ||||
|   CloudWatchLogsQuery, | ||||
|   CloudWatchLogsQueryStatus, | ||||
|   CloudWatchMetricsQuery, | ||||
|   CloudWatchQuery, | ||||
|   LogAction, | ||||
| } from '../types'; | ||||
| import { CloudWatchLogsQuery, CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, LogAction } from '../types'; | ||||
| import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
 | ||||
| import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; | ||||
| import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState'; | ||||
| import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies'; | ||||
| import { interval, of } from 'rxjs'; | ||||
| import { CustomVariableModel, initialVariableModelState, VariableHide } from '../../../../features/variables/types'; | ||||
| import { TimeSrvStub } from '../../../../../test/specs/helpers'; | ||||
| 
 | ||||
| import * as rxjsUtils from '../utils/rxjs/increasingInterval'; | ||||
| 
 | ||||
|  | @ -38,18 +34,22 @@ jest.mock('@grafana/runtime', () => ({ | |||
|   getBackendSrv: () => backendSrv, | ||||
| })); | ||||
| 
 | ||||
| describe('CloudWatchDatasource', () => { | ||||
|   const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); | ||||
| type Args = { response?: any; throws?: boolean; templateSrv?: TemplateSrv }; | ||||
| 
 | ||||
| function getTestContext({ response = {}, throws = false, templateSrv = new TemplateSrv() }: Args = {}) { | ||||
|   jest.clearAllMocks(); | ||||
| 
 | ||||
|   const fetchMock = jest.spyOn(backendSrv, 'fetch'); | ||||
| 
 | ||||
|   throws | ||||
|     ? fetchMock.mockImplementation(() => throwError(response)) | ||||
|     : fetchMock.mockImplementation(() => of(createFetchResponse(response))); | ||||
| 
 | ||||
|   const instanceSettings = { | ||||
|     jsonData: { defaultRegion: 'us-east-1' }, | ||||
|     name: 'TestDatasource', | ||||
|   } as DataSourceInstanceSettings; | ||||
| 
 | ||||
|   let templateSrv = new TemplateSrv(); | ||||
|   const start = 1483196400 * 1000; | ||||
|   const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) }; | ||||
| 
 | ||||
|   const timeSrv = { | ||||
|     time: { from: '2016-12-31 15:00:00Z', to: '2016-12-31 16:00:00Z' }, | ||||
|     timeRange: () => { | ||||
|  | @ -60,20 +60,22 @@ describe('CloudWatchDatasource', () => { | |||
|     }, | ||||
|   } as TimeSrv; | ||||
| 
 | ||||
|   const ctx = { | ||||
|     templateSrv, | ||||
|   } as any; | ||||
|   const ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); | ||||
| 
 | ||||
|   return { ds, fetchMock, instanceSettings }; | ||||
| } | ||||
| 
 | ||||
| describe('CloudWatchDatasource', () => { | ||||
|   const start = 1483196400 * 1000; | ||||
|   const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) }; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); | ||||
|     jest.clearAllMocks(); | ||||
|   }); | ||||
| 
 | ||||
|   describe('When getting log groups', () => { | ||||
|     beforeEach(() => { | ||||
|       datasourceRequestMock.mockImplementation(() => | ||||
|         Promise.resolve({ | ||||
|           data: { | ||||
|     it('should return log groups as an array of strings', async () => { | ||||
|       const response = { | ||||
|         results: { | ||||
|           A: { | ||||
|             dataframes: [ | ||||
|  | @ -82,13 +84,8 @@ describe('CloudWatchDatasource', () => { | |||
|             refId: 'A', | ||||
|           }, | ||||
|         }, | ||||
|           }, | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return log groups as an array of strings', async () => { | ||||
|       const logGroups = await ctx.ds.describeLogGroups(); | ||||
|       }; | ||||
|       const { ds } = getTestContext({ response }); | ||||
|       const expectedLogGroups = [ | ||||
|         '/aws/containerinsights/dev303-workshop/application', | ||||
|         '/aws/containerinsights/dev303-workshop/dataplane', | ||||
|  | @ -124,6 +121,9 @@ describe('CloudWatchDatasource', () => { | |||
|         'container-insights-prometheus-beta', | ||||
|         'container-insights-prometheus-demo', | ||||
|       ]; | ||||
| 
 | ||||
|       const logGroups = await ds.describeLogGroups({}); | ||||
| 
 | ||||
|       expect(logGroups).toEqual(expectedLogGroups); | ||||
|     }); | ||||
|   }); | ||||
|  | @ -134,6 +134,7 @@ describe('CloudWatchDatasource', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should add data links to response', () => { | ||||
|       const { ds } = getTestContext(); | ||||
|       const mockResponse: DataQueryResponse = { | ||||
|         data: [ | ||||
|           { | ||||
|  | @ -149,7 +150,7 @@ describe('CloudWatchDatasource', () => { | |||
|         ], | ||||
|       }; | ||||
| 
 | ||||
|       const mockOptions = { | ||||
|       const mockOptions: any = { | ||||
|         targets: [ | ||||
|           { | ||||
|             refId: 'A', | ||||
|  | @ -160,7 +161,7 @@ describe('CloudWatchDatasource', () => { | |||
|         ], | ||||
|       }; | ||||
| 
 | ||||
|       const saturatedResponse = ctx.ds.addDataLinksToLogsResponse(mockResponse, mockOptions); | ||||
|       const saturatedResponse = ds['addDataLinksToLogsResponse'](mockResponse, mockOptions); | ||||
|       expect(saturatedResponse).toMatchObject({ | ||||
|         data: [ | ||||
|           { | ||||
|  | @ -185,6 +186,7 @@ describe('CloudWatchDatasource', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should stop querying when no more data received a number of times in a row', async () => { | ||||
|       const { ds } = getTestContext(); | ||||
|       const fakeFrames = genMockFrames(20); | ||||
|       const initialRecordsMatched = fakeFrames[0].meta!.stats!.find(stat => stat.displayName === 'Records scanned')! | ||||
|         .value!; | ||||
|  | @ -209,7 +211,7 @@ describe('CloudWatchDatasource', () => { | |||
|       } | ||||
| 
 | ||||
|       let i = 0; | ||||
|       jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { | ||||
|       jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { | ||||
|         if (subtype === 'GetQueryResults') { | ||||
|           const mockObservable = of([fakeFrames[i]]); | ||||
|           i++; | ||||
|  | @ -219,7 +221,7 @@ describe('CloudWatchDatasource', () => { | |||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise(); | ||||
|       const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise(); | ||||
| 
 | ||||
|       const expectedData = [ | ||||
|         { | ||||
|  | @ -246,10 +248,11 @@ describe('CloudWatchDatasource', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should continue querying as long as new data is being received', async () => { | ||||
|       const { ds } = getTestContext(); | ||||
|       const fakeFrames = genMockFrames(15); | ||||
| 
 | ||||
|       let i = 0; | ||||
|       jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { | ||||
|       jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { | ||||
|         if (subtype === 'GetQueryResults') { | ||||
|           const mockObservable = of([fakeFrames[i]]); | ||||
|           i++; | ||||
|  | @ -259,7 +262,7 @@ describe('CloudWatchDatasource', () => { | |||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise(); | ||||
|       const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise(); | ||||
|       expect(myResponse).toEqual({ | ||||
|         data: [fakeFrames[fakeFrames.length - 1]], | ||||
|         key: 'test-key', | ||||
|  | @ -269,9 +272,10 @@ describe('CloudWatchDatasource', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should stop querying when results come back with status "Complete"', async () => { | ||||
|       const { ds } = getTestContext(); | ||||
|       const fakeFrames = genMockFrames(3); | ||||
|       let i = 0; | ||||
|       jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { | ||||
|       jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { | ||||
|         if (subtype === 'GetQueryResults') { | ||||
|           const mockObservable = of([fakeFrames[i]]); | ||||
|           i++; | ||||
|  | @ -281,7 +285,7 @@ describe('CloudWatchDatasource', () => { | |||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise(); | ||||
|       const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise(); | ||||
| 
 | ||||
|       expect(myResponse).toEqual({ | ||||
|         data: [fakeFrames[2]], | ||||
|  | @ -292,8 +296,9 @@ describe('CloudWatchDatasource', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should call the replace method on provided log groups', () => { | ||||
|       const replaceSpy = jest.spyOn(ctx.ds, 'replace').mockImplementation((target: string) => target); | ||||
|       ctx.ds.makeLogActionRequest('StartQuery', [ | ||||
|       const { ds } = getTestContext(); | ||||
|       const replaceSpy = jest.spyOn(ds, 'replace').mockImplementation((target: string) => target); | ||||
|       ds.makeLogActionRequest('StartQuery', [ | ||||
|         { | ||||
|           queryString: 'test query string', | ||||
|           region: 'default', | ||||
|  | @ -308,7 +313,7 @@ describe('CloudWatchDatasource', () => { | |||
|   }); | ||||
| 
 | ||||
|   describe('When performing CloudWatch metrics query', () => { | ||||
|     const query = { | ||||
|     const query: any = { | ||||
|       range: defaultTimeRange, | ||||
|       rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|       targets: [ | ||||
|  | @ -353,15 +358,13 @@ describe('CloudWatchDatasource', () => { | |||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       datasourceRequestMock.mockImplementation(() => { | ||||
|         return Promise.resolve({ data: response }); | ||||
|       }); | ||||
|     }); | ||||
|     it('should generate the correct query', done => { | ||||
|       const { ds, fetchMock } = getTestContext({ response }); | ||||
| 
 | ||||
|     it('should generate the correct query', async () => { | ||||
|       await ctx.ds.query(query).toPromise(); | ||||
|       expect(datasourceRequestMock.mock.calls[0][0].data.queries).toMatchObject( | ||||
|       observableTester().subscribeAndExpectOnComplete({ | ||||
|         observable: ds.query(query), | ||||
|         expect: () => { | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries).toMatchObject( | ||||
|             expect.arrayContaining([ | ||||
|               expect.objectContaining({ | ||||
|                 namespace: query.targets[0].namespace, | ||||
|  | @ -372,9 +375,12 @@ describe('CloudWatchDatasource', () => { | |||
|               }), | ||||
|             ]) | ||||
|           ); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should generate the correct query with interval variable', async () => { | ||||
|     it('should generate the correct query with interval variable', done => { | ||||
|       const period: CustomVariableModel = { | ||||
|         ...initialVariableModelState, | ||||
|         id: 'period', | ||||
|  | @ -388,9 +394,10 @@ describe('CloudWatchDatasource', () => { | |||
|         hide: VariableHide.dontHide, | ||||
|         type: 'custom', | ||||
|       }; | ||||
|       const templateSrv = new TemplateSrv(); | ||||
|       templateSrv.init([period]); | ||||
| 
 | ||||
|       const query = { | ||||
|       const query: any = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|         targets: [ | ||||
|  | @ -409,11 +416,19 @@ describe('CloudWatchDatasource', () => { | |||
|         ], | ||||
|       }; | ||||
| 
 | ||||
|       await ctx.ds.query(query).toPromise(); | ||||
|       expect(datasourceRequestMock.mock.calls[0][0].data.queries[0].period).toEqual('600'); | ||||
|       const { ds, fetchMock } = getTestContext({ response, templateSrv }); | ||||
| 
 | ||||
|       observableTester().subscribeAndExpectOnComplete({ | ||||
|         observable: ds.query(query), | ||||
|         expect: () => { | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].period).toEqual('600'); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it.each(['pNN.NN', 'p9', 'p99.', 'p99.999'])('should cancel query for invalid extended statistics (%s)', stat => { | ||||
|       const { ds } = getTestContext({ response }); | ||||
|       const query = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|  | @ -432,84 +447,93 @@ describe('CloudWatchDatasource', () => { | |||
|           }, | ||||
|         ], | ||||
|       }; | ||||
|       expect(ctx.ds.query.bind(ctx.ds, query)).toThrow(/Invalid extended statistics/); | ||||
| 
 | ||||
|       expect(ds.query.bind(ds, query)).toThrow(/Invalid extended statistics/); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return series list', done => { | ||||
|       ctx.ds | ||||
|         .query(query) | ||||
|         .toPromise() | ||||
|         .then((result: any) => { | ||||
|       const { ds } = getTestContext({ response }); | ||||
| 
 | ||||
|       observableTester().subscribeAndExpectOnNext({ | ||||
|         observable: ds.query(query), | ||||
|         expect: result => { | ||||
|           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(); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('a correct cloudwatch url should be built for each time series in the response', () => { | ||||
|       beforeEach(() => { | ||||
|         datasourceRequestMock.mockImplementation(() => { | ||||
|           return Promise.resolve({ data: response }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('should be built correctly if theres one search expressions returned in meta for a given query row', done => { | ||||
|         const { ds } = getTestContext({ response }); | ||||
| 
 | ||||
|         response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }]; | ||||
|         ctx.ds | ||||
|           .query(query) | ||||
|           .toPromise() | ||||
|           .then((result: any) => { | ||||
| 
 | ||||
|         observableTester().subscribeAndExpectOnNext({ | ||||
|           observable: ds.query(query), | ||||
|           expect: result => { | ||||
|             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(); | ||||
|           }, | ||||
|           done, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('should be built correctly if theres two search expressions returned in meta for a given query row', done => { | ||||
|         const { ds } = getTestContext({ response }); | ||||
| 
 | ||||
|         response.results['A'].meta.gmdMeta = [ | ||||
|           { Expression: `REMOVE_EMPTY(SEARCH('first expression'))` }, | ||||
|           { Expression: `REMOVE_EMPTY(SEARCH('second expression'))` }, | ||||
|         ]; | ||||
|         ctx.ds | ||||
|           .query(query) | ||||
|           .toPromise() | ||||
|           .then((result: any) => { | ||||
| 
 | ||||
|         observableTester().subscribeAndExpectOnNext({ | ||||
|           observable: ds.query(query), | ||||
|           expect: result => { | ||||
|             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(); | ||||
|           }, | ||||
|           done, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('should be built correctly if the query is a metric stat query', done => { | ||||
|         const { ds } = getTestContext({ response }); | ||||
| 
 | ||||
|         response.results['A'].meta.gmdMeta = [{ Period: '300' }]; | ||||
|         ctx.ds | ||||
|           .query(query) | ||||
|           .toPromise() | ||||
|           .then((result: any) => { | ||||
| 
 | ||||
|         observableTester().subscribeAndExpectOnNext({ | ||||
|           observable: ds.query(query), | ||||
|           expect: result => { | ||||
|             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(); | ||||
|           }, | ||||
|           done, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('should not be added at all if query is a math expression', done => { | ||||
|         const { ds } = getTestContext({ response }); | ||||
| 
 | ||||
|         query.targets[0].expression = 'a * 2'; | ||||
|         response.results['A'].meta.searchExpressions = []; | ||||
|         ctx.ds | ||||
|           .query(query) | ||||
|           .toPromise() | ||||
|           .then((result: any) => { | ||||
| 
 | ||||
|         observableTester().subscribeAndExpectOnNext({ | ||||
|           observable: ds.query(query), | ||||
|           expect: result => { | ||||
|             expect(result.data[0].fields[1].config.links).toBeUndefined(); | ||||
|             done(); | ||||
|           }, | ||||
|           done, | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|  | @ -527,7 +551,7 @@ describe('CloudWatchDatasource', () => { | |||
|         expression: '', | ||||
|       }; | ||||
| 
 | ||||
|       const query = { | ||||
|       const query: any = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|         targets: [ | ||||
|  | @ -576,56 +600,51 @@ describe('CloudWatchDatasource', () => { | |||
|         redux.setStore({ | ||||
|           dispatch: jest.fn(), | ||||
|         } as any); | ||||
| 
 | ||||
|         datasourceRequestMock.mockImplementation(() => { | ||||
|           return Promise.reject(backendErrorResponse); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('should display one alert error message per region+datasource combination', done => { | ||||
|         const memoizedDebounceSpy = jest.spyOn(ctx.ds, 'debouncedAlert'); | ||||
|         ctx.ds | ||||
|           .query(query) | ||||
|           .toPromise() | ||||
|           .catch(() => { | ||||
|         const { ds } = getTestContext({ response: backendErrorResponse, throws: true }); | ||||
|         const memoizedDebounceSpy = jest.spyOn(ds, 'debouncedAlert'); | ||||
| 
 | ||||
|         observableTester().subscribeAndExpectOnError({ | ||||
|           observable: ds.query(query), | ||||
|           expect: err => { | ||||
|             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(); | ||||
|           }, | ||||
|           done, | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('when regions query is used', () => { | ||||
|       beforeEach(() => { | ||||
|         datasourceRequestMock.mockImplementation(() => { | ||||
|           return Promise.resolve({}); | ||||
|         }); | ||||
|         ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); | ||||
|         ctx.ds.doMetricQueryRequest = jest.fn(() => []); | ||||
|       }); | ||||
|       describe('and region param is left out', () => { | ||||
|         it('should use the default region', done => { | ||||
|           ctx.ds.metricFindQuery('metrics(testNamespace)').then(() => { | ||||
|             expect(ctx.ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { | ||||
|         it('should use the default region', async () => { | ||||
|           const { ds, instanceSettings } = getTestContext(); | ||||
|           ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]); | ||||
| 
 | ||||
|           await ds.metricFindQuery('metrics(testNamespace)'); | ||||
| 
 | ||||
|           expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { | ||||
|             namespace: 'testNamespace', | ||||
|             region: instanceSettings.jsonData.defaultRegion, | ||||
|           }); | ||||
|             done(); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('and region param is defined by user', () => { | ||||
|         it('should use the user defined region', done => { | ||||
|           ctx.ds.metricFindQuery('metrics(testNamespace2, custom-region)').then(() => { | ||||
|             expect(ctx.ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { | ||||
|         it('should use the user defined region', async () => { | ||||
|           const { ds } = getTestContext(); | ||||
|           ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]); | ||||
| 
 | ||||
|           await ds.metricFindQuery('metrics(testNamespace2, custom-region)'); | ||||
| 
 | ||||
|           expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { | ||||
|             namespace: 'testNamespace2', | ||||
|             region: 'custom-region', | ||||
|           }); | ||||
|             done(); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|  | @ -633,27 +652,25 @@ describe('CloudWatchDatasource', () => { | |||
| 
 | ||||
|   describe('When query region is "default"', () => { | ||||
|     it('should return the datasource region if empty or "default"', () => { | ||||
|       const { ds, instanceSettings } = getTestContext(); | ||||
|       const defaultRegion = instanceSettings.jsonData.defaultRegion; | ||||
| 
 | ||||
|       expect(ctx.ds.getActualRegion()).toBe(defaultRegion); | ||||
|       expect(ctx.ds.getActualRegion('')).toBe(defaultRegion); | ||||
|       expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion); | ||||
|       expect(ds.getActualRegion()).toBe(defaultRegion); | ||||
|       expect(ds.getActualRegion('')).toBe(defaultRegion); | ||||
|       expect(ds.getActualRegion('default')).toBe(defaultRegion); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return the specified region if specified', () => { | ||||
|       expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); | ||||
|     }); | ||||
|       const { ds } = getTestContext(); | ||||
| 
 | ||||
|     let requestParams: { queries: CloudWatchQuery[] }; | ||||
|     beforeEach(() => { | ||||
|       ctx.ds.performTimeSeriesQuery = jest.fn(request => { | ||||
|         requestParams = request; | ||||
|         return Promise.resolve({ data: {} }); | ||||
|       }); | ||||
|       expect(ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should query for the datasource region if empty or "default"', done => { | ||||
|       const query = { | ||||
|       const { ds, instanceSettings } = getTestContext(); | ||||
|       const performTimeSeriesQueryMock = jest.spyOn(ds, 'performTimeSeriesQuery').mockReturnValue(of({})); | ||||
| 
 | ||||
|       const query: any = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|         targets: [ | ||||
|  | @ -672,36 +689,29 @@ describe('CloudWatchDatasource', () => { | |||
|         ], | ||||
|       }; | ||||
| 
 | ||||
|       ctx.ds | ||||
|         .query(query) | ||||
|         .toPromise() | ||||
|         .then((result: any) => { | ||||
|           expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion); | ||||
|           done(); | ||||
|       observableTester().subscribeAndExpectOnComplete({ | ||||
|         observable: ds.query(query), | ||||
|         expect: () => { | ||||
|           expect(performTimeSeriesQueryMock.mock.calls[0][0].queries[0].region).toBe( | ||||
|             instanceSettings.jsonData.defaultRegion | ||||
|           ); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('When interpolating variables', () => { | ||||
|     beforeEach(() => { | ||||
|       jest.clearAllMocks(); | ||||
| 
 | ||||
|       ctx.mockedTemplateSrv = { | ||||
|         replace: jest.fn(), | ||||
|       }; | ||||
| 
 | ||||
|       ctx.ds = new CloudWatchDatasource( | ||||
|         instanceSettings, | ||||
|         ctx.mockedTemplateSrv, | ||||
|         (new TimeSrvStub() as unknown) as TimeSrv | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return an empty array if no queries are provided', () => { | ||||
|       expect(ctx.ds.interpolateVariablesInQueries([], {})).toHaveLength(0); | ||||
|       const templateSrv: any = { replace: jest.fn() }; | ||||
|       const { ds } = getTestContext({ templateSrv }); | ||||
| 
 | ||||
|       expect(ds.interpolateVariablesInQueries([], {})).toHaveLength(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('should replace correct variables in CloudWatchLogsQuery', () => { | ||||
|       const templateSrv: any = { replace: jest.fn() }; | ||||
|       const { ds } = getTestContext({ templateSrv }); | ||||
|       const variableName = 'someVar'; | ||||
|       const logQuery: CloudWatchLogsQuery = { | ||||
|         id: 'someId', | ||||
|  | @ -711,14 +721,16 @@ describe('CloudWatchDatasource', () => { | |||
|         region: `$${variableName}`, | ||||
|       }; | ||||
| 
 | ||||
|       ctx.ds.interpolateVariablesInQueries([logQuery], {}); | ||||
|       ds.interpolateVariablesInQueries([logQuery], {}); | ||||
| 
 | ||||
|       // We interpolate `expression` and `region` in CloudWatchLogsQuery
 | ||||
|       expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); | ||||
|       expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(2); | ||||
|       expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); | ||||
|       expect(templateSrv.replace).toHaveBeenCalledTimes(2); | ||||
|     }); | ||||
| 
 | ||||
|     it('should replace correct variables in CloudWatchMetricsQuery', () => { | ||||
|       const templateSrv: any = { replace: jest.fn() }; | ||||
|       const { ds } = getTestContext({ templateSrv }); | ||||
|       const variableName = 'someVar'; | ||||
|       const logQuery: CloudWatchMetricsQuery = { | ||||
|         id: 'someId', | ||||
|  | @ -737,16 +749,16 @@ describe('CloudWatchDatasource', () => { | |||
|         statistics: [], | ||||
|       }; | ||||
| 
 | ||||
|       ctx.ds.interpolateVariablesInQueries([logQuery], {}); | ||||
|       ds.interpolateVariablesInQueries([logQuery], {}); | ||||
| 
 | ||||
|       // We interpolate `expression`, `region`, `period`, `alias`, `metricName`, `nameSpace` and `dimensions` in CloudWatchMetricsQuery
 | ||||
|       expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); | ||||
|       expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(8); | ||||
|       expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); | ||||
|       expect(templateSrv.replace).toHaveBeenCalledTimes(8); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('When performing CloudWatch query for extended statistics', () => { | ||||
|     const query = { | ||||
|     const query: any = { | ||||
|       range: defaultTimeRange, | ||||
|       rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|       targets: [ | ||||
|  | @ -797,26 +809,22 @@ describe('CloudWatchDatasource', () => { | |||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       datasourceRequestMock.mockImplementation(params => { | ||||
|         return Promise.resolve({ data: response }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return series list', done => { | ||||
|       ctx.ds | ||||
|         .query(query) | ||||
|         .toPromise() | ||||
|         .then((result: any) => { | ||||
|       const { ds } = getTestContext({ response }); | ||||
| 
 | ||||
|       observableTester().subscribeAndExpectOnNext({ | ||||
|         observable: ds.query(query), | ||||
|         expect: result => { | ||||
|           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(); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('When performing CloudWatch query with template variables', () => { | ||||
|     let requestParams: { queries: CloudWatchMetricsQuery[] }; | ||||
|     let templateSrv: TemplateSrv; | ||||
|     beforeEach(() => { | ||||
|       const var1: CustomVariableModel = { | ||||
|         ...initialVariableModelState, | ||||
|  | @ -880,18 +888,13 @@ describe('CloudWatchDatasource', () => { | |||
|       }; | ||||
|       const variables = [var1, var2, var3, var4]; | ||||
|       const state = convertToStoreState(variables); | ||||
|       const _templateSrv = new TemplateSrv(getTemplateSrvDependencies(state)); | ||||
|       _templateSrv.init(variables); | ||||
|       ctx.ds = new CloudWatchDatasource(instanceSettings, _templateSrv, timeSrv); | ||||
| 
 | ||||
|       datasourceRequestMock.mockImplementation(params => { | ||||
|         requestParams = params.data; | ||||
|         return Promise.resolve({ data: {} }); | ||||
|       }); | ||||
|       templateSrv = new TemplateSrv(getTemplateSrvDependencies(state)); | ||||
|       templateSrv.init(variables); | ||||
|     }); | ||||
| 
 | ||||
|     it('should generate the correct query for single template variable', done => { | ||||
|       const query = { | ||||
|       const { ds, fetchMock } = getTestContext({ templateSrv }); | ||||
|       const query: any = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|         targets: [ | ||||
|  | @ -910,17 +913,18 @@ describe('CloudWatchDatasource', () => { | |||
|         ], | ||||
|       }; | ||||
| 
 | ||||
|       ctx.ds | ||||
|         .query(query) | ||||
|         .toPromise() | ||||
|         .then(() => { | ||||
|           expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); | ||||
|           done(); | ||||
|       observableTester().subscribeAndExpectOnComplete({ | ||||
|         observable: ds.query(query), | ||||
|         expect: () => { | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should generate the correct query in the case of one multilple template variables', done => { | ||||
|       const query = { | ||||
|       const { ds, fetchMock } = getTestContext({ templateSrv }); | ||||
|       const query: any = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|         targets: [ | ||||
|  | @ -945,19 +949,20 @@ describe('CloudWatchDatasource', () => { | |||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       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(); | ||||
|       observableTester().subscribeAndExpectOnComplete({ | ||||
|         observable: ds.query(query), | ||||
|         expect: () => { | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); | ||||
|           expect(fetchMock.mock.calls[0][0].data.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 => { | ||||
|       const query = { | ||||
|       const { ds, fetchMock } = getTestContext({ templateSrv }); | ||||
|       const query: any = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|         targets: [ | ||||
|  | @ -978,19 +983,20 @@ describe('CloudWatchDatasource', () => { | |||
|         ], | ||||
|       }; | ||||
| 
 | ||||
|       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(); | ||||
|       observableTester().subscribeAndExpectOnComplete({ | ||||
|         observable: ds.query(query), | ||||
|         expect: () => { | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should generate the correct query for multilple template variables, lack scopedVars', done => { | ||||
|       const query = { | ||||
|       const { ds, fetchMock } = getTestContext({ templateSrv }); | ||||
|       const query: any = { | ||||
|         range: defaultTimeRange, | ||||
|         rangeRaw: { from: 1483228800, to: 1483232400 }, | ||||
|         targets: [ | ||||
|  | @ -1014,14 +1020,14 @@ describe('CloudWatchDatasource', () => { | |||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       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(); | ||||
|       observableTester().subscribeAndExpectOnComplete({ | ||||
|         observable: ds.query(query), | ||||
|         expect: () => { | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); | ||||
|           expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); | ||||
|         }, | ||||
|         done, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | @ -1032,11 +1038,8 @@ describe('CloudWatchDatasource', () => { | |||
|       scenario.setup = async (setupCallback: any) => { | ||||
|         beforeEach(async () => { | ||||
|           await setupCallback(); | ||||
|           datasourceRequestMock.mockImplementation(args => { | ||||
|             scenario.request = args.data; | ||||
|             return Promise.resolve({ data: scenario.requestResponse }); | ||||
|           }); | ||||
|           ctx.ds.metricFindQuery(query).then((args: any) => { | ||||
|           const { ds } = getTestContext({ response: scenario.requestResponse }); | ||||
|           ds.metricFindQuery(query).then((args: any) => { | ||||
|             scenario.result = args; | ||||
|           }); | ||||
|         }); | ||||
|  | @ -1210,3 +1213,17 @@ function genMockFrames(numResponses: number): DataFrame[] { | |||
| 
 | ||||
|   return mockFrames; | ||||
| } | ||||
| 
 | ||||
| function createFetchResponse<T>(data: T): FetchResponse<T> { | ||||
|   return { | ||||
|     data, | ||||
|     status: 200, | ||||
|     url: 'http://localhost:3000/api/query', | ||||
|     config: { url: 'http://localhost:3000/api/query' }, | ||||
|     type: 'basic', | ||||
|     statusText: 'Ok', | ||||
|     redirected: false, | ||||
|     headers: ({} as unknown) as Headers, | ||||
|     ok: true, | ||||
|   }; | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue