mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
* 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 commit3d003ca4bc
. * Revert "Chore: comment line filters pending implementation" This reverts commit84bfe76a6a
. 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>
185 lines
7.0 KiB
TypeScript
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;
|