[KQL] Better warning messages when using Lucene-like syntax (#31837)

* Update grammar to show lucene errors

* Add warning message

* Add tests

* Fix test & add opt out message

* Fix message namespace

* Fix tests
This commit is contained in:
Lukas Olson 2019-03-04 16:53:02 -07:00
parent 6449e59255
commit a5cb224d14
7 changed files with 1695 additions and 203 deletions

View file

@ -418,4 +418,81 @@ describe('kuery AST API', function () {
});
describe('doesKueryExpressionHaveLuceneSyntaxError', function () {
it('should return true for Lucene ranges', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]');
expect(result).to.eql(true);
});
it('should return false for KQL ranges', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar < 1');
expect(result).to.eql(false);
});
it('should return true for Lucene exists', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar');
expect(result).to.eql(true);
});
it('should return false for KQL exists', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar:*');
expect(result).to.eql(false);
});
it('should return true for Lucene wildcards', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba?');
expect(result).to.eql(true);
});
it('should return false for KQL wildcards', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba*');
expect(result).to.eql(false);
});
it('should return true for Lucene regex', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/');
expect(result).to.eql(true);
});
it('should return true for Lucene fuzziness', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba~');
expect(result).to.eql(true);
});
it('should return true for Lucene proximity', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2');
expect(result).to.eql(true);
});
it('should return true for Lucene boosting', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2');
expect(result).to.eql(true);
});
it('should return true for Lucene + operator', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('+foo: bar');
expect(result).to.eql(true);
});
it('should return true for Lucene - operators', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('-foo: bar');
expect(result).to.eql(true);
});
it('should return true for Lucene && operators', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux');
expect(result).to.eql(true);
});
it('should return true for Lucene || operators', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux');
expect(result).to.eql(true);
});
it('should return true for mixed KQL/Lucene queries', function () {
const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)');
expect(result).to.eql(true);
});
});
});

View file

@ -46,3 +46,5 @@ export function fromKueryExpression(
): KueryNode;
export function toElasticsearchQuery(node: KueryNode, indexPattern: StaticIndexPattern): JsonObject;
export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean;

View file

@ -61,3 +61,12 @@ export function toElasticsearchQuery(node, indexPattern) {
return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern);
}
export function doesKueryExpressionHaveLuceneSyntaxError(expression) {
try {
fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery);
return false;
} catch (e) {
return (e.message.startsWith('Lucene'));
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { fromLegacyKueryExpression, fromKueryExpression, fromLiteralExpression, toElasticsearchQuery } from './ast';
export * from './ast';

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@
// Initialization block
{
const { parseCursor, cursorSymbol, allowLeadingWildcards = true, helpers: { nodeTypes } } = options;
const { errorOnLuceneSyntax, parseCursor, cursorSymbol, allowLeadingWildcards = true, helpers: { nodeTypes } } = options;
const buildFunctionNode = nodeTypes.function.buildNodeWithArgumentNodes;
const buildLiteralNode = nodeTypes.literal.buildNode;
const buildWildcardNode = nodeTypes.wildcard.buildNode;
@ -26,7 +26,8 @@ start
}
OrQuery
= left:AndQuery Or right:OrQuery {
= &{ return errorOnLuceneSyntax; } LuceneQuery
/ left:AndQuery Or right:OrQuery {
const cursor = [left, right].find(node => node.type === 'cursor');
if (cursor) return cursor;
return buildFunctionNode('or', [left, right]);
@ -66,7 +67,7 @@ Expression
/ ValueExpression
FieldRangeExpression
= field:Literal Space* operator:RangeOperator Space* value:(QuotedString / UnquotedLiteral) {
= field:Literal Space* operator:RangeOperator Space* value:Literal {
if (value.type === 'cursor') {
return {
...value,
@ -172,12 +173,15 @@ Value
Or
= Space+ 'or'i Space+
/ &{ return errorOnLuceneSyntax; } LuceneOr
And
= Space+ 'and'i Space+
/ &{ return errorOnLuceneSyntax; } LuceneAnd
Not
= 'not'i Space+
/ &{ return errorOnLuceneSyntax; } LuceneNot
Literal
= QuotedString / UnquotedLiteral
@ -278,3 +282,109 @@ Space
Cursor
= &{ return parseCursor; } '@kuery-cursor@' { return cursorSymbol; }
// Temporary error rules (to help users transition from Lucene... should be removed at some point)
LuceneOr
= Space* '||' Space* {
error('LuceneOr');
}
LuceneAnd
= Space* '&&' Space* {
error('LuceneAnd');
}
/ '+' {
error('LuceneAnd');
}
LuceneNot
= '-' {
error('LuceneNot');
}
/ '!' {
error('LuceneNot');
}
LuceneQuery
= LuceneFieldQuery
/ LuceneValue
/ LuceneExists
LuceneFieldQuery
= LuceneLiteral Space* ':' Space* LuceneValue
LuceneValue
= LuceneRange
/ LuceneWildcard
/ LuceneRegex
/ LuceneFuzzy
/ LuceneProximity
/ LuceneBoost
LuceneExists
= '_exists_' Space* ':' Space* LuceneLiteral {
error('LuceneExists');
}
LuceneRange
= RangeOperator Space* LuceneLiteral {
error('LuceneRange');
}
/ LuceneRangeStart Space* LuceneLiteral LuceneTo LuceneLiteral LuceneRangeEnd {
error('LuceneRange');
}
LuceneWildcard
= (LuceneUnquotedCharacter / '*')* '?' LuceneWildcard* {
error('LuceneWildcard');
}
LuceneRegex
= '/' [^/]* '/' {
error('LuceneRegex');
}
LuceneFuzzy
= LuceneUnquotedLiteral '~' [0-9]* {
error('LuceneFuzzy');
}
LuceneProximity
= QuotedString '~' [0-9]* {
error('LuceneProximity');
}
LuceneBoost
= LuceneLiteral '^' [0-9]* {
error('LuceneBoost');
}
LuceneLiteral
= QuotedString / LuceneUnquotedLiteral
LuceneUnquotedLiteral
= LuceneUnquotedCharacter+
LuceneUnquotedCharacter
= EscapedWhitespace
/ EscapedLuceneSpecialCharacter
/ !LuceneSpecialCharacter !LuceneKeyword .
LuceneKeyword
= Or / And / LuceneOr / LuceneAnd / LuceneNot / LuceneTo
EscapedLuceneSpecialCharacter
= '\\' LuceneSpecialCharacter { return char; }
LuceneSpecialCharacter
= '+' / '-' / '=' / '>' / '<' / '!' / '(' / ')' / '{' / '}' / '[' / ']' / '^' / '"' / '~' / '*' / '?' / ':' / '\\' / '/'
LuceneTo
= Space+ 'TO' Space+
LuceneRangeStart
= '[' / '{'
LuceneRangeEnd
= ']' / '}'

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query';
import { IndexPattern } from 'ui/index_patterns';
import classNames from 'classnames';
@ -38,12 +39,21 @@ import { matchPairs } from '../lib/match_pairs';
import { QueryLanguageSwitcher } from './language_switcher';
import { SuggestionsComponent } from './typeahead/suggestions_component';
import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui';
import {
EuiButton,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiOutsideClickDetector,
} from '@elastic/eui';
// @ts-ignore
import { EuiSuperDatePicker, EuiSuperUpdateButton } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { documentationLinks } from 'ui/documentation_links';
import { Toast, toastNotifications } from 'ui/notify';
const KEY_CODES = {
LEFT: 37,
@ -462,6 +472,8 @@ export class QueryBarUI extends Component<Props, State> {
preventDefault();
}
this.handleLuceneSyntaxWarning();
if (this.persistedLog) {
this.persistedLog.add(this.state.query.query);
}
@ -683,6 +695,56 @@ export class QueryBarUI extends Component<Props, State> {
</EuiFlexItem>
);
}
private handleLuceneSyntaxWarning() {
const { intl, store } = this.props;
const { query, language } = this.state.query;
if (
language === 'kuery' &&
!store.get('kibana.luceneSyntaxWarningOptOut') &&
doesKueryExpressionHaveLuceneSyntaxError(query)
) {
const toast = toastNotifications.addWarning({
title: intl.formatMessage({
id: 'common.ui.queryBar.luceneSyntaxWarningTitle',
defaultMessage: 'Lucene syntax warning',
}),
text: (
<div>
<p>
<FormattedMessage
id="common.ui.queryBar.luceneSyntaxWarningMessage"
defaultMessage="It looks like you may be trying to use Lucene query syntax, although you
have Kibana Query Language (KQL) selected. Please review the KQL docs {link}."
values={{
link: (
<EuiLink href={documentationLinks.query.kueryQuerySyntax} target="_blank">
<FormattedMessage
id="common.ui.queryBar.syntaxOptionsDescription.docsLinkText"
defaultMessage="here"
/>
</EuiLink>
),
}}
/>
</p>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton size="s" onClick={() => this.onLuceneSyntaxWarningOptOut(toast)}>
Don't show again
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
),
});
}
}
private onLuceneSyntaxWarningOptOut(toast: Toast) {
this.props.store.set('kibana.luceneSyntaxWarningOptOut', true);
toastNotifications.remove(toast);
}
}
// @ts-ignore