diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts
index 5ce973cca66..5b907055215 100644
--- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts
+++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts
@@ -49,7 +49,9 @@ export function setupMockedDataSource({
} as any
);
datasource.getVariables = () => ['test'];
- datasource.getRegions = () => Promise.resolve([]);
+
+ datasource.getNamespaces = jest.fn().mockResolvedValue([]);
+ datasource.getRegions = jest.fn().mockResolvedValue([]);
const fetchMock = jest.fn().mockReturnValue(of({ data }));
setBackendSrv({ fetch: fetchMock } as any);
diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.test.tsx
index 73dd9d21e2b..f74ddbf21d8 100644
--- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.test.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.test.tsx
@@ -10,7 +10,7 @@ import { CustomVariableModel, initialVariableModelState } from '../../../../feat
import { CloudWatchDatasource } from '../datasource';
import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types';
-import { MetricsQueryEditor, normalizeQuery, Props } from './MetricsQueryEditor';
+import { MetricsQueryEditor, Props } from './MetricsQueryEditor';
const setup = () => {
const instanceSettings = {
@@ -79,64 +79,6 @@ describe('QueryEditor', () => {
});
});
- it('normalizes query on mount', async () => {
- const { act } = renderer;
- const props = setup();
- // This does not actually even conform to the prop type but this happens on initialisation somehow
- props.query = {
- queryMode: 'Metrics',
- apiMode: 'Metrics',
- refId: '',
- expression: '',
- matchExact: true,
- metricQueryType: MetricQueryType.Search,
- metricEditorMode: MetricEditorMode.Builder,
- } as any;
- await act(async () => {
- renderer.create();
- });
- expect((props.onChange as jest.Mock).mock.calls[0][0]).toEqual({
- namespace: '',
- metricName: '',
- expression: '',
- sqlExpression: '',
- dimensions: {},
- region: 'default',
- id: '',
- alias: '',
- statistic: 'Average',
- period: '',
- queryMode: 'Metrics',
- apiMode: 'Metrics',
- refId: '',
- matchExact: true,
- metricQueryType: MetricQueryType.Search,
- metricEditorMode: MetricEditorMode.Builder,
- });
- });
-
- describe('should use correct default values', () => {
- it('should normalize query with default values', () => {
- expect(normalizeQuery({ refId: '42' } as any)).toEqual({
- namespace: '',
- metricName: '',
- expression: '',
- sqlExpression: '',
- dimensions: {},
- region: 'default',
- id: '',
- alias: '',
- statistic: 'Average',
- matchExact: true,
- period: '',
- queryMode: 'Metrics',
- refId: '42',
- metricQueryType: MetricQueryType.Search,
- metricEditorMode: MetricEditorMode.Builder,
- });
- });
- });
-
describe('should handle editor modes correctly', () => {
it('when metric query type is metric search and editor mode is builder', async () => {
await act(async () => {
diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx
index d4884fdeab3..c484dec6cbc 100644
--- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx
@@ -1,4 +1,4 @@
-import React, { ChangeEvent, PureComponent } from 'react';
+import React, { ChangeEvent, useState } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { EditorField, EditorRow, Space } from '@grafana/experimental';
@@ -16,181 +16,132 @@ import {
} from '../types';
import QueryHeader from './QueryHeader';
+import usePreparedMetricsQuery from './usePreparedMetricsQuery';
import { Alias, MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from './';
-export type Props = QueryEditorProps;
-
-interface State {
- sqlCodeEditorIsDirty: boolean;
+export interface Props extends QueryEditorProps {
+ query: CloudWatchMetricsQuery;
}
-export const normalizeQuery = ({
- namespace,
- metricName,
- expression,
- dimensions,
- region,
- id,
- alias,
- statistic,
- period,
- sqlExpression,
- metricQueryType,
- metricEditorMode,
- ...rest
-}: CloudWatchMetricsQuery): CloudWatchMetricsQuery => {
- const normalizedQuery = {
- queryMode: 'Metrics' as const,
- namespace: namespace ?? '',
- metricName: metricName ?? '',
- expression: expression ?? '',
- dimensions: dimensions ?? {},
- region: region ?? 'default',
- id: id ?? '',
- alias: alias ?? '',
- statistic: statistic ?? 'Average',
- period: period ?? '',
- metricQueryType: metricQueryType ?? MetricQueryType.Search,
- metricEditorMode: metricEditorMode ?? MetricEditorMode.Builder,
- sqlExpression: sqlExpression ?? '',
- ...rest,
- };
- return !rest.hasOwnProperty('matchExact') ? { ...normalizedQuery, matchExact: true } : normalizedQuery;
-};
-
-export class MetricsQueryEditor extends PureComponent {
- state = {
- sqlCodeEditorIsDirty: false,
- };
+export const MetricsQueryEditor = (props: Props) => {
+ const { query, onRunQuery, datasource } = props;
+ const [sqlCodeEditorIsDirty, setSQLCodeEditorIsDirty] = useState(false);
+ const preparedQuery = usePreparedMetricsQuery(query, props.onChange);
- componentDidMount = () => {
- const metricsQuery = this.props.query as CloudWatchMetricsQuery;
- const query = normalizeQuery(metricsQuery);
- this.props.onChange(query);
- };
-
- onChange = (query: CloudWatchQuery) => {
- const { onChange, onRunQuery } = this.props;
+ const onChange = (query: CloudWatchQuery) => {
+ const { onChange, onRunQuery } = props;
onChange(query);
onRunQuery();
};
- render() {
- const { onRunQuery, datasource } = this.props;
- const metricsQuery = this.props.query as CloudWatchMetricsQuery;
- const query = normalizeQuery(metricsQuery);
-
- return (
- <>
- {
- if (isCloudWatchMetricsQuery(newQuery) && newQuery.metricEditorMode !== query.metricEditorMode) {
- this.setState({ sqlCodeEditorIsDirty: false });
- }
- this.onChange(newQuery);
- }}
- sqlCodeEditorIsDirty={this.state.sqlCodeEditorIsDirty}
- />
-
+ return (
+ <>
+ {
+ if (isCloudWatchMetricsQuery(newQuery) && newQuery.metricEditorMode !== query.metricEditorMode) {
+ setSQLCodeEditorIsDirty(false);
+ }
+ onChange(newQuery);
+ }}
+ sqlCodeEditorIsDirty={sqlCodeEditorIsDirty}
+ />
+
+
+ {query.metricQueryType === MetricQueryType.Search && (
+ <>
+ {query.metricEditorMode === MetricEditorMode.Builder && (
+ props.onChange({ ...query, ...metricStat })}
+ >
+ )}
+ {query.metricEditorMode === MetricEditorMode.Code && (
+ props.onChange({ ...query, expression })}
+ datasource={datasource}
+ >
+ )}
+ >
+ )}
+ {query.metricQueryType === MetricQueryType.Query && (
+ <>
+ {query.metricEditorMode === MetricEditorMode.Code && (
+ {
+ if (!sqlCodeEditorIsDirty) {
+ setSQLCodeEditorIsDirty(true);
+ }
+ props.onChange({ ...preparedQuery, sqlExpression });
+ }}
+ onRunQuery={onRunQuery}
+ datasource={datasource}
+ />
+ )}
- {query.metricQueryType === MetricQueryType.Search && (
- <>
- {query.metricEditorMode === MetricEditorMode.Builder && (
- this.props.onChange({ ...query, ...metricStat })}
- >
- )}
- {query.metricEditorMode === MetricEditorMode.Code && (
-
+ this.props.onChange({ ...query, expression })}
datasource={datasource}
- >
- )}
- >
- )}
- {query.metricQueryType === MetricQueryType.Query && (
- <>
- {query.metricEditorMode === MetricEditorMode.Code && (
- {
- if (!this.state.sqlCodeEditorIsDirty) {
- this.setState({ sqlCodeEditorIsDirty: true });
- }
- this.props.onChange({ ...metricsQuery, sqlExpression });
- }}
- onRunQuery={onRunQuery}
- datasource={datasource}
- />
- )}
-
- {query.metricEditorMode === MetricEditorMode.Builder && (
- <>
-
- >
- )}
- >
- )}
-
-
-
- ) =>
- this.onChange({ ...metricsQuery, id: event.target.value })
- }
- type="text"
- value={query.id}
- />
-
-
-
- ) =>
- this.onChange({ ...metricsQuery, period: event.target.value })
- }
- />
-
-
-
- this.onChange({ ...metricsQuery, alias: value })}
- />
-
-
- >
- );
- }
-}
+ >
+ >
+ )}
+ >
+ )}
+
+
+
+ ) => onChange({ ...preparedQuery, id: event.target.value })}
+ type="text"
+ value={query.id}
+ />
+
+
+
+ ) =>
+ onChange({ ...preparedQuery, period: event.target.value })
+ }
+ />
+
+
+
+ onChange({ ...preparedQuery, alias: value })}
+ />
+
+
+ >
+ );
+};
diff --git a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx
new file mode 100644
index 00000000000..eefeca932f7
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx
@@ -0,0 +1,133 @@
+import { act, render, screen } from '@testing-library/react';
+import React from 'react';
+
+import { QueryEditorProps } from '@grafana/data';
+
+import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
+import { CloudWatchDatasource } from '../datasource';
+import { CloudWatchQuery, CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types';
+
+import { PanelQueryEditor } from './PanelQueryEditor';
+
+// the following three fields are added to legacy queries in the dashboard migrator
+const migratedFields = {
+ statistic: 'Average',
+ metricEditorMode: MetricEditorMode.Builder,
+ metricQueryType: MetricQueryType.Query,
+};
+
+const props: QueryEditorProps = {
+ datasource: setupMockedDataSource().datasource,
+ onRunQuery: jest.fn(),
+ onChange: jest.fn(),
+ query: {} as CloudWatchQuery,
+};
+
+describe('PanelQueryEditor should render right editor', () => {
+ describe('when using grafana 6.3.0 metric query', () => {
+ it('should render the metrics query editor', async () => {
+ const query = {
+ ...migratedFields,
+ dimensions: {
+ InstanceId: 'i-123',
+ },
+ expression: '',
+ highResolution: false,
+ id: '',
+ metricName: 'CPUUtilization',
+ namespace: 'AWS/EC2',
+ period: '',
+ refId: 'A',
+ region: 'default',
+ returnData: false,
+ };
+ await act(async () => {
+ render();
+ });
+ expect(screen.getByText('Metric name')).toBeInTheDocument();
+ });
+ });
+
+ describe('when using grafana 7.0.0 style metrics query', () => {
+ it('should render the metrics query editor', async () => {
+ const query = {
+ ...migratedFields,
+ alias: '',
+ apiMode: 'Logs',
+ dimensions: {
+ InstanceId: 'i-123',
+ },
+ expression: '',
+ id: '',
+ logGroupNames: [],
+ matchExact: true,
+ metricName: 'CPUUtilization',
+ namespace: 'AWS/EC2',
+ period: '',
+ queryMode: 'Logs',
+ refId: 'A',
+ region: 'ap-northeast-2',
+ statistics: 'Average',
+ } as any;
+ await act(async () => {
+ render();
+ });
+ expect(screen.getByText('Choose Log Groups')).toBeInTheDocument();
+ });
+ });
+
+ describe('when using grafana 7.0.0 style logs query', () => {
+ it('should render the metrics query editor', async () => {
+ const query = {
+ ...migratedFields,
+ alias: '',
+ apiMode: 'Logs',
+ dimensions: {
+ InstanceId: 'i-123',
+ },
+ expression: '',
+ id: '',
+ logGroupNames: [],
+ matchExact: true,
+ metricName: 'CPUUtilization',
+ namespace: 'AWS/EC2',
+ period: '',
+ queryMode: 'Logs',
+ refId: 'A',
+ region: 'ap-northeast-2',
+ statistic: 'Average',
+ } as any;
+ await act(async () => {
+ render();
+ });
+ expect(screen.getByText('Log Groups')).toBeInTheDocument();
+ });
+ });
+
+ describe('when using grafana query from curated ec2 dashboard', () => {
+ it('should render the metrics query editor', async () => {
+ const query = {
+ ...migratedFields,
+
+ alias: 'Inbound',
+ dimensions: {
+ InstanceId: '*',
+ },
+ expression:
+ "SUM(REMOVE_EMPTY(SEARCH('{AWS/EC2,InstanceId} MetricName=\"NetworkIn\"', 'Sum', $period)))/$period",
+ id: '',
+ matchExact: true,
+ metricName: 'NetworkOut',
+ namespace: 'AWS/EC2',
+ period: '$period',
+ refId: 'B',
+ region: '$region',
+ statistic: 'Average',
+ } as any;
+ await act(async () => {
+ render();
+ });
+ expect(screen.getByText('Metric name')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx
index 3c9938b98df..079fdae5671 100644
--- a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx
@@ -1,8 +1,9 @@
import React, { PureComponent } from 'react';
-import { QueryEditorProps, ExploreMode } from '@grafana/data';
+import { QueryEditorProps } from '@grafana/data';
import { CloudWatchDatasource } from '../datasource';
+import { isCloudWatchMetricsQuery } from '../guards';
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
import LogsQueryEditor from './LogsQueryEditor';
@@ -13,14 +14,13 @@ export type Props = QueryEditorProps {
render() {
const { query } = this.props;
- const apiMode = query.queryMode ?? 'Metrics';
return (
<>
- {apiMode === ExploreMode.Logs ? (
-
+ {isCloudWatchMetricsQuery(query) ? (
+
) : (
-
+
)}
>
);
diff --git a/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.test.ts b/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.test.ts
new file mode 100644
index 00000000000..8f179549d0a
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.test.ts
@@ -0,0 +1,76 @@
+import { renderHook } from '@testing-library/react-hooks';
+
+import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
+
+import usePreparedMetricsQuery, { DEFAULT_QUERY } from './usePreparedMetricsQuery';
+
+interface TestScenario {
+ name: string;
+ query: any;
+ expectedQuery: CloudWatchMetricsQuery;
+}
+
+const baseQuery: CloudWatchMetricsQuery = {
+ refId: 'A',
+ id: '',
+ region: 'us-east-2',
+ namespace: 'AWS/EC2',
+ dimensions: { InstanceId: 'x-123' },
+};
+
+describe('usePrepareMetricsQuery', () => {
+ describe('when an incomplete query is provided', () => {
+ const testTable: TestScenario[] = [
+ { name: 'Empty query', query: { refId: 'A' }, expectedQuery: { ...DEFAULT_QUERY, refId: 'A' } },
+ {
+ name: 'Match exact is not part of the query',
+ query: { ...baseQuery },
+ expectedQuery: { ...DEFAULT_QUERY, ...baseQuery, matchExact: true },
+ },
+ {
+ name: 'Match exact is part of the query',
+ query: { ...baseQuery, matchExact: false },
+ expectedQuery: { ...DEFAULT_QUERY, ...baseQuery, matchExact: false },
+ },
+ {
+ name: 'When editor mode and builder mode different from default is specified',
+ query: { ...baseQuery, metricQueryType: MetricQueryType.Query, metricEditorMode: MetricEditorMode.Code },
+ expectedQuery: {
+ ...DEFAULT_QUERY,
+ ...baseQuery,
+ metricQueryType: MetricQueryType.Query,
+ metricEditorMode: MetricEditorMode.Code,
+ },
+ },
+ ];
+ describe.each(testTable)('scenario %#: $name', (scenario) => {
+ it('should set the default values and trigger onChangeQuery', async () => {
+ const onChangeQuery = jest.fn();
+ const { result } = renderHook(() => usePreparedMetricsQuery(scenario.query, onChangeQuery));
+ expect(onChangeQuery).toHaveBeenLastCalledWith(result.current);
+ expect(result.current).toEqual(scenario.expectedQuery);
+ });
+ });
+ });
+
+ describe('when a complete query is provided', () => {
+ it('should not change the query and should not call onChangeQuery', async () => {
+ const onChangeQuery = jest.fn();
+ const completeQuery: CloudWatchMetricsQuery = {
+ ...baseQuery,
+ expression: '',
+ queryMode: 'Metrics',
+ metricName: '',
+ statistic: 'Sum',
+ period: '300',
+ metricQueryType: MetricQueryType.Query,
+ metricEditorMode: MetricEditorMode.Code,
+ sqlExpression: 'SELECT 1',
+ matchExact: false,
+ };
+ const { result } = renderHook(() => usePreparedMetricsQuery(completeQuery, onChangeQuery));
+ expect(onChangeQuery).not.toHaveBeenCalled();
+ expect(result.current).toEqual(completeQuery);
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.ts b/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.ts
new file mode 100644
index 00000000000..a52d5e277e9
--- /dev/null
+++ b/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.ts
@@ -0,0 +1,48 @@
+import deepEqual from 'fast-deep-equal';
+import { useEffect, useMemo } from 'react';
+
+import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
+
+export const DEFAULT_QUERY: Omit = {
+ queryMode: 'Metrics',
+ namespace: '',
+ metricName: '',
+ expression: '',
+ dimensions: {},
+ region: 'default',
+ id: '',
+ statistic: 'Average',
+ period: '',
+ metricQueryType: MetricQueryType.Search,
+ metricEditorMode: MetricEditorMode.Builder,
+ sqlExpression: '',
+ matchExact: true,
+};
+
+const prepareQuery = (query: CloudWatchMetricsQuery) => {
+ const withDefaults = { ...DEFAULT_QUERY, ...query };
+
+ // If we didn't make any changes to the object, then return the original object to keep the
+ // identity the same, and not trigger any other useEffects or anything.
+ return deepEqual(withDefaults, query) ? query : withDefaults;
+};
+
+/**
+ * Returns queries with some defaults + migrations, and calls onChange function to notify if it changes
+ */
+const usePreparedMetricsQuery = (
+ query: CloudWatchMetricsQuery,
+ onChangeQuery: (newQuery: CloudWatchMetricsQuery) => void
+) => {
+ const preparedQuery = useMemo(() => prepareQuery(query), [query]);
+
+ useEffect(() => {
+ if (preparedQuery !== query) {
+ onChangeQuery(preparedQuery);
+ }
+ }, [preparedQuery, query, onChangeQuery]);
+
+ return preparedQuery;
+};
+
+export default usePreparedMetricsQuery;
diff --git a/public/app/plugins/datasource/cloudwatch/guards.ts b/public/app/plugins/datasource/cloudwatch/guards.ts
index 8f7a3f4ea3a..b5346df3c25 100644
--- a/public/app/plugins/datasource/cloudwatch/guards.ts
+++ b/public/app/plugins/datasource/cloudwatch/guards.ts
@@ -6,7 +6,7 @@ export const isCloudWatchLogsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwa
cloudwatchQuery.queryMode === 'Logs';
export const isCloudWatchMetricsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwatchQuery is CloudWatchMetricsQuery =>
- cloudwatchQuery.queryMode === 'Metrics';
+ cloudwatchQuery.queryMode === 'Metrics' || !cloudwatchQuery.hasOwnProperty('queryMode'); // in early versions of this plugin, queryMode wasn't defined in a CloudWatchMetricsQuery
export const isCloudWatchAnnotationQuery = (
cloudwatchQuery: CloudWatchQuery