Commit 01bbcf4e by David Committed by GitHub

Editor: New line on Enter, run query on Shift+Enter (#24654)

* Editor: New line on Enter, run query on Shift+Enter

- default Enter behavior on query editor fields should be a new line
- special behavior should require a special key: running a query is now
done on Shift-Enter
- Plugins order had to be changed because when typeahead is shown, Enter
is accepting the suggestion

* Run with ctrl-enter, hint in query placeholder

* Fix Kusto field behavior for Enter

* Fix Kusto field behavior for default suggestion
parent e11504dc
......@@ -69,10 +69,12 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
// Base plugins
this.plugins = [
NewlinePlugin(),
// SuggestionsPlugin and RunnerPlugin need to be before NewlinePlugin
// because they override Enter behavior
SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion }),
ClearPlugin(),
RunnerPlugin({ handler: this.runOnChangeAndRunQuery }),
NewlinePlugin(),
ClearPlugin(),
SelectionShortcutsPlugin(),
IndentationPlugin(),
ClipboardPlugin(),
......
......@@ -23,7 +23,7 @@ export function NewlinePlugin(): Plugin {
return next();
}
if (keyEvent.key === 'Enter' && keyEvent.shiftKey) {
if (keyEvent.key === 'Enter') {
keyEvent.preventDefault();
const { startBlock } = value;
......
......@@ -8,10 +8,14 @@ describe('runner', () => {
const mockHandler = jest.fn();
const handler = RunnerPlugin({ handler: mockHandler }).onKeyDown!;
it('should execute query when enter is pressed and there are no suggestions visible', () => {
it('should execute query when enter with shift is pressed', () => {
const value = Plain.deserialize('');
const editor = shallow<Editor>(<Editor value={value} />);
handler({ key: 'Enter', preventDefault: () => {} } as KeyboardEvent, editor.instance() as any, () => {});
handler(
{ key: 'Enter', shiftKey: true, preventDefault: () => {} } as KeyboardEvent,
editor.instance() as any,
() => {}
);
expect(mockHandler).toBeCalled();
});
});
......@@ -7,11 +7,11 @@ export function RunnerPlugin({ handler }: any): Plugin {
const keyEvent = event as KeyboardEvent;
// Handle enter
if (handler && keyEvent.key === 'Enter' && !keyEvent.shiftKey) {
if (handler && keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) {
// Submit on Enter
keyEvent.preventDefault();
handler(keyEvent);
return true;
return editor;
}
return next();
......
......@@ -97,7 +97,15 @@ export function SuggestionsPlugin({
break;
case 'Enter':
case 'Enter': {
if (!(keyEvent.shiftKey || keyEvent.ctrlKey) && hasSuggestions) {
keyEvent.preventDefault();
return typeaheadRef.insertSuggestion();
}
break;
}
case 'Tab': {
if (hasSuggestions) {
keyEvent.preventDefault();
......@@ -108,7 +116,10 @@ export function SuggestionsPlugin({
}
default: {
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
// Don't react on meta keys
if (keyEvent.key.length === 1) {
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
}
break;
}
}
......
......@@ -357,7 +357,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
onRunQuery={this.props.onRunQuery}
onTypeahead={this.onTypeahead}
cleanText={cleanText}
placeholder="Enter a CloudWatch Logs Insights query"
placeholder="Enter a CloudWatch Logs Insights query (run with Shift+Enter)"
portalOrigin="cloudwatch"
syntaxLoaded={syntaxLoaded}
disabled={loadingLogGroups || selectedLogGroups.length === 0}
......
......@@ -71,7 +71,7 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
query={query.query}
onChange={this.onChangeQuery}
onRunQuery={this.props.onRunQuery}
placeholder="Enter a Lucene query"
placeholder="Enter a Lucene query (run with Shift+Enter)"
portalOrigin="elasticsearch"
syntaxLoaded={syntaxLoaded}
/>
......
......@@ -73,7 +73,7 @@ class QueryField extends React.Component<any, any> {
labelKeys: {},
labelValues: {},
suggestions: [],
typeaheadIndex: 0,
typeaheadIndex: null,
typeaheadPrefix: '',
value: getInitialValue(props.initialQuery || ''),
};
......@@ -144,10 +144,10 @@ class QueryField extends React.Component<any, any> {
case 'Tab':
case 'Enter': {
if (this.menuEl) {
if (this.menuEl && typeaheadIndex !== null) {
// Dont blur input
keyboardEvent.preventDefault();
if (!suggestions || !suggestions.length) {
if (!suggestions || !suggestions.length || keyboardEvent.shiftKey || keyboardEvent.ctrlKey) {
return next();
}
......@@ -166,7 +166,7 @@ class QueryField extends React.Component<any, any> {
if (this.menuEl) {
// Select next suggestion
keyboardEvent.preventDefault();
this.setState({ typeaheadIndex: typeaheadIndex + 1 });
this.setState({ typeaheadIndex: (typeaheadIndex || 0) + 1 });
}
break;
}
......@@ -175,7 +175,7 @@ class QueryField extends React.Component<any, any> {
if (this.menuEl) {
// Select previous suggestion
keyboardEvent.preventDefault();
this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
this.setState({ typeaheadIndex: Math.max(0, (typeaheadIndex || 0) - 1) });
}
break;
}
......@@ -203,7 +203,7 @@ class QueryField extends React.Component<any, any> {
this.setState(
{
suggestions: [],
typeaheadIndex: 0,
typeaheadIndex: null,
typeaheadPrefix: '',
typeaheadContext: null,
},
......@@ -298,19 +298,20 @@ class QueryField extends React.Component<any, any> {
renderMenu = () => {
const { portalPrefix } = this.props;
const { suggestions } = this.state;
const { suggestions, typeaheadIndex } = this.state;
const hasSuggesstions = suggestions && suggestions.length > 0;
if (!hasSuggesstions) {
return null;
}
// Guard selectedIndex to be within the length of the suggestions
let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
let selectedIndex = Math.max(typeaheadIndex, 0);
const flattenedSuggestions = flattenSuggestions(suggestions);
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(i =>
typeof i === 'object' ? i.text : i
);
const selectedKeys = (typeaheadIndex !== null && flattenedSuggestions.length > 0
? [flattenedSuggestions[selectedIndex]]
: []
).map(i => (typeof i === 'object' ? i.text : i));
// Create typeahead in DOM root so we can later position it absolutely
return (
......
......@@ -210,7 +210,7 @@
<button class="btn btn-primary width-10" ng-click="ctrl.refresh()">Run</button>
</div>
<div class="gf-form">
<label class="gf-form-label">(New Line: Shift+Enter, Run Query: Enter, Trigger Suggestion: Ctrl+Space)</label>
<label class="gf-form-label">(Run Query: Shift+Enter, Trigger Suggestion: Ctrl+Space)</label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
......
......@@ -174,7 +174,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
onChange={this.onChangeQuery}
onBlur={this.props.onBlur}
onRunQuery={this.props.onRunQuery}
placeholder="Enter a Loki query"
placeholder="Enter a Loki query (run with Shift+Enter)"
portalOrigin="loki"
syntaxLoaded={syntaxLoaded}
/>
......
......@@ -340,7 +340,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
onBlur={this.props.onBlur}
onChange={this.onChangeQuery}
onRunQuery={this.props.onRunQuery}
placeholder="Enter a PromQL query"
placeholder="Enter a PromQL query (run with Shift+Enter)"
portalOrigin="prometheus"
syntaxLoaded={syntaxLoaded}
/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment