github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/pages/graph/ExpressionInput.tsx (about) 1 import React, { FC, useEffect, useRef } from 'react'; 2 import { Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap'; 3 import { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder } from '@codemirror/view'; 4 import { EditorState, Prec, Compartment } from '@codemirror/state'; 5 import { bracketMatching, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language'; 6 import { defaultKeymap, historyKeymap, history, insertNewlineAndIndent } from '@codemirror/commands'; 7 import { highlightSelectionMatches } from '@codemirror/search'; 8 import { lintKeymap } from '@codemirror/lint'; 9 import { PromQLExtension, CompleteStrategy, newCompleteStrategy } from '@prometheus-io/codemirror-promql'; 10 import { 11 autocompletion, 12 completionKeymap, 13 CompletionContext, 14 CompletionResult, 15 closeBracketsKeymap, 16 closeBrackets, 17 } from '@codemirror/autocomplete'; 18 import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme'; 19 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 20 import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons'; 21 import PathPrefixProps from '../../types/PathPrefixProps'; 22 import { useTheme } from '../../contexts/ThemeContext'; 23 24 const promqlExtension = new PromQLExtension(); 25 26 interface CMExpressionInputProps { 27 value: string; 28 onExpressionChange: (expr: string) => void; 29 queryHistory: string[]; 30 metricNames: string[]; 31 executeQuery: () => void; 32 loading: boolean; 33 enableAutocomplete: boolean; 34 enableHighlighting: boolean; 35 enableLinter: boolean; 36 } 37 38 const dynamicConfigCompartment = new Compartment(); 39 40 // Autocompletion strategy that wraps the main one and enriches 41 // it with past query items. 42 export class HistoryCompleteStrategy implements CompleteStrategy { 43 private complete: CompleteStrategy; 44 private queryHistory: string[]; 45 constructor(complete: CompleteStrategy, queryHistory: string[]) { 46 this.complete = complete; 47 this.queryHistory = queryHistory; 48 } 49 50 promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null { 51 return Promise.resolve(this.complete.promQL(context)).then((res) => { 52 const { state, pos } = context; 53 const tree = syntaxTree(state).resolve(pos, -1); 54 const start = res != null ? res.from : tree.from; 55 56 if (start !== 0) { 57 return res; 58 } 59 60 const historyItems: CompletionResult = { 61 from: start, 62 to: pos, 63 options: this.queryHistory.map((q) => ({ 64 label: q.length < 80 ? q : q.slice(0, 76).concat('...'), 65 detail: 'past query', 66 apply: q, 67 info: q.length < 80 ? undefined : q, 68 })), 69 validFor: /^[a-zA-Z0-9_:]+$/, 70 }; 71 72 if (res !== null) { 73 historyItems.options = historyItems.options.concat(res.options); 74 } 75 return historyItems; 76 }); 77 } 78 } 79 80 const ExpressionInput: FC<PathPrefixProps & CMExpressionInputProps> = ({ 81 pathPrefix, 82 value, 83 onExpressionChange, 84 queryHistory, 85 metricNames, 86 executeQuery, 87 loading, 88 enableAutocomplete, 89 enableHighlighting, 90 enableLinter, 91 }) => { 92 const containerRef = useRef<HTMLDivElement>(null); 93 const viewRef = useRef<EditorView | null>(null); 94 const { theme } = useTheme(); 95 96 // (Re)initialize editor based on settings / setting changes. 97 useEffect(() => { 98 // Build the dynamic part of the config. 99 promqlExtension 100 .activateCompletion(enableAutocomplete) 101 .activateLinter(enableLinter) 102 .setComplete({ 103 completeStrategy: new HistoryCompleteStrategy( 104 newCompleteStrategy({ 105 remote: { url: pathPrefix ? pathPrefix : '', cache: { initialMetricList: metricNames } }, 106 }), 107 queryHistory 108 ), 109 }); 110 const dynamicConfig = [ 111 enableHighlighting ? syntaxHighlighting(promqlHighlighter) : [], 112 promqlExtension.asExtension(), 113 theme === 'dark' ? darkTheme : lightTheme, 114 ]; 115 116 // Create or reconfigure the editor. 117 const view = viewRef.current; 118 if (view === null) { 119 // If the editor does not exist yet, create it. 120 if (!containerRef.current) { 121 throw new Error('expected CodeMirror container element to exist'); 122 } 123 124 const startState = EditorState.create({ 125 doc: value, 126 extensions: [ 127 baseTheme, 128 highlightSpecialChars(), 129 history(), 130 EditorState.allowMultipleSelections.of(true), 131 indentOnInput(), 132 bracketMatching(), 133 closeBrackets(), 134 autocompletion(), 135 highlightSelectionMatches(), 136 EditorView.lineWrapping, 137 keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap, ...completionKeymap, ...lintKeymap]), 138 placeholder('Expression (press Shift+Enter for newlines)'), 139 dynamicConfigCompartment.of(dynamicConfig), 140 // This keymap is added without precedence so that closing the autocomplete dropdown 141 // via Escape works without blurring the editor. 142 keymap.of([ 143 { 144 key: 'Escape', 145 run: (v: EditorView): boolean => { 146 v.contentDOM.blur(); 147 return false; 148 }, 149 }, 150 ]), 151 Prec.highest( 152 keymap.of([ 153 { 154 key: 'Enter', 155 run: (v: EditorView): boolean => { 156 executeQuery(); 157 return true; 158 }, 159 }, 160 { 161 key: 'Shift-Enter', 162 run: insertNewlineAndIndent, 163 }, 164 ]) 165 ), 166 EditorView.updateListener.of((update: ViewUpdate): void => { 167 onExpressionChange(update.state.doc.toString()); 168 }), 169 ], 170 }); 171 172 const view = new EditorView({ 173 state: startState, 174 parent: containerRef.current, 175 }); 176 177 viewRef.current = view; 178 179 view.focus(); 180 } else { 181 // The editor already exists, just reconfigure the dynamically configured parts. 182 view.dispatch( 183 view.state.update({ 184 effects: dynamicConfigCompartment.reconfigure(dynamicConfig), 185 }) 186 ); 187 } 188 // "value" is only used in the initial render, so we don't want to 189 // re-run this effect every time that "value" changes. 190 // 191 // eslint-disable-next-line react-hooks/exhaustive-deps 192 }, [enableAutocomplete, enableHighlighting, enableLinter, executeQuery, onExpressionChange, queryHistory, theme]); 193 194 return ( 195 <> 196 <InputGroup className="expression-input"> 197 <InputGroupAddon addonType="prepend"> 198 <InputGroupText> 199 {loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />} 200 </InputGroupText> 201 </InputGroupAddon> 202 <div ref={containerRef} className="cm-expression-input" /> 203 <InputGroupAddon addonType="append"> 204 <Button className="execute-btn" color="primary" onClick={executeQuery}> 205 Execute 206 </Button> 207 </InputGroupAddon> 208 </InputGroup> 209 </> 210 ); 211 }; 212 213 export default ExpressionInput;