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 ( + + 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, + })); + } +}