mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
e11504dcd2
commit
01bbcf4eea
@ -69,10 +69,12 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
|||||||
|
|
||||||
// Base plugins
|
// Base plugins
|
||||||
this.plugins = [
|
this.plugins = [
|
||||||
NewlinePlugin(),
|
// SuggestionsPlugin and RunnerPlugin need to be before NewlinePlugin
|
||||||
|
// because they override Enter behavior
|
||||||
SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion }),
|
SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion }),
|
||||||
ClearPlugin(),
|
|
||||||
RunnerPlugin({ handler: this.runOnChangeAndRunQuery }),
|
RunnerPlugin({ handler: this.runOnChangeAndRunQuery }),
|
||||||
|
NewlinePlugin(),
|
||||||
|
ClearPlugin(),
|
||||||
SelectionShortcutsPlugin(),
|
SelectionShortcutsPlugin(),
|
||||||
IndentationPlugin(),
|
IndentationPlugin(),
|
||||||
ClipboardPlugin(),
|
ClipboardPlugin(),
|
||||||
|
@ -23,7 +23,7 @@ export function NewlinePlugin(): Plugin {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyEvent.key === 'Enter' && keyEvent.shiftKey) {
|
if (keyEvent.key === 'Enter') {
|
||||||
keyEvent.preventDefault();
|
keyEvent.preventDefault();
|
||||||
|
|
||||||
const { startBlock } = value;
|
const { startBlock } = value;
|
||||||
|
@ -8,10 +8,14 @@ describe('runner', () => {
|
|||||||
const mockHandler = jest.fn();
|
const mockHandler = jest.fn();
|
||||||
const handler = RunnerPlugin({ handler: mockHandler }).onKeyDown!;
|
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 value = Plain.deserialize('');
|
||||||
const editor = shallow<Editor>(<Editor value={value} />);
|
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();
|
expect(mockHandler).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,11 +7,11 @@ export function RunnerPlugin({ handler }: any): Plugin {
|
|||||||
const keyEvent = event as KeyboardEvent;
|
const keyEvent = event as KeyboardEvent;
|
||||||
|
|
||||||
// Handle enter
|
// Handle enter
|
||||||
if (handler && keyEvent.key === 'Enter' && !keyEvent.shiftKey) {
|
if (handler && keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) {
|
||||||
// Submit on Enter
|
// Submit on Enter
|
||||||
keyEvent.preventDefault();
|
keyEvent.preventDefault();
|
||||||
handler(keyEvent);
|
handler(keyEvent);
|
||||||
return true;
|
return editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
@ -97,7 +97,15 @@ export function SuggestionsPlugin({
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Enter':
|
case 'Enter': {
|
||||||
|
if (!(keyEvent.shiftKey || keyEvent.ctrlKey) && hasSuggestions) {
|
||||||
|
keyEvent.preventDefault();
|
||||||
|
return typeaheadRef.insertSuggestion();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'Tab': {
|
case 'Tab': {
|
||||||
if (hasSuggestions) {
|
if (hasSuggestions) {
|
||||||
keyEvent.preventDefault();
|
keyEvent.preventDefault();
|
||||||
@ -108,7 +116,10 @@ export function SuggestionsPlugin({
|
|||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
|
// Don't react on meta keys
|
||||||
|
if (keyEvent.key.length === 1) {
|
||||||
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
|
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,7 +357,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
|||||||
onRunQuery={this.props.onRunQuery}
|
onRunQuery={this.props.onRunQuery}
|
||||||
onTypeahead={this.onTypeahead}
|
onTypeahead={this.onTypeahead}
|
||||||
cleanText={cleanText}
|
cleanText={cleanText}
|
||||||
placeholder="Enter a CloudWatch Logs Insights query"
|
placeholder="Enter a CloudWatch Logs Insights query (run with Shift+Enter)"
|
||||||
portalOrigin="cloudwatch"
|
portalOrigin="cloudwatch"
|
||||||
syntaxLoaded={syntaxLoaded}
|
syntaxLoaded={syntaxLoaded}
|
||||||
disabled={loadingLogGroups || selectedLogGroups.length === 0}
|
disabled={loadingLogGroups || selectedLogGroups.length === 0}
|
||||||
|
@ -71,7 +71,7 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
|
|||||||
query={query.query}
|
query={query.query}
|
||||||
onChange={this.onChangeQuery}
|
onChange={this.onChangeQuery}
|
||||||
onRunQuery={this.props.onRunQuery}
|
onRunQuery={this.props.onRunQuery}
|
||||||
placeholder="Enter a Lucene query"
|
placeholder="Enter a Lucene query (run with Shift+Enter)"
|
||||||
portalOrigin="elasticsearch"
|
portalOrigin="elasticsearch"
|
||||||
syntaxLoaded={syntaxLoaded}
|
syntaxLoaded={syntaxLoaded}
|
||||||
/>
|
/>
|
||||||
|
@ -73,7 +73,7 @@ class QueryField extends React.Component<any, any> {
|
|||||||
labelKeys: {},
|
labelKeys: {},
|
||||||
labelValues: {},
|
labelValues: {},
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
typeaheadIndex: 0,
|
typeaheadIndex: null,
|
||||||
typeaheadPrefix: '',
|
typeaheadPrefix: '',
|
||||||
value: getInitialValue(props.initialQuery || ''),
|
value: getInitialValue(props.initialQuery || ''),
|
||||||
};
|
};
|
||||||
@ -144,10 +144,10 @@ class QueryField extends React.Component<any, any> {
|
|||||||
|
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
if (this.menuEl) {
|
if (this.menuEl && typeaheadIndex !== null) {
|
||||||
// Dont blur input
|
// Dont blur input
|
||||||
keyboardEvent.preventDefault();
|
keyboardEvent.preventDefault();
|
||||||
if (!suggestions || !suggestions.length) {
|
if (!suggestions || !suggestions.length || keyboardEvent.shiftKey || keyboardEvent.ctrlKey) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ class QueryField extends React.Component<any, any> {
|
|||||||
if (this.menuEl) {
|
if (this.menuEl) {
|
||||||
// Select next suggestion
|
// Select next suggestion
|
||||||
keyboardEvent.preventDefault();
|
keyboardEvent.preventDefault();
|
||||||
this.setState({ typeaheadIndex: typeaheadIndex + 1 });
|
this.setState({ typeaheadIndex: (typeaheadIndex || 0) + 1 });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ class QueryField extends React.Component<any, any> {
|
|||||||
if (this.menuEl) {
|
if (this.menuEl) {
|
||||||
// Select previous suggestion
|
// Select previous suggestion
|
||||||
keyboardEvent.preventDefault();
|
keyboardEvent.preventDefault();
|
||||||
this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
|
this.setState({ typeaheadIndex: Math.max(0, (typeaheadIndex || 0) - 1) });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -203,7 +203,7 @@ class QueryField extends React.Component<any, any> {
|
|||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
typeaheadIndex: 0,
|
typeaheadIndex: null,
|
||||||
typeaheadPrefix: '',
|
typeaheadPrefix: '',
|
||||||
typeaheadContext: null,
|
typeaheadContext: null,
|
||||||
},
|
},
|
||||||
@ -298,19 +298,20 @@ class QueryField extends React.Component<any, any> {
|
|||||||
|
|
||||||
renderMenu = () => {
|
renderMenu = () => {
|
||||||
const { portalPrefix } = this.props;
|
const { portalPrefix } = this.props;
|
||||||
const { suggestions } = this.state;
|
const { suggestions, typeaheadIndex } = this.state;
|
||||||
const hasSuggesstions = suggestions && suggestions.length > 0;
|
const hasSuggesstions = suggestions && suggestions.length > 0;
|
||||||
if (!hasSuggesstions) {
|
if (!hasSuggesstions) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard selectedIndex to be within the length of the suggestions
|
// 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);
|
const flattenedSuggestions = flattenSuggestions(suggestions);
|
||||||
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
|
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
|
||||||
const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(i =>
|
const selectedKeys = (typeaheadIndex !== null && flattenedSuggestions.length > 0
|
||||||
typeof i === 'object' ? i.text : i
|
? [flattenedSuggestions[selectedIndex]]
|
||||||
);
|
: []
|
||||||
|
).map(i => (typeof i === 'object' ? i.text : i));
|
||||||
|
|
||||||
// Create typeahead in DOM root so we can later position it absolutely
|
// Create typeahead in DOM root so we can later position it absolutely
|
||||||
return (
|
return (
|
||||||
|
@ -210,7 +210,7 @@
|
|||||||
<button class="btn btn-primary width-10" ng-click="ctrl.refresh()">Run</button>
|
<button class="btn btn-primary width-10" ng-click="ctrl.refresh()">Run</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<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>
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
|
@ -174,7 +174,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
onChange={this.onChangeQuery}
|
onChange={this.onChangeQuery}
|
||||||
onBlur={this.props.onBlur}
|
onBlur={this.props.onBlur}
|
||||||
onRunQuery={this.props.onRunQuery}
|
onRunQuery={this.props.onRunQuery}
|
||||||
placeholder="Enter a Loki query"
|
placeholder="Enter a Loki query (run with Shift+Enter)"
|
||||||
portalOrigin="loki"
|
portalOrigin="loki"
|
||||||
syntaxLoaded={syntaxLoaded}
|
syntaxLoaded={syntaxLoaded}
|
||||||
/>
|
/>
|
||||||
|
@ -340,7 +340,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
onBlur={this.props.onBlur}
|
onBlur={this.props.onBlur}
|
||||||
onChange={this.onChangeQuery}
|
onChange={this.onChangeQuery}
|
||||||
onRunQuery={this.props.onRunQuery}
|
onRunQuery={this.props.onRunQuery}
|
||||||
placeholder="Enter a PromQL query"
|
placeholder="Enter a PromQL query (run with Shift+Enter)"
|
||||||
portalOrigin="prometheus"
|
portalOrigin="prometheus"
|
||||||
syntaxLoaded={syntaxLoaded}
|
syntaxLoaded={syntaxLoaded}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user