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