/usr/share/grafana/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor
import { css } from '@emotion/css'; import { useEffect, useRef } from 'react'; import { useAsync, useLatest } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { CodeEditor, Monaco, useStyles2, monacoTypes } from '@grafana/ui'; import { languageDefinition } from '../pyroscopeql'; import { CompletionProvider } from './autocomplete'; interface Props { value: string; onChange: (val: string) => void; onRunQuery: (value: string) => void; labels?: string[]; getLabelValues: (label: string) => Promise<string[]>; } export function LabelsEditor(props: Props) { const setupAutocompleteFn = useAutocomplete(props.getLabelValues, props.labels); const styles = useStyles2(getStyles); const onRunQueryRef = useLatest(props.onRunQuery); const containerRef = useRef<HTMLDivElement>(null); return ( <div className={styles.wrapper} // NOTE: we will be setting inline-style-width/height on this element ref={containerRef} > <CodeEditor value={props.value} language={langId} onChange={props.onChange} containerStyles={styles.queryField} monacoOptions={{ folding: false, fontSize: 14, lineNumbers: 'off', overviewRulerLanes: 0, renderLineHighlight: 'none', scrollbar: { vertical: 'hidden', verticalScrollbarSize: 8, // used as "padding-right" horizontal: 'hidden', horizontalScrollbarSize: 0, }, scrollBeyondLastLine: false, wordWrap: 'on', padding: { top: 4, bottom: 5, }, }} onBeforeEditorMount={ensurePyroscopeQL} onEditorDidMount={(editor, monaco) => { setupAutocompleteFn(editor, monaco); 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(); editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { onRunQueryRef.current(editor.getValue()); }); }} /> </div> ); } // 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; /** * Hook that returns function that will set up monaco autocomplete for the label selector */ function useAutocomplete(getLabelValues: (label: string) => Promise<string[]>, labels?: string[]) { const providerRef = useRef<CompletionProvider>(); if (providerRef.current === undefined) { providerRef.current = new CompletionProvider(); } useAsync(async () => { if (providerRef.current) { providerRef.current.init(labels || [], getLabelValues); } }, [labels, getLabelValues]); const autocompleteDisposeFun = useRef<(() => void) | null>(null); useEffect(() => { // when we unmount, we unregister the autocomplete-function, if it was registered return () => { autocompleteDisposeFun.current?.(); }; }, []); // This should be run in monaco onEditorDidMount return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => { if (providerRef.current) { providerRef.current.editor = editor; providerRef.current.monaco = monaco; const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current); autocompleteDisposeFun.current = dispose; } }; } // we must only run the setup code once let pyroscopeqlSetupDone = false; const langId = 'pyroscopeql'; function ensurePyroscopeQL(monaco: Monaco) { if (pyroscopeqlSetupDone === false) { pyroscopeqlSetupDone = true; const { aliases, extensions, mimetypes, def } = languageDefinition; monaco.languages.register({ id: langId, aliases, extensions, mimetypes }); monaco.languages.setMonarchTokensProvider(langId, def.language); monaco.languages.setLanguageConfiguration(langId, def.languageConfiguration); } } const getStyles = (theme: GrafanaTheme2) => { return { queryField: css({ label: 'LabelsEditorQueryField', flex: 1, // Not exactly sure but without this the editor does not shrink after resizing (so you can make it bigger but not // smaller). At the same time this does not actually make the editor 100px because it has flex 1 so I assume // this should sort of act as a flex-basis (but flex-basis does not work for this). So yeah CSS magic. width: '100px', }), wrapper: css({ label: 'LabelsEditorWrapper', display: 'flex', flex: 1, border: '1px solid rgba(36, 41, 46, 0.3)', borderRadius: theme.shape.radius.default, }), }; };
.
Edit
..
Edit
EditorField.tsx
Edit
EditorRow.tsx
Edit
EditorRows.tsx
Edit
LabelsEditor.tsx
Edit
ProfileTypesCascader.tsx
Edit
QueryEditor.test.tsx
Edit
QueryEditor.tsx
Edit
QueryOptionGroup.tsx
Edit
QueryOptions.tsx
Edit
Stack.tsx
Edit
autocomplete.test.ts
Edit
autocomplete.ts
Edit