diff --git a/public/app/features/explore/QueryField.test.tsx b/public/app/features/explore/QueryField.test.tsx index e09f00a7b9e..71cdce520ab 100644 --- a/public/app/features/explore/QueryField.test.tsx +++ b/public/app/features/explore/QueryField.test.tsx @@ -4,17 +4,17 @@ import { QueryField } from './QueryField'; describe('', () => { it('should render with null initial value', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should render with empty initial value', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should render with initial value', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); }); diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index f59a4e4f95f..c9c8cc70f49 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -15,18 +15,18 @@ 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; + // We have both value and local state. This is usually an antipattern but we need to keep local state + // for perf reasons and also have outside value in for example in Explore redux that is mutable from logs + // creating a two way binding. + query: string | null; onRunQuery?: () => void; onChange?: (value: string) => void; onTypeahead?: (typeahead: TypeaheadInput) => Promise; @@ -43,7 +43,6 @@ export interface QueryFieldState { typeaheadPrefix: string; typeaheadText: string; value: Value; - lastExecutedValue: Value; } export interface TypeaheadInput { @@ -62,18 +61,19 @@ export interface TypeaheadInput { * Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example. */ export class QueryField extends React.PureComponent { - menuEl: HTMLElement | null; plugins: Plugin[]; resetTimer: NodeJS.Timer; mounted: boolean; - updateHighlightsTimer: Function; + runOnChangeDebounced: Function; editor: Editor; + // Is required by SuggestionsPlugin typeaheadRef: Typeahead; + lastExecutedValue: Value | null = null; constructor(props: QueryFieldProps, context: Context) { super(props, context); - this.updateHighlightsTimer = _.debounce(this.updateLogsHighlights, HIGHLIGHT_WAIT); + this.runOnChangeDebounced = _.debounce(this.runOnChange, 500); const { onTypeahead, cleanText, portalOrigin, onWillApplySuggestion } = props; @@ -82,7 +82,7 @@ export class QueryField extends React.PureComponent { + /** + * Update local state, propagate change upstream and optionally run the query afterwards. + */ + onChange = (value: Value, runQuery?: boolean) => { const documentChanged = value.document !== this.state.value.document; const prevValue = this.state.value; - // Control editor loop, then pass text change up to parent + // Update local state with new value and optionally change value upstream. this.setState({ value }, () => { + // The diff is needed because the actual value of editor have much more metadata (for example text selection) + // that is not passed upstream so every change of editor value does not mean change of the query text. if (documentChanged) { const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value); - if (textChanged && invokeParentOnValueChanged) { - this.executeOnChangeAndRunQueries(); + if (textChanged && runQuery) { + this.runOnChangeAndRunQuery(); } - if (textChanged && !invokeParentOnValueChanged) { - this.updateHighlightsTimer(); + if (textChanged && !runQuery) { + // Debounce change propagation by default for perf reasons. + this.runOnChangeDebounced(); } } }); }; - updateLogsHighlights = () => { + runOnChange = () => { const { onChange } = this.props; if (onChange) { @@ -155,30 +161,32 @@ export class QueryField extends React.PureComponent { - // Send text change to parent - const { onChange, onRunQuery } = this.props; - if (onChange) { - onChange(Plain.serialize(this.state.value)); - } + runOnRunQuery = () => { + const { onRunQuery } = this.props; if (onRunQuery) { onRunQuery(); - this.setState({ lastExecutedValue: this.state.value }); + this.lastExecutedValue = this.state.value; } }; + runOnChangeAndRunQuery = () => { + // onRunQuery executes query from Redux in Explore so it needs to be updated sync in case we want to run + // the query. + this.runOnChange(); + this.runOnRunQuery(); + }; + + /** + * We need to handle blur events here mainly because of dashboard panels which expect to have query executed on blur. + */ handleBlur = (event: Event, editor: CoreEditor, next: Function) => { - const { lastExecutedValue } = this.state; - const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null; + const previousValue = this.lastExecutedValue ? Plain.serialize(this.lastExecutedValue) : null; const currentValue = Plain.serialize(editor.value); if (previousValue !== currentValue) { - this.executeOnChangeAndRunQueries(); + this.runOnChangeAndRunQuery(); } - - editor.blur(); - return next(); }; diff --git a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx index e248b479cb0..6c3b00933f1 100644 --- a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx @@ -71,7 +71,7 @@ class ElasticsearchQueryField extends React.PureComponent {