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 {