| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- import _ from 'lodash';
- import React, { Context } from 'react';
- import { Value, Editor as CoreEditor } from 'slate';
- import { Editor, Plugin } from '@grafana/slate-react';
- import Plain from 'slate-plain-serializer';
- import classnames from 'classnames';
- import { CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
- import ClearPlugin from './slate-plugins/clear';
- import NewlinePlugin from './slate-plugins/newline';
- import SelectionShortcutsPlugin from './slate-plugins/selection_shortcuts';
- import IndentationPlugin from './slate-plugins/indentation';
- import ClipboardPlugin from './slate-plugins/clipboard';
- import RunnerPlugin from './slate-plugins/runner';
- import SuggestionsPlugin, { SuggestionsState } from './slate-plugins/suggestions';
- import { Typeahead } from './Typeahead';
- import { makeValue, SCHEMA } from '@grafana/ui';
- export const HIGHLIGHT_WAIT = 500;
- export interface QueryFieldProps {
- additionalPlugins?: Plugin[];
- cleanText?: (text: string) => string;
- disabled?: boolean;
- initialQuery: string | null;
- onRunQuery?: () => void;
- onChange?: (value: string) => void;
- onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
- onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
- placeholder?: string;
- portalOrigin?: string;
- syntax?: string;
- syntaxLoaded?: boolean;
- }
- export interface QueryFieldState {
- suggestions: CompletionItemGroup[];
- typeaheadContext: string | null;
- typeaheadPrefix: string;
- typeaheadText: string;
- value: Value;
- lastExecutedValue: Value;
- }
- export interface TypeaheadInput {
- prefix: string;
- selection?: Selection;
- text: string;
- value: Value;
- wrapperClasses: string[];
- labelKey?: string;
- }
- /**
- * Renders an editor field.
- * Pass initial value as initialQuery and listen to changes in props.onValueChanged.
- * This component can only process strings. Internally it uses Slate Value.
- * Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example.
- */
- export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
- menuEl: HTMLElement | null;
- plugins: Plugin[];
- resetTimer: NodeJS.Timer;
- mounted: boolean;
- updateHighlightsTimer: Function;
- editor: Editor;
- typeaheadRef: Typeahead;
- constructor(props: QueryFieldProps, context: Context<any>) {
- super(props, context);
- this.updateHighlightsTimer = _.debounce(this.updateLogsHighlights, HIGHLIGHT_WAIT);
- const { onTypeahead, cleanText, portalOrigin, onWillApplySuggestion } = props;
- // Base plugins
- this.plugins = [
- SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion, component: this }),
- ClearPlugin(),
- RunnerPlugin({ handler: this.executeOnChangeAndRunQueries }),
- NewlinePlugin(),
- SelectionShortcutsPlugin(),
- IndentationPlugin(),
- ClipboardPlugin(),
- ...(props.additionalPlugins || []),
- ].filter(p => p);
- this.state = {
- suggestions: [],
- typeaheadContext: null,
- typeaheadPrefix: '',
- typeaheadText: '',
- value: makeValue(props.initialQuery || '', props.syntax),
- lastExecutedValue: null,
- };
- }
- componentDidMount() {
- this.mounted = true;
- }
- componentWillUnmount() {
- this.mounted = false;
- clearTimeout(this.resetTimer);
- }
- componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
- const { initialQuery, syntax } = this.props;
- const { value } = this.state;
- // if query changed from the outside
- if (initialQuery !== prevProps.initialQuery) {
- // and we have a version that differs
- if (initialQuery !== Plain.serialize(value)) {
- this.setState({ value: makeValue(initialQuery || '', syntax) });
- }
- }
- }
- UNSAFE_componentWillReceiveProps(nextProps: QueryFieldProps) {
- if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
- // Need a bogus edit to re-render the editor after syntax has fully loaded
- const editor = this.editor.insertText(' ').deleteBackward(1);
- this.onChange(editor.value, true);
- }
- }
- onChange = (value: Value, invokeParentOnValueChanged?: boolean) => {
- const documentChanged = value.document !== this.state.value.document;
- const prevValue = this.state.value;
- // Control editor loop, then pass text change up to parent
- this.setState({ value }, () => {
- if (documentChanged) {
- const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value);
- if (textChanged && invokeParentOnValueChanged) {
- this.executeOnChangeAndRunQueries();
- }
- if (textChanged && !invokeParentOnValueChanged) {
- this.updateHighlightsTimer();
- }
- }
- });
- };
- updateLogsHighlights = () => {
- const { onChange } = this.props;
- if (onChange) {
- onChange(Plain.serialize(this.state.value));
- }
- };
- executeOnChangeAndRunQueries = () => {
- // Send text change to parent
- const { onChange, onRunQuery } = this.props;
- if (onChange) {
- onChange(Plain.serialize(this.state.value));
- }
- if (onRunQuery) {
- onRunQuery();
- this.setState({ lastExecutedValue: this.state.value });
- }
- };
- handleBlur = (event: Event, editor: CoreEditor, next: Function) => {
- const { lastExecutedValue } = this.state;
- const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null;
- const currentValue = Plain.serialize(editor.value);
- if (previousValue !== currentValue) {
- this.executeOnChangeAndRunQueries();
- }
- editor.blur();
- return next();
- };
- render() {
- const { disabled } = this.props;
- const wrapperClassName = classnames('slate-query-field__wrapper', {
- 'slate-query-field__wrapper--disabled': disabled,
- });
- return (
- <div className={wrapperClassName}>
- <div className="slate-query-field">
- <Editor
- ref={editor => (this.editor = editor)}
- schema={SCHEMA}
- autoCorrect={false}
- readOnly={this.props.disabled}
- onBlur={this.handleBlur}
- // onKeyDown={this.onKeyDown}
- onChange={(change: { value: Value }) => {
- this.onChange(change.value, false);
- }}
- placeholder={this.props.placeholder}
- plugins={this.plugins}
- spellCheck={false}
- value={this.state.value}
- />
- </div>
- </div>
- );
- }
- }
- export default QueryField;
|