+
);
};
@@ -120,4 +122,6 @@ const getOperatorStyles = (theme: GrafanaTheme2) => ({
padding: theme.spacing(0, 1),
alignSelf: 'center',
}),
+ container: css({ display: 'inline-block' }),
+ alert: css({ minWidth: '100%', width: 'min-content' }),
});
diff --git a/public/app/plugins/datasource/cloudwatch/hooks.test.ts b/public/app/plugins/datasource/cloudwatch/hooks.test.ts
index 65ab0f6c1e4..326a56b4b6d 100644
--- a/public/app/plugins/datasource/cloudwatch/hooks.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/hooks.test.ts
@@ -11,7 +11,13 @@ import {
setupMockedDataSource,
} from './__mocks__/CloudWatchDataSource';
import { setupMockedResourcesAPI } from './__mocks__/ResourcesAPI';
-import { useAccountOptions, useDimensionKeys, useIsMonitoringAccount, useMetrics } from './hooks';
+import {
+ useAccountOptions,
+ useDimensionKeys,
+ useIsMonitoringAccount,
+ useMetrics,
+ useEnsureVariableHasSingleSelection,
+} from './hooks';
const originalFeatureToggleValue = config.featureToggles.cloudWatchCrossAccountQuerying;
@@ -82,15 +88,18 @@ describe('hooks', () => {
);
await waitFor(() => {
expect(getDimensionKeysMock).toHaveBeenCalledTimes(1);
- expect(getDimensionKeysMock).toHaveBeenCalledWith({
- region: regionVariable.current.value,
- namespace: namespaceVariable.current.value,
- metricName: metricVariable.current.value,
- accountId: accountIdVariable.current.value,
- dimensionFilters: {
- environment: [dimensionVariable.current.value],
+ expect(getDimensionKeysMock).toHaveBeenCalledWith(
+ {
+ region: regionVariable.current.value,
+ namespace: namespaceVariable.current.value,
+ metricName: metricVariable.current.value,
+ accountId: accountIdVariable.current.value,
+ dimensionFilters: {
+ environment: [dimensionVariable.current.value],
+ },
},
- });
+ false
+ );
});
});
});
@@ -139,4 +148,35 @@ describe('hooks', () => {
});
});
});
+
+ describe('useEnsureVariableHasSingleSelection', () => {
+ it('should return an error if a variable has multiple options selected', () => {
+ const { datasource } = setupMockedDataSource();
+ datasource.resources.isVariableWithMultipleOptionsSelected = jest.fn().mockReturnValue(true);
+
+ const variable = '$variable';
+ const { result } = renderHook(() => useEnsureVariableHasSingleSelection(datasource, variable));
+ expect(result.current).toEqual(
+ `Template variables with multiple selected options are not supported for ${variable}`
+ );
+ });
+
+ it('should not return an error if a variable is a multi-variable but does not have multiple options selected', () => {
+ const { datasource } = setupMockedDataSource();
+ datasource.resources.isVariableWithMultipleOptionsSelected = jest.fn().mockReturnValue(false);
+
+ const variable = '$variable';
+ const { result } = renderHook(() => useEnsureVariableHasSingleSelection(datasource, variable));
+ expect(result.current).toEqual('');
+ });
+
+ it('should not return an error if a variable is not a multi-variable', () => {
+ const { datasource } = setupMockedDataSource();
+ datasource.resources.isMultiVariable = jest.fn().mockReturnValue(false);
+
+ const variable = '$variable';
+ const { result } = renderHook(() => useEnsureVariableHasSingleSelection(datasource, variable));
+ expect(result.current).toEqual('');
+ });
+ });
});
diff --git a/public/app/plugins/datasource/cloudwatch/hooks.ts b/public/app/plugins/datasource/cloudwatch/hooks.ts
index a4a6738580a..8fe4c3540e5 100644
--- a/public/app/plugins/datasource/cloudwatch/hooks.ts
+++ b/public/app/plugins/datasource/cloudwatch/hooks.ts
@@ -87,13 +87,13 @@ export const useDimensionKeys = (
}
if (dimensionFilters) {
- dimensionFilters = datasource.resources.convertDimensionFormat(dimensionFilters, {});
+ dimensionFilters = datasource.resources.convertDimensionFormat(dimensionFilters, {}, false);
}
// doing deep comparison to avoid making new api calls to list metrics unless dimension filter object props changes
useDeepCompareEffect(() => {
datasource.resources
- .getDimensionKeys({ namespace, region, metricName, accountId, dimensionFilters })
+ .getDimensionKeys({ namespace, region, metricName, accountId, dimensionFilters }, false)
.then((result: Array
>) => {
setDimensionKeys(appendTemplateVariables(datasource, result));
});
@@ -102,6 +102,28 @@ export const useDimensionKeys = (
return dimensionKeys;
};
+export const useEnsureVariableHasSingleSelection = (datasource: CloudWatchDatasource, target?: string) => {
+ const [error, setError] = useState('');
+ // interpolate the target to ensure the check in useEffect runs when the variable selection is changed
+ const interpolatedTarget = datasource.templateSrv.replace(target);
+
+ useEffect(() => {
+ if (datasource.resources.isVariableWithMultipleOptionsSelected(target)) {
+ const newErrorMessage = `Template variables with multiple selected options are not supported for ${target}`;
+ if (error !== newErrorMessage) {
+ setError(newErrorMessage);
+ }
+ return;
+ }
+
+ if (error) {
+ setError('');
+ }
+ }, [datasource.resources, target, interpolatedTarget, error]);
+
+ return error;
+};
+
export const useIsMonitoringAccount = (resources: ResourcesAPI, region: string) => {
const [isMonitoringAccount, setIsMonitoringAccount] = useState(false);
// we call this before the use effect to ensure dependency array below
diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/CompletionItemProvider.ts
index 80539dc4f57..1b2f030eba7 100644
--- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/CompletionItemProvider.ts
+++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/CompletionItemProvider.ts
@@ -179,12 +179,15 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
dimensionFilters = (labelKeyTokens || []).reduce((acc, curr) => {
return { ...acc, [curr.value]: null };
}, {});
- const keys = await this.resources.getDimensionKeys({
- namespace: this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
- region: this.templateSrv.replace(this.region),
- metricName: metricNameToken?.value,
- dimensionFilters,
- });
+ const keys = await this.resources.getDimensionKeys(
+ {
+ namespace: this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
+ region: this.templateSrv.replace(this.region),
+ metricName: metricNameToken?.value,
+ dimensionFilters,
+ },
+ false
+ );
keys.map((m) => {
const key = /[\s\.-]/.test(m.value ?? '') ? `"${m.value}"` : m.value;
key && addSuggestion(key);
diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts
index 81939eb5f5c..0ac0bedbc3e 100644
--- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts
@@ -773,8 +773,28 @@ describe('CloudWatchMetricsQueryRunner', () => {
beforeEach(() => {
const { runner, request, queryMock } = setupMockedMetricsQueryRunner({
variables: [
- { ...namespaceVariable, multi: true },
- { ...metricVariable, multi: true },
+ {
+ ...namespaceVariable,
+ current: {
+ value: ['AWS/Redshift', 'AWS/EC2'],
+ text: ['AWS/Redshift', 'AWS/EC2'].toString(),
+ selected: true,
+ },
+ multi: true,
+ },
+ {
+ ...metricVariable,
+ current: {
+ value: ['CPUUtilization', 'DroppedBytes'],
+ text: ['CPUUtilization', 'DroppedBytes'].toString(),
+ selected: true,
+ },
+ multi: true,
+ },
+ {
+ ...dimensionVariable,
+ multi: true,
+ },
],
});
runner.debouncedCustomAlert = debouncedAlert;
@@ -789,7 +809,7 @@ describe('CloudWatchMetricsQueryRunner', () => {
metricName: '$' + metricVariable.name,
period: '',
alias: '',
- dimensions: {},
+ dimensions: { [`$${dimensionVariable.name}`]: '' },
matchExact: true,
statistic: '',
refId: '',
@@ -802,7 +822,7 @@ describe('CloudWatchMetricsQueryRunner', () => {
queryMock
);
});
- it('should show debounced alert for namespace and metric name', async () => {
+ it('should show debounced alert for namespace and metric name when multiple options are selected', async () => {
expect(debouncedAlert).toHaveBeenCalledWith(
'CloudWatch templating error',
'Multi template variables are not supported for namespace'
@@ -813,6 +833,13 @@ describe('CloudWatchMetricsQueryRunner', () => {
);
});
+ it('should not show debounced alert for a multi-variable if it only has one option selected', async () => {
+ expect(debouncedAlert).not.toHaveBeenCalledWith(
+ 'CloudWatch templating error',
+ `Multi template variables are not supported for dimension keys`
+ );
+ });
+
it('should not show debounced alert for region', async () => {
expect(debouncedAlert).not.toHaveBeenCalledWith(
'CloudWatch templating error',
diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts
index c20ca70b19b..460a2e7ee49 100644
--- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts
+++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchRequest.ts
@@ -1,11 +1,7 @@
import { Observable } from 'rxjs';
-import { DataSourceInstanceSettings, DataSourceRef, getDataSourceRef, ScopedVars } from '@grafana/data';
-import { BackendDataSourceResponse, FetchResponse, getBackendSrv, TemplateSrv } from '@grafana/runtime';
-import { notifyApp } from 'app/core/actions';
-import { createErrorNotification } from 'app/core/copy/appNotification';
-import { store } from 'app/store/store';
-import { AppNotificationTimeout } from 'app/types';
+import { DataSourceInstanceSettings, DataSourceRef, getDataSourceRef, ScopedVars, AppEvents } from '@grafana/data';
+import { BackendDataSourceResponse, FetchResponse, getBackendSrv, TemplateSrv, getAppEvents } from '@grafana/runtime';
import memoizedDebounce from '../memoizedDebounce';
import { CloudWatchJsonData, Dimensions, MetricRequest, MultiFilters } from '../types';
@@ -15,10 +11,7 @@ export abstract class CloudWatchRequest {
templateSrv: TemplateSrv;
ref: DataSourceRef;
dsQueryEndpoint = '/api/ds/query';
- debouncedCustomAlert: (title: string, message: string) => void = memoizedDebounce(
- displayCustomError,
- AppNotificationTimeout.Error
- );
+ debouncedCustomAlert: (title: string, message: string) => void = memoizedDebounce(displayCustomError);
constructor(
public instanceSettings: DataSourceInstanceSettings,
@@ -43,9 +36,18 @@ export abstract class CloudWatchRequest {
return getBackendSrv().fetch(options);
}
- convertDimensionFormat(dimensions: Dimensions, scopedVars: ScopedVars): Dimensions {
+ convertDimensionFormat(
+ dimensions: Dimensions,
+ scopedVars: ScopedVars,
+ displayErrorIfIsMultiTemplateVariable = true
+ ): Dimensions {
return Object.entries(dimensions).reduce((result, [key, value]) => {
- key = this.replaceVariableAndDisplayWarningIfMulti(key, scopedVars, true, 'dimension keys');
+ key = this.replaceVariableAndDisplayWarningIfMulti(
+ key,
+ scopedVars,
+ displayErrorIfIsMultiTemplateVariable,
+ 'dimension keys'
+ );
if (Array.isArray(value)) {
return { ...result, [key]: value };
@@ -93,23 +95,35 @@ export abstract class CloudWatchRequest {
}, {});
}
+ isMultiVariable(target?: string) {
+ if (target) {
+ const variables = this.templateSrv.getVariables();
+ const variable = variables.find(({ name }) => name === getVariableName(target));
+ const type = variable?.type;
+ return (type === 'custom' || type === 'query' || type === 'datasource') && variable?.multi;
+ }
+
+ return false;
+ }
+
+ isVariableWithMultipleOptionsSelected(target?: string, scopedVars?: ScopedVars) {
+ if (!target || !this.isMultiVariable(target)) {
+ return false;
+ }
+ return this.expandVariableToArray(target, scopedVars || {}).length > 1;
+ }
+
replaceVariableAndDisplayWarningIfMulti(
target?: string,
scopedVars?: ScopedVars,
displayErrorIfIsMultiTemplateVariable?: boolean,
fieldName?: string
) {
- if (displayErrorIfIsMultiTemplateVariable && !!target) {
- const variables = this.templateSrv.getVariables();
- const variable = variables.find(({ name }) => name === getVariableName(target));
- const isMultiVariable =
- variable?.type === 'custom' || variable?.type === 'query' || variable?.type === 'datasource';
- if (isMultiVariable && variable.multi) {
- this.debouncedCustomAlert(
- 'CloudWatch templating error',
- `Multi template variables are not supported for ${fieldName || target}`
- );
- }
+ if (displayErrorIfIsMultiTemplateVariable && this.isVariableWithMultipleOptionsSelected(target)) {
+ this.debouncedCustomAlert(
+ 'CloudWatch templating error',
+ `Multi template variables are not supported for ${fieldName || target}`
+ );
}
return this.templateSrv.replace(target, scopedVars);
@@ -128,4 +142,7 @@ export abstract class CloudWatchRequest {
}
const displayCustomError = (title: string, message: string) =>
- store.dispatch(notifyApp(createErrorNotification(title, message)));
+ getAppEvents().publish({
+ type: AppEvents.alertError.name,
+ payload: [title, message],
+ });
diff --git a/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts
index a7b2f1a67dc..b518ce9d29f 100644
--- a/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts
+++ b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts
@@ -110,19 +110,18 @@ export class ResourcesAPI extends CloudWatchRequest {
}).then((metrics) => metrics.map((m) => ({ metricName: m.value.name, namespace: m.value.namespace })));
}
- getDimensionKeys({
- region,
- namespace = '',
- dimensionFilters = {},
- metricName = '',
- accountId,
- }: GetDimensionKeysRequest): Promise>> {
+ getDimensionKeys(
+ { region, namespace = '', dimensionFilters = {}, metricName = '', accountId }: GetDimensionKeysRequest,
+ displayErrorIfIsMultiTemplateVariable?: boolean
+ ): Promise>> {
return this.memoizedGetRequest>>('dimension-keys', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
accountId: this.templateSrv.replace(accountId),
metricName: this.templateSrv.replace(metricName),
- dimensionFilters: JSON.stringify(this.convertDimensionFormat(dimensionFilters, {})),
+ dimensionFilters: JSON.stringify(
+ this.convertDimensionFormat(dimensionFilters, {}, displayErrorIfIsMultiTemplateVariable)
+ ),
}).then((r) => r.map((r) => ({ label: r.value, value: r.value })));
}
@@ -142,7 +141,7 @@ export class ResourcesAPI extends CloudWatchRequest {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
metricName: this.templateSrv.replace(metricName.trim()),
- dimensionKey: this.templateSrv.replace(dimensionKey),
+ dimensionKey: this.replaceVariableAndDisplayWarningIfMulti(dimensionKey, {}, true),
dimensionFilters: JSON.stringify(this.convertDimensionFormat(dimensionFilters, {})),
accountId: this.templateSrv.replace(accountId),
}).then((r) => r.map((r) => ({ label: r.value, value: r.value })));