diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts index 2079094ad8c..719bbc97166 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts @@ -10,7 +10,9 @@ import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchData export function setupMockedAPI({ variables, response, + getMock, }: { + getMock?: jest.Func; response?: Array<{ text: string; label: string; value: string }>; variables?: CustomVariableModel[]; mockGetVariableName?: boolean; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index fe3fbe1f94b..4cf7c5ad931 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -61,7 +61,9 @@ export const CloudWatchSettings: DataSourceInstanceSettings export function setupMockedDataSource({ variables, mockGetVariableName = true, + getMock = jest.fn(), }: { + getMock?: jest.Func; variables?: CustomVariableModel[]; mockGetVariableName?: boolean; } = {}) { @@ -84,7 +86,7 @@ export function setupMockedDataSource({ setBackendSrv({ ...getBackendSrv(), fetch: fetchMock, - get: jest.fn(), + get: getMock, }); return { datasource, fetchMock, templateService, timeSrv }; diff --git a/public/app/plugins/datasource/cloudwatch/api.test.ts b/public/app/plugins/datasource/cloudwatch/api.test.ts index 146f11a3a55..479100a2e68 100644 --- a/public/app/plugins/datasource/cloudwatch/api.test.ts +++ b/public/app/plugins/datasource/cloudwatch/api.test.ts @@ -1,55 +1,72 @@ import { setupMockedAPI } from './__mocks__/API'; -describe('describeLogGroup', () => { - it('replaces region correctly in the query', async () => { - const { api, resourceRequestMock } = setupMockedAPI(); - await api.describeLogGroups({ region: 'default' }); - expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1'); +describe('api', () => { + describe('describeLogGroup', () => { + it('replaces region correctly in the query', async () => { + const { api, resourceRequestMock } = setupMockedAPI(); + await api.describeLogGroups({ region: 'default' }); + expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1'); - await api.describeLogGroups({ region: 'eu-east' }); - expect(resourceRequestMock.mock.calls[1][1].region).toBe('eu-east'); + await api.describeLogGroups({ region: 'eu-east' }); + expect(resourceRequestMock.mock.calls[1][1].region).toBe('eu-east'); + }); + + it('should return log groups as an array of options', async () => { + const response = [ + { + text: '/aws/containerinsights/dev303-workshop/application', + value: '/aws/containerinsights/dev303-workshop/application', + label: '/aws/containerinsights/dev303-workshop/application', + }, + { + text: '/aws/containerinsights/dev303-workshop/flowlogs', + value: '/aws/containerinsights/dev303-workshop/flowlogs', + label: '/aws/containerinsights/dev303-workshop/flowlogs', + }, + { + text: '/aws/containerinsights/dev303-workshop/dataplane', + value: '/aws/containerinsights/dev303-workshop/dataplane', + label: '/aws/containerinsights/dev303-workshop/dataplane', + }, + ]; + + const { api } = setupMockedAPI({ response }); + const expectedLogGroups = [ + { + text: '/aws/containerinsights/dev303-workshop/application', + value: '/aws/containerinsights/dev303-workshop/application', + label: '/aws/containerinsights/dev303-workshop/application', + }, + { + text: '/aws/containerinsights/dev303-workshop/flowlogs', + value: '/aws/containerinsights/dev303-workshop/flowlogs', + label: '/aws/containerinsights/dev303-workshop/flowlogs', + }, + { + text: '/aws/containerinsights/dev303-workshop/dataplane', + value: '/aws/containerinsights/dev303-workshop/dataplane', + label: '/aws/containerinsights/dev303-workshop/dataplane', + }, + ]; + + const logGroups = await api.describeLogGroups({ region: 'default' }); + + expect(logGroups).toEqual(expectedLogGroups); + }); }); - it('should return log groups as an array of options', async () => { - const response = [ - { - text: '/aws/containerinsights/dev303-workshop/application', - value: '/aws/containerinsights/dev303-workshop/application', - label: '/aws/containerinsights/dev303-workshop/application', - }, - { - text: '/aws/containerinsights/dev303-workshop/flowlogs', - value: '/aws/containerinsights/dev303-workshop/flowlogs', - label: '/aws/containerinsights/dev303-workshop/flowlogs', - }, - { - text: '/aws/containerinsights/dev303-workshop/dataplane', - value: '/aws/containerinsights/dev303-workshop/dataplane', - label: '/aws/containerinsights/dev303-workshop/dataplane', - }, - ]; - - const { api } = setupMockedAPI({ response }); - const expectedLogGroups = [ - { - text: '/aws/containerinsights/dev303-workshop/application', - value: '/aws/containerinsights/dev303-workshop/application', - label: '/aws/containerinsights/dev303-workshop/application', - }, - { - text: '/aws/containerinsights/dev303-workshop/flowlogs', - value: '/aws/containerinsights/dev303-workshop/flowlogs', - label: '/aws/containerinsights/dev303-workshop/flowlogs', - }, - { - text: '/aws/containerinsights/dev303-workshop/dataplane', - value: '/aws/containerinsights/dev303-workshop/dataplane', - label: '/aws/containerinsights/dev303-workshop/dataplane', - }, - ]; - - const logGroups = await api.describeLogGroups({ region: 'default' }); - - expect(logGroups).toEqual(expectedLogGroups); + describe('memoization', () => { + it('should not initiate new api request in case a previous request had same args', async () => { + const getMock = jest.fn(); + const { api, resourceRequestMock } = setupMockedAPI({ getMock }); + await Promise.all([ + api.getMetrics('AWS/EC2', 'us-east-1'), + api.getMetrics('AWS/EC2', 'us-east-1'), + api.getMetrics('AWS/EC2', 'us-east-2'), + api.getMetrics('AWS/EC2', 'us-east-2'), + api.getMetrics('AWS/EC2', 'us-east-2'), + ]); + expect(resourceRequestMock).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/api.ts b/public/app/plugins/datasource/cloudwatch/api.ts index 8782c9d1f70..615bfcfb6aa 100644 --- a/public/app/plugins/datasource/cloudwatch/api.ts +++ b/public/app/plugins/datasource/cloudwatch/api.ts @@ -1,3 +1,5 @@ +import { memoize } from 'lodash'; + import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; @@ -10,37 +12,39 @@ export interface SelectableResourceValue extends SelectableValue { } export class CloudWatchAPI extends CloudWatchRequest { + private memoizedGetRequest; + constructor(instanceSettings: DataSourceInstanceSettings, templateSrv: TemplateSrv) { super(instanceSettings, templateSrv); + this.memoizedGetRequest = memoize(this.getRequest.bind(this), (path, parameters) => + JSON.stringify({ path, parameters }) + ); } - resourceRequest( - subtype: string, - parameters?: Record - ): Promise { + private getRequest(subtype: string, parameters?: Record): Promise { return getBackendSrv().get(`/api/datasources/${this.instanceSettings.id}/resources/${subtype}`, parameters); } getRegions() { - return this.resourceRequest('regions').then((regions) => [ + return this.memoizedGetRequest('regions').then((regions) => [ { label: 'default', value: 'default', text: 'default' }, ...regions.filter((r) => r.value), ]); } getNamespaces() { - return this.resourceRequest('namespaces'); + return this.memoizedGetRequest('namespaces'); } async describeLogGroups(params: DescribeLogGroupsRequest) { - return this.resourceRequest('log-groups', { + return this.memoizedGetRequest('log-groups', { ...params, region: this.templateSrv.replace(this.getActualRegion(params.region)), }); } async describeAllLogGroups(params: DescribeLogGroupsRequest) { - return this.resourceRequest('all-log-groups', { + return this.memoizedGetRequest('all-log-groups', { ...params, region: this.templateSrv.replace(this.getActualRegion(params.region)), }); @@ -51,14 +55,14 @@ export class CloudWatchAPI extends CloudWatchRequest { return []; } - return this.resourceRequest('metrics', { + return this.memoizedGetRequest('metrics', { region: this.templateSrv.replace(this.getActualRegion(region)), namespace: this.templateSrv.replace(namespace), }); } async getAllMetrics(region: string): Promise> { - const values = await this.resourceRequest('all-metrics', { + const values = await this.memoizedGetRequest('all-metrics', { region: this.templateSrv.replace(this.getActualRegion(region)), }); @@ -75,7 +79,7 @@ export class CloudWatchAPI extends CloudWatchRequest { return []; } - return this.resourceRequest('dimension-keys', { + return this.memoizedGetRequest('dimension-keys', { region: this.templateSrv.replace(this.getActualRegion(region)), namespace: this.templateSrv.replace(namespace), dimensionFilters: JSON.stringify(this.convertDimensionFormat(dimensionFilters, {})), @@ -94,7 +98,7 @@ export class CloudWatchAPI extends CloudWatchRequest { return []; } - const values = await this.resourceRequest('dimension-values', { + const values = await this.memoizedGetRequest('dimension-values', { region: this.templateSrv.replace(this.getActualRegion(region)), namespace: this.templateSrv.replace(namespace), metricName: this.templateSrv.replace(metricName.trim()), @@ -106,14 +110,14 @@ export class CloudWatchAPI extends CloudWatchRequest { } getEbsVolumeIds(region: string, instanceId: string) { - return this.resourceRequest('ebs-volume-ids', { + return this.memoizedGetRequest('ebs-volume-ids', { region: this.templateSrv.replace(this.getActualRegion(region)), instanceId: this.templateSrv.replace(instanceId), }); } getEc2InstanceAttribute(region: string, attributeName: string, filters: MultiFilters) { - return this.resourceRequest('ec2-instance-attribute', { + return this.memoizedGetRequest('ec2-instance-attribute', { region: this.templateSrv.replace(this.getActualRegion(region)), attributeName: this.templateSrv.replace(attributeName), filters: JSON.stringify(this.convertMultiFilterFormat(filters, 'filter key')), @@ -121,7 +125,7 @@ export class CloudWatchAPI extends CloudWatchRequest { } getResourceARNs(region: string, resourceType: string, tags: MultiFilters) { - return this.resourceRequest('resource-arns', { + return this.memoizedGetRequest('resource-arns', { region: this.templateSrv.replace(this.getActualRegion(region)), resourceType: this.templateSrv.replace(resourceType), tags: JSON.stringify(this.convertMultiFilterFormat(tags, 'tag name')), diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx index c50784b2e1f..6906b730933 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx @@ -53,7 +53,7 @@ export const FilterItem: FunctionComponent = ({ return datasource.api .getDimensionValues(region, namespace, metricName, filter.key, dimensionsExcludingCurrentKey) .then((result: Array>) => { - if (result.length && !disableExpressions) { + if (result.length && !disableExpressions && !result.some((o) => o.value === wildcardOption.value)) { result.unshift(wildcardOption); } return appendTemplateVariables(datasource, result); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index c07c0558958..5ca3c56a6a3 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -203,17 +203,18 @@ describe('datasource', () => { describe('resource requests', () => { it('should map resource response to metric response', async () => { - const datasource = setupMockedDataSource().datasource; - datasource.api.resourceRequest = jest.fn().mockResolvedValue([ - { - text: 'AWS/EC2', - value: 'CPUUtilization', - }, - { - text: 'AWS/Redshift', - value: 'CPUPercentage', - }, - ]); + const datasource = setupMockedDataSource({ + getMock: jest.fn().mockResolvedValue([ + { + text: 'AWS/EC2', + value: 'CPUUtilization', + }, + { + text: 'AWS/Redshift', + value: 'CPUPercentage', + }, + ]), + }).datasource; const allMetrics = await datasource.api.getAllMetrics('us-east-2'); expect(allMetrics[0].metricName).toEqual('CPUUtilization'); expect(allMetrics[0].namespace).toEqual('AWS/EC2');