grafana/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx
Matias Chomicki 729ce8bb72
Loki: query editor using Monaco (#55391)
* loki: switch to a monaco-based query field, step 1 (#46291)

* loki: use monaco-logql (#46318)

* loki: use monaco-logql

* updated monaco-logql

* fix all the tests (#46327)

* loki: recommend parser (#46362)

* loki: recommend parser

* additional improvements

* more improvements

* type and lint fixes

* more improvements

* trigger autocomplete on focus

* rename

* loki: more smart features (#46414)

* loki: more smart features

* loki: updated syntax-highlight version

* better explanation (#46443)

* better explanation

* improved help-text

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>

* Fix label

* feat(loki-monaco-editor): add monaco-logql as a dependency

* feat(loki-monaco-editor): add back range function removed during merge

* feat(loki-monaco-editor): sync imports with recent changes

* feat(loki-monaco-editor): add missing lang provider functions

* feat(loki-monaco-editor): fix imports

* feat(loki-monaco-editor): display monaco editor by default

Temporarily

* Chore: remove commented code

* Chore: minor refactor to NeverCaseError

* Chore: minor code cleanups

* feat(loki-monaco-editor): add history implementation

Will see how it behaves and base the history slicing on tangible feedback

* feat(loki-monaco-editor): turn completion data provider into a class

* Chore: fix missing imports

* feat(loki-monaco-editor): refactor data provider methods

Move complexity scattered everywhere to the provider

* Chore: clean up redundant code

* Chore: minor comments cleanup

* Chore: simplify override services

* Chore: rename callback

* feat(loki-monaco-editor): use query hints implementation to parse expression

* feat(loki-monaco-editor): improve function name

* Chore: remove superfluous variable in favor of destructuring

* Chore: remove unused imports

* Chore: make method async

* feat(loki-monaco-editor): fix deprecations and errors in situation

* feat(loki-monaco-editor): comment failing test case

* Chore: remove comment from test

* Chore: remove duplicated completion item

* Chore: fix linting issues

* Chore: update language provider test

* Chore: update datasource test

* feat(loki-monaco-editor): create feature flag

* feat(loki-monaco-editor): place the editor under a feature flag

* Chore: add completion unit test

* Chore: add completion data provider test

* Chore: remove unwanted export

* Chore: remove unused export

* Chore(loki-query-field): destructure all props

* chore(loki-completions): remove odd string

* fix(loki-completions): remove rate_interval

Not supported

* fix(loki-completions): remove line filters for after pipe case

We shouldn't offer line filters if we are after first pipe.

* refactor(loki-datasource): update default parameter

* fix(loki-syntax): remove outdated documentation

* Update capitalization in pkg/services/featuremgmt/registry.go

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>

* refactor(situation): use node types instead of names

* Chore: comment line filters pending implementation

It's breaking the build due to a linting error.

* Chore: update feature flag test after capitalization change

* Revert "fix(loki-completions): remove line filters for after pipe case"

This reverts commit 3d003ca4bc.

* Revert "Chore: comment line filters pending implementation"

This reverts commit 84bfe76a6a.

Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>
Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Co-authored-by: Ivana Huckova <ivana.huckova@gmail.com>
2022-10-06 10:35:30 -04:00

185 lines
7.0 KiB
TypeScript

import { css } from '@emotion/css';
import React, { useRef, useEffect } from 'react';
import { useLatest } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { languageConfiguration, monarchlanguage } from '@grafana/monaco-logql';
import { useTheme2, ReactMonacoEditor, Monaco, monacoTypes } from '@grafana/ui';
import { Props } from './MonacoQueryFieldProps';
import { getOverrideServices } from './getOverrideServices';
import { getCompletionProvider, getSuggestOptions } from './monaco-completion-provider';
import { CompletionDataProvider } from './monaco-completion-provider/CompletionDataProvider';
const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {
codeLens: false,
contextmenu: false,
// we need `fixedOverflowWidgets` because otherwise in grafana-dashboards
// the popup is clipped by the panel-visualizations.
fixedOverflowWidgets: true,
folding: false,
fontSize: 14,
lineDecorationsWidth: 8, // used as "padding-left"
lineNumbers: 'off',
minimap: { enabled: false },
overviewRulerBorder: false,
overviewRulerLanes: 0,
padding: {
// these numbers were picked so that visually this matches the previous version
// of the query-editor the best
top: 4,
bottom: 5,
},
renderLineHighlight: 'none',
scrollbar: {
vertical: 'hidden',
verticalScrollbarSize: 8, // used as "padding-right"
horizontal: 'hidden',
horizontalScrollbarSize: 0,
},
scrollBeyondLastLine: false,
suggest: getSuggestOptions(),
suggestFontSize: 12,
wordWrap: 'on',
};
// this number was chosen by testing various values. it might be necessary
// because of the width of the border, not sure.
//it needs to do 2 things:
// 1. when the editor is single-line, it should make the editor height be visually correct
// 2. when the editor is multi-line, the editor should not be "scrollable" (meaning,
// you do a scroll-movement in the editor, and it will scroll the content by a couple pixels
// up & down. this we want to avoid)
const EDITOR_HEIGHT_OFFSET = 2;
const LANG_ID = 'logql';
// we must only run the lang-setup code once
let LANGUAGE_SETUP_STARTED = false;
function ensureLogQL(monaco: Monaco) {
if (LANGUAGE_SETUP_STARTED === false) {
LANGUAGE_SETUP_STARTED = true;
monaco.languages.register({ id: LANG_ID });
monaco.languages.setMonarchTokensProvider(LANG_ID, monarchlanguage);
monaco.languages.setLanguageConfiguration(LANG_ID, languageConfiguration);
}
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css`
border-radius: ${theme.shape.borderRadius()};
border: 1px solid ${theme.components.input.borderColor};
`,
};
};
const MonacoQueryField = ({ languageProvider, history, onBlur, onRunQuery, initialValue }: Props) => {
// we need only one instance of `overrideServices` during the lifetime of the react component
const overrideServicesRef = useRef(getOverrideServices());
const containerRef = useRef<HTMLDivElement>(null);
const langProviderRef = useLatest(languageProvider);
const historyRef = useLatest(history);
const onRunQueryRef = useLatest(onRunQuery);
const onBlurRef = useLatest(onBlur);
const autocompleteCleanupCallback = useRef<(() => void) | null>(null);
const theme = useTheme2();
const styles = getStyles(theme);
useEffect(() => {
// when we unmount, we unregister the autocomplete-function, if it was registered
return () => {
autocompleteCleanupCallback.current?.();
};
}, []);
return (
<div
aria-label={selectors.components.QueryField.container}
className={styles.container}
// NOTE: we will be setting inline-style-width/height on this element
ref={containerRef}
>
<ReactMonacoEditor
overrideServices={overrideServicesRef.current}
options={options}
language={LANG_ID}
value={initialValue}
beforeMount={(monaco) => {
ensureLogQL(monaco);
}}
onMount={(editor, monaco) => {
// we setup on-blur
editor.onDidBlurEditorWidget(() => {
onBlurRef.current(editor.getValue());
});
const dataProvider = new CompletionDataProvider(langProviderRef.current, historyRef.current);
const completionProvider = getCompletionProvider(monaco, dataProvider);
// completion-providers in monaco are not registered directly to editor-instances,
// they are registered to languages. this makes it hard for us to have
// separate completion-providers for every query-field-instance
// (but we need that, because they might connect to different datasources).
// the trick we do is, we wrap the callback in a "proxy",
// and in the proxy, the first thing is, we check if we are called from
// "our editor instance", and if not, we just return nothing. if yes,
// we call the completion-provider.
const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = {
...completionProvider,
provideCompletionItems: (model, position, context, token) => {
// if the model-id does not match, then this call is from a different editor-instance,
// not "our instance", so return nothing
if (editor.getModel()?.id !== model.id) {
return { suggestions: [] };
}
return completionProvider.provideCompletionItems(model, position, context, token);
},
};
const { dispose } = monaco.languages.registerCompletionItemProvider(LANG_ID, filteringCompletionProvider);
autocompleteCleanupCallback.current = dispose;
// this code makes the editor resize itself so that the content fits
// (it will grow taller when necessary)
// FIXME: maybe move this functionality into CodeEditor, like:
// <CodeEditor resizingMode="single-line"/>
const updateElementHeight = () => {
const containerDiv = containerRef.current;
if (containerDiv !== null) {
const pixelHeight = editor.getContentHeight();
containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`;
containerDiv.style.width = '100%';
const pixelWidth = containerDiv.clientWidth;
editor.layout({ width: pixelWidth, height: pixelHeight });
}
};
editor.onDidContentSizeChange(updateElementHeight);
updateElementHeight();
// handle: shift + enter
// FIXME: maybe move this functionality into CodeEditor?
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
onRunQueryRef.current(editor.getValue());
});
editor.onDidFocusEditorText(() => {
if (editor.getValue().trim() === '') {
editor.trigger('', 'editor.action.triggerSuggest', {});
}
});
}}
/>
</div>
);
};
// Default export for lazy load.
export default MonacoQueryField;