Cloudwatch: Dynamic labels autocomplete (#49794)

* add completeable interface

* add basic labels language

* render monaco editor for label field

* align styling in math expression field

* add unit tests

* fix broken test

* remove unused import

* use theme

* remove comment

* pr feedback

* fix broken imports

* improve test

* make it possible to override code editor styles

* use input styles and align border styles
This commit is contained in:
Erik Sundell
2022-06-02 10:54:51 +02:00
committed by GitHub
parent a0f1a4b716
commit 467e375fe6
17 changed files with 355 additions and 18 deletions

View File

@@ -0,0 +1,82 @@
import { css, cx } from '@emotion/css';
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
import React, { useCallback, useRef } from 'react';
import { CodeEditor, getInputStyles, Monaco, useTheme2 } from '@grafana/ui';
import { DynamicLabelsCompletionItemProvider } from '../dynamic-labels/CompletionItemProvider';
import language from '../dynamic-labels/definition';
import { TRIGGER_SUGGEST } from '../monarch/commands';
import { registerLanguage } from '../monarch/register';
const dynamicLabelsCompletionItemProvider = new DynamicLabelsCompletionItemProvider();
export interface Props {
onChange: (query: string) => void;
onRunQuery: () => void;
label: string;
width: number;
}
export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props) {
const theme = useTheme2();
const styles = getInputStyles({ theme, width });
const containerRef = useRef<HTMLDivElement>(null);
const onEditorMount = useCallback(
(editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => {
editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {}));
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]
);
return (
<div ref={containerRef} className={cx(styles.wrapper)}>
<CodeEditor
containerStyles={css`
border: 1px solid ${theme.colors.action.disabledBackground};
&:hover {
border-color: ${theme.components.input.borderColor};
}
`}
monacoOptions={{
// without this setting, the auto-resize functionality causes an infinite loop, don't remove it!
scrollBeyondLastLine: false,
// These additional options are style focused and are a subset of those in the query editor in Prometheus
fontSize: 14,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerLanes: 0,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
suggestFontSize: 12,
padding: {
top: 6,
},
}}
language={language.id}
value={label}
onBlur={(value) => {
if (value !== label) {
onChange(value);
onRunQuery();
}
}}
onBeforeEditorMount={(monaco: Monaco) =>
registerLanguage(monaco, language, dynamicLabelsCompletionItemProvider)
}
onEditorDidMount={onEditorMount}
/>
</div>
);
}

View File

@@ -37,7 +37,7 @@ export function MathExpressionQueryField({
const updateElementHeight = () => {
const containerDiv = containerRef.current;
if (containerDiv !== null && editor.getContentHeight() < 200) {
const pixelHeight = editor.getContentHeight();
const pixelHeight = Math.max(32, editor.getContentHeight());
containerDiv.style.height = `${pixelHeight}px`;
containerDiv.style.width = '100%';
const pixelWidth = containerDiv.clientWidth;
@@ -68,6 +68,9 @@ export function MathExpressionQueryField({
},
suggestFontSize: 12,
wordWrap: 'on',
padding: {
top: 6,
},
}}
language={language.id}
value={query}

View File

@@ -4,6 +4,7 @@ import selectEvent from 'react-select-event';
import { DataSourceInstanceSettings } from '@grafana/data';
import { config } from '@grafana/runtime';
import * as ui from '@grafana/ui';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariableModel, initialVariableModelState } from '../../../../../features/variables/types';
@@ -12,6 +13,13 @@ import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../../typ
import { MetricsQueryEditor, Props } from './MetricsQueryEditor';
jest.mock('@grafana/ui', () => ({
...jest.requireActual<typeof ui>('@grafana/ui'),
CodeEditor: function CodeEditor({ value }: { value: string }) {
return <pre>{value}</pre>;
},
}));
const setup = () => {
const instanceSettings = {
jsonData: { defaultRegion: 'us-east-1' },
@@ -173,9 +181,7 @@ describe('QueryEditor', () => {
expect(screen.getByText('Label')).toBeInTheDocument();
expect(screen.queryByText('Alias')).toBeNull();
expect(screen.getByLabelText('Label - optional')).toHaveValue(
"Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}"
);
expect(screen.getByText("Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}"));
config.featureToggles.cloudWatchDynamicLabels = originalValue;
});

View File

@@ -16,6 +16,7 @@ import {
MetricQueryType,
MetricStat,
} from '../../types';
import { DynamicLabelsField } from '../DynamicLabelsField';
import QueryHeader from '../QueryHeader';
import { Alias } from './Alias';
@@ -138,14 +139,12 @@ export const MetricsQueryEditor = (props: Props) => {
optional
tooltip="Change time series legend name using Dynamic labels. See documentation for details."
>
<Input
id={`${query.refId}-cloudwatch-metric-query-editor-label`}
onBlur={onRunQuery}
value={preparedQuery.label ?? ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange({ ...preparedQuery, label: event.target.value })
}
/>
<DynamicLabelsField
width={52}
onRunQuery={onRunQuery}
label={preparedQuery.label ?? ''}
onChange={(label) => props.onChange({ ...query, label })}
></DynamicLabelsField>
</EditorField>
) : (
<EditorField