From 6385b1f471e46c3f7e003ab463840af94a181508 Mon Sep 17 00:00:00 2001 From: Alyssa Joyner <58453566+alyssajoyner@users.noreply.github.com> Date: Wed, 14 Jan 2026 08:17:21 -0700 Subject: [PATCH] [Azure Monitor]: Preserve logs builder query when switching to KQL mode (#116161) --- .../QueryEditor/QueryHeader.test.tsx | 247 ++++++++++++++++++ .../components/QueryEditor/QueryHeader.tsx | 12 +- .../grafana-azure-monitor-datasource.json | 1 - 3 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.test.tsx diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.test.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.test.tsx new file mode 100644 index 00000000000..5b365cb98d6 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.test.tsx @@ -0,0 +1,247 @@ +import { render, screen, waitFor, cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { CoreApp, LoadingState, PanelData } from '@grafana/data'; +import { config, reportInteraction } from '@grafana/runtime'; + +import { AzureQueryType, LogsEditorMode } from '../../dataquery.gen'; +import { selectors } from '../../e2e/selectors'; +import createMockQuery from '../../mocks/query'; +import { AzureMonitorQuery } from '../../types/query'; +import { selectOptionInTest } from '../../utils/testUtils'; + +import { QueryHeader } from './QueryHeader'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), +})); + +describe('Azure Monitor QueryHeader', () => { + const setAzureLogsCheatSheetModalOpen = jest.fn(); + const onRunQuery = jest.fn(); + + const renderComponent = (query: AzureMonitorQuery, props?: Partial>) => { + return render( + + ); + }; + + beforeEach(() => { + config.featureToggles = {}; + }); + + afterEach(() => { + cleanup(); + jest.clearAllMocks(); + }); + + it('renders the service selector', async () => { + const query = createMockQuery(); + + renderComponent(query); + + expect(screen.getByTestId(selectors.components.queryEditor.header.select)).toBeInTheDocument(); + expect(screen.getByLabelText(/Service/i)).toBeInTheDocument(); + }); + + it('changes query type when a new service is selected', async () => { + const query = createMockQuery(); + const onQueryChange = jest.fn(); + + renderComponent(query, { onQueryChange }); + + const serviceSelect = await screen.findByLabelText(/Service/i); + + await selectOptionInTest(serviceSelect, 'Logs'); + + await waitFor(() => { + expect(onQueryChange).toHaveBeenCalled(); + }); + + const lastCall = onQueryChange.mock.calls[onQueryChange.mock.calls.length - 1][0]; + + expect(lastCall).toEqual( + expect.objectContaining({ + queryType: AzureQueryType.LogAnalytics, + }) + ); + }); + + it('initializes logs editor mode to Raw when a raw query exists and builder is enabled', async () => { + config.featureToggles.azureMonitorLogsBuilderEditor = true; + + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + query: 'SecurityEvent | take 10', + }, + }; + + const onQueryChange = jest.fn(); + + renderComponent(query, { onQueryChange }); + + await waitFor(() => + expect(onQueryChange).toHaveBeenCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + mode: LogsEditorMode.Raw, + }), + }) + ) + ); + }); + + it('renders the logs editor mode radio buttons when builder is enabled', async () => { + config.featureToggles.azureMonitorLogsBuilderEditor = true; + + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + mode: LogsEditorMode.Builder, + }, + }; + + renderComponent(query); + + expect(screen.getByRole('radiogroup')).toBeInTheDocument(); + + expect(screen.getByLabelText('Builder')).toBeInTheDocument(); + expect(screen.getByLabelText('KQL')).toBeInTheDocument(); + }); + + it('shows the kick start button when in Logs + Raw mode', async () => { + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + mode: LogsEditorMode.Raw, + }, + }; + + renderComponent(query); + + expect(screen.getByRole('button', { name: /Kick start your query/i })).toBeInTheDocument(); + }); + + it('opens the logs cheat sheet modal and reports interaction when kick start button is clicked', async () => { + const user = userEvent.setup(); + + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + mode: LogsEditorMode.Raw, + }, + }; + + renderComponent(query); + + await user.click(screen.getByRole('button', { name: /Kick start your query/i })); + + expect(setAzureLogsCheatSheetModalOpen).toHaveBeenCalled(); + expect(reportInteraction).toHaveBeenCalledWith( + 'grafana_azure_logs_query_patterns_opened', + expect.objectContaining({ + version: 'v2', + }) + ); + }); + + it('shows confirmation modal when switching from Raw to Builder with existing KQL', async () => { + const user = userEvent.setup(); + config.featureToggles.azureMonitorLogsBuilderEditor = true; + + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + mode: LogsEditorMode.Raw, + query: 'SecurityEvent | take 10', + }, + }; + + renderComponent(query); + + await user.click(screen.getByLabelText('Builder')); + + expect(screen.getByText(/Switch editor mode\?/i)).toBeInTheDocument(); + }); + + it('applies mode change when confirming the switch modal', async () => { + const user = userEvent.setup(); + config.featureToggles.azureMonitorLogsBuilderEditor = true; + + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + mode: LogsEditorMode.Raw, + query: 'SecurityEvent | take 10', + }, + }; + + const onQueryChange = jest.fn(); + + renderComponent(query, { onQueryChange }); + + await user.click(screen.getByLabelText('Builder')); + await user.click(screen.getByText(/Switch to Builder/i)); + + await waitFor(() => + expect(onQueryChange).toHaveBeenCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + mode: LogsEditorMode.Builder, + query: undefined, + }), + }) + ) + ); + }); + + it('renders the Run query button in Builder mode when not in Explore', async () => { + config.featureToggles.azureMonitorLogsBuilderEditor = true; + + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + mode: LogsEditorMode.Builder, + }, + }; + + renderComponent(query, { app: CoreApp.Dashboard }); + + expect(screen.getByTestId(selectors.components.queryEditor.logsQueryEditor.runQuery.button)).toBeInTheDocument(); + }); + + it('disables the Run query button spinner while loading', async () => { + config.featureToggles.azureMonitorLogsBuilderEditor = true; + + const query: AzureMonitorQuery = { + ...createMockQuery(), + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + mode: LogsEditorMode.Builder, + }, + }; + + renderComponent(query, { + app: CoreApp.Dashboard, + data: { state: LoadingState.Loading } as PanelData, + }); + + expect(screen.getByTestId(selectors.components.queryEditor.logsQueryEditor.runQuery.button)).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx index ab7c817c935..7c371b7df11 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx @@ -84,12 +84,9 @@ export const QueryHeader = ({ } const goingToBuilder = newMode === LogsEditorMode.Builder; - const goingToRaw = newMode === LogsEditorMode.Raw; - const hasRawKql = !!query.azureLogAnalytics?.query; - const hasBuilderQuery = !!query.azureLogAnalytics?.builderQuery; - if ((goingToBuilder && hasRawKql) || (goingToRaw && hasBuilderQuery)) { + if (goingToBuilder && hasRawKql) { setPendingModeChange(newMode); setShowModeSwitchWarning(true); } else { @@ -103,7 +100,7 @@ export const QueryHeader = ({ azureLogAnalytics: { ...query.azureLogAnalytics, mode, - query: '', + query: mode === LogsEditorMode.Builder ? undefined : query.azureLogAnalytics?.query, builderQuery: mode === LogsEditorMode.Raw ? undefined : query.azureLogAnalytics?.builderQuery, dashboardTime: mode === LogsEditorMode.Builder ? true : undefined, }, @@ -123,10 +120,7 @@ export const QueryHeader = ({ 'components.query-header.body-switching-to-builder', 'Switching to Builder will discard your current KQL query and clear the KQL editor. Are you sure?' ) - : t( - 'components.query-header.body-switching-to-kql', - 'Switching to KQL will discard your current builder settings. Are you sure?' - ) + : null } confirmText={t('components.query-header.confirmText-switch-to', 'Switch to {{newMode}}', { newMode: pendingModeChange === LogsEditorMode.Builder ? 'Builder' : 'KQL', diff --git a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json index 47d60ee13f7..cea54779bb1 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json @@ -204,7 +204,6 @@ "query-header": { "aria-label-kick-start": "Azure logs kick start your query button", "body-switching-to-builder": "Switching to Builder will discard your current KQL query and clear the KQL editor. Are you sure?", - "body-switching-to-kql": "Switching to KQL will discard your current builder settings. Are you sure?", "button-kick-start-your-query": "Kick start your query", "button-run-query": "Run query", "confirmText-switch-to": "Switch to {{newMode}}",