From 6928ad2949e711de46446f55604f6b29c1d778c1 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 15 Dec 2022 14:29:57 +0100 Subject: [PATCH] Cloudwatch: Add run query button (#60089) * refactor header * split left and right header actions * cleanup * fix broken tests * move MetricsQueryEditor tests to QueryEditor tests * fix mock imports * remove no longer used onRunQuery func * move run queries button * apply defaults also when changing query type * remove not used prop --- .betterer.results | 8 +- .../cloudwatch/__mocks__/queries.ts | 30 ++-- .../components/AnnotationQueryEditor.tsx | 1 - .../CrossAccountLogsQueryField.test.tsx | 10 -- .../components/CrossAccountLogsQueryField.tsx | 2 - .../components/Dimensions/Dimensions.test.tsx | 1 - .../components/DynamicLabelsField.tsx | 7 +- .../components/LogGroupSelection.test.tsx | 1 - .../components/LogGroupSelection.tsx | 5 +- .../components/LogGroupSelector.tsx | 3 - .../cloudwatch/components/LogsQueryEditor.tsx | 9 +- .../components/LogsQueryField.test.tsx | 25 +-- .../cloudwatch/components/LogsQueryField.tsx | 13 +- .../components/MathExpressionQueryField.tsx | 12 +- .../MetricStatEditor.test.tsx | 13 +- .../MetricStatEditor/MetricStatEditor.tsx | 18 +-- .../MetricsQueryEditor.test.tsx | 62 +------ .../MetricsQueryEditor/MetricsQueryEditor.tsx | 121 +++++++++----- .../MetricsQueryHeader.test.tsx | 139 ---------------- .../MetricsQueryEditor/MetricsQueryHeader.tsx | 105 ------------ .../components/PanelQueryEditor.tsx | 25 --- ...ryEditor.test.tsx => QueryEditor.test.tsx} | 153 ++++++++++++++++-- .../cloudwatch/components/QueryEditor.tsx | 55 +++++++ .../components/QueryHeader.test.tsx | 77 +-------- .../cloudwatch/components/QueryHeader.tsx | 121 ++++++++------ .../SQLBuilderEditor.test.tsx | 1 - .../SQLBuilderEditor/SQLBuilderEditor.tsx | 6 +- .../cloudwatch/components/SQLCodeEditor.tsx | 6 +- .../datasource/cloudwatch/components/index.ts | 3 +- .../datasource/cloudwatch/datasource.ts | 2 +- .../plugins/datasource/cloudwatch/module.tsx | 4 +- .../plugins/datasource/cloudwatch/types.ts | 6 +- 32 files changed, 404 insertions(+), 640 deletions(-) delete mode 100644 public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.test.tsx delete mode 100644 public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.tsx delete mode 100644 public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx rename public/app/plugins/datasource/cloudwatch/components/{PanelQueryEditor.test.tsx => QueryEditor.test.tsx} (52%) create mode 100644 public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx diff --git a/.betterer.results b/.betterer.results index b329dcfffa5..fafff607bb6 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5346,8 +5346,7 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -5361,14 +5360,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx:5381": [ + "public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts index ecfd25d3f1b..00c5d4c0d18 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts @@ -38,32 +38,30 @@ export const validMetricSearchBuilderQuery: CloudWatchMetricsQuery = { }; export const validMetricQueryBuilderQuery: CloudWatchMetricsQuery = { - id: '', queryMode: 'Metrics', - region: 'us-east-2', - namespace: 'AWS/EC2', - period: '3000', - alias: '', - metricName: 'CPUUtilization', - dimensions: { InstanceId: 'i-123' }, - matchExact: true, - statistic: 'Average', + refId: '', + id: '', + region: 'us-east-1', + namespace: 'ec2', + dimensions: { somekey: 'somevalue' }, + metricQueryType: MetricQueryType.Query, + metricEditorMode: MetricEditorMode.Builder, sql: { - select: { + from: { type: QueryEditorExpressionType.Function, - name: 'AVERAGE', + name: 'SCHEMA', parameters: [ { type: QueryEditorExpressionType.FunctionParameter, - name: 'CPUUtilization', + name: 'AWS/EC2', + }, + { + type: QueryEditorExpressionType.FunctionParameter, + name: 'InstanceId', }, ], }, }, - refId: 'A', - metricQueryType: MetricQueryType.Query, - metricEditorMode: MetricEditorMode.Builder, - hide: false, }; export const validMetricQueryCodeQuery: CloudWatchMetricsQuery = { diff --git a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx index 6d52f7d2728..9d6f8405d47 100644 --- a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx @@ -45,7 +45,6 @@ export const AnnotationQueryEditor = (props: Props) => { metricStat={query} disableExpressions={true} onChange={(metricStat: MetricStat) => onChange({ ...query, ...metricStat })} - onRunQuery={() => {}} > diff --git a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx index 98ab3909d11..5dc04fc59fb 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx @@ -35,7 +35,6 @@ const defaultProps = { }, ]), onChange: jest.fn(), - onRunQuery: jest.fn(), }; const originalDebounce = lodash.debounce; @@ -178,13 +177,4 @@ describe('CrossAccountLogsQueryField', () => { }, ]); }); - - it('runs the query on close of the modal', async () => { - const onRunQuery = jest.fn(); - render(); - await userEvent.click(screen.getByText('Select Log Groups')); - expect(screen.getByText('Log Group Name')).toBeInTheDocument(); - await userEvent.click(screen.getByLabelText('Close dialogue')); - expect(onRunQuery).toBeCalledTimes(1); - }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx index 7a20f6267be..6dacc9fd6d6 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx @@ -16,7 +16,6 @@ type CrossAccountLogsQueryProps = { accountOptions: Array>; fetchLogGroups: (params: Partial) => Promise; onChange: (selectedLogGroups: SelectableResourceValue[]) => void; - onRunQuery: () => void; }; export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => { @@ -30,7 +29,6 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => const toggleModal = () => { setIsModalOpen(!isModalOpen); if (isModalOpen) { - props.onRunQuery(); } else { setSelectedLogGroups(props.selectedLogGroups); searchFn(searchPhrase, searchAccountId); diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx index 1829732b5d8..7b038b53379 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx @@ -33,7 +33,6 @@ const props = { query: q, disableExpressions: false, onChange: jest.fn(), - onRunQuery: jest.fn(), }; describe('Dimensions', () => { diff --git a/public/app/plugins/datasource/cloudwatch/components/DynamicLabelsField.tsx b/public/app/plugins/datasource/cloudwatch/components/DynamicLabelsField.tsx index 63dcc0b3370..0a69a062266 100644 --- a/public/app/plugins/datasource/cloudwatch/components/DynamicLabelsField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/DynamicLabelsField.tsx @@ -13,12 +13,11 @@ const dynamicLabelsCompletionItemProvider = new DynamicLabelsCompletionItemProvi export interface Props { onChange: (query: string) => void; - onRunQuery: () => void; label: string; width: number; } -export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props) { +export function DynamicLabelsField({ label, width, onChange }: Props) { const theme = useTheme2(); const styles = getInputStyles({ theme, width }); const containerRef = useRef(null); @@ -28,13 +27,12 @@ export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { const text = editor.getValue(); onChange(text); - onRunQuery(); }); const containerDiv = containerRef.current; containerDiv !== null && editor.layout({ width: containerDiv.clientWidth, height: containerDiv.clientHeight }); }, - [onChange, onRunQuery] + [onChange] ); return ( @@ -69,7 +67,6 @@ export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props onBlur={(value) => { if (value !== label) { onChange(value); - onRunQuery(); } }} onBeforeEditorMount={(monaco: Monaco) => diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.test.tsx index 63c4c904bbc..d68dc464452 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.test.tsx @@ -22,7 +22,6 @@ const defaultProps = { refId: '', } as CloudWatchLogsQuery, onChange: jest.fn(), - onRunQuery: jest.fn(), }; describe('LogGroupSelection', () => { beforeEach(() => { diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx index 36b0cc7ef88..bc0e633c112 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx @@ -16,14 +16,13 @@ type Props = { datasource: CloudWatchDatasource; query: CloudWatchLogsQuery; onChange: (value: CloudWatchQuery) => void; - onRunQuery: () => void; }; const rowGap = css` gap: 3px; `; -export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: Props) => { +export const LogGroupSelection = ({ datasource, query, onChange }: Props) => { const accountState = useAccountOptions(datasource.api, query.region); return ( @@ -37,7 +36,6 @@ export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: P onChange({ ...query, logGroups: selectedLogGroups, logGroupNames: [] }); }} accountOptions={accountState.value} - onRunQuery={onRunQuery} selectedLogGroups={query.logGroups ?? []} /* todo handle defaults */ /> ) : ( @@ -53,7 +51,6 @@ export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: P onChange={function (logGroupNames: string[]): void { onChange({ ...query, logGroupNames, logGroups: [] }); }} - onRunQuery={onRunQuery} refId={query.refId} /> } diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.tsx index b3aa44e9c85..29013b78047 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelector.tsx @@ -21,7 +21,6 @@ export interface LogGroupSelectorProps { onChange: (logGroups: string[]) => void; datasource?: CloudWatchDatasource; - onRunQuery?: () => void; onOpenMenu?: () => Promise; refId?: string; width?: number | 'auto'; @@ -33,7 +32,6 @@ export const LogGroupSelector: React.FC = ({ selectedLogGroups, onChange, datasource, - onRunQuery, onOpenMenu, refId, width, @@ -135,7 +133,6 @@ export const LogGroupSelector: React.FC = ({ options={datasource ? appendTemplateVariables(datasource, logGroupOptions) : logGroupOptions} value={selectedLogGroups} onChange={(v) => onChange(v.filter(({ value }) => value).map(({ value }) => value))} - onBlur={onRunQuery} closeMenuOnSelect={false} isClearable isOptionDisabled={() => selectedLogGroups.length >= MAX_LOG_GROUPS} diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx index 69dfe3077f9..69f35a5dd64 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/css'; import React, { memo } from 'react'; -// Types import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data'; import { InlineFormLabel } from '@grafana/ui'; @@ -22,7 +21,7 @@ const labelClass = css` `; export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) { - const { query, data, datasource, onRunQuery, onChange, exploreId } = props; + const { query, data, datasource, exploreId } = props; let absolute: AbsoluteTimeRange; if (data?.request?.range?.from) { @@ -40,13 +39,9 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor return ( diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx index 099b153f8bf..95fdb2f42d8 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx @@ -1,7 +1,6 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope import React from 'react'; -import { act } from 'react-dom/test-utils'; import { ExploreId } from '../../../../types'; import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; @@ -13,28 +12,6 @@ jest .mockImplementation((func: (...args: any) => any, wait?: number) => func as DebouncedFunc); describe('CloudWatchLogsQueryField', () => { - it('runs onRunQuery on blur of Log Groups', async () => { - const onRunQuery = jest.fn(); - const ds = setupMockedDataSource(); - - render( - {}} - /> - ); - - const multiSelect = screen.getByLabelText('Log Groups'); - await act(async () => { - fireEvent.blur(multiSelect); - }); - expect(onRunQuery).toHaveBeenCalled(); - }); - it('loads defaultLogGroups', async () => { const onRunQuery = jest.fn(); const ds = setupMockedDataSource(); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index 77c0fc7d7f2..b1f09053028 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -24,7 +24,6 @@ import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../typ import { getStatsGroups } from '../utils/query/getStatsGroups'; import { LogGroupSelection } from './LogGroupSelection'; -import QueryHeader from './QueryHeader'; export interface CloudWatchLogsQueryFieldProps extends QueryEditorProps, @@ -46,7 +45,7 @@ const plugins: Array> = [ ), ]; export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => { - const { query, datasource, onChange, onRunQuery, ExtraFieldElement, data } = props; + const { query, datasource, onChange, ExtraFieldElement, data } = props; const showError = data?.error?.refId === query.refId; const cleanText = datasource.languageProvider.cleanText; @@ -85,21 +84,13 @@ export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) = return ( <> - - +
void; - onRunQuery: () => void; expression: string; datasource: CloudWatchDatasource; } -export function MathExpressionQueryField({ - expression: query, - onChange, - onRunQuery, - datasource, -}: React.PropsWithChildren) { +export function MathExpressionQueryField({ expression: query, onChange, datasource }: React.PropsWithChildren) { const containerRef = useRef(null); const onEditorMount = useCallback( (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { @@ -28,7 +22,6 @@ export function MathExpressionQueryField({ editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { const text = editor.getValue(); onChange(text); - onRunQuery(); }); // auto resizes the editor to be the height of the content it holds @@ -48,7 +41,7 @@ export function MathExpressionQueryField({ editor.onDidContentSizeChange(updateElementHeight); updateElementHeight(); }, - [onChange, onRunQuery] + [onChange] ); return ( @@ -77,7 +70,6 @@ export function MathExpressionQueryField({ onBlur={(value) => { if (value !== query) { onChange(value); - onRunQuery(); } }} onBeforeEditorMount={(monaco: Monaco) => diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx index e97032161c8..815cbdda001 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx @@ -33,7 +33,6 @@ const props = { datasource: ds.datasource, metricStat, onChange: jest.fn(), - onRunQuery: jest.fn(), }; describe('MetricStatEditor', () => { @@ -43,10 +42,9 @@ describe('MetricStatEditor', () => { describe('statistics field', () => { test.each([['Average', 'p23.23', 'p34', '$statistic']])('should accept valid values', async (statistic) => { const onChange = jest.fn(); - const onRunQuery = jest.fn(); props.datasource.getVariables = jest.fn().mockReturnValue(['$statistic']); - render(); + render(); const statisticElement = await screen.findByLabelText('Statistic'); expect(statisticElement).toBeInTheDocument(); @@ -54,14 +52,12 @@ describe('MetricStatEditor', () => { await userEvent.type(statisticElement, statistic); fireEvent.keyDown(statisticElement, { keyCode: 13 }); expect(onChange).toHaveBeenCalledWith({ ...props.metricStat, statistic }); - expect(onRunQuery).toHaveBeenCalled(); }); test.each([['CustomStat', 'p23,23', '$statistic']])('should not accept invalid values', async (statistic) => { const onChange = jest.fn(); - const onRunQuery = jest.fn(); - render(); + render(); const statisticElement = await screen.findByLabelText('Statistic'); expect(statisticElement).toBeInTheDocument(); @@ -69,7 +65,6 @@ describe('MetricStatEditor', () => { await userEvent.type(statisticElement, statistic); fireEvent.keyDown(statisticElement, { keyCode: 13 }); expect(onChange).not.toHaveBeenCalled(); - expect(onRunQuery).not.toHaveBeenCalled(); }); }); @@ -120,18 +115,15 @@ describe('MetricStatEditor', () => { { value: 'm2', label: 'm2', text: 'm2' }, ]; const onChange = jest.fn(); - const onRunQuery = jest.fn(); const propsNamespaceMetrics = { ...props, onChange, - onRunQuery, }; beforeEach(() => { propsNamespaceMetrics.datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces); propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics); onChange.mockClear(); - onRunQuery.mockClear(); }); it('should select namespace and metric name correctly', async () => { @@ -151,7 +143,6 @@ describe('MetricStatEditor', () => { [{ ...propsNamespaceMetrics.metricStat, namespace: 'n1' }], // First call, namespace select [{ ...propsNamespaceMetrics.metricStat, metricName: 'm1' }], // Second call, metric select ]); - expect(onRunQuery).toHaveBeenCalledTimes(2); }); it('should remove metricName from metricStat if it does not exist in new namespace', async () => { diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx index 72fc3b17817..fe26e36cf6a 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx @@ -19,7 +19,6 @@ export type Props = { datasource: CloudWatchDatasource; disableExpressions?: boolean; onChange: (value: MetricStat) => void; - onRunQuery: () => void; }; export function MetricStatEditor({ @@ -28,7 +27,6 @@ export function MetricStatEditor({ datasource, disableExpressions = false, onChange, - onRunQuery, }: React.PropsWithChildren) { const namespaces = useNamespaces(datasource); const metrics = useMetrics(datasource, metricStat); @@ -47,14 +45,9 @@ export function MetricStatEditor({ }); }, [accountState, metricStat, onChange, datasource.api]); - const onMetricStatChange = (metricStat: MetricStat) => { - onChange(metricStat); - onRunQuery(); - }; - const onNamespaceChange = async (metricStat: MetricStat) => { const validatedQuery = await validateMetricName(metricStat); - onMetricStatChange(validatedQuery); + onChange(validatedQuery); }; const validateMetricName = async (metricStat: MetricStat) => { @@ -78,7 +71,6 @@ export function MetricStatEditor({ accountId={metricStat.accountId} onChange={(accountId?: string) => { onChange({ ...metricStat, accountId }); - onRunQuery(); }} accountOptions={accountState?.value || []} > @@ -105,7 +97,7 @@ export function MetricStatEditor({ options={metrics} onChange={({ value: metricName }) => { if (metricName) { - onMetricStatChange({ ...metricStat, metricName }); + onChange({ ...metricStat, metricName }); } }} /> @@ -130,7 +122,7 @@ export function MetricStatEditor({ return; } - onMetricStatChange({ ...metricStat, statistic }); + onChange({ ...metricStat, statistic }); }} /> @@ -141,7 +133,7 @@ export function MetricStatEditor({ onMetricStatChange({ ...metricStat, dimensions })} + onChange={(dimensions) => onChange({ ...metricStat, dimensions })} dimensionKeys={dimensionKeys} disableExpressions={disableExpressions} datasource={datasource} @@ -157,7 +149,7 @@ export function MetricStatEditor({ id={`${refId}-cloudwatch-match-exact`} value={!!metricStat.matchExact} onChange={(e) => { - onMetricStatChange({ + onChange({ ...metricStat, matchExact: e.currentTarget.checked, }); diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index 0b8b92f488f..bac03da437f 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -69,6 +69,8 @@ const setup = () => { metricQueryType: MetricQueryType.Search, metricEditorMode: MetricEditorMode.Builder, }, + extraHeaderElementLeft: () => {}, + extraHeaderElementRight: () => {}, datasource, history: [], onChange: jest.fn(), @@ -79,66 +81,6 @@ const setup = () => { }; describe('QueryEditor', () => { - describe('should handle editor modes correctly', () => { - it('when metric query type is metric search and editor mode is builder', async () => { - await act(async () => { - const props = setup(); - render(); - - expect(screen.getByText('Metric Search')).toBeInTheDocument(); - const radio = screen.getByLabelText('Builder'); - expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); - }); - }); - - it('when metric query type is metric search and editor mode is raw', async () => { - await act(async () => { - const props = setup(); - if (props.query.queryMode !== 'Metrics') { - fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`); - } - props.query.metricEditorMode = MetricEditorMode.Code; - render(); - - expect(screen.getByText('Metric Search')).toBeInTheDocument(); - const radio = screen.getByLabelText('Code'); - expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); - }); - }); - - it('when metric query type is metric query and editor mode is builder', async () => { - await act(async () => { - const props = setup(); - if (props.query.queryMode !== 'Metrics') { - fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`); - } - props.query.metricQueryType = MetricQueryType.Query; - props.query.metricEditorMode = MetricEditorMode.Builder; - render(); - - expect(screen.getByText('Metric Query')).toBeInTheDocument(); - const radio = screen.getByLabelText('Builder'); - expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); - }); - }); - - it('when metric query type is metric query and editor mode is raw', async () => { - await act(async () => { - const props = setup(); - if (props.query.queryMode !== 'Metrics') { - fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`); - } - props.query.metricQueryType = MetricQueryType.Query; - props.query.metricEditorMode = MetricEditorMode.Code; - render(); - - expect(screen.getByText('Metric Query')).toBeInTheDocument(); - const radio = screen.getByLabelText('Code'); - expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); - }); - }); - }); - describe('should handle expression options correctly', () => { it('should display match exact switch', async () => { const props = setup(); diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 0a389fd6484..eb4b90524aa 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -1,13 +1,12 @@ -import React, { ChangeEvent, useState } from 'react'; +import React, { ChangeEvent, useCallback, useEffect, useState } from 'react'; -import { QueryEditorProps } from '@grafana/data'; -import { EditorField, EditorRow, Space } from '@grafana/experimental'; +import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { EditorField, EditorRow, InlineSelect, Space } from '@grafana/experimental'; import { config } from '@grafana/runtime'; -import { Input } from '@grafana/ui'; +import { ConfirmModal, Input, RadioButtonGroup } from '@grafana/ui'; -import { MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from '../'; +import { MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor } from '../'; import { CloudWatchDatasource } from '../../datasource'; -import { isCloudWatchMetricsQuery } from '../../guards'; import useMigratedMetricsQuery from '../../migrations/useMigratedMetricsQuery'; import { CloudWatchJsonData, @@ -18,39 +17,99 @@ import { MetricStat, } from '../../types'; import { DynamicLabelsField } from '../DynamicLabelsField'; -import QueryHeader from '../QueryHeader'; +import { SQLCodeEditor } from '../SQLCodeEditor'; import { Alias } from './Alias'; export interface Props extends QueryEditorProps { query: CloudWatchMetricsQuery; + extraHeaderElementLeft?: React.Dispatch; + extraHeaderElementRight?: React.Dispatch; } +const metricEditorModes: Array> = [ + { label: 'Metric Search', value: MetricQueryType.Search }, + { label: 'Metric Query', value: MetricQueryType.Query }, +]; +const editorModes = [ + { label: 'Builder', value: MetricEditorMode.Builder }, + { label: 'Code', value: MetricEditorMode.Code }, +]; + export const MetricsQueryEditor = (props: Props) => { - const { query, onRunQuery, datasource } = props; + const { query, datasource, extraHeaderElementLeft, extraHeaderElementRight, onChange } = props; + const [showConfirm, setShowConfirm] = useState(false); const [sqlCodeEditorIsDirty, setSQLCodeEditorIsDirty] = useState(false); const migratedQuery = useMigratedMetricsQuery(query, props.onChange); - const onChange = (query: CloudWatchQuery) => { - const { onChange, onRunQuery } = props; - onChange(query); - onRunQuery(); - }; + const onEditorModeChange = useCallback( + (newMetricEditorMode: MetricEditorMode) => { + if ( + sqlCodeEditorIsDirty && + query.metricQueryType === MetricQueryType.Query && + query.metricEditorMode === MetricEditorMode.Code + ) { + setShowConfirm(true); + return; + } + onChange({ ...query, metricEditorMode: newMetricEditorMode }); + }, + [setShowConfirm, onChange, sqlCodeEditorIsDirty, query] + ); + + useEffect(() => { + extraHeaderElementLeft?.( + m.value === query.metricQueryType)} + options={metricEditorModes} + onChange={({ value }) => { + onChange({ ...query, metricQueryType: value }); + }} + /> + ); + + extraHeaderElementRight?.( + <> + + { + setShowConfirm(false); + onChange({ ...query, metricEditorMode: MetricEditorMode.Builder }); + }} + onDismiss={() => setShowConfirm(false)} + /> + + ); + + return () => { + extraHeaderElementLeft?.(undefined); + extraHeaderElementRight?.(undefined); + }; + }, [ + query, + sqlCodeEditorIsDirty, + datasource, + onChange, + extraHeaderElementLeft, + extraHeaderElementRight, + showConfirm, + onEditorModeChange, + ]); return ( <> - { - if (isCloudWatchMetricsQuery(newQuery) && newQuery.metricEditorMode !== query.metricEditorMode) { - setSQLCodeEditorIsDirty(false); - } - onChange(newQuery); - }} - sqlCodeEditorIsDirty={sqlCodeEditorIsDirty} - /> {query.metricQueryType === MetricQueryType.Search && ( @@ -65,7 +124,6 @@ export const MetricsQueryEditor = (props: Props) => { )} {query.metricEditorMode === MetricEditorMode.Code && ( props.onChange({ ...query, expression })} datasource={datasource} @@ -85,19 +143,13 @@ export const MetricsQueryEditor = (props: Props) => { } props.onChange({ ...migratedQuery, sqlExpression }); }} - onRunQuery={onRunQuery} datasource={datasource} /> )} {query.metricEditorMode === MetricEditorMode.Builder && ( <> - + )} @@ -113,7 +165,6 @@ export const MetricsQueryEditor = (props: Props) => { > ) => onChange({ ...migratedQuery, id: event.target.value })} type="text" value={query.id} @@ -125,7 +176,6 @@ export const MetricsQueryEditor = (props: Props) => { id={`${query.refId}-cloudwatch-metric-query-editor-period`} value={query.period || ''} placeholder="auto" - onBlur={onRunQuery} onChange={(event: ChangeEvent) => onChange({ ...migratedQuery, period: event.target.value }) } @@ -141,7 +191,6 @@ export const MetricsQueryEditor = (props: Props) => { > props.onChange({ ...query, label })} > diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.test.tsx deleted file mode 100644 index 1f0d639620e..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; -import { act } from 'react-dom/test-utils'; - -import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; -import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../../types'; - -import MetricsQueryHeader from './MetricsQueryHeader'; - -const ds = setupMockedDataSource({ - variables: [], -}); -ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]); -const query: CloudWatchMetricsQuery = { - id: '', - region: 'us-east-2', - namespace: '', - period: '', - alias: '', - metricName: '', - dimensions: {}, - matchExact: true, - statistic: '', - expression: '', - refId: '', -}; - -describe('MetricsQueryHeader', () => { - describe('confirm modal', () => { - it('should be shown when moving from code editor to builder when in sql mode', async () => { - const onChange = jest.fn(); - const onRunQuery = jest.fn(); - query.metricEditorMode = MetricEditorMode.Code; - query.metricQueryType = MetricQueryType.Query; - - render( - - ); - - const builderElement = screen.getByLabelText('Builder'); - expect(builderElement).toBeInTheDocument(); - await act(async () => { - await builderElement.click(); - }); - - const modalTitleElem = screen.getByText('Are you sure?'); - expect(modalTitleElem).toBeInTheDocument(); - expect(onChange).not.toHaveBeenCalled(); - }); - - it('should not be shown when moving from builder to code when in sql mode', async () => { - const onChange = jest.fn(); - const onRunQuery = jest.fn(); - query.metricEditorMode = MetricEditorMode.Builder; - query.metricQueryType = MetricQueryType.Query; - - render( - - ); - - const builderElement = screen.getByLabelText('Code'); - expect(builderElement).toBeInTheDocument(); - await act(async () => { - await builderElement.click(); - }); - - const modalTitleElem = screen.queryByText('Are you sure?'); - expect(modalTitleElem).toBeNull(); - expect(onChange).toHaveBeenCalled(); - }); - - it('should not be shown when moving from code to builder when in standard mode', async () => { - const onChange = jest.fn(); - const onRunQuery = jest.fn(); - query.metricEditorMode = MetricEditorMode.Code; - query.metricQueryType = MetricQueryType.Search; - - render( - - ); - - const builderElement = screen.getByLabelText('Builder'); - expect(builderElement).toBeInTheDocument(); - await act(async () => { - await builderElement.click(); - }); - - const modalTitleElem = screen.queryByText('Are you sure?'); - expect(modalTitleElem).toBeNull(); - expect(onChange).toHaveBeenCalled(); - }); - }); - - it('should call run query when run button is clicked when in metric query mode', async () => { - const onChange = jest.fn(); - const onRunQuery = jest.fn(); - query.metricEditorMode = MetricEditorMode.Code; - query.metricQueryType = MetricQueryType.Query; - - render( - - ); - - const runQueryButton = screen.getByText('Run query'); - expect(runQueryButton).toBeInTheDocument(); - await act(async () => { - await runQueryButton.click(); - }); - expect(onRunQuery).toHaveBeenCalled(); - }); -}); diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.tsx deleted file mode 100644 index 6520db778ae..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useCallback, useState } from 'react'; - -import { SelectableValue } from '@grafana/data'; -import { FlexItem, InlineSelect } from '@grafana/experimental'; -import { config } from '@grafana/runtime'; -import { Badge, Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui'; - -import { CloudWatchDatasource } from '../../datasource'; -import { CloudWatchMetricsQuery, CloudWatchQuery, MetricEditorMode, MetricQueryType } from '../../types'; - -interface MetricsQueryHeaderProps { - query: CloudWatchMetricsQuery; - datasource: CloudWatchDatasource; - onChange: (query: CloudWatchQuery) => void; - onRunQuery: () => void; - sqlCodeEditorIsDirty: boolean; - isMonitoringAccount: boolean; -} - -const metricEditorModes: Array> = [ - { label: 'Metric Search', value: MetricQueryType.Search }, - { label: 'Metric Query', value: MetricQueryType.Query }, -]; - -const editorModes = [ - { label: 'Builder', value: MetricEditorMode.Builder }, - { label: 'Code', value: MetricEditorMode.Code }, -]; - -const MetricsQueryHeader: React.FC = ({ - query, - sqlCodeEditorIsDirty, - onChange, - onRunQuery, - isMonitoringAccount, -}) => { - const { metricEditorMode, metricQueryType } = query; - const [showConfirm, setShowConfirm] = useState(false); - - const onEditorModeChange = useCallback( - (newMetricEditorMode: MetricEditorMode) => { - if ( - sqlCodeEditorIsDirty && - metricQueryType === MetricQueryType.Query && - metricEditorMode === MetricEditorMode.Code - ) { - setShowConfirm(true); - return; - } - onChange({ ...query, metricEditorMode: newMetricEditorMode }); - }, - [setShowConfirm, onChange, sqlCodeEditorIsDirty, query, metricEditorMode, metricQueryType] - ); - - const shouldDisplayMonitoringBadge = - query.metricQueryType === MetricQueryType.Search && - isMonitoringAccount && - config.featureToggles.cloudWatchCrossAccountQuerying; - - return ( - <> - m.value === metricQueryType)} - options={metricEditorModes} - onChange={({ value }) => { - onChange({ ...query, metricQueryType: value }); - }} - /> - - - {shouldDisplayMonitoringBadge && ( - - )} - - - - {query.metricQueryType === MetricQueryType.Query && query.metricEditorMode === MetricEditorMode.Code && ( - - )} - - { - setShowConfirm(false); - onChange({ ...query, metricEditorMode: MetricEditorMode.Builder }); - }} - onDismiss={() => setShowConfirm(false)} - /> - - ); -}; - -export default MetricsQueryHeader; diff --git a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx deleted file mode 100644 index ef9979991f1..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { PureComponent } from 'react'; - -import { QueryEditorProps } from '@grafana/data'; - -import { CloudWatchDatasource } from '../datasource'; -import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../guards'; -import { CloudWatchJsonData, CloudWatchQuery } from '../types'; - -import { MetricsQueryEditor } from '././MetricsQueryEditor/MetricsQueryEditor'; -import LogsQueryEditor from './LogsQueryEditor'; - -export type Props = QueryEditorProps; - -export class PanelQueryEditor extends PureComponent { - render() { - const { query } = this.props; - - return ( - <> - {isCloudWatchMetricsQuery(query) && } - {isCloudWatchLogsQuery(query) && } - - ); - } -} diff --git a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx similarity index 52% rename from public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx rename to public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx index 0a62600b4d2..c0d12af2044 100644 --- a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { QueryEditorProps } from '@grafana/data'; @@ -15,7 +15,7 @@ import { import { CloudWatchDatasource } from '../datasource'; import { CloudWatchQuery, CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types'; -import { PanelQueryEditor } from './PanelQueryEditor'; +import { QueryEditor } from './QueryEditor'; // the following three fields are added to legacy queries in the dashboard migrator const migratedFields = { @@ -31,7 +31,22 @@ const props: QueryEditorProps { +const FAKE_EDITOR_LABEL = 'FakeEditor'; + +jest.mock('./SQLCodeEditor', () => ({ + SQLCodeEditor: ({ sql, onChange }: { sql: string; onChange: (val: string) => void }) => { + return ( + <> + + onChange(e.currentTarget.value)}> + + ); + }, +})); + +export { SQLCodeEditor } from './SQLCodeEditor'; + +describe('QueryEditor should render right editor', () => { describe('when using grafana 6.3.0 metric query', () => { it('should render the metrics query editor', async () => { const query = { @@ -50,7 +65,7 @@ describe('PanelQueryEditor should render right editor', () => { returnData: false, }; await act(async () => { - render(); + render(); }); expect(screen.getByText('Metric name')).toBeInTheDocument(); }); @@ -78,7 +93,7 @@ describe('PanelQueryEditor should render right editor', () => { statistics: 'Average', } as any; await act(async () => { - render(); + render(); }); expect(screen.getByText('Choose Log Groups')).toBeInTheDocument(); }); @@ -106,7 +121,7 @@ describe('PanelQueryEditor should render right editor', () => { statistic: 'Average', } as any; await act(async () => { - render(); + render(); }); expect(screen.getByText('Log Groups')).toBeInTheDocument(); }); @@ -133,7 +148,7 @@ describe('PanelQueryEditor should render right editor', () => { statistic: 'Average', } as any; await act(async () => { - render(); + render(); }); expect(screen.getByText('Metric name')).toBeInTheDocument(); }); @@ -177,7 +192,7 @@ describe('PanelQueryEditor should render right editor', () => { test.each(cases)('$name', async ({ query, toggle }) => { config.featureToggles.cloudWatchCrossAccountQuerying = toggle; await act(async () => { - render(); + render(); }); expect(await screen.getByText('Monitoring account')).toBeInTheDocument(); }); @@ -193,7 +208,7 @@ describe('PanelQueryEditor should render right editor', () => { { name: 'it is metric query code query and toggle is not enabled', query: validMetricQueryCodeQuery, - toggle: true, + toggle: false, }, { name: 'it is logs query and feature is not enabled', query: validLogsQuery, toggle: false }, { @@ -210,10 +225,128 @@ describe('PanelQueryEditor should render right editor', () => { test.each(cases)('$name', async ({ query, toggle }) => { config.featureToggles.cloudWatchCrossAccountQuerying = toggle; await act(async () => { - render(); + render(); }); expect(await screen.queryByText('Monitoring account')).toBeNull(); }); }); }); + + describe('QueryHeader', () => { + it('should display metric actions in header when metric query is used', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByLabelText(/Region.*/)).toBeInTheDocument(); + expect(screen.getByText('CloudWatch Metrics')).toBeInTheDocument(); + expect(screen.getByLabelText('Builder')).toBeInTheDocument(); + expect(screen.getByLabelText('Code')).toBeInTheDocument(); + expect(screen.getByText('Metric Query')).toBeInTheDocument(); + }); + + it('should display metric actions in header when metric query is used', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByLabelText(/Region.*/)).toBeInTheDocument(); + expect(screen.getByText('CloudWatch Logs')).toBeInTheDocument(); + expect(screen.queryByLabelText('Builder')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Code')).not.toBeInTheDocument(); + expect(screen.queryByText('Metric Query')).not.toBeInTheDocument(); + }); + }); + + describe('metrics editor should handle editor modes correctly', () => { + it('when metric query type is metric search and editor mode is builder', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByText('Metric Search')).toBeInTheDocument(); + const radio = screen.getByLabelText('Builder'); + expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); + }); + + it('when metric query type is metric search and editor mode is raw', async () => { + await act(async () => { + render(); + }); + expect(screen.getByText('Metric Search')).toBeInTheDocument(); + const radio = screen.getByLabelText('Code'); + expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); + }); + + it('when metric query type is metric query and editor mode is builder', async () => { + await act(async () => { + await act(async () => { + render(); + }); + + expect(screen.getByText('Metric Query')).toBeInTheDocument(); + const radio = screen.getByLabelText('Builder'); + expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); + }); + }); + + it('when metric query type is metric query and editor mode is raw', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByText('Metric Query')).toBeInTheDocument(); + const radio = screen.getByLabelText('Code'); + expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy(); + }); + }); + + describe('confirm modal', () => { + it('should be shown when moving from code editor to builder when in sql mode', async () => { + const sqlQuery = 'SELECT * FROM test'; + render( + + ); + + // the modal should not be shown unless the code editor is "dirty", so need to trigger a change + const codeEditorElement = screen.getByLabelText(FAKE_EDITOR_LABEL); + fireEvent.change(codeEditorElement, { target: { value: 'select * from ' } }); + const builderElement = screen.getByLabelText('Builder'); + expect(builderElement).toBeInTheDocument(); + await act(async () => { + await builderElement.click(); + }); + + const modalTitleElem = screen.getByText('Are you sure?'); + expect(modalTitleElem).toBeInTheDocument(); + }); + + it('should not be shown when moving from builder to code when in sql mode', async () => { + render( + + ); + const builderElement = screen.getByLabelText('Builder'); + expect(builderElement).toBeInTheDocument(); + await act(async () => { + await builderElement.click(); + }); + expect(screen.queryByText('Are you sure?')).toBeNull(); + }); + + it('should not be shown when moving from code to builder when in search mode', async () => { + render(); + + const builderElement = screen.getByLabelText('Builder'); + expect(builderElement).toBeInTheDocument(); + await act(async () => { + await builderElement.click(); + }); + expect(screen.queryByText('Are you sure?')).toBeNull(); + }); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx new file mode 100644 index 00000000000..bb5fc239f1c --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx @@ -0,0 +1,55 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import { QueryEditorProps } from '@grafana/data'; + +import { CloudWatchDatasource } from '../datasource'; +import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../guards'; +import { CloudWatchJsonData, CloudWatchQuery } from '../types'; + +import LogsQueryEditor from './LogsQueryEditor'; +import { MetricsQueryEditor } from './MetricsQueryEditor/MetricsQueryEditor'; +import QueryHeader from './QueryHeader'; + +export type Props = QueryEditorProps; + +export const QueryEditor = (props: Props) => { + const { query, onChange, data } = props; + const [dataIsStale, setDataIsStale] = useState(false); + const [extraHeaderElementLeft, setExtraHeaderElementLeft] = useState(); + const [extraHeaderElementRight, setExtraHeaderElementRight] = useState(); + + useEffect(() => { + setDataIsStale(false); + }, [data]); + + const onChangeInternal = useCallback( + (query: CloudWatchQuery) => { + setDataIsStale(true); + onChange(query); + }, + [onChange] + ); + + return ( + <> + + + {isCloudWatchMetricsQuery(query) && ( + {}} + onChange={onChangeInternal} + extraHeaderElementLeft={setExtraHeaderElementLeft} + extraHeaderElementRight={setExtraHeaderElementRight} + /> + )} + {isCloudWatchLogsQuery(query) && } + + ); +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx index 7a2a0b10b40..3466b9fcae0 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx @@ -6,7 +6,6 @@ import { config } from '@grafana/runtime'; import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; import { validLogsQuery, validMetricSearchBuilderQuery } from '../__mocks__/queries'; -import { CloudWatchLogsQuery, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types'; import QueryHeader from './QueryHeader'; @@ -20,74 +19,6 @@ describe('QueryHeader', () => { afterEach(() => { config.featureToggles.cloudWatchCrossAccountQuerying = originalFeatureToggleValue; }); - it('should display metric options for metrics', async () => { - const query: CloudWatchMetricsQuery = { - queryMode: 'Metrics', - id: '', - region: 'us-east-2', - namespace: '', - period: '', - alias: '', - metricName: '', - dimensions: {}, - matchExact: true, - statistic: '', - expression: '', - refId: '', - }; - const onChange = jest.fn(); - const onRunQuery = jest.fn(); - query.metricEditorMode = MetricEditorMode.Code; - query.metricQueryType = MetricQueryType.Query; - - render( - - ); - - const builderElement = screen.getByLabelText('Builder'); - expect(builderElement).toBeInTheDocument(); - await act(async () => { - await builderElement.click(); - }); - - const modalTitleElem = screen.getByText('Are you sure?'); - expect(modalTitleElem).toBeInTheDocument(); - expect(onChange).not.toHaveBeenCalled(); - }); - - it('should not display metric options for logs', async () => { - const onChange = jest.fn(); - const onRunQuery = jest.fn(); - const query: CloudWatchLogsQuery = { - queryType: 'Metrics', - id: '', - region: 'us-east-2', - expression: '', - refId: '', - queryMode: 'Logs', - }; - - render( - - ); - - await waitFor(() => { - expect(screen.queryByLabelText('Builder')).toBeNull(); - expect(screen.queryByLabelText('Code')).toBeNull(); - }); - }); describe('when changing region', () => { const { datasource } = setupMockedDataSource(); @@ -101,11 +32,11 @@ describe('QueryHeader', () => { datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); render( ); await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument()); @@ -126,11 +57,11 @@ describe('QueryHeader', () => { render( ); await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument()); @@ -151,7 +82,7 @@ describe('QueryHeader', () => { render( { render( void; - onRunQuery: () => void; - sqlCodeEditorIsDirty: boolean; +export interface Props extends QueryEditorProps { + extraHeaderElementLeft?: JSX.Element; + extraHeaderElementRight?: JSX.Element; + dataIsStale: boolean; } const apiModes: Array> = [ @@ -25,18 +21,27 @@ const apiModes: Array> = [ { label: 'CloudWatch Logs', value: 'Logs' }, ]; -const QueryHeader: React.FC = ({ query, sqlCodeEditorIsDirty, datasource, onChange, onRunQuery }) => { +const QueryHeader: React.FC = ({ + query, + onChange, + datasource, + extraHeaderElementLeft, + extraHeaderElementRight, + dataIsStale, + data, + onRunQuery, +}) => { const { queryMode, region } = query; const isMonitoringAccount = useIsMonitoringAccount(datasource.api, query.region); - const [regions, regionIsLoading] = useRegions(datasource); const onQueryModeChange = ({ value }: SelectableValue) => { - if (value !== queryMode) { + if (value && value !== queryMode) { onChange({ + ...datasource.getDefaultQuery(CoreApp.Unknown), ...query, queryMode: value, - } as CloudWatchQuery); + }); } }; const onRegionChange = async (region: string) => { @@ -49,44 +54,60 @@ const QueryHeader: React.FC = ({ query, sqlCodeEditorIsDirty, }; const shouldDisplayMonitoringBadge = - queryMode === 'Logs' && isMonitoringAccount && config.featureToggles.cloudWatchCrossAccountQuerying; + config.featureToggles.cloudWatchCrossAccountQuerying && + isMonitoringAccount && + (query.queryMode === 'Logs' || + (isCloudWatchMetricsQuery(query) && query.metricQueryType === MetricQueryType.Search)); return ( - - region && onRegionChange(region)} - options={regions} - isLoading={regionIsLoading} - /> - - - - {shouldDisplayMonitoringBadge && ( - <> - - - - )} - - {queryMode === ExploreMode.Metrics && ( - + + region && onRegionChange(region)} + options={regions} + isLoading={regionIsLoading} /> - )} - + + + + {extraHeaderElementLeft} + + + + {shouldDisplayMonitoringBadge && ( + <> + + + )} + + + + {extraHeaderElementRight} + + ); }; diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx index fa4060aaa12..bffc5440a59 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx @@ -32,7 +32,6 @@ describe('Cloudwatch SQLBuilderEditor', () => { query: makeSQLQuery(), datasource, onChange: () => {}, - onRunQuery: () => {}, }; it('Displays the namespace', async () => { diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.tsx index 174fdbb5a6c..f44d1c20146 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.tsx @@ -17,10 +17,9 @@ export type Props = { query: CloudWatchMetricsQuery; datasource: CloudWatchDatasource; onChange: (value: CloudWatchMetricsQuery) => void; - onRunQuery: () => void; }; -export function SQLBuilderEditor({ query, datasource, onChange, onRunQuery }: React.PropsWithChildren) { +export function SQLBuilderEditor({ query, datasource, onChange }: React.PropsWithChildren) { const sql = query.sql ?? {}; const onQueryChange = useCallback( @@ -33,9 +32,8 @@ export function SQLBuilderEditor({ query, datasource, onChange, onRunQuery }: Re }; onChange(fullQuery); - onRunQuery(); }, - [onChange, onRunQuery] + [onChange] ); const [sqlPreview, setSQLPreview] = useState(); diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx index 03ca4b998fc..aaef0cc651e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx @@ -12,11 +12,10 @@ export interface Props { region: string; sql: string; onChange: (sql: string) => void; - onRunQuery: () => void; datasource: CloudWatchDatasource; } -export const SQLCodeEditor: FunctionComponent = ({ region, sql, onChange, onRunQuery, datasource }) => { +export const SQLCodeEditor: FunctionComponent = ({ region, sql, onChange, datasource }) => { useEffect(() => { datasource.sqlCompletionItemProvider.setRegion(region); }, [region, datasource]); @@ -27,10 +26,9 @@ export const SQLCodeEditor: FunctionComponent = ({ region, sql, onChange, editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { const text = editor.getValue(); onChange(text); - onRunQuery(); }); }, - [onChange, onRunQuery] + [onChange] ); return ( diff --git a/public/app/plugins/datasource/cloudwatch/components/index.ts b/public/app/plugins/datasource/cloudwatch/components/index.ts index 3c3d07f66f0..9ea328575b4 100644 --- a/public/app/plugins/datasource/cloudwatch/components/index.ts +++ b/public/app/plugins/datasource/cloudwatch/components/index.ts @@ -1,8 +1,7 @@ export { Dimensions } from './Dimensions/Dimensions'; export { QueryInlineField, QueryField } from './Forms'; -export { PanelQueryEditor } from './PanelQueryEditor'; +export { QueryEditor as PanelQueryEditor } from './QueryEditor'; export { CloudWatchLogsQueryEditor } from './LogsQueryEditor'; export { MetricStatEditor } from './MetricStatEditor'; export { SQLBuilderEditor } from './SQLBuilderEditor'; export { MathExpressionQueryField } from './MathExpressionQueryField'; -export { SQLCodeEditor } from './SQLCodeEditor'; diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 5e6cab8bd84..67d0eb62b96 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -159,7 +159,7 @@ export class CloudWatchDatasource } getQueryDisplayText(query: CloudWatchQuery) { - if (query.queryMode === 'Logs') { + if (isCloudWatchLogsQuery(query)) { return query.expression ?? ''; } else { return JSON.stringify(query); diff --git a/public/app/plugins/datasource/cloudwatch/module.tsx b/public/app/plugins/datasource/cloudwatch/module.tsx index e922010b5f7..c8a62b1a134 100644 --- a/public/app/plugins/datasource/cloudwatch/module.tsx +++ b/public/app/plugins/datasource/cloudwatch/module.tsx @@ -4,7 +4,7 @@ import { getAppEvents } from '@grafana/runtime'; import { ConfigEditor } from './components/ConfigEditor'; import LogsCheatSheet from './components/LogsCheatSheet'; import { MetaInspector } from './components/MetaInspector'; -import { PanelQueryEditor } from './components/PanelQueryEditor'; +import { QueryEditor } from './components/QueryEditor'; import { CloudWatchDatasource } from './datasource'; import { onDashboardLoadedHandler } from './tracking'; import { CloudWatchJsonData, CloudWatchQuery } from './types'; @@ -14,7 +14,7 @@ export const plugin = new DataSourcePlugin