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
This commit is contained in:
+12
-15
@@ -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<SelectableValue<EditorType>> = [
|
||||
{ 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 (
|
||||
<RadioButtonGroup<EditorType> fullWidth={false} options={BASE_OPTIONS} value={editorType} onChange={onChange} />
|
||||
<RadioButtonGroup<EditorType>
|
||||
data-testid="elasticsearch-editor-type-toggle"
|
||||
size="sm"
|
||||
options={BASE_OPTIONS}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+36
-14
@@ -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<monacoTypes.editor.IStandaloneCodeEditor | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(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 (
|
||||
<Box>
|
||||
<div className={styles.header}>
|
||||
<div ref={containerRef} className={styles.editorContainer}>
|
||||
<CodeEditor
|
||||
value={value ?? ''}
|
||||
language="json"
|
||||
width="100%"
|
||||
onBlur={handleQueryChange}
|
||||
monacoOptions={monacoOptions}
|
||||
onEditorDidMount={handleEditorDidMount}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Stack gap={1}>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -76,20 +106,8 @@ export function RawQueryEditor({ value, onChange, onRunQuery }: Props) {
|
||||
>
|
||||
Format
|
||||
</Button>
|
||||
<Button size="sm" variant="primary" icon="play" onClick={onRunQuery} tooltip="Run query (Ctrl/Cmd+Enter)">
|
||||
Run
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
<CodeEditor
|
||||
value={value ?? ''}
|
||||
language="json"
|
||||
height={200}
|
||||
width="100%"
|
||||
onBlur={handleQueryChange}
|
||||
monacoOptions={monacoOptions}
|
||||
onEditorDidMount={handleEditorDidMount}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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<ElasticDatasource, ElasticsearchDataQuery, ElasticsearchOptions>;
|
||||
|
||||
@@ -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<EditorType | null>(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 (
|
||||
<>
|
||||
<ConfirmModal
|
||||
isOpen={switchModalOpen}
|
||||
title="Switch editor"
|
||||
body="Switching between editors will reset your query. Are you sure you want to continue?"
|
||||
confirmText="Continue"
|
||||
onConfirm={confirmEditorTypeChange}
|
||||
onDismiss={cancelEditorTypeChange}
|
||||
/>
|
||||
<div className={styles.root}>
|
||||
<InlineLabel width={17}>Query type</InlineLabel>
|
||||
<div className={styles.queryItem}>
|
||||
<QueryTypeSelector />
|
||||
</div>
|
||||
</div>
|
||||
{rawDSLFeatureEnabled && (
|
||||
<div className={styles.root}>
|
||||
<InlineLabel width={17}>Editor type</InlineLabel>
|
||||
<div className={styles.queryItem}>
|
||||
<EditorTypeSelector />
|
||||
{rawDSLFeatureEnabled && (
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<EditorTypeSelector value={currentEditorType} onChange={onEditorTypeChange} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCodeEditor && rawDSLFeatureEnabled && (
|
||||
<RawQueryEditor
|
||||
|
||||
Reference in New Issue
Block a user