diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx
new file mode 100644
index 00000000000..62d5c7cc65c
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx
@@ -0,0 +1,141 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import React from 'react';
+import { select } from 'react-select-event';
+import { VariableQueryType } from '../../types';
+import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
+import { VariableQueryEditor, Props } from './VariableQueryEditor';
+
+const defaultQuery = {
+ queryType: VariableQueryType.Regions,
+ namespace: '',
+ region: '',
+ metricName: '',
+ dimensionKey: '',
+ dimensionFilters: '',
+ ec2Filters: '',
+ instanceID: '',
+ attributeName: '',
+ resourceType: '',
+ tags: '',
+ refId: '',
+};
+
+const ds = setupMockedDataSource();
+
+ds.datasource.getRegions = jest.fn().mockResolvedValue([
+ { label: 'a1', value: 'a1' },
+ { label: 'b1', value: 'b1' },
+ { label: 'c1', value: 'c1' },
+]);
+ds.datasource.getNamespaces = jest.fn().mockResolvedValue([
+ { label: 'x2', value: 'x2' },
+ { label: 'y2', value: 'y2' },
+ { label: 'z2', value: 'z2' },
+]);
+ds.datasource.getMetrics = jest.fn().mockResolvedValue([
+ { label: 'h3', value: 'h3' },
+ { label: 'i3', value: 'i3' },
+ { label: 'j3', value: 'j3' },
+]);
+ds.datasource.getDimensionKeys = jest.fn().mockImplementation((namespace: string, region: string) => {
+ if (region === 'a1') {
+ return Promise.resolve([
+ { label: 'q4', value: 'q4' },
+ { label: 'r4', value: 'r4' },
+ { label: 's4', value: 's4' },
+ ]);
+ }
+ return Promise.resolve([{ label: 't4', value: 't4' }]);
+});
+ds.datasource.getVariables = jest.fn().mockReturnValue([]);
+
+const defaultProps: Props = {
+ onChange: jest.fn(),
+ query: defaultQuery,
+ datasource: ds.datasource,
+ onRunQuery: () => {},
+};
+
+describe('VariableEditor', () => {
+ describe('and a new variable is created', () => {
+ it('should trigger a query using the first query type in the array', async () => {
+ const props = defaultProps;
+ props.query = defaultQuery;
+ render();
+
+ await waitFor(() => {
+ const querySelect = screen.queryByRole('combobox', { name: 'Query Type' });
+ expect(querySelect).toBeInTheDocument();
+ expect(screen.queryByText('Regions')).toBeInTheDocument();
+ // Should not render any fields besides Query Type
+ const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
+ expect(regionSelect).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('and an existing variable is edited', () => {
+ it('should trigger new query using the saved query type', async () => {
+ const props = defaultProps;
+ props.query = {
+ ...defaultQuery,
+ queryType: VariableQueryType.Metrics,
+ namespace: 'z2',
+ region: 'a1',
+ };
+ render();
+
+ await waitFor(() => {
+ const querySelect = screen.queryByRole('combobox', { name: 'Query Type' });
+ expect(querySelect).toBeInTheDocument();
+ expect(screen.queryByText('Metrics')).toBeInTheDocument();
+ const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
+ expect(regionSelect).toBeInTheDocument();
+ expect(screen.queryByText('a1')).toBeInTheDocument();
+ const namespaceSelect = screen.queryByRole('combobox', { name: 'Namespace' });
+ expect(namespaceSelect).toBeInTheDocument();
+ expect(screen.queryByText('z2')).toBeInTheDocument();
+ // Should only render Query Type, Region, and Namespace selectors
+ const metricSelect = screen.queryByRole('combobox', { name: 'Metric' });
+ expect(metricSelect).not.toBeInTheDocument();
+ });
+ });
+ });
+ describe('and a different region is selected', () => {
+ it('should clear invalid fields', async () => {
+ const props = defaultProps;
+ props.query = {
+ ...defaultQuery,
+ queryType: VariableQueryType.DimensionValues,
+ namespace: 'z2',
+ region: 'a1',
+ metricName: 'i3',
+ dimensionKey: 's4',
+ };
+ render();
+
+ const querySelect = screen.queryByLabelText('Query Type');
+ expect(querySelect).toBeInTheDocument();
+ expect(screen.queryByText('Dimension Values')).toBeInTheDocument();
+ const regionSelect = screen.getByRole('combobox', { name: 'Region' });
+ regionSelect.click();
+ await select(regionSelect, 'b1', {
+ container: document.body,
+ });
+
+ expect(ds.datasource.getMetrics).toHaveBeenCalledWith('z2', 'b1');
+ expect(ds.datasource.getDimensionKeys).toHaveBeenCalledWith('z2', 'b1');
+ expect(props.onChange).toHaveBeenCalledWith({
+ ...defaultQuery,
+ refId: 'CloudWatchVariableQueryEditor-VariableQuery',
+ queryType: VariableQueryType.DimensionValues,
+ namespace: 'z2',
+ region: 'b1',
+ // metricName i3 exists in the new region and should not be removed
+ metricName: 'i3',
+ // dimensionKey s4 does not exist in the new region and should be removed
+ dimensionKey: '',
+ });
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx
new file mode 100644
index 00000000000..6a02346cfb7
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx
@@ -0,0 +1,181 @@
+import React from 'react';
+
+import { QueryEditorProps, SelectableValue } from '@grafana/data';
+import { VariableTextField } from './VariableTextField';
+import { CloudWatchDatasource } from '../../datasource';
+import { useDimensionKeys, useMetrics, useNamespaces, useRegions } from '../../hooks';
+import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
+import { migrateVariableQuery } from '../../migrations';
+import { VariableQueryField } from './VariableQueryField';
+
+export type Props = QueryEditorProps;
+
+const queryTypes: Array<{ value: string; label: string }> = [
+ { value: VariableQueryType.Regions, label: 'Regions' },
+ { value: VariableQueryType.Namespaces, label: 'Namespaces' },
+ { value: VariableQueryType.Metrics, label: 'Metrics' },
+ { value: VariableQueryType.DimensionKeys, label: 'Dimension Keys' },
+ { value: VariableQueryType.DimensionValues, label: 'Dimension Values' },
+ { value: VariableQueryType.EBSVolumeIDs, label: 'EBS Volume IDs' },
+ { value: VariableQueryType.EC2InstanceAttributes, label: 'EC2 Instance Attributes' },
+ { value: VariableQueryType.ResourceArns, label: 'Resource ARNs' },
+ { value: VariableQueryType.Statistics, label: 'Statistics' },
+];
+
+export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
+ const parsedQuery = migrateVariableQuery(query);
+
+ const { region, namespace, metricName, dimensionKey } = parsedQuery;
+ const [regions, regionIsLoading] = useRegions(datasource);
+ const namespaces = useNamespaces(datasource);
+ const metrics = useMetrics(datasource, region, namespace);
+ const dimensionKeys = useDimensionKeys(datasource, region, namespace, metricName);
+
+ const onRegionChange = async (region: string) => {
+ const validatedQuery = await sanitizeQuery({
+ ...parsedQuery,
+ region,
+ });
+ onQueryChange(validatedQuery);
+ };
+
+ const onNamespaceChange = async (namespace: string) => {
+ const validatedQuery = await sanitizeQuery({
+ ...parsedQuery,
+ namespace,
+ });
+ onQueryChange(validatedQuery);
+ };
+
+ const onQueryChange = (newQuery: VariableQuery) => {
+ onChange({ ...newQuery, refId: 'CloudWatchVariableQueryEditor-VariableQuery' });
+ };
+
+ // Reset dimensionValue parameters if namespace or region change
+ const sanitizeQuery = async (query: VariableQuery) => {
+ let { metricName, dimensionKey, dimensionFilters, namespace, region } = query;
+ if (metricName) {
+ await datasource.getMetrics(namespace, region).then((result: Array>) => {
+ if (!result.find((metric) => metric.value === metricName)) {
+ metricName = '';
+ dimensionFilters = '';
+ }
+ });
+ }
+ if (dimensionKey) {
+ await datasource.getDimensionKeys(namespace, region).then((result: Array>) => {
+ if (!result.find((key) => key.value === dimensionKey)) {
+ dimensionKey = '';
+ dimensionFilters = '';
+ }
+ });
+ }
+ return { ...query, metricName, dimensionKey, dimensionFilters };
+ };
+
+ const hasRegionField = [
+ VariableQueryType.Metrics,
+ VariableQueryType.DimensionKeys,
+ VariableQueryType.DimensionValues,
+ VariableQueryType.EBSVolumeIDs,
+ VariableQueryType.EC2InstanceAttributes,
+ VariableQueryType.ResourceArns,
+ ].includes(parsedQuery.queryType);
+ const hasNamespaceField = [
+ VariableQueryType.Metrics,
+ VariableQueryType.DimensionKeys,
+ VariableQueryType.DimensionValues,
+ ].includes(parsedQuery.queryType);
+
+ return (
+ <>
+ onQueryChange({ ...parsedQuery, queryType: value })}
+ label="Query Type"
+ />
+ {hasRegionField && (
+ onRegionChange(value)}
+ label="Region"
+ isLoading={regionIsLoading}
+ />
+ )}
+ {hasNamespaceField && (
+ onNamespaceChange(value)}
+ label="Namespace"
+ />
+ )}
+ {parsedQuery.queryType === VariableQueryType.DimensionValues && (
+ <>
+ onQueryChange({ ...parsedQuery, metricName: value })}
+ label="Metric"
+ />
+ onQueryChange({ ...parsedQuery, dimensionKey: value })}
+ label="Dimension Key"
+ />
+ onQueryChange({ ...parsedQuery, dimensionFilters: value })}
+ label="Filters"
+ />
+ >
+ )}
+ {parsedQuery.queryType === VariableQueryType.EBSVolumeIDs && (
+ onQueryChange({ ...parsedQuery, instanceID: value })}
+ label="Instance ID"
+ />
+ )}
+ {parsedQuery.queryType === VariableQueryType.EC2InstanceAttributes && (
+ <>
+ onQueryChange({ ...parsedQuery, attributeName: value })}
+ label="Attribute Name"
+ />
+ onQueryChange({ ...parsedQuery, ec2Filters: value })}
+ label="Filters"
+ />
+ >
+ )}
+ {parsedQuery.queryType === VariableQueryType.ResourceArns && (
+ <>
+ onQueryChange({ ...parsedQuery, resourceType: value })}
+ label="Resource Type"
+ />
+ onQueryChange({ ...parsedQuery, tags: value })}
+ label="Tags"
+ />
+ >
+ )}
+ >
+ );
+};
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx
new file mode 100644
index 00000000000..cb1fa8e82dc
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx
@@ -0,0 +1,41 @@
+import { SelectableValue } from '@grafana/data';
+import { InlineField, Select } from '@grafana/ui';
+import React from 'react';
+import { VariableQueryType } from '../../types';
+
+const LABEL_WIDTH = 20;
+
+interface VariableQueryFieldProps {
+ onChange: (value: T) => void;
+ options: SelectableValue[];
+ value: T | null;
+ label: string;
+ allowCustomValue?: boolean;
+ isLoading?: boolean;
+ inputId?: string;
+}
+
+export const VariableQueryField = ({
+ label,
+ onChange,
+ value,
+ options,
+ allowCustomValue = false,
+ isLoading = false,
+}: VariableQueryFieldProps) => {
+ return (
+
+
+ );
+};
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableTextField.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableTextField.tsx
new file mode 100644
index 00000000000..4fdc6f8f99f
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableTextField.tsx
@@ -0,0 +1,29 @@
+import { InlineField, Input } from '@grafana/ui';
+import React, { FC, useState } from 'react';
+
+const LABEL_WIDTH = 20;
+const TEXT_WIDTH = 100;
+
+interface VariableTextFieldProps {
+ onBlur: (value: string) => void;
+ placeholder: string;
+ value: string;
+ label: string;
+ tooltip?: string;
+}
+
+export const VariableTextField: FC = ({ label, onBlur, placeholder, value, tooltip }) => {
+ const [localValue, setLocalValue] = useState(value);
+ return (
+
+ setLocalValue(e.currentTarget.value)}
+ onBlur={() => onBlur(localValue)}
+ width={TEXT_WIDTH}
+ />
+
+ );
+};
diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/index.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/index.tsx
new file mode 100644
index 00000000000..423ec3dd3f6
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/index.tsx
@@ -0,0 +1 @@
+export { VariableQueryEditor } from './VariableQueryEditor';
diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts
index 2d126b05570..7a98c88710d 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.ts
+++ b/public/app/plugins/datasource/cloudwatch/datasource.ts
@@ -59,6 +59,7 @@ import {
import { addDataLinksToLogsResponse } from './utils/datalinks';
import { runWithRetry } from './utils/logsRetry';
import { increasingInterval } from './utils/rxjs/increasingInterval';
+import { CloudWatchVariableSupport } from './variables';
const DS_QUERY_ENDPOINT = '/api/ds/query';
@@ -122,6 +123,7 @@ export class CloudWatchDatasource
this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m';
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this, this.templateSrv);
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this, this.templateSrv);
+ this.variables = new CloudWatchVariableSupport(this);
}
query(options: DataQueryRequest): Observable {
@@ -735,83 +737,6 @@ export class CloudWatchDatasource
});
}
- async metricFindQuery(query: string) {
- let region;
- let namespace;
- let metricName;
- let filterJson;
-
- const regionQuery = query.match(/^regions\(\)/);
- if (regionQuery) {
- return this.getRegions();
- }
-
- const namespaceQuery = query.match(/^namespaces\(\)/);
- if (namespaceQuery) {
- return this.getNamespaces();
- }
-
- const metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
- if (metricNameQuery) {
- namespace = metricNameQuery[1];
- region = metricNameQuery[3];
- return this.getMetrics(namespace, region);
- }
-
- const dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
- if (dimensionKeysQuery) {
- namespace = dimensionKeysQuery[1];
- region = dimensionKeysQuery[3];
- return this.getDimensionKeys(namespace, region);
- }
-
- const dimensionValuesQuery = query.match(
- /^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?(.+))?\)/
- );
- if (dimensionValuesQuery) {
- region = dimensionValuesQuery[1];
- namespace = dimensionValuesQuery[2];
- metricName = dimensionValuesQuery[3];
- const dimensionKey = dimensionValuesQuery[4];
- filterJson = {};
- if (dimensionValuesQuery[6]) {
- filterJson = JSON.parse(this.templateSrv.replace(dimensionValuesQuery[6]));
- }
-
- return this.getDimensionValues(region, namespace, metricName, dimensionKey, filterJson);
- }
-
- const ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
- if (ebsVolumeIdsQuery) {
- region = ebsVolumeIdsQuery[1];
- const instanceId = ebsVolumeIdsQuery[2];
- return this.getEbsVolumeIds(region, instanceId);
- }
-
- const ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
- if (ec2InstanceAttributeQuery) {
- region = ec2InstanceAttributeQuery[1];
- const targetAttributeName = ec2InstanceAttributeQuery[2];
- filterJson = JSON.parse(this.templateSrv.replace(ec2InstanceAttributeQuery[3]));
- return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
- }
-
- const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
- if (resourceARNsQuery) {
- region = resourceARNsQuery[1];
- const resourceType = resourceARNsQuery[2];
- const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3]));
- return this.getResourceARNs(region, resourceType, tagsJSON);
- }
-
- const statsQuery = query.match(/^statistics\(\)/);
- if (statsQuery) {
- return this.standardStatistics.map((s: string) => ({ value: s, label: s, text: s }));
- }
-
- return Promise.resolve([]);
- }
-
annotationQuery(options: any) {
const annotation = options.annotation;
const statistic = this.templateSrv.replace(annotation.statistic);
diff --git a/public/app/plugins/datasource/cloudwatch/migration.test.ts b/public/app/plugins/datasource/cloudwatch/migration.test.ts
index 9ea899a2157..4c56fd06946 100644
--- a/public/app/plugins/datasource/cloudwatch/migration.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/migration.test.ts
@@ -3,8 +3,15 @@ import {
migrateMultipleStatsAnnotationQuery,
migrateMultipleStatsMetricsQuery,
migrateCloudWatchQuery,
+ migrateVariableQuery,
} from './migrations';
-import { CloudWatchAnnotationQuery, CloudWatchMetricsQuery, MetricQueryType, MetricEditorMode } from './types';
+import {
+ CloudWatchAnnotationQuery,
+ CloudWatchMetricsQuery,
+ MetricQueryType,
+ MetricEditorMode,
+ VariableQueryType,
+} from './types';
describe('migration', () => {
describe('migrateMultipleStatsMetricsQuery', () => {
@@ -170,4 +177,51 @@ describe('migration', () => {
});
});
});
+ describe('migrateVariableQuery', () => {
+ describe('when metrics query is used', () => {
+ describe('and region param is left out', () => {
+ it('should leave an empty region', () => {
+ const query = migrateVariableQuery('metrics(testNamespace)');
+ expect(query.queryType).toBe(VariableQueryType.Metrics);
+ expect(query.namespace).toBe('testNamespace');
+ expect(query.region).toBe('');
+ });
+ });
+
+ describe('and region param is defined by user', () => {
+ it('should use the user defined region', () => {
+ const query = migrateVariableQuery('metrics(testNamespace2, custom-region)');
+ expect(query.queryType).toBe(VariableQueryType.Metrics);
+ expect(query.namespace).toBe('testNamespace2');
+ expect(query.region).toBe('custom-region');
+ });
+ });
+ });
+ describe('when dimension_values query is used', () => {
+ describe('and filter param is left out', () => {
+ it('should leave an empty filter', () => {
+ const query = migrateVariableQuery('dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)');
+ expect(query.queryType).toBe(VariableQueryType.DimensionValues);
+ expect(query.region).toBe('us-east-1');
+ expect(query.namespace).toBe('AWS/RDS');
+ expect(query.metricName).toBe('CPUUtilization');
+ expect(query.dimensionKey).toBe('DBInstanceIdentifier');
+ expect(query.dimensionFilters).toBe('');
+ });
+ });
+ describe('and filter param is defined by user', () => {
+ it('should use the user defined filter', () => {
+ const query = migrateVariableQuery(
+ 'dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier,{"InstanceId":"$instance_id"})'
+ );
+ expect(query.queryType).toBe(VariableQueryType.DimensionValues);
+ expect(query.region).toBe('us-east-1');
+ expect(query.namespace).toBe('AWS/RDS');
+ expect(query.metricName).toBe('CPUUtilization');
+ expect(query.dimensionKey).toBe('DBInstanceIdentifier');
+ expect(query.dimensionFilters).toBe('{"InstanceId":"$instance_id"}');
+ });
+ });
+ });
+ });
});
diff --git a/public/app/plugins/datasource/cloudwatch/migrations.ts b/public/app/plugins/datasource/cloudwatch/migrations.ts
index fea24b5d1d9..8bc3387e28d 100644
--- a/public/app/plugins/datasource/cloudwatch/migrations.ts
+++ b/public/app/plugins/datasource/cloudwatch/migrations.ts
@@ -1,6 +1,13 @@
import { AnnotationQuery, DataQuery } from '@grafana/data';
import { getNextRefIdChar } from 'app/core/utils/query';
-import { MetricEditorMode, CloudWatchAnnotationQuery, CloudWatchMetricsQuery, MetricQueryType } from './types';
+import {
+ MetricEditorMode,
+ CloudWatchAnnotationQuery,
+ CloudWatchMetricsQuery,
+ MetricQueryType,
+ VariableQuery,
+ VariableQueryType,
+} from './types';
// Migrates a metric query that use more than one statistic into multiple queries
// E.g query.statistics = ['Max', 'Min'] will be migrated to two queries - query1.statistic = 'Max' and query2.statistic = 'Min'
@@ -61,3 +68,94 @@ export function migrateCloudWatchQuery(query: CloudWatchMetricsQuery) {
}
}
}
+
+export function migrateVariableQuery(rawQuery: string | VariableQuery): VariableQuery {
+ if (typeof rawQuery !== 'string') {
+ return rawQuery;
+ }
+ const newQuery: VariableQuery = {
+ refId: 'CloudWatchVariableQueryEditor-VariableQuery',
+ queryType: VariableQueryType.Regions,
+ namespace: '',
+ region: '',
+ metricName: '',
+ dimensionKey: '',
+ dimensionFilters: '',
+ ec2Filters: '',
+ instanceID: '',
+ attributeName: '',
+ resourceType: '',
+ tags: '',
+ };
+ if (rawQuery === '') {
+ return newQuery;
+ }
+
+ if (rawQuery.match(/^regions\(\)/)) {
+ return newQuery;
+ }
+ if (rawQuery.match(/^namespaces\(\)/)) {
+ newQuery.queryType = VariableQueryType.Namespaces;
+ return newQuery;
+ }
+ const metricNameQuery = rawQuery.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
+ if (metricNameQuery) {
+ newQuery.queryType = VariableQueryType.Metrics;
+ newQuery.namespace = metricNameQuery[1];
+ newQuery.region = metricNameQuery[3] || '';
+ return newQuery;
+ }
+ const dimensionKeysQuery = rawQuery.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
+ if (dimensionKeysQuery) {
+ newQuery.queryType = VariableQueryType.DimensionKeys;
+ newQuery.namespace = dimensionKeysQuery[1];
+ newQuery.region = dimensionKeysQuery[3] || '';
+ return newQuery;
+ }
+
+ const dimensionValuesQuery = rawQuery.match(
+ /^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?(.+))?\)/
+ );
+ if (dimensionValuesQuery) {
+ newQuery.queryType = VariableQueryType.DimensionValues;
+ newQuery.region = dimensionValuesQuery[1];
+ newQuery.namespace = dimensionValuesQuery[2];
+ newQuery.metricName = dimensionValuesQuery[3];
+ newQuery.dimensionKey = dimensionValuesQuery[4];
+ newQuery.dimensionFilters = dimensionValuesQuery[6] || '';
+ return newQuery;
+ }
+
+ const ebsVolumeIdsQuery = rawQuery.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
+ if (ebsVolumeIdsQuery) {
+ newQuery.queryType = VariableQueryType.EBSVolumeIDs;
+ newQuery.region = ebsVolumeIdsQuery[1];
+ newQuery.instanceID = ebsVolumeIdsQuery[2];
+ return newQuery;
+ }
+
+ const ec2InstanceAttributeQuery = rawQuery.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
+ if (ec2InstanceAttributeQuery) {
+ newQuery.queryType = VariableQueryType.EC2InstanceAttributes;
+ newQuery.region = ec2InstanceAttributeQuery[1];
+ newQuery.attributeName = ec2InstanceAttributeQuery[2];
+ newQuery.ec2Filters = ec2InstanceAttributeQuery[3] || '';
+ return newQuery;
+ }
+
+ const resourceARNsQuery = rawQuery.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
+ if (resourceARNsQuery) {
+ newQuery.queryType = VariableQueryType.ResourceArns;
+ newQuery.region = resourceARNsQuery[1];
+ newQuery.resourceType = resourceARNsQuery[2];
+ newQuery.tags = JSON.parse(resourceARNsQuery[3]) || '';
+ return newQuery;
+ }
+
+ const statsQuery = rawQuery.match(/^statistics\(\)/);
+ if (statsQuery) {
+ newQuery.queryType = VariableQueryType.Statistics;
+ return newQuery;
+ }
+ throw new Error('unable to parse old variable query');
+}
diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
index 2db9cbf4f52..6e91e209b2a 100644
--- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
@@ -506,35 +506,6 @@ describe('CloudWatchDatasource', () => {
});
});
});
- describe('when regions query is used', () => {
- describe('and region param is left out', () => {
- it('should use the default region', async () => {
- const { ds, instanceSettings } = getTestContext();
- ds.doMetricResourceRequest = jest.fn().mockResolvedValue([]);
-
- await ds.metricFindQuery('metrics(testNamespace)');
-
- expect(ds.doMetricResourceRequest).toHaveBeenCalledWith('metrics', {
- namespace: 'testNamespace',
- region: instanceSettings.jsonData.defaultRegion,
- });
- });
- });
-
- describe('and region param is defined by user', () => {
- it('should use the user defined region', async () => {
- const { ds } = getTestContext();
- ds.doMetricResourceRequest = jest.fn().mockResolvedValue([]);
-
- await ds.metricFindQuery('metrics(testNamespace2, custom-region)');
-
- expect(ds.doMetricResourceRequest).toHaveBeenCalledWith('metrics', {
- namespace: 'testNamespace2',
- region: 'custom-region',
- });
- });
- });
- });
});
describe('When query region is "default"', () => {
diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts
index 2401c0c3721..472d400b55b 100644
--- a/public/app/plugins/datasource/cloudwatch/types.ts
+++ b/public/app/plugins/datasource/cloudwatch/types.ts
@@ -367,3 +367,29 @@ export interface MetricQuery {
maxDataPoints?: number;
intervalMs?: number;
}
+
+export enum VariableQueryType {
+ Regions = 'regions',
+ Namespaces = 'namespaces',
+ Metrics = 'metrics',
+ DimensionKeys = 'dimensionKeys',
+ DimensionValues = 'dimensionValues',
+ EBSVolumeIDs = 'ebsVolumeIDs',
+ EC2InstanceAttributes = 'ec2InstanceAttributes',
+ ResourceArns = 'resourceARNs',
+ Statistics = 'statistics',
+}
+
+export interface VariableQuery extends DataQuery {
+ queryType: VariableQueryType;
+ namespace: string;
+ region: string;
+ metricName: string;
+ dimensionKey: string;
+ dimensionFilters: string;
+ ec2Filters: string;
+ instanceID: string;
+ attributeName: string;
+ resourceType: string;
+ tags: string;
+}
diff --git a/public/app/plugins/datasource/cloudwatch/variables.ts b/public/app/plugins/datasource/cloudwatch/variables.ts
new file mode 100644
index 00000000000..1443df38ce3
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/variables.ts
@@ -0,0 +1,157 @@
+import { from, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { CustomVariableSupport, DataQueryRequest, DataQueryResponse } from '@grafana/data';
+
+import { CloudWatchDatasource } from './datasource';
+import { VariableQuery, VariableQueryType } from './types';
+import { migrateVariableQuery } from './migrations';
+import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor';
+
+export class CloudWatchVariableSupport extends CustomVariableSupport {
+ private readonly datasource: CloudWatchDatasource;
+
+ constructor(datasource: CloudWatchDatasource) {
+ super();
+ this.datasource = datasource;
+ this.query = this.query.bind(this);
+ }
+
+ editor = VariableQueryEditor;
+
+ query(request: DataQueryRequest): Observable {
+ const queryObj = migrateVariableQuery(request.targets[0]);
+ return from(this.execute(queryObj)).pipe(map((data) => ({ data })));
+ }
+
+ async execute(query: VariableQuery) {
+ try {
+ switch (query.queryType) {
+ case VariableQueryType.Regions:
+ return this.handleRegionsQuery();
+ case VariableQueryType.Namespaces:
+ return this.handleNamespacesQuery();
+ case VariableQueryType.Metrics:
+ return this.handleMetricsQuery(query);
+ case VariableQueryType.DimensionKeys:
+ return this.handleDimensionKeysQuery(query);
+ case VariableQueryType.DimensionValues:
+ return this.handleDimensionValuesQuery(query);
+ case VariableQueryType.EBSVolumeIDs:
+ return this.handleEbsVolumeIdsQuery(query);
+ case VariableQueryType.EC2InstanceAttributes:
+ return this.handleEc2InstanceAttributeQuery(query);
+ case VariableQueryType.ResourceArns:
+ return this.handleResourceARNsQuery(query);
+ case VariableQueryType.Statistics:
+ return this.handleStatisticsQuery();
+ }
+ } catch (error) {
+ console.error(`Could not run CloudWatchMetricFindQuery ${query}`, error);
+ return [];
+ }
+ }
+
+ async handleRegionsQuery() {
+ const regions = await this.datasource.getRegions();
+ return regions.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleNamespacesQuery() {
+ const namespaces = await this.datasource.getNamespaces();
+ return namespaces.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleMetricsQuery({ namespace, region }: VariableQuery) {
+ const metrics = await this.datasource.getMetrics(namespace, region);
+ return metrics.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleDimensionKeysQuery({ namespace, region }: VariableQuery) {
+ const keys = await this.datasource.getDimensionKeys(namespace, region);
+ return keys.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleDimensionValuesQuery({ namespace, region, dimensionKey, metricName, dimensionFilters }: VariableQuery) {
+ if (!dimensionKey || !metricName) {
+ return [];
+ }
+ var filterJson = {};
+ if (dimensionFilters) {
+ filterJson = JSON.parse(dimensionFilters);
+ }
+ const keys = await this.datasource.getDimensionValues(region, namespace, metricName, dimensionKey, filterJson);
+ return keys.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleEbsVolumeIdsQuery({ region, instanceID }: VariableQuery) {
+ if (!instanceID) {
+ return [];
+ }
+ const ids = await this.datasource.getEbsVolumeIds(region, instanceID);
+ return ids.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleEc2InstanceAttributeQuery({ region, attributeName, ec2Filters }: VariableQuery) {
+ if (!attributeName) {
+ return [];
+ }
+ var filterJson = {};
+ if (ec2Filters) {
+ filterJson = JSON.parse(ec2Filters);
+ }
+ const values = await this.datasource.getEc2InstanceAttribute(region, attributeName, filterJson);
+ return values.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleResourceARNsQuery({ region, resourceType, tags }: VariableQuery) {
+ if (!resourceType) {
+ return [];
+ }
+ var tagJson = {};
+ if (tags) {
+ tagJson = JSON.parse(tags);
+ }
+ const keys = await this.datasource.getResourceARNs(region, resourceType, tagJson);
+ return keys.map((s: { label: string; value: string }) => ({
+ text: s.label,
+ value: s.value,
+ expandable: true,
+ }));
+ }
+
+ async handleStatisticsQuery() {
+ return this.datasource.standardStatistics.map((s: string) => ({
+ text: s,
+ value: s,
+ expandable: true,
+ }));
+ }
+}