2018-07-26 07:04:12 -05:00
|
|
|
import _ from 'lodash';
|
2019-03-25 01:57:17 -05:00
|
|
|
import React, { Context } from 'react';
|
2019-09-23 06:26:05 -05:00
|
|
|
|
|
|
|
import { Value, Editor as CoreEditor } from 'slate';
|
|
|
|
import { Editor, Plugin } from '@grafana/slate-react';
|
2018-04-26 04:58:42 -05:00
|
|
|
import Plain from 'slate-plain-serializer';
|
2018-12-09 11:44:59 -06:00
|
|
|
import classnames from 'classnames';
|
2018-04-26 04:58:42 -05:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
import { CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
|
2018-10-25 05:24:24 -05:00
|
|
|
|
2018-04-26 04:58:42 -05:00
|
|
|
import ClearPlugin from './slate-plugins/clear';
|
|
|
|
import NewlinePlugin from './slate-plugins/newline';
|
2019-09-23 06:26:05 -05:00
|
|
|
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';
|
2018-04-26 04:58:42 -05:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
import { Typeahead } from './Typeahead';
|
2018-10-02 04:19:07 -05:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
import { makeValue, SCHEMA } from '@grafana/ui';
|
2019-09-17 06:21:50 -05:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
export const HIGHLIGHT_WAIT = 500;
|
2018-04-26 04:58:42 -05:00
|
|
|
|
2018-11-21 07:45:57 -06:00
|
|
|
export interface QueryFieldProps {
|
2019-09-23 06:26:05 -05:00
|
|
|
additionalPlugins?: Plugin[];
|
2018-07-26 07:04:12 -05:00
|
|
|
cleanText?: (text: string) => string;
|
2018-12-09 11:44:59 -06:00
|
|
|
disabled?: boolean;
|
2018-11-21 07:45:57 -06:00
|
|
|
initialQuery: string | null;
|
2019-05-10 07:00:39 -05:00
|
|
|
onRunQuery?: () => void;
|
|
|
|
onChange?: (value: string) => void;
|
2019-09-23 06:26:05 -05:00
|
|
|
onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
|
|
|
|
onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
|
2018-07-26 07:04:12 -05:00
|
|
|
placeholder?: string;
|
2018-10-05 06:00:45 -05:00
|
|
|
portalOrigin?: string;
|
2018-09-14 09:32:54 -05:00
|
|
|
syntax?: string;
|
2018-10-05 11:27:33 -05:00
|
|
|
syntaxLoaded?: boolean;
|
2018-07-26 07:04:12 -05:00
|
|
|
}
|
2018-04-26 04:58:42 -05:00
|
|
|
|
2018-10-30 08:38:34 -05:00
|
|
|
export interface QueryFieldState {
|
2018-10-25 05:24:24 -05:00
|
|
|
suggestions: CompletionItemGroup[];
|
2018-07-26 07:04:12 -05:00
|
|
|
typeaheadContext: string | null;
|
|
|
|
typeaheadPrefix: string;
|
|
|
|
typeaheadText: string;
|
2019-09-23 06:26:05 -05:00
|
|
|
value: Value;
|
2019-02-05 00:03:16 -06:00
|
|
|
lastExecutedValue: Value;
|
2018-07-26 07:04:12 -05:00
|
|
|
}
|
2018-04-26 04:58:42 -05:00
|
|
|
|
2018-07-26 07:04:12 -05:00
|
|
|
export interface TypeaheadInput {
|
|
|
|
prefix: string;
|
|
|
|
selection?: Selection;
|
|
|
|
text: string;
|
2018-08-06 07:36:02 -05:00
|
|
|
value: Value;
|
2019-09-23 06:26:05 -05:00
|
|
|
wrapperClasses: string[];
|
|
|
|
labelKey?: string;
|
2018-07-26 07:04:12 -05:00
|
|
|
}
|
|
|
|
|
2018-11-21 07:45:57 -06:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2018-10-30 08:38:34 -05:00
|
|
|
export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
|
2018-07-26 07:04:12 -05:00
|
|
|
menuEl: HTMLElement | null;
|
2019-09-23 06:26:05 -05:00
|
|
|
plugins: Plugin[];
|
|
|
|
resetTimer: NodeJS.Timer;
|
2019-01-28 06:03:33 -06:00
|
|
|
mounted: boolean;
|
2019-09-23 06:26:05 -05:00
|
|
|
updateHighlightsTimer: Function;
|
|
|
|
editor: Editor;
|
|
|
|
typeaheadRef: Typeahead;
|
2018-04-26 04:58:42 -05:00
|
|
|
|
2019-03-25 01:57:17 -05:00
|
|
|
constructor(props: QueryFieldProps, context: Context<any>) {
|
2018-04-26 04:58:42 -05:00
|
|
|
super(props, context);
|
|
|
|
|
2019-07-02 03:06:21 -05:00
|
|
|
this.updateHighlightsTimer = _.debounce(this.updateLogsHighlights, HIGHLIGHT_WAIT);
|
2018-10-28 08:03:39 -05:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
const { onTypeahead, cleanText, portalOrigin, onWillApplySuggestion } = props;
|
|
|
|
|
2018-07-26 07:04:12 -05:00
|
|
|
// Base plugins
|
2019-09-23 06:26:05 -05:00
|
|
|
this.plugins = [
|
|
|
|
NewlinePlugin(),
|
|
|
|
SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion, component: this }),
|
|
|
|
ClearPlugin(),
|
|
|
|
RunnerPlugin({ handler: this.executeOnChangeAndRunQueries }),
|
|
|
|
SelectionShortcutsPlugin(),
|
|
|
|
IndentationPlugin(),
|
|
|
|
ClipboardPlugin(),
|
|
|
|
...(props.additionalPlugins || []),
|
|
|
|
].filter(p => p);
|
2018-04-26 04:58:42 -05:00
|
|
|
|
|
|
|
this.state = {
|
|
|
|
suggestions: [],
|
2018-07-26 07:04:12 -05:00
|
|
|
typeaheadContext: null,
|
2018-04-26 04:58:42 -05:00
|
|
|
typeaheadPrefix: '',
|
2018-07-26 07:04:12 -05:00
|
|
|
typeaheadText: '',
|
2019-08-09 10:08:59 -05:00
|
|
|
value: makeValue(props.initialQuery || '', props.syntax),
|
2019-02-05 00:03:16 -06:00
|
|
|
lastExecutedValue: null,
|
2018-04-26 04:58:42 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
2019-01-28 06:03:33 -06:00
|
|
|
this.mounted = true;
|
2018-04-26 04:58:42 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2019-01-28 06:03:33 -06:00
|
|
|
this.mounted = false;
|
2018-04-26 04:58:42 -05:00
|
|
|
clearTimeout(this.resetTimer);
|
|
|
|
}
|
|
|
|
|
2018-11-21 07:45:57 -06:00
|
|
|
componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
|
2019-01-23 10:44:22 -06:00
|
|
|
const { initialQuery, syntax } = this.props;
|
2019-09-23 06:26:05 -05:00
|
|
|
const { value } = this.state;
|
2019-01-23 10:44:22 -06:00
|
|
|
|
|
|
|
// if query changed from the outside
|
|
|
|
if (initialQuery !== prevProps.initialQuery) {
|
|
|
|
// and we have a version that differs
|
|
|
|
if (initialQuery !== Plain.serialize(value)) {
|
2019-08-16 09:10:22 -05:00
|
|
|
this.setState({ value: makeValue(initialQuery || '', syntax) });
|
2019-01-23 10:44:22 -06:00
|
|
|
}
|
|
|
|
}
|
2018-04-26 04:58:42 -05:00
|
|
|
}
|
|
|
|
|
2019-08-13 03:08:33 -05:00
|
|
|
UNSAFE_componentWillReceiveProps(nextProps: QueryFieldProps) {
|
2018-10-05 11:27:33 -05:00
|
|
|
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
|
|
|
|
// Need a bogus edit to re-render the editor after syntax has fully loaded
|
2019-09-23 06:26:05 -05:00
|
|
|
const editor = this.editor.insertText(' ').deleteBackward(1);
|
|
|
|
this.onChange(editor.value, true);
|
2018-04-26 04:58:42 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
onChange = (value: Value, invokeParentOnValueChanged?: boolean) => {
|
2019-01-29 08:53:51 -06:00
|
|
|
const documentChanged = value.document !== this.state.value.document;
|
|
|
|
const prevValue = this.state.value;
|
2018-10-04 10:25:31 -05:00
|
|
|
|
|
|
|
// Control editor loop, then pass text change up to parent
|
2018-04-26 04:58:42 -05:00
|
|
|
this.setState({ value }, () => {
|
2019-01-29 08:53:51 -06:00
|
|
|
if (documentChanged) {
|
|
|
|
const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value);
|
2019-01-28 10:41:33 -06:00
|
|
|
if (textChanged && invokeParentOnValueChanged) {
|
2019-05-10 07:00:39 -05:00
|
|
|
this.executeOnChangeAndRunQueries();
|
2019-01-29 08:53:51 -06:00
|
|
|
}
|
2019-04-16 09:27:57 -05:00
|
|
|
if (textChanged && !invokeParentOnValueChanged) {
|
2019-07-02 03:06:21 -05:00
|
|
|
this.updateHighlightsTimer();
|
2019-04-16 09:27:57 -05:00
|
|
|
}
|
2018-04-26 04:58:42 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2019-04-16 09:27:57 -05:00
|
|
|
updateLogsHighlights = () => {
|
2019-05-10 07:00:39 -05:00
|
|
|
const { onChange } = this.props;
|
2019-07-02 03:06:21 -05:00
|
|
|
|
2019-05-10 07:00:39 -05:00
|
|
|
if (onChange) {
|
|
|
|
onChange(Plain.serialize(this.state.value));
|
2019-04-16 09:27:57 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-05-10 07:00:39 -05:00
|
|
|
executeOnChangeAndRunQueries = () => {
|
2018-04-26 04:58:42 -05:00
|
|
|
// Send text change to parent
|
2019-05-10 07:00:39 -05:00
|
|
|
const { onChange, onRunQuery } = this.props;
|
|
|
|
if (onChange) {
|
|
|
|
onChange(Plain.serialize(this.state.value));
|
2019-02-01 04:55:01 -06:00
|
|
|
}
|
|
|
|
|
2019-05-10 07:00:39 -05:00
|
|
|
if (onRunQuery) {
|
|
|
|
onRunQuery();
|
2019-02-05 00:03:16 -06:00
|
|
|
this.setState({ lastExecutedValue: this.state.value });
|
2018-04-26 04:58:42 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
handleBlur = (event: Event, editor: CoreEditor, next: Function) => {
|
2019-02-05 00:03:16 -06:00
|
|
|
const { lastExecutedValue } = this.state;
|
|
|
|
const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null;
|
2019-09-23 06:26:05 -05:00
|
|
|
const currentValue = Plain.serialize(editor.value);
|
2019-02-05 00:03:16 -06:00
|
|
|
|
|
|
|
if (previousValue !== currentValue) {
|
2019-05-10 07:00:39 -05:00
|
|
|
this.executeOnChangeAndRunQueries();
|
2019-02-05 00:03:16 -06:00
|
|
|
}
|
2019-08-23 08:17:13 -05:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
editor.blur();
|
2019-02-05 06:13:52 -06:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
return next();
|
2019-02-05 06:13:52 -06:00
|
|
|
};
|
|
|
|
|
2018-04-26 04:58:42 -05:00
|
|
|
render() {
|
2018-12-09 11:44:59 -06:00
|
|
|
const { disabled } = this.props;
|
|
|
|
const wrapperClassName = classnames('slate-query-field__wrapper', {
|
|
|
|
'slate-query-field__wrapper--disabled': disabled,
|
|
|
|
});
|
2019-09-23 06:26:05 -05:00
|
|
|
|
2018-04-26 04:58:42 -05:00
|
|
|
return (
|
2018-12-09 11:44:59 -06:00
|
|
|
<div className={wrapperClassName}>
|
2018-10-09 12:46:31 -05:00
|
|
|
<div className="slate-query-field">
|
|
|
|
<Editor
|
2019-09-23 06:26:05 -05:00
|
|
|
ref={editor => (this.editor = editor)}
|
|
|
|
schema={SCHEMA}
|
2018-10-09 12:46:31 -05:00
|
|
|
autoCorrect={false}
|
2018-12-09 11:44:59 -06:00
|
|
|
readOnly={this.props.disabled}
|
2018-10-09 12:46:31 -05:00
|
|
|
onBlur={this.handleBlur}
|
2019-09-23 06:26:05 -05:00
|
|
|
// onKeyDown={this.onKeyDown}
|
|
|
|
onChange={(change: { value: Value }) => {
|
|
|
|
this.onChange(change.value, false);
|
|
|
|
}}
|
2018-10-09 12:46:31 -05:00
|
|
|
placeholder={this.props.placeholder}
|
|
|
|
plugins={this.plugins}
|
|
|
|
spellCheck={false}
|
|
|
|
value={this.state.value}
|
|
|
|
/>
|
|
|
|
</div>
|
2018-04-26 04:58:42 -05:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default QueryField;
|