From 5e68b07cac44c9961bac14efbc6b180bb0afe7a8 Mon Sep 17 00:00:00 2001 From: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:50:35 -0600 Subject: [PATCH] Elasticsearch: Make code editor look more like prometheus (#115461) * Make code editor look more prometheus * add warning when switching builders * address adam's feedback * yarn --- .../QueryEditor/EditorTypeSelector.tsx | 27 +++++----- .../components/QueryEditor/RawQueryEditor.tsx | 50 ++++++++++++----- .../components/QueryEditor/index.tsx | 54 ++++++++++++++----- 3 files changed, 90 insertions(+), 41 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/EditorTypeSelector.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/EditorTypeSelector.tsx index c9d52ecd49d..5939f413163 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/EditorTypeSelector.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/EditorTypeSelector.tsx @@ -1,29 +1,26 @@ import { SelectableValue } from '@grafana/data'; import { RadioButtonGroup } from '@grafana/ui'; -import { useDispatch } from '../../hooks/useStatelessReducer'; import { EditorType } from '../../types'; -import { useQuery } from './ElasticsearchQueryContext'; -import { changeEditorTypeAndResetQuery } from './state'; - const BASE_OPTIONS: Array> = [ { value: 'builder', label: 'Builder' }, { value: 'code', label: 'Code' }, ]; -export const EditorTypeSelector = () => { - const query = useQuery(); - const dispatch = useDispatch(); - - // Default to 'builder' if editorType is empty - const editorType: EditorType = query.editorType === 'code' ? 'code' : 'builder'; - - const onChange = (newEditorType: EditorType) => { - dispatch(changeEditorTypeAndResetQuery(newEditorType)); - }; +interface Props { + value: EditorType; + onChange: (editorType: EditorType) => void; +} +export const EditorTypeSelector = ({ value, onChange }: Props) => { return ( - fullWidth={false} options={BASE_OPTIONS} value={editorType} onChange={onChange} /> + + data-testid="elasticsearch-editor-type-toggle" + size="sm" + options={BASE_OPTIONS} + value={value} + onChange={onChange} + /> ); }; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/RawQueryEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/RawQueryEditor.tsx index 92a5a8b0b9e..bda3cf85e76 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/RawQueryEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/RawQueryEditor.tsx @@ -10,9 +10,13 @@ interface Props { onRunQuery: () => void; } +// This offset was chosen by testing to match Prometheus behavior +const EDITOR_HEIGHT_OFFSET = 2; + export function RawQueryEditor({ value, onChange, onRunQuery }: Props) { const styles = useStyles2(getStyles); const editorRef = useRef(null); + const containerRef = useRef(null); const handleEditorDidMount = useCallback( (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => { @@ -22,6 +26,22 @@ export function RawQueryEditor({ value, onChange, onRunQuery }: Props) { editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { onRunQuery(); }); + + // Make the editor resize itself so that the content fits (grows taller when necessary) + // this code comes from the Prometheus query editor. + // We may wish to consider abstracting it into the grafana/ui repo in the future + const updateElementHeight = () => { + const containerDiv = containerRef.current; + if (containerDiv !== null) { + const pixelHeight = editor.getContentHeight(); + containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`; + const pixelWidth = containerDiv.clientWidth; + editor.layout({ width: pixelWidth, height: pixelHeight }); + } + }; + + editor.onDidContentSizeChange(updateElementHeight); + updateElementHeight(); }, [onRunQuery] ); @@ -65,7 +85,17 @@ export function RawQueryEditor({ value, onChange, onRunQuery }: Props) { return ( -
+
+ +
+
-
- ); } @@ -100,7 +118,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ flexDirection: 'column', gap: theme.spacing(1), }), - header: css({ + editorContainer: css({ + width: '100%', + overflow: 'hidden', + }), + footer: css({ display: 'flex', justifyContent: 'flex-end', padding: theme.spacing(0.5, 0), diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx index b54de44bb63..a3731c3c99a 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx @@ -1,16 +1,16 @@ import { css } from '@emotion/css'; -import { useEffect, useId, useState } from 'react'; +import { useCallback, useEffect, useId, useState } from 'react'; import { SemVer } from 'semver'; import { getDefaultTimeRange, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Alert, InlineField, InlineLabel, Input, QueryField, useStyles2 } from '@grafana/ui'; +import { Alert, ConfirmModal, InlineField, InlineLabel, Input, QueryField, useStyles2 } from '@grafana/ui'; import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { ElasticDatasource } from '../../datasource'; import { useNextId } from '../../hooks/useNextId'; import { useDispatch } from '../../hooks/useStatelessReducer'; -import { ElasticsearchOptions } from '../../types'; +import { EditorType, ElasticsearchOptions } from '../../types'; import { isSupportedVersion, isTimeSeriesQuery, unsupportedVersionMessage } from '../../utils'; import { BucketAggregationsEditor } from './BucketAggregationsEditor'; @@ -20,7 +20,7 @@ import { MetricAggregationsEditor } from './MetricAggregationsEditor'; import { metricAggregationConfig } from './MetricAggregationsEditor/utils'; import { QueryTypeSelector } from './QueryTypeSelector'; import { RawQueryEditor } from './RawQueryEditor'; -import { changeAliasPattern, changeQuery, changeRawDSLQuery } from './state'; +import { changeAliasPattern, changeEditorTypeAndResetQuery, changeQuery, changeRawDSLQuery } from './state'; export type ElasticQueryEditorProps = QueryEditorProps; @@ -97,31 +97,61 @@ const QueryEditorForm = ({ value, onRunQuery }: Props & { onRunQuery: () => void const inputId = useId(); const styles = useStyles2(getStyles); + const [switchModalOpen, setSwitchModalOpen] = useState(false); + const [pendingEditorType, setPendingEditorType] = useState(null); + const isTimeSeries = isTimeSeriesQuery(value); const isCodeEditor = value.editorType === 'code'; const rawDSLFeatureEnabled = config.featureToggles.elasticsearchRawDSLQuery; + // Default to 'builder' if editorType is empty + const currentEditorType: EditorType = value.editorType === 'code' ? 'code' : 'builder'; + const showBucketAggregationsEditor = value.metrics?.every( (metric) => metricAggregationConfig[metric.type].impliedQueryType === 'metrics' ); + const onEditorTypeChange = useCallback((newEditorType: EditorType) => { + // Show warning modal when switching modes + setPendingEditorType(newEditorType); + setSwitchModalOpen(true); + }, []); + + const confirmEditorTypeChange = useCallback(() => { + if (pendingEditorType) { + dispatch(changeEditorTypeAndResetQuery(pendingEditorType)); + } + setSwitchModalOpen(false); + setPendingEditorType(null); + }, [dispatch, pendingEditorType]); + + const cancelEditorTypeChange = useCallback(() => { + setSwitchModalOpen(false); + setPendingEditorType(null); + }, []); + return ( <> +
Query type
-
- {rawDSLFeatureEnabled && ( -
- Editor type -
- + {rawDSLFeatureEnabled && ( +
+
-
- )} + )} +
{isCodeEditor && rawDSLFeatureEnabled && (