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;