The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx

185 lines
7.0 KiB

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 3d003ca4bcb792b440add04b883fafe637e4c4c9. * Revert "Chore: comment line filters pending implementation" This reverts commit 84bfe76a6a29e79a15a3e080929d1f387691700b. 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>
3 years ago
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;