prometheus

Форк
0
/
ExpressionInput.tsx 
316 строк · 10.1 Кб
1
import React, { FC, useState, useEffect, useRef } from 'react';
2
import { Alert, Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
3

4
import { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder } from '@codemirror/view';
5
import { EditorState, Prec, Compartment } from '@codemirror/state';
6
import { bracketMatching, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
7
import { defaultKeymap, history, historyKeymap, insertNewlineAndIndent } from '@codemirror/commands';
8
import { highlightSelectionMatches } from '@codemirror/search';
9
import { lintKeymap } from '@codemirror/lint';
10
import {
11
  autocompletion,
12
  completionKeymap,
13
  CompletionContext,
14
  CompletionResult,
15
  closeBrackets,
16
  closeBracketsKeymap,
17
} from '@codemirror/autocomplete';
18
import { baseTheme, lightTheme, darkTheme, promqlHighlighter, darkPromqlHighlighter } from './CMTheme';
19

20
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
21
import { faSearch, faSpinner, faGlobeEurope, faIndent, faCheck } from '@fortawesome/free-solid-svg-icons';
22
import MetricsExplorer from './MetricsExplorer';
23
import { usePathPrefix } from '../../contexts/PathPrefixContext';
24
import { useTheme } from '../../contexts/ThemeContext';
25
import { CompleteStrategy, PromQLExtension } from '@prometheus-io/codemirror-promql';
26
import { newCompleteStrategy } from '@prometheus-io/codemirror-promql/dist/esm/complete';
27
import { API_PATH } from '../../constants/constants';
28

29
const promqlExtension = new PromQLExtension();
30

31
interface CMExpressionInputProps {
32
  value: string;
33
  onExpressionChange: (expr: string) => void;
34
  queryHistory: string[];
35
  metricNames: string[];
36
  executeQuery: () => void;
37
  loading: boolean;
38
  enableAutocomplete: boolean;
39
  enableHighlighting: boolean;
40
  enableLinter: boolean;
41
}
42

43
const dynamicConfigCompartment = new Compartment();
44

45
// Autocompletion strategy that wraps the main one and enriches
46
// it with past query items.
47
export class HistoryCompleteStrategy implements CompleteStrategy {
48
  private complete: CompleteStrategy;
49
  private queryHistory: string[];
50
  constructor(complete: CompleteStrategy, queryHistory: string[]) {
51
    this.complete = complete;
52
    this.queryHistory = queryHistory;
53
  }
54

55
  promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null {
56
    return Promise.resolve(this.complete.promQL(context)).then((res) => {
57
      const { state, pos } = context;
58
      const tree = syntaxTree(state).resolve(pos, -1);
59
      const start = res != null ? res.from : tree.from;
60

61
      if (start !== 0) {
62
        return res;
63
      }
64

65
      const historyItems: CompletionResult = {
66
        from: start,
67
        to: pos,
68
        options: this.queryHistory.map((q) => ({
69
          label: q.length < 80 ? q : q.slice(0, 76).concat('...'),
70
          detail: 'past query',
71
          apply: q,
72
          info: q.length < 80 ? undefined : q,
73
        })),
74
        validFor: /^[a-zA-Z0-9_:]+$/,
75
      };
76

77
      if (res !== null) {
78
        historyItems.options = historyItems.options.concat(res.options);
79
      }
80
      return historyItems;
81
    });
82
  }
83
}
84

85
const ExpressionInput: FC<CMExpressionInputProps> = ({
86
  value,
87
  onExpressionChange,
88
  queryHistory,
89
  metricNames,
90
  executeQuery,
91
  loading,
92
  enableAutocomplete,
93
  enableHighlighting,
94
  enableLinter,
95
}) => {
96
  const containerRef = useRef<HTMLDivElement>(null);
97
  const viewRef = useRef<EditorView | null>(null);
98
  const [showMetricsExplorer, setShowMetricsExplorer] = useState<boolean>(false);
99
  const pathPrefix = usePathPrefix();
100
  const { theme } = useTheme();
101

102
  const [formatError, setFormatError] = useState<string | null>(null);
103
  const [isFormatting, setIsFormatting] = useState<boolean>(false);
104
  const [exprFormatted, setExprFormatted] = useState<boolean>(false);
105

106
  // (Re)initialize editor based on settings / setting changes.
107
  useEffect(() => {
108
    // Build the dynamic part of the config.
109
    promqlExtension
110
      .activateCompletion(enableAutocomplete)
111
      .activateLinter(enableLinter)
112
      .setComplete({
113
        completeStrategy: new HistoryCompleteStrategy(
114
          newCompleteStrategy({
115
            remote: { url: pathPrefix, cache: { initialMetricList: metricNames } },
116
          }),
117
          queryHistory
118
        ),
119
      });
120

121
    let highlighter = syntaxHighlighting(theme === 'dark' ? darkPromqlHighlighter : promqlHighlighter);
122
    if (theme === 'dark') {
123
      highlighter = syntaxHighlighting(darkPromqlHighlighter);
124
    }
125

126
    const dynamicConfig = [
127
      enableHighlighting ? highlighter : [],
128
      promqlExtension.asExtension(),
129
      theme === 'dark' ? darkTheme : lightTheme,
130
    ];
131

132
    // Create or reconfigure the editor.
133
    const view = viewRef.current;
134
    if (view === null) {
135
      // If the editor does not exist yet, create it.
136
      if (!containerRef.current) {
137
        throw new Error('expected CodeMirror container element to exist');
138
      }
139

140
      const startState = EditorState.create({
141
        doc: value,
142
        extensions: [
143
          baseTheme,
144
          highlightSpecialChars(),
145
          history(),
146
          EditorState.allowMultipleSelections.of(true),
147
          indentOnInput(),
148
          bracketMatching(),
149
          closeBrackets(),
150
          autocompletion(),
151
          highlightSelectionMatches(),
152
          EditorView.lineWrapping,
153
          keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap, ...completionKeymap, ...lintKeymap]),
154
          placeholder('Expression (press Shift+Enter for newlines)'),
155
          dynamicConfigCompartment.of(dynamicConfig),
156
          // This keymap is added without precedence so that closing the autocomplete dropdown
157
          // via Escape works without blurring the editor.
158
          keymap.of([
159
            {
160
              key: 'Escape',
161
              run: (v: EditorView): boolean => {
162
                v.contentDOM.blur();
163
                return false;
164
              },
165
            },
166
          ]),
167
          Prec.highest(
168
            keymap.of([
169
              {
170
                key: 'Enter',
171
                run: (v: EditorView): boolean => {
172
                  executeQuery();
173
                  return true;
174
                },
175
              },
176
              {
177
                key: 'Shift-Enter',
178
                run: insertNewlineAndIndent,
179
              },
180
            ])
181
          ),
182
          EditorView.updateListener.of((update: ViewUpdate): void => {
183
            if (update.docChanged) {
184
              onExpressionChange(update.state.doc.toString());
185
              setExprFormatted(false);
186
            }
187
          }),
188
        ],
189
      });
190

191
      const view = new EditorView({
192
        state: startState,
193
        parent: containerRef.current,
194
      });
195

196
      viewRef.current = view;
197

198
      view.focus();
199
    } else {
200
      // The editor already exists, just reconfigure the dynamically configured parts.
201
      view.dispatch(
202
        view.state.update({
203
          effects: dynamicConfigCompartment.reconfigure(dynamicConfig),
204
        })
205
      );
206
    }
207
    // "value" is only used in the initial render, so we don't want to
208
    // re-run this effect every time that "value" changes.
209
    //
210
    // eslint-disable-next-line react-hooks/exhaustive-deps
211
  }, [enableAutocomplete, enableHighlighting, enableLinter, executeQuery, onExpressionChange, queryHistory, theme]);
212

213
  const insertAtCursor = (value: string) => {
214
    const view = viewRef.current;
215
    if (view === null) {
216
      return;
217
    }
218
    const { from, to } = view.state.selection.ranges[0];
219
    view.dispatch(
220
      view.state.update({
221
        changes: { from, to, insert: value },
222
      })
223
    );
224
  };
225

226
  const formatExpression = () => {
227
    setFormatError(null);
228
    setIsFormatting(true);
229

230
    fetch(
231
      `${pathPrefix}/${API_PATH}/format_query?${new URLSearchParams({
232
        query: value,
233
      })}`,
234
      {
235
        cache: 'no-store',
236
        credentials: 'same-origin',
237
      }
238
    )
239
      .then((resp) => {
240
        if (!resp.ok && resp.status !== 400) {
241
          throw new Error(`format HTTP request failed: ${resp.statusText}`);
242
        }
243

244
        return resp.json();
245
      })
246
      .then((json) => {
247
        if (json.status !== 'success') {
248
          throw new Error(json.error || 'invalid response JSON');
249
        }
250

251
        const view = viewRef.current;
252
        if (view === null) {
253
          return;
254
        }
255

256
        view.dispatch(view.state.update({ changes: { from: 0, to: view.state.doc.length, insert: json.data } }));
257
        setExprFormatted(true);
258
      })
259
      .catch((err) => {
260
        setFormatError(err.message);
261
      })
262
      .finally(() => {
263
        setIsFormatting(false);
264
      });
265
  };
266

267
  return (
268
    <>
269
      <InputGroup className="expression-input">
270
        <InputGroupAddon addonType="prepend">
271
          <InputGroupText>
272
            {loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />}
273
          </InputGroupText>
274
        </InputGroupAddon>
275
        <div ref={containerRef} className="cm-expression-input" />
276
        <InputGroupAddon addonType="append">
277
          <Button
278
            className="expression-input-action-btn"
279
            title={isFormatting ? 'Formatting expression' : exprFormatted ? 'Expression formatted' : 'Format expression'}
280
            onClick={formatExpression}
281
            disabled={isFormatting || exprFormatted}
282
          >
283
            {isFormatting ? (
284
              <FontAwesomeIcon icon={faSpinner} spin />
285
            ) : exprFormatted ? (
286
              <FontAwesomeIcon icon={faCheck} />
287
            ) : (
288
              <FontAwesomeIcon icon={faIndent} />
289
            )}
290
          </Button>
291
          <Button
292
            className="expression-input-action-btn"
293
            title="Open metrics explorer"
294
            onClick={() => setShowMetricsExplorer(true)}
295
          >
296
            <FontAwesomeIcon icon={faGlobeEurope} />
297
          </Button>
298
          <Button className="execute-btn" color="primary" onClick={executeQuery}>
299
            Execute
300
          </Button>
301
        </InputGroupAddon>
302
      </InputGroup>
303

304
      {formatError && <Alert color="danger">Error formatting expression: {formatError}</Alert>}
305

306
      <MetricsExplorer
307
        show={showMetricsExplorer}
308
        updateShow={setShowMetricsExplorer}
309
        metrics={metricNames}
310
        insertAtCursor={insertAtCursor}
311
      />
312
    </>
313
  );
314
};
315

316
export default ExpressionInput;
317

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.