From 8ea7af9511957ba8e422f7d9cf2374e1d2c1908d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 18 Jul 2018 21:11:48 -0600 Subject: [PATCH] merge conflicts (#20959) --- .../__snapshots__/field_editor.test.js.snap | 601 +++++++++++------- .../editors/default/default.js | 23 +- .../editors/default/default.test.js | 35 +- .../__snapshots__/help_flyout.test.js.snap | 134 ---- .../scripting_call_outs/help_flyout.js | 113 ---- .../components/scripting_call_outs/index.js | 1 - .../__snapshots__/help_flyout.test.js.snap | 48 ++ .../components/scripting_help/help_flyout.js | 81 +++ .../help_flyout.test.js | 11 +- .../scripting_help/index.js} | 21 +- .../scripting_help/scripting_syntax.js | 104 +++ .../components/scripting_help/test_script.js | 226 +++++++ src/ui/public/field_editor/field_editor.js | 195 ++++-- .../public/field_editor/field_editor.test.js | 2 + .../__tests__/convert_sample_input.test.js | 52 -- src/ui/public/field_editor/lib/index.js | 2 +- .../field_editor/lib/validate_script.js | 63 ++ .../apps/management/_scripted_fields.js | 15 + .../management/_scripted_fields_preview.js | 66 ++ test/functional/apps/management/index.js | 1 + test/functional/page_objects/settings_page.js | 51 +- 21 files changed, 1244 insertions(+), 601 deletions(-) delete mode 100644 src/ui/public/field_editor/components/scripting_call_outs/__snapshots__/help_flyout.test.js.snap delete mode 100644 src/ui/public/field_editor/components/scripting_call_outs/help_flyout.js create mode 100644 src/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap create mode 100644 src/ui/public/field_editor/components/scripting_help/help_flyout.js rename src/ui/public/field_editor/components/{scripting_call_outs => scripting_help}/help_flyout.test.js (84%) rename src/ui/public/field_editor/{lib/convert_sample_input.js => components/scripting_help/index.js} (67%) create mode 100644 src/ui/public/field_editor/components/scripting_help/scripting_syntax.js create mode 100644 src/ui/public/field_editor/components/scripting_help/test_script.js delete mode 100644 src/ui/public/field_editor/lib/__tests__/convert_sample_input.test.js create mode 100644 src/ui/public/field_editor/lib/validate_script.js create mode 100644 test/functional/apps/management/_scripted_fields_preview.js diff --git a/src/ui/public/field_editor/__snapshots__/field_editor.test.js.snap b/src/ui/public/field_editor/__snapshots__/field_editor.test.js.snap index 84bc861c30dc5..292db61bbea44 100644 --- a/src/ui/public/field_editor/__snapshots__/field_editor.test.js.snap +++ b/src/ui/public/field_editor/__snapshots__/field_editor.test.js.snap @@ -11,16 +11,29 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` size="m" /> - - - + + + + + - - Scripting help - - } - isInvalid={true} - label="Script" - > - + - - - - + + + + + Access fields with + + doc['some_field'].value + + . + +
+ + Get help with the syntax and preview the results of your script. + +
+
+ + + + - Create field - - - - + Create field + + + - Cancel - - - + + Cancel + + + +
- - - + + + + + - - Scripting help - - } - isInvalid={false} - label="Script" - > - + - - - - - Save field - - - - + + + + + Access fields with + + doc['some_field'].value + + . + +
+ + Get help with the syntax and preview the results of your script. + +
+
+ + + + - Cancel - - - - + Save field + + + - Delete - - - + + Cancel + + + + + + + Delete + + + + + +
- - - + + + + + - - Scripting help - - } - isInvalid={true} - label="Script" - > - + - - - - + + + + + Access fields with + + doc['some_field'].value + + . + +
+ + Get help with the syntax and preview the results of your script. + +
+
+ + + + - Create field - - - - + Create field + + + - Cancel - - - + + Cancel + + + +
- - - + + + + + @@ -735,59 +886,81 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` onChange={[Function]} /> - - Scripting help - - } - isInvalid={false} - label="Script" - > - + - - - - - Save field - - - - + + + + + Access fields with + + doc['some_field'].value + + . + +
+ + Get help with the syntax and preview the results of your script. + +
+
+ + + + - Cancel - - - - + Save field + + + - Delete - - - + + Cancel + + + + + + + Delete + + + + + +
{ + let error = null; + let samples = []; + + try { + samples = inputs.map(input => { + return { + input, + output: converter(input), + }; + }); + } catch(e) { + error = `An error occurred while trying to use this format configuration: ${e.message}`; + } + + return { + error, + samples, + }; +}; export class DefaultFormatEditor extends PureComponent { static propTypes = { diff --git a/src/ui/public/field_editor/components/field_format_editor/editors/default/default.test.js b/src/ui/public/field_editor/components/field_format_editor/editors/default/default.test.js index 2f1b1b2107eb0..069846ccfadc4 100644 --- a/src/ui/public/field_editor/components/field_format_editor/editors/default/default.test.js +++ b/src/ui/public/field_editor/components/field_format_editor/editors/default/default.test.js @@ -20,7 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { DefaultFormatEditor } from './default'; +import { DefaultFormatEditor, convertSampleInput } from './default'; const fieldType = 'number'; const format = { @@ -31,6 +31,39 @@ const onChange = jest.fn(); const onError = jest.fn(); describe('DefaultFormatEditor', () => { + + describe('convertSampleInput', () => { + const converter = (input) => { + if(isNaN(input)) { + throw { + message: 'Input is not a number' + }; + } else { + return input * 2; + } + }; + + it('should convert a set of inputs', () => { + const inputs = [1, 10, 15]; + const output = convertSampleInput(converter, inputs); + + expect(output.error).toEqual(null); + expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([ + { input: 1, output: 2 }, + { input: 10, output: 20 }, + { input: 15, output: 30 }, + ])); + }); + + it('should return error if converter throws one', () => { + const inputs = [1, 10, 15, 'invalid']; + const output = convertSampleInput(converter, inputs); + + expect(output.error).toEqual('An error occurred while trying to use this format configuration: Input is not a number'); + expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([])); + }); + }); + it('should render nothing', async () => { const component = shallow( - - -

- Scripting help -

-

- By default, Kibana scripted fields use - - Painless - - - , a simple and secure scripting language designed specifically for use with Elasticsearch, to access values in the document use the following format: -

-

- - doc['some_field'].value - -

-

- Painless is powerful but easy to use. It provides access to many - - native Java APIs - - - . Read up on its - - syntax - - - and you'll be up to speed in no time! -

-

- Kibana currently imposes one special limitation on the painless scripts you write. They cannot contain named functions. -

-

- Coming from an older version of Kibana? The - - Lucene Expressions - - - you know and love are still available. Lucene expressions are a lot like JavaScript, but limited to basic arithmetic, bitwise and comparison operations. -

-

- There are a few limitations when using Lucene Expressions: -

-
    -
  • - Only numeric, boolean, date, and geo_point fields may be accessed -
  • -
  • - Stored fields are not available -
  • -
  • - If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0 -
  • -
-

- Here are all the operations available to lucene expressions: -

-
    -
  • - Arithmetic operators: + - * / % -
  • -
  • - Bitwise operators: | & ^ ~ << >> >>> -
  • -
  • - Boolean operators (including the ternary operator): && || ! ?: -
  • -
  • - Comparison operators: < <= == >= > -
  • -
  • - Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow -
  • -
  • - Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan -
  • -
  • - Distance functions: haversin -
  • -
  • - Miscellaneous functions: min, max -
  • -
-
-
- -`; - -exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = `""`; diff --git a/src/ui/public/field_editor/components/scripting_call_outs/help_flyout.js b/src/ui/public/field_editor/components/scripting_call_outs/help_flyout.js deleted file mode 100644 index 3d7c9b2025867..0000000000000 --- a/src/ui/public/field_editor/components/scripting_call_outs/help_flyout.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { getDocLink } from 'ui/documentation_links'; - -import { - EuiCode, - EuiFlyout, - EuiFlyoutBody, - EuiIcon, - EuiLink, - EuiText, -} from '@elastic/eui'; - -export const ScriptingHelpFlyout = ({ - isVisible = false, - onClose = () => {}, -}) => { - return isVisible ? ( - - - -

Scripting help

-

- By default, Kibana scripted fields use {( - - Painless - - )}, a simple and secure scripting language designed specifically for use with Elasticsearch, - to access values in the document use the following format: -

-

- doc['some_field'].value -

-

- Painless is powerful but easy to use. It provides access to many {( - - native Java APIs - - )}. Read up on its {( - - syntax - - )} and you'll be up to speed in no time! -

-

- Kibana currently imposes one special limitation on the painless scripts you write. They cannot contain named functions. -

-

- Coming from an older version of Kibana? The {( - - Lucene Expressions - - )} you know and love are still available. Lucene expressions are a lot like JavaScript, - but limited to basic arithmetic, bitwise and comparison operations. -

-

- There are a few limitations when using Lucene Expressions: -

-
    -
  • Only numeric, boolean, date, and geo_point fields may be accessed
  • -
  • Stored fields are not available
  • -
  • If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0
  • -
-

- Here are all the operations available to lucene expressions: -

-
    -
  • Arithmetic operators: + - * / %
  • -
  • Bitwise operators: | & ^ ~ << >> >>>
  • -
  • Boolean operators (including the ternary operator): && || ! ?:
  • -
  • Comparison operators: < <= == >= >
  • -
  • Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow
  • -
  • Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan
  • -
  • Distance functions: haversin
  • -
  • Miscellaneous functions: min, max
  • -
-
-
-
- ) : null; -}; - -ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout'; diff --git a/src/ui/public/field_editor/components/scripting_call_outs/index.js b/src/ui/public/field_editor/components/scripting_call_outs/index.js index 44da62f3e22f2..1ecf919ce6b16 100644 --- a/src/ui/public/field_editor/components/scripting_call_outs/index.js +++ b/src/ui/public/field_editor/components/scripting_call_outs/index.js @@ -19,4 +19,3 @@ export { ScriptingDisabledCallOut } from './disabled_call_out'; export { ScriptingWarningCallOut } from './warning_call_out'; -export { ScriptingHelpFlyout } from './help_flyout'; diff --git a/src/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap b/src/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap new file mode 100644 index 0000000000000..5e222240624e4 --- /dev/null +++ b/src/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScriptingHelpFlyout should render normally 1`] = ` + + + , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + } + } + tabs={ + Array [ + Object { + "content": , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + }, + Object { + "content": , + "data-test-subj": "testTab", + "id": "test", + "name": "Preview results", + }, + ] + } + /> + + +`; + +exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = `""`; diff --git a/src/ui/public/field_editor/components/scripting_help/help_flyout.js b/src/ui/public/field_editor/components/scripting_help/help_flyout.js new file mode 100644 index 0000000000000..45975fe07194b --- /dev/null +++ b/src/ui/public/field_editor/components/scripting_help/help_flyout.js @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiTabbedContent, +} from '@elastic/eui'; + +import { ScriptingSyntax } from './scripting_syntax'; +import { TestScript } from './test_script'; + +export const ScriptingHelpFlyout = ({ + isVisible = false, + onClose = () => {}, + indexPattern, + lang, + name, + script, + executeScript, +}) => { + const tabs = [{ + id: 'syntax', + name: 'Syntax', + ['data-test-subj']: 'syntaxTab', + content: , + }, { + id: 'test', + name: 'Preview results', + ['data-test-subj']: 'testTab', + content: ( + + ), + }]; + + return isVisible ? ( + + + + + + ) : null; +}; + +ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout'; + +ScriptingHelpFlyout.propTypes = { + indexPattern: PropTypes.object.isRequired, + lang: PropTypes.string.isRequired, + name: PropTypes.string, + script: PropTypes.string, + executeScript: PropTypes.func.isRequired, +}; diff --git a/src/ui/public/field_editor/components/scripting_call_outs/help_flyout.test.js b/src/ui/public/field_editor/components/scripting_help/help_flyout.test.js similarity index 84% rename from src/ui/public/field_editor/components/scripting_call_outs/help_flyout.test.js rename to src/ui/public/field_editor/components/scripting_help/help_flyout.test.js index 0ffa92dde9ef2..bfcd2ea7e3c5c 100644 --- a/src/ui/public/field_editor/components/scripting_call_outs/help_flyout.test.js +++ b/src/ui/public/field_editor/components/scripting_help/help_flyout.test.js @@ -26,11 +26,16 @@ jest.mock('ui/documentation_links', () => ({ getDocLink: (doc) => `(docLink for ${doc})`, })); +const indexPatternMock = {}; + describe('ScriptingHelpFlyout', () => { it('should render normally', async () => { const component = shallow( {}} /> ); @@ -39,7 +44,11 @@ describe('ScriptingHelpFlyout', () => { it('should render nothing if not visible', async () => { const component = shallow( - + {}} + /> ); expect(component).toMatchSnapshot(); diff --git a/src/ui/public/field_editor/lib/convert_sample_input.js b/src/ui/public/field_editor/components/scripting_help/index.js similarity index 67% rename from src/ui/public/field_editor/lib/convert_sample_input.js rename to src/ui/public/field_editor/components/scripting_help/index.js index 58d1917afc574..13b3b10fb5859 100644 --- a/src/ui/public/field_editor/lib/convert_sample_input.js +++ b/src/ui/public/field_editor/components/scripting_help/index.js @@ -17,23 +17,4 @@ * under the License. */ -export const convertSampleInput = (converter, inputs) => { - let error = null; - let samples = []; - - try { - samples = inputs.map(input => { - return { - input, - output: converter(input), - }; - }); - } catch(e) { - error = `An error occurred while trying to use this format configuration: ${e.message}`; - } - - return { - error, - samples, - }; -}; +export { ScriptingHelpFlyout } from './help_flyout'; diff --git a/src/ui/public/field_editor/components/scripting_help/scripting_syntax.js b/src/ui/public/field_editor/components/scripting_help/scripting_syntax.js new file mode 100644 index 0000000000000..892bf5376ea1b --- /dev/null +++ b/src/ui/public/field_editor/components/scripting_help/scripting_syntax.js @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { getDocLink } from 'ui/documentation_links'; + +import { + EuiCode, + EuiIcon, + EuiLink, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +export const ScriptingSyntax = () => ( + + + +

Syntax

+

+ By default, Kibana scripted fields use {( + + Painless + + )}, a simple and secure scripting language designed specifically for use with Elasticsearch, + to access values in the document use the following format: +

+

+ doc['some_field'].value +

+

+ Painless is powerful but easy to use. It provides access to many {( + + native Java APIs + + )}. Read up on its {( + + syntax + + )} and you'll be up to speed in no time! +

+

+ Kibana currently imposes one special limitation on the painless scripts you write. They cannot contain named functions. +

+

+ Coming from an older version of Kibana? The {( + + Lucene Expressions + + )} you know and love are still available. Lucene expressions are a lot like JavaScript, + but limited to basic arithmetic, bitwise and comparison operations. +

+

+ There are a few limitations when using Lucene Expressions: +

+
    +
  • Only numeric, boolean, date, and geo_point fields may be accessed
  • +
  • Stored fields are not available
  • +
  • If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0
  • +
+

+ Here are all the operations available to lucene expressions: +

+
    +
  • Arithmetic operators: + - * / %
  • +
  • Bitwise operators: | & ^ ~ << >> >>>
  • +
  • Boolean operators (including the ternary operator): && || ! ?:
  • +
  • Comparison operators: < <= == >= >
  • +
  • Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow
  • +
  • Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan
  • +
  • Distance functions: haversin
  • +
  • Miscellaneous functions: min, max
  • +
+
+
+); diff --git a/src/ui/public/field_editor/components/scripting_help/test_script.js b/src/ui/public/field_editor/components/scripting_help/test_script.js new file mode 100644 index 0000000000000..04a457c118e55 --- /dev/null +++ b/src/ui/public/field_editor/components/scripting_help/test_script.js @@ -0,0 +1,226 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiButton, + EuiCodeBlock, + EuiComboBox, + EuiFormRow, + EuiText, + EuiSpacer, + EuiTitle, + EuiCallOut, +} from '@elastic/eui'; + +export class TestScript extends Component { + state = { + isLoading: false, + additionalFields: [], + } + + componentDidMount() { + if (this.props.script) { + this.previewScript(); + } + } + + previewScript = async () => { + const { + indexPattern, + lang, + name, + script, + executeScript, + } = this.props; + + if (!script || script.length === 0) { + return; + } + + this.setState({ + isLoading: true, + }); + + const scriptResponse = await executeScript({ + name, + lang, + script, + indexPatternTitle: indexPattern.title, + additionalFields: this.state.additionalFields.map(option => { + return option.value; + }) + }); + + if (scriptResponse.status !== 200) { + this.setState({ + isLoading: false, + previewData: scriptResponse + }); + return; + } + + this.setState({ + isLoading: false, + previewData: scriptResponse.hits.hits.map(hit => ({ + _id: hit._id, + ...hit._source, + ...hit.fields, + })), + }); + } + + onAdditionalFieldsChange = (selectedOptions) => { + this.setState({ + additionalFields: selectedOptions + }); + } + + renderPreview() { + const { previewData } = this.state; + + if (!previewData) { + return null; + } + + if (previewData.error) { + return ( + + + {JSON.stringify(previewData.error, null, ' ')} + + + ); + } + + return ( + +

First 10 results

+ + + {JSON.stringify(previewData, null, ' ')} + +
+ ); + } + + renderToolbar() { + const fieldsByTypeMap = new Map(); + const fields = []; + + this.props.indexPattern.fields + .filter(field => { + return !field.name.startsWith('_'); + }) + .forEach(field => { + if (fieldsByTypeMap.has(field.type)) { + const fieldsList = fieldsByTypeMap.get(field.type); + fieldsList.push(field.name); + fieldsByTypeMap.set(field.type, fieldsList); + } else { + fieldsByTypeMap.set(field.type, [field.name]); + } + }); + + fieldsByTypeMap.forEach((fieldsList, fieldType) => { + fields.push({ + label: fieldType, + options: fieldsList.sort().map(fieldName => { + return { value: fieldName, label: fieldName }; + }) + }); + }); + + fields.sort((a, b) => { + if (a.label < b.label) return -1; + if (a.label > b.label) return 1; + return 0; + }); + + return ( + + + + + + + Run script + + + ); + } + + render() { + return ( + + + +

Preview results

+

+ Run your script to preview the first 10 results. You can also select some + additional fields to include in your results to gain more context. +

+
+ + {this.renderToolbar()} + + {this.renderPreview()} +
+ ); + } +} + +TestScript.propTypes = { + indexPattern: PropTypes.object.isRequired, + lang: PropTypes.string.isRequired, + name: PropTypes.string, + script: PropTypes.string, + executeScript: PropTypes.func.isRequired, +}; + +TestScript.defaultProps = { + name: 'myScriptedField', +}; diff --git a/src/ui/public/field_editor/field_editor.js b/src/ui/public/field_editor/field_editor.js index 0f748c35bc1b4..62687a8e155ae 100644 --- a/src/ui/public/field_editor/field_editor.js +++ b/src/ui/public/field_editor/field_editor.js @@ -63,15 +63,18 @@ import { import { ScriptingDisabledCallOut, ScriptingWarningCallOut, - ScriptingHelpFlyout, } from './components/scripting_call_outs'; +import { + ScriptingHelpFlyout, +} from './components/scripting_help'; + import { FieldFormatEditor } from './components/field_format_editor'; import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants'; -import { copyField, getDefaultFormat } from './lib'; +import { copyField, getDefaultFormat, executeScript, isScriptValid } from './lib'; export class FieldEditor extends PureComponent { static propTypes = { @@ -109,6 +112,8 @@ export class FieldEditor extends PureComponent { showScriptingHelp: false, showDeleteModal: false, hasFormatError: false, + hasScriptError: false, + isSaving: false, }; this.supportedLangs = getSupportedScriptingLanguages(); this.deprecatedLangs = getDeprecatedScriptingLanguages(); @@ -339,24 +344,46 @@ export class FieldEditor extends PureComponent { ); } + onScriptChange = (e) => { + this.setState({ + hasScriptError: false + }); + this.onFieldChange('script', e.target.value); + } + renderScript() { - const { field } = this.state; - const isInvalid = !field.script || !field.script.trim(); + const { field, hasScriptError } = this.state; + const isInvalid = !field.script || !field.script.trim() || hasScriptError; + const errorMsg = hasScriptError + ? (Script is invalid. View script preview for details) + : 'Script is required'; return field.scripted ? ( - Scripting help)} - isInvalid={isInvalid} - error={isInvalid ? 'Script is required' : null} - > - { this.onFieldChange('script', e.target.value); }} + + - + error={isInvalid ? errorMsg : null} + > + + + + + + Access fields with {`doc['some_field'].value`}. +
+ + Get help with the syntax and preview the results of your script. + +
+
+ + ) : null; } @@ -409,42 +436,73 @@ export class FieldEditor extends PureComponent { } renderActions() { - const { isCreating, field } = this.state; + const { isCreating, field, isSaving } = this.state; const { redirectAway } = this.props.helpers; return ( - - - - {isCreating ? 'Create field' : 'Save field'} - - - - - Cancel - - - { - !isCreating && field.scripted ? ( - - - Delete - - - ) : null - } - + + + + + {isCreating ? 'Create field' : 'Save field'} + + + + + Cancel + + + { + !isCreating && field.scripted ? ( + + + + + Delete + + + + + ) : null + } + + + ); + } + + renderScriptingPanels = () => { + const { scriptingLangs, field, showScriptingHelp } = this.state; + + if (!field.scripted) { + return; + } + + return ( + + + + + ); } @@ -464,12 +522,33 @@ export class FieldEditor extends PureComponent { } } - saveField = () => { - const { redirectAway } = this.props.helpers; + saveField = async () => { + const field = this.state.field.toActualField(); const { indexPattern } = this.props; const { fieldFormatId } = this.state; - const field = this.state.field.toActualField(); + if (field.scripted) { + this.setState({ + isSaving: true + }); + + const isValid = await isScriptValid({ + name: field.name, + lang: field.lang, + script: field.script, + indexPatternTitle: indexPattern.title + }); + + if (!isValid) { + this.setState({ + hasScriptError: true, + isSaving: false + }); + return; + } + } + + const { redirectAway } = this.props.helpers; const index = indexPattern.fields.findIndex(f => f.name === field.name); if (index > -1) { @@ -492,10 +571,11 @@ export class FieldEditor extends PureComponent { } isSavingDisabled() { - const { field, hasFormatError } = this.state; + const { field, hasFormatError, hasScriptError } = this.state; if( hasFormatError + || hasScriptError || !field.name || !field.name.trim() || (field.scripted && (!field.script || !field.script.trim())) @@ -507,7 +587,7 @@ export class FieldEditor extends PureComponent { } render() { - const { isReady, isCreating, scriptingLangs, field, showScriptingHelp } = this.state; + const { isReady, isCreating, field } = this.state; return isReady ? (
@@ -516,12 +596,7 @@ export class FieldEditor extends PureComponent { - - - + {this.renderScriptingPanels()} {this.renderName()} {this.renderLanguage()} {this.renderType()} diff --git a/src/ui/public/field_editor/field_editor.test.js b/src/ui/public/field_editor/field_editor.test.js index 1a30028ef4862..c6a64fd519a89 100644 --- a/src/ui/public/field_editor/field_editor.test.js +++ b/src/ui/public/field_editor/field_editor.test.js @@ -17,6 +17,8 @@ * under the License. */ +jest.mock('ui/kfetch', () => ({})); + import React from 'react'; import { shallow } from 'enzyme'; diff --git a/src/ui/public/field_editor/lib/__tests__/convert_sample_input.test.js b/src/ui/public/field_editor/lib/__tests__/convert_sample_input.test.js deleted file mode 100644 index 7a3fcea15e615..0000000000000 --- a/src/ui/public/field_editor/lib/__tests__/convert_sample_input.test.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { convertSampleInput } from '../convert_sample_input'; - -const converter = (input) => { - if(isNaN(input)) { - throw { - message: 'Input is not a number' - }; - } else { - return input * 2; - } -}; - -describe('convertSampleInput', () => { - it('should convert a set of inputs', () => { - const inputs = [1, 10, 15]; - const output = convertSampleInput(converter, inputs); - - expect(output.error).toEqual(null); - expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([ - { input: 1, output: 2 }, - { input: 10, output: 20 }, - { input: 15, output: 30 }, - ])); - }); - - it('should return error if converter throws one', () => { - const inputs = [1, 10, 15, 'invalid']; - const output = convertSampleInput(converter, inputs); - - expect(output.error).toEqual('An error occurred while trying to use this format configuration: Input is not a number'); - expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([])); - }); -}); diff --git a/src/ui/public/field_editor/lib/index.js b/src/ui/public/field_editor/lib/index.js index c97f6db91c5fd..5e12d51763a18 100644 --- a/src/ui/public/field_editor/lib/index.js +++ b/src/ui/public/field_editor/lib/index.js @@ -19,4 +19,4 @@ export { copyField } from './copy_field'; export { getDefaultFormat } from './get_default_format'; -export { convertSampleInput } from './convert_sample_input'; +export { executeScript, isScriptValid } from './validate_script'; diff --git a/src/ui/public/field_editor/lib/validate_script.js b/src/ui/public/field_editor/lib/validate_script.js new file mode 100644 index 0000000000000..c62e0dbdecde8 --- /dev/null +++ b/src/ui/public/field_editor/lib/validate_script.js @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { kfetch } from 'ui/kfetch'; + +export const executeScript = async ({ name, lang, script, indexPatternTitle, additionalFields = [] }) => { + // Using _msearch because _search with index name in path dorks everything up + const header = { + index: indexPatternTitle, + ignore_unavailable: true, + timeout: 30000 + }; + + const search = { + query: { + match_all: {} + }, + script_fields: { + [name]: { + script: { + lang, + source: script + } + } + }, + size: 10, + }; + + if (additionalFields.length > 0) { + search._source = additionalFields; + } + + const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; + const esResp = await kfetch({ method: 'POST', pathname: '/elasticsearch/_msearch', body }); + // unwrap _msearch response + return esResp.responses[0]; +}; + +export const isScriptValid = async ({ name, lang, script, indexPatternTitle }) => { + const scriptResponse = await executeScript({ name, lang, script, indexPatternTitle }); + + if (scriptResponse.status !== 200) { + return false; + } + + return true; +}; diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 4a22a6e54cf85..746ea4cfc4029 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -37,6 +37,7 @@ export default function ({ getService, getPageObjects }) { const log = getService('log'); const remote = getService('remote'); const retry = getService('retry'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'header', 'settings', 'visualize', 'discover']); describe('scripted fields', () => { @@ -57,6 +58,20 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.removeIndexPattern(); }); + it('should not allow saving of invalid scripts', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndices(); + await PageObjects.settings.clickScriptedFieldsTab(); + await PageObjects.settings.clickAddScriptedField(); + await PageObjects.settings.setScriptedFieldName('doomedScriptedField'); + await PageObjects.settings.setScriptedFieldScript(`doc['iHaveNoClosingTick].value`); + await PageObjects.settings.clickSaveScriptedField(); + await retry.try(async () => { + const invalidScriptErrorExists = await testSubjects.exists('invalidScriptError'); + expect(invalidScriptErrorExists).to.be(true); + }); + }); + describe('creating and using Lucence expression scripted fields', function describeIndexTests() { const scriptedExpressionFieldName = 'ram_expr1'; diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.js new file mode 100644 index 0000000000000..21eef4bebdbc3 --- /dev/null +++ b/test/functional/apps/management/_scripted_fields_preview.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from 'expect.js'; + +export default function ({ getService, getPageObjects }) { + const kibanaServer = getService('kibanaServer'); + const remote = getService('remote'); + const PageObjects = getPageObjects(['settings']); + const SCRIPTED_FIELD_NAME = 'myScriptedField'; + + describe('scripted fields preview', () => { + before(async function () { + await remote.setWindowSize(1200, 800); + // delete .kibana index and then wait for Kibana to re-create it + await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndices(); + await PageObjects.settings.createIndexPattern(); + await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'UTC' }); + + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndices(); + await PageObjects.settings.clickScriptedFieldsTab(); + await PageObjects.settings.clickAddScriptedField(); + await PageObjects.settings.setScriptedFieldName(SCRIPTED_FIELD_NAME); + }); + + after(async function afterAll() { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndices(); + await PageObjects.settings.removeIndexPattern(); + }); + + it('should display script error when script is invalid', async function () { + const scriptResults = await PageObjects.settings.executeScriptedField(`doc['iHaveNoClosingTick].value`); + expect(scriptResults.includes('search_phase_execution_exception')).to.be(true); + }); + + it('should display script results when script is valid', async function () { + const scriptResults = await PageObjects.settings.executeScriptedField(`doc['bytes'].value * 2`); + expect(scriptResults.replace(/\s/g, '').includes('"myScriptedField":[6196')).to.be(true); + }); + + it('should display additional fields', async function () { + const scriptResults = await PageObjects.settings.executeScriptedField(`doc['bytes'].value * 2`, ['bytes']); + expect(scriptResults.replace(/\s/g, '').includes('"bytes":3098')).to.be(true); + }); + }); +} diff --git a/test/functional/apps/management/index.js b/test/functional/apps/management/index.js index 346125b4cea89..adc9a9b882f3c 100644 --- a/test/functional/apps/management/index.js +++ b/test/functional/apps/management/index.js @@ -40,6 +40,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_index_pattern_popularity')); loadTestFile(require.resolve('./_kibana_settings')); loadTestFile(require.resolve('./_scripted_fields')); + loadTestFile(require.resolve('./_scripted_fields_preview')); loadTestFile(require.resolve('./_index_pattern_filter')); loadTestFile(require.resolve('./_scripted_fields_filter')); loadTestFile(require.resolve('./_import_objects')); diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 51c3f40c2dbea..163d3e089aa35 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -25,8 +25,9 @@ export function SettingsPageProvider({ getService, getPageObjects }) { const config = getService('config'); const remote = getService('remote'); const find = getService('find'); + const flyout = getService('flyout'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['header', 'common']); + const PageObjects = getPageObjects(['header', 'common', 'visualize']); const defaultFindTimeout = config.get('timeouts.find'); @@ -503,6 +504,54 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await field.type(script); } + async openScriptedFieldHelp(activeTab) { + log.debug('open Scripted Fields help'); + let isOpen = await testSubjects.exists('scriptedFieldsHelpFlyout'); + if (!isOpen) { + await retry.try(async () => { + await testSubjects.click('scriptedFieldsHelpLink'); + isOpen = await testSubjects.exists('scriptedFieldsHelpFlyout'); + if (!isOpen) { + throw new Error('Failed to open scripted fields help'); + } + }); + } + + if (activeTab) { + await testSubjects.click(activeTab); + } + } + + async closeScriptedFieldHelp() { + log.debug('close Scripted Fields help'); + let isOpen = await testSubjects.exists('scriptedFieldsHelpFlyout'); + if (isOpen) { + await retry.try(async () => { + await flyout.close('scriptedFieldsHelpFlyout'); + isOpen = await testSubjects.exists('scriptedFieldsHelpFlyout'); + if (isOpen) { + throw new Error('Failed to close scripted fields help'); + } + }); + } + } + + async executeScriptedField(script, additionalField) { + log.debug('execute Scripted Fields help'); + await this.closeScriptedFieldHelp(); // ensure script help is closed so script input is not blocked + await this.setScriptedFieldScript(script); + await this.openScriptedFieldHelp('testTab'); + if (additionalField) { + await PageObjects.visualize.setComboBox('additionalFieldsSelect', additionalField); + await testSubjects.click('runScriptButton'); + } + let scriptResults; + await retry.try(async () => { + scriptResults = await testSubjects.getVisibleText('scriptedFieldPreview'); + }); + return scriptResults; + } + async importFile(path, overwriteAll = true) { log.debug(`importFile(${path})`);