diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 0dc17c57bbeb4..1d31d32d7f424 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -8,7 +8,7 @@ for displayed decimal values. To set advanced options: . Go to *Settings > Advanced*. -. Click the *Edit* button for the option you want to modify. +. Scroll or use the search bar to find the option you want to modify. . Enter a new value for the option. . Click the *Save* button. diff --git a/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap new file mode 100644 index 0000000000000..08e105b50d295 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap @@ -0,0 +1,357 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AdvancedSettings should render normally 1`] = ` +
+ + + +

+ Settings +

+
+
+ + + +
+ + + +
+
+`; + +exports[`AdvancedSettings should render specific setting if given setting key 1`] = ` +
+ + + +

+ Settings +

+
+
+ + + +
+ + + + +
+`; diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_row.html b/src/core_plugins/kibana/public/management/sections/settings/advanced_row.html deleted file mode 100644 index ecf48f4c7bf06..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/settings/advanced_row.html +++ /dev/null @@ -1,225 +0,0 @@ - - - -
-

- {{conf.name}} -

- -

- Default: {{conf.defVal == undefined || conf.defVal === '' ? 'null' : conf.defVal}} -

- -

- (Custom setting) -

- -

-
- - - - -
- - - - - - - - -

- Invalid JSON syntax -

- - - - - - - - - -

- Image is too large, maximum size is {{conf.options.maxSize.description}} -

- - - - - - - {{conf.value || conf.defVal}} - - - {{(conf.value || conf.defVal).join(', ')}} - - - {{conf.value === undefined ? conf.defVal : conf.value}} - - - Image - -
- - - - -
-
- - - - - - - - - - - -
-
- - diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_row.js b/src/core_plugins/kibana/public/management/sections/settings/advanced_row.js deleted file mode 100644 index 6f3b4b07cacb2..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/settings/advanced_row.js +++ /dev/null @@ -1,81 +0,0 @@ -import 'ui/elastic_textarea'; -import 'ui/filters/markdown'; -import { uiModules } from 'ui/modules'; -import { fatalError } from 'ui/notify'; -import { keyCodes } from '@elastic/eui'; -import advancedRowTemplate from './advanced_row.html'; - -uiModules.get('apps/management') - .directive('advancedRow', function (config) { - return { - restrict: 'A', - replace: true, - template: advancedRowTemplate, - scope: { - conf: '=advancedRow', - configs: '=' - }, - link: function ($scope) { - // To allow passing form validation state back - $scope.forms = {}; - - // setup loading flag, run async op, then clear loading and editing flag (just in case) - const loading = function (conf, fn) { - conf.loading = true; - fn() - .then(function () { - conf.loading = conf.editing = false; - }) - .catch(fatalError); - }; - - $scope.maybeCancel = function ($event, conf) { - if ($event.keyCode === keyCodes.ESCAPE) { - $scope.cancelEdit(conf); - } - }; - - $scope.edit = function (conf) { - conf.unsavedValue = conf.value == null ? conf.defVal : conf.value; - $scope.configs.forEach(function (c) { - c.editing = (c === conf); - }); - }; - - $scope.save = function (conf) { - // an empty JSON is valid as per the validateJson directive. - // set the value to empty JSON in this case so that its parsing upon retrieving the setting does not fail. - if (conf.type === 'json' && conf.unsavedValue === '') { - conf.unsavedValue = '{}'; - } - - loading(conf, function () { - if (conf.unsavedValue === conf.defVal) { - return config.remove(conf.name); - } - - return config.set(conf.name, conf.unsavedValue); - }); - }; - - $scope.cancelEdit = function (conf) { - conf.editing = false; - }; - - $scope.clear = function (conf) { - return loading(conf, function () { - return config.remove(conf.name); - }); - }; - - $scope.isDefaultValue = (conf) => { - // conf.isCustom = custom setting, provided by user, so there is no notion of - // having a default or non-default value for it - return conf.isCustom - || conf.value === undefined - || conf.value === '' - || String(conf.value) === String(conf.defVal); - }; - } - }; - }); diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js new file mode 100644 index 0000000000000..a62669ab8add8 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js @@ -0,0 +1,146 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { + Comparators, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + Query, +} from '@elastic/eui'; + +import { CallOuts } from './components/call_outs'; +import { Search } from './components/search'; +import { Form } from './components/form'; + +import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib'; + +import './advanced_settings.less'; + +export class AdvancedSettings extends Component { + static propTypes = { + config: PropTypes.object.isRequired, + query: PropTypes.string, + } + + constructor(props) { + super(props); + const { config, query } = this.props; + const parsedQuery = Query.parse(query ? `ariaName:"${getAriaName(query)}"` : ''); + this.init(config); + this.state = { + query: parsedQuery, + filteredSettings: this.mapSettings(Query.execute(parsedQuery, this.settings)), + }; + } + + init(config) { + this.settings = this.mapConfig(config); + this.groupedSettings = this.mapSettings(this.settings); + + this.categories = Object.keys(this.groupedSettings).sort((a, b) => { + if(a === DEFAULT_CATEGORY) return -1; + if(b === DEFAULT_CATEGORY) return 1; + if(a > b) return 1; + return a === b ? 0 : -1; + }); + + this.categoryCounts = Object.keys(this.groupedSettings).reduce((counts, category) => { + counts[category] = this.groupedSettings[category].length; + return counts; + }, {}); + } + + componentWillReceiveProps(nextProps) { + const { config } = nextProps; + const { query } = this.state; + + this.init(config); + this.setState({ + filteredSettings: this.mapSettings(Query.execute(query, this.settings)), + }); + } + + mapConfig(config) { + const all = config.getAll(); + return Object.entries(all) + .map((setting) => { + return toEditableConfig({ + def: setting[1], + name: setting[0], + value: setting[1].userValue, + isCustom: config.isCustom(setting[0]), + }); + }) + .filter((c) => !c.readonly) + .sort(Comparators.property('name', Comparators.default('asc'))); + } + + mapSettings(settings) { + // Group settings by category + return settings.reduce((groupedSettings, setting) => { + // We will want to change this logic when we put each category on its + // own page aka allowing a setting to be included in multiple categories. + const category = setting.category[0]; + (groupedSettings[category] = groupedSettings[category] || []).push(setting); + return groupedSettings; + }, {}); + } + + saveConfig = (name, value) => { + return this.props.config.set(name, value); + } + + clearConfig = (name) => { + return this.props.config.remove(name); + } + + onQueryChange = (query) => { + this.setState({ + query, + filteredSettings: this.mapSettings(Query.execute(query, this.settings)), + }); + } + + clearQuery = () => { + this.setState({ + query: Query.parse(''), + filteredSettings: this.groupedSettings, + }); + } + + render() { + const { filteredSettings, query } = this.state; + + return ( +
+ + + +

Settings

+
+
+ + + +
+ + + +
+
+ ); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.less b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.less new file mode 100644 index 0000000000000..5201e5781595a --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.less @@ -0,0 +1,30 @@ +@import (reference) '~ui/styles/variables/colors'; + +.advancedSettings { + padding: 20px; + background: @globalColorLightestGray; + min-height: calc(~"100vh - 70px"); + + > div { + max-width: 1000px; + margin: 0 auto; + } + + .advancedSettings__field { + + * { + margin-top: 24px; + } + + &__wrapper { + width: 720px; + } + + &__actions { + padding-top: 30px; + } + + .euiFormHelpText { + padding-bottom: 0; + } + } +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.test.js b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.test.js new file mode 100644 index 0000000000000..5c9076a8541da --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.test.js @@ -0,0 +1,119 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AdvancedSettings } from './advanced_settings'; + +jest.mock('./components/field', () => ({ + Field: () => { + return 'field'; + } +})); + +jest.mock('./components/call_outs', () => ({ + CallOuts: () => { + return 'callOuts'; + } +})); + +jest.mock('./components/search', () => ({ + Search: () => { + return 'search'; + } +})); + +const config = { + set: () => {}, + remove: () => {}, + isCustom: (setting) => setting.isCustom, + getAll: () => { + return { + 'test:array:setting': { + value: ['default_value'], + name: 'Test array setting', + description: 'Description for Test array setting', + category: ['elasticsearch'], + }, + 'test:boolean:setting': { + value: true, + name: 'Test boolean setting', + description: 'Description for Test boolean setting', + category: ['elasticsearch'], + }, + 'test:image:setting': { + value: null, + name: 'Test image setting', + description: 'Description for Test image setting', + type: 'image', + }, + 'test:json:setting': { + value: '{"foo": "bar"}', + name: 'Test json setting', + description: 'Description for Test json setting', + type: 'json', + }, + 'test:markdown:setting': { + value: '', + name: 'Test markdown setting', + description: 'Description for Test markdown setting', + type: 'markdown', + }, + 'test:number:setting': { + value: 5, + name: 'Test number setting', + description: 'Description for Test number setting', + }, + 'test:select:setting': { + value: 'orange', + name: 'Test select setting', + description: 'Description for Test select setting', + type: 'select', + options: ['apple', 'orange', 'banana'], + }, + 'test:string:setting': { + value: null, + name: 'Test string setting', + description: 'Description for Test string setting', + type: 'string', + isCustom: true, + }, + 'test:readonlystring:setting': { + value: null, + name: 'Test readonly string setting', + description: 'Description for Test readonly string setting', + type: 'string', + readonly: true, + }, + 'test:customstring:setting': { + value: null, + name: 'Test custom string setting', + description: 'Description for Test custom string setting', + type: 'string', + isCustom: true, + }, + }; + } +}; + +describe('AdvancedSettings', () => { + + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render specific setting if given setting key', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/__snapshots__/call_outs.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/__snapshots__/call_outs.test.js.snap new file mode 100644 index 0000000000000..07d2370841f96 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/__snapshots__/call_outs.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CallOuts should render normally 1`] = ` +
+ +

+ Be careful in here, these settings are for very advanced users only. Tweaks you make here can break large portions of Kibana. Some of these settings may be undocumented, unsupported or experimental. If a field has a default value, blanking the field will reset it to its default which may be unacceptable given other configuration directives. Deleting a custom setting will permanently remove it from Kibana's config. +

+
+
+`; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.js b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.js new file mode 100644 index 0000000000000..ba84d73ff61d6 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.js @@ -0,0 +1,26 @@ +import React from 'react'; + +import { + EuiCallOut, +} from '@elastic/eui'; + +export const CallOuts = () => { + return ( +
+ +

+ Be careful in here, these settings are for very advanced users only. + Tweaks you make here can break large portions of Kibana. + Some of these settings may be undocumented, unsupported or experimental. + If a field has a default value, blanking the field will reset it to its default which may be + unacceptable given other configuration directives. + Deleting a custom setting will permanently remove it from Kibana's config. +

+
+
+ ); +}; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.test.js new file mode 100644 index 0000000000000..a5843a8f78206 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/call_outs.test.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { CallOuts } from './call_outs'; + +describe('CallOuts', () => { + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/index.js b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/index.js new file mode 100644 index 0000000000000..0df8edb404e9d --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/call_outs/index.js @@ -0,0 +1 @@ +export { CallOuts } from './call_outs'; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap new file mode 100644 index 0000000000000..42a3d440ecc64 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap @@ -0,0 +1,2301 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Field for array setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="array:test:setting-aria" + title={ +

+ Array test setting + +

+ } + titleSize="xs" + > + + array:test:setting + + } + > + + + + + + +`; + +exports[`Field for array setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="array:test:setting-aria" + title={ +

+ Array test setting + +

+ } + titleSize="xs" + > + + array:test:setting + + } + > + + + + + + +`; + +exports[`Field for array setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + default_value + + + + + } + fullWidth={false} + gutterSize="l" + idAria="array:test:setting-aria" + title={ +

+ Array test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + array:test:setting + + } + > + + + + + + +`; + +exports[`Field for boolean setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="boolean:test:setting-aria" + title={ +

+ Boolean test setting + +

+ } + titleSize="xs" + > + + boolean:test:setting + + } + > + + + + + + +`; + +exports[`Field for boolean setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="boolean:test:setting-aria" + title={ +

+ Boolean test setting + +

+ } + titleSize="xs" + > + + boolean:test:setting + + } + > + + + + + + +`; + +exports[`Field for boolean setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + true + + + + + } + fullWidth={false} + gutterSize="l" + idAria="boolean:test:setting-aria" + title={ +

+ Boolean test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + boolean:test:setting + + } + > + + + + + + +`; + +exports[`Field for image setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="image:test:setting-aria" + title={ +

+ Image test setting + +

+ } + titleSize="xs" + > + + image:test:setting + + } + > + + + + + + +`; + +exports[`Field for image setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="image:test:setting-aria" + title={ +

+ Image test setting + +

+ } + titleSize="xs" + > + + image:test:setting + + } + > + + + + + + +`; + +exports[`Field for image setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + null + + + + + } + fullWidth={false} + gutterSize="l" + idAria="image:test:setting-aria" + title={ +

+ Image test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + + Change image + + + + } + isInvalid={false} + label={ + + image:test:setting + + } + > + + + + + + +`; + +exports[`Field for json setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="json:test:setting-aria" + title={ +

+ Json test setting + +

+ } + titleSize="xs" + > + + json:test:setting + + } + > +
+ +
+
+ + + + +`; + +exports[`Field for json setting should render default value if there is no user value set 1`] = ` + + + +
+ + + + Default: + + {} + + + + + } + fullWidth={false} + gutterSize="l" + idAria="json:test:setting-aria" + title={ +

+ Json test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + json:test:setting + + } + > +
+ +
+
+ + + + +`; + +exports[`Field for json setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + {} + + + + + } + fullWidth={false} + gutterSize="l" + idAria="json:test:setting-aria" + title={ +

+ Json test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + json:test:setting + + } + > +
+ +
+
+ + + + +`; + +exports[`Field for markdown setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="markdown:test:setting-aria" + title={ +

+ Markdown test setting + +

+ } + titleSize="xs" + > + + markdown:test:setting + + } + > +
+ +
+
+ + + + +`; + +exports[`Field for markdown setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="markdown:test:setting-aria" + title={ +

+ Markdown test setting + +

+ } + titleSize="xs" + > + + markdown:test:setting + + } + > +
+ +
+
+ + + + +`; + +exports[`Field for markdown setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + null + + + + + } + fullWidth={false} + gutterSize="l" + idAria="markdown:test:setting-aria" + title={ +

+ Markdown test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + markdown:test:setting + + } + > +
+ +
+
+ + + + +`; + +exports[`Field for number setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="number:test:setting-aria" + title={ +

+ Number test setting + +

+ } + titleSize="xs" + > + + number:test:setting + + } + > + + + + + + +`; + +exports[`Field for number setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="number:test:setting-aria" + title={ +

+ Number test setting + +

+ } + titleSize="xs" + > + + number:test:setting + + } + > + + + + + + +`; + +exports[`Field for number setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + 5 + + + + + } + fullWidth={false} + gutterSize="l" + idAria="number:test:setting-aria" + title={ +

+ Number test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + number:test:setting + + } + > + + + + + + +`; + +exports[`Field for select setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="select:test:setting-aria" + title={ +

+ Select test setting + +

+ } + titleSize="xs" + > + + select:test:setting + + } + > + + + + + + +`; + +exports[`Field for select setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="select:test:setting-aria" + title={ +

+ Select test setting + +

+ } + titleSize="xs" + > + + select:test:setting + + } + > + + + + + + +`; + +exports[`Field for select setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + orange + + + + + } + fullWidth={false} + gutterSize="l" + idAria="select:test:setting-aria" + title={ +

+ Select test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + select:test:setting + + } + > + + + + + + +`; + +exports[`Field for string setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test:setting-aria" + title={ +

+ String test setting + +

+ } + titleSize="xs" + > + + string:test:setting + + } + > + + + + + + +`; + +exports[`Field for string setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test:setting-aria" + title={ +

+ String test setting + +

+ } + titleSize="xs" + > + + string:test:setting + + } + > + + + + + + +`; + +exports[`Field for string setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + Default: + + null + + + + + } + fullWidth={false} + gutterSize="l" + idAria="string:test:setting-aria" + title={ +

+ String test setting + +

+ } + titleSize="xs" + > + + + + Reset to default + +     + + + } + isInvalid={false} + label={ + + string:test:setting + + } + > + + + + + + +`; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js new file mode 100644 index 0000000000000..38dbc781f3d2f --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js @@ -0,0 +1,568 @@ +import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import 'brace/theme/textmate'; +import 'brace/mode/markdown'; + +import { toastNotifications } from 'ui/notify'; +import { + EuiButton, + EuiButtonEmpty, + EuiCode, + EuiCodeEditor, + EuiDescribedFormGroup, + EuiFieldNumber, + EuiFieldText, + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, + EuiImage, + EuiLink, + EuiSpacer, + EuiText, + EuiSelect, + EuiSwitch, + keyCodes, +} from '@elastic/eui'; + +import { isDefaultValue } from '../../lib'; + +export class Field extends PureComponent { + + static propTypes = { + setting: PropTypes.object.isRequired, + save: PropTypes.func.isRequired, + clear: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + const { type, value, defVal } = this.props.setting; + const editableValue = this.getEditableValue(type, value, defVal); + + this.state = { + isInvalid: false, + error: null, + loading: false, + changeImage: false, + savedValue: editableValue, + unsavedValue: editableValue, + isJsonArray: type === 'json' ? Array.isArray(JSON.parse(defVal || '{}')) : false, + }; + this.changeImageForm = null; + } + + componentWillReceiveProps(nextProps) { + const { unsavedValue } = this.state; + const { type, value, defVal } = nextProps.setting; + const editableValue = this.getEditableValue(type, value, defVal); + + this.setState({ + savedValue: editableValue, + unsavedValue: (value === null || value === undefined) ? editableValue : unsavedValue, + }); + } + + getEditableValue(type, value, defVal) { + const val = (value === null || value === undefined) ? defVal : value; + switch(type) { + case 'array': + return val.join(', '); + case 'boolean': + return !!val; + case 'number': + return Number(val); + case 'image': + return val; + default: + return val || ''; + } + } + + getDisplayedDefaultValue(type, defVal) { + if(defVal === undefined || defVal === null || defVal === '') { + return 'null'; + } + switch(type) { + case 'array': + return defVal.join(', '); + default: + return String(defVal); + } + } + + setLoading(loading) { + this.setState({ + loading + }); + } + + clearError() { + this.setState({ + isInvalid: false, + error: null, + }); + } + + onCodeEditorChange = (value) => { + const { type } = this.props.setting; + const { isJsonArray } = this.state; + + let newUnsavedValue = undefined; + let isInvalid = false; + let error = null; + + switch (type) { + case 'json': + newUnsavedValue = value.trim() || (isJsonArray ? '[]' : '{}'); + try { + JSON.parse(newUnsavedValue); + } catch (e) { + isInvalid = true; + error = 'Invalid JSON syntax'; + } + break; + default: + newUnsavedValue = value; + } + + this.setState({ + error, + isInvalid, + unsavedValue: newUnsavedValue, + }); + } + + onFieldChange = (e) => { + const value = e.target.value; + const { type } = this.props.setting; + const { unsavedValue } = this.state; + + let newUnsavedValue = undefined; + + switch (type) { + case 'boolean': + newUnsavedValue = !unsavedValue; + break; + case 'number': + newUnsavedValue = Number(value); + break; + default: + newUnsavedValue = value; + } + this.setState({ + unsavedValue: newUnsavedValue, + }); + } + + onFieldKeyDown = ({ keyCode }) => { + if (keyCode === keyCodes.ENTER) { + this.saveEdit(); + } + if (keyCode === keyCodes.ESCAPE) { + this.cancelEdit(); + } + } + + onFieldEscape = ({ keyCode }) => { + if (keyCode === keyCodes.ESCAPE) { + this.cancelEdit(); + } + } + + onImageChange = async (files) => { + if(!files.length) { + this.clearError(); + this.setState({ + unsavedValue: null, + }); + return; + } + + const file = files[0]; + const { maxSize } = this.props.setting.options; + try { + const base64Image = await this.getImageAsBase64(file); + const isInvalid = !!(maxSize && maxSize.length && base64Image.length > maxSize.length); + this.setState({ + isInvalid, + error: isInvalid ? `Image is too large, maximum size is ${maxSize.description}` : null, + changeImage: true, + unsavedValue: base64Image, + }); + } catch(err) { + toastNotifications.addDanger('Image could not be saved'); + this.cancelChangeImage(); + } + } + + getImageAsBase64(file) { + if(!file instanceof File) { + return null; + } + + const reader = new FileReader(); + reader.readAsDataURL(file); + + return new Promise((resolve, reject) => { + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = (err) => { + reject(err); + }; + }); + } + + changeImage = () => { + this.setState({ + changeImage: true, + }); + } + + cancelChangeImage = () => { + const { savedValue } = this.state; + + if(this.changeImageForm) { + this.changeImageForm.fileInput.value = null; + this.changeImageForm.handleChange(); + } + + this.setState({ + changeImage: false, + unsavedValue: savedValue, + }); + } + + cancelEdit = () => { + const { savedValue } = this.state; + this.clearError(); + this.setState({ + unsavedValue: savedValue, + }); + } + + saveEdit = async () => { + const { name, defVal, type } = this.props.setting; + const { changeImage, savedValue, unsavedValue, isJsonArray } = this.state; + + if(savedValue === unsavedValue) { + return; + } + + let valueToSave = unsavedValue; + let isSameValue = false; + + switch(type) { + case 'array': + valueToSave = valueToSave.split(',').map(val => val.trim()); + isSameValue = valueToSave.join(',') === defVal.join(','); + break; + case 'json': + valueToSave = valueToSave.trim(); + valueToSave = valueToSave || (isJsonArray ? '[]' : '{}'); + default: + isSameValue = valueToSave === defVal; + } + + this.setLoading(true); + try { + if (isSameValue) { + await this.props.clear(name); + } else { + await this.props.save(name, valueToSave); + } + + if(changeImage) { + this.cancelChangeImage(); + } + } catch(e) { + toastNotifications.addDanger(`Unable to save ${name}`); + } + this.setLoading(false); + } + + resetField = async () => { + const { name } = this.props.setting; + this.setLoading(true); + try { + await this.props.clear(name); + this.cancelChangeImage(); + this.clearError(); + } catch(e) { + toastNotifications.addDanger(`Unable to reset ${name}`); + } + this.setLoading(false); + } + + renderField(setting) { + const { loading, changeImage, unsavedValue } = this.state; + const { name, value, type, options } = setting; + + switch(type) { + case 'boolean': + return ( + + ); + case 'markdown': + case 'json': + return ( +
+ +
+ ); + case 'image': + if(!isDefaultValue(setting) && !changeImage) { + return ( + + ); + } else { + return ( + { this.changeImageForm = input; }} + onKeyDown={this.onFieldEscape} + data-test-subj={`advancedSetting-editField-${name}`} + /> + ); + } + case 'select': + return ( + { + return { text, value: text }; + })} + onChange={this.onFieldChange} + isLoading={loading} + disabled={loading} + onKeyDown={this.onFieldKeyDown} + data-test-subj={`advancedSetting-editField-${name}`} + /> + ); + case 'number': + return ( + + ); + default: + return ( + + ); + } + } + + renderLabel(setting) { + return( + + {setting.name} + + ); + } + + renderHelpText(setting) { + const defaultLink = this.renderResetToDefaultLink(setting); + const imageLink = this.renderChangeImageLink(setting); + + if(defaultLink || imageLink) { + return ( + + {defaultLink} + {imageLink} + + ); + } + + return null; + } + + renderTitle(setting) { + return ( +

+ {setting.displayName || setting.name} + {setting.isCustom ? + + : ''} +

+ ); + } + + renderDescription(setting) { + return ( + +
+ {this.renderDefaultValue(setting)} + + ); + } + + renderDefaultValue(setting) { + const { type, defVal } = setting; + if(isDefaultValue(setting)) { + return; + } + return ( + + + + Default: {this.getDisplayedDefaultValue(type, defVal)} + + + ); + } + + renderResetToDefaultLink(setting) { + const { ariaName, name } = setting; + if(isDefaultValue(setting)) { + return; + } + return ( + + + Reset to default + +     + + ); + } + + renderChangeImageLink(setting) { + const { changeImage } = this.state; + const { type, value, ariaName, name } = setting; + if(type !== 'image' || !value || changeImage) { + return; + } + return ( + + + Change image + + + ); + } + + renderActions(setting) { + const { ariaName, name } = setting; + const { loading, isInvalid, changeImage, savedValue, unsavedValue } = this.state; + if(savedValue === unsavedValue && !changeImage) { + return; + } + return ( + + + + + Save + + + + changeImage ? this.cancelChangeImage() : this.cancelEdit()} + disabled={loading} + data-test-subj={`advancedSetting-cancelEditField-${name}`} + > + Cancel + + + + + ); + } + + render() { + const { setting } = this.props; + const { error, isInvalid } = this.state; + + return ( + + + + + {this.renderField(setting)} + + + + + {this.renderActions(setting)} + + + ); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js new file mode 100644 index 0000000000000..783f156a0671b --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js @@ -0,0 +1,312 @@ +import React from 'react'; +import { mount, shallow } from 'enzyme'; + +import { findTestSubject } from '@elastic/eui/lib/test'; +import { Field } from './field'; + +jest.mock('ui/notify', () => ({ + toastNotifications: { + addDanger: () => {} + } +})); + +jest.mock('brace/theme/textmate', () => 'brace/theme/textmate'); +jest.mock('brace/mode/markdown', () => 'brace/mode/markdown'); + +const settings = { + array: { + name: 'array:test:setting', + ariaName: 'array test setting', + displayName: 'Array test setting', + description: 'Description for Array test setting', + type: 'array', + value: undefined, + defVal: ['default_value'], + isCustom: false, + options: null, + }, + boolean: { + name: 'boolean:test:setting', + ariaName: 'boolean test setting', + displayName: 'Boolean test setting', + description: 'Description for Boolean test setting', + type: 'boolean', + value: undefined, + defVal: true, + isCustom: false, + options: null, + }, + image: { + name: 'image:test:setting', + ariaName: 'image test setting', + displayName: 'Image test setting', + description: 'Description for Image test setting', + type: 'image', + value: undefined, + defVal: null, + isCustom: false, + options: { + maxSize: { + length: 1000, + displayName: '1 kB', + description: 'Description for 1 kB', + } + }, + }, + json: { + name: 'json:test:setting', + ariaName: 'json test setting', + displayName: 'Json test setting', + description: 'Description for Json test setting', + type: 'json', + value: '{"foo": "bar"}', + defVal: '{}', + isCustom: false, + options: null, + }, + markdown: { + name: 'markdown:test:setting', + ariaName: 'markdown test setting', + displayName: 'Markdown test setting', + description: 'Description for Markdown test setting', + type: 'markdown', + value: undefined, + defVal: '', + isCustom: false, + options: null, + }, + number: { + name: 'number:test:setting', + ariaName: 'number test setting', + displayName: 'Number test setting', + description: 'Description for Number test setting', + type: 'number', + value: undefined, + defVal: 5, + isCustom: false, + options: null, + }, + select: { + name: 'select:test:setting', + ariaName: 'select test setting', + displayName: 'Select test setting', + description: 'Description for Select test setting', + type: 'select', + value: undefined, + defVal: 'orange', + isCustom: false, + options: ['apple', 'orange', 'banana'], + }, + string: { + name: 'string:test:setting', + ariaName: 'string test setting', + displayName: 'String test setting', + description: 'Description for String test setting', + type: 'string', + value: undefined, + defVal: null, + isCustom: false, + options: null, + }, +}; +const userValues = { + array: ['user', 'value'], + boolean: false, + image: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', + json: '{"hello": "world"}', + markdown: '**bold**', + number: 10, + select: 'banana', + string: 'foo', +}; +const save = jest.fn(() => Promise.resolve()); +const clear = jest.fn(() => Promise.resolve()); + +describe('Field', () => { + Object.keys(settings).forEach(type => { + const setting = settings[type]; + + describe(`for ${type} setting`, () => { + it('should render default value if there is no user value set', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render user value if there is user value is set', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render custom setting icon if it is custom', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + + if(type === 'image') { + describe(`for changing ${type} setting`, () => { + const component = mount( + + ); + + const userValue = userValues[type]; + component.instance().getImageAsBase64 = (file) => Promise.resolve(file); + + it('should be able to change value from no value and cancel', async () => { + await component.instance().onImageChange([userValue]); + component.update(); + findTestSubject(component, `advancedSetting-cancelEditField-${setting.name}`).simulate('click'); + expect(component.instance().state.unsavedValue === component.instance().state.savedValue).toBe(true); + }); + + it('should be able to change value and save', async () => { + await component.instance().onImageChange([userValue]); + component.update(); + findTestSubject(component, `advancedSetting-saveEditField-${setting.name}`).simulate('click'); + expect(save).toBeCalled(); + component.setState({ savedValue: userValue }); + await component.setProps({ setting: { + ...component.instance().props.setting, + value: userValue, + } }); + component.update(); + }); + + it('should be able to change value from existing value and save', async () => { + const newUserValue = `${userValue}=`; + findTestSubject(component, `advancedSetting-changeImage-${setting.name}`).simulate('click'); + await component.instance().onImageChange([newUserValue]); + component.update(); + findTestSubject(component, `advancedSetting-saveEditField-${setting.name}`).simulate('click'); + expect(save).toBeCalled(); + component.setState({ savedValue: newUserValue }); + await component.setProps({ setting: { + ...component.instance().props.setting, + value: newUserValue, + } }); + component.update(); + }); + + it('should be able to reset to default value', async () => { + findTestSubject(component, `advancedSetting-resetField-${setting.name}`).simulate('click'); + expect(clear).toBeCalled(); + }); + }); + } else if(type === 'markdown' || type === 'json') { + describe(`for changing ${type} setting`, () => { + const component = mount( + + ); + + const userValue = userValues[type]; + const fieldUserValue = userValue; + + it('should be able to change value and cancel', async () => { + component.instance().onCodeEditorChange(fieldUserValue); + component.update(); + findTestSubject(component, `advancedSetting-cancelEditField-${setting.name}`).simulate('click'); + expect(component.instance().state.unsavedValue === component.instance().state.savedValue).toBe(true); + }); + + it('should be able to change value and save', async () => { + component.instance().onCodeEditorChange(fieldUserValue); + component.update(); + findTestSubject(component, `advancedSetting-saveEditField-${setting.name}`).simulate('click'); + expect(save).toBeCalled(); + component.setState({ savedValue: fieldUserValue }); + await component.setProps({ setting: { + ...component.instance().props.setting, + value: userValue, + } }); + component.update(); + }); + + if(type === 'json') { + it('should be able to clear value and have empty object populate', async () => { + component.instance().onCodeEditorChange(''); + component.update(); + expect(component.instance().state.unsavedValue).toEqual('{}'); + }); + } + + it('should be able to reset to default value', async () => { + findTestSubject(component, `advancedSetting-resetField-${setting.name}`).simulate('click'); + expect(clear).toBeCalled(); + }); + }); + } else { + describe(`for changing ${type} setting`, () => { + const component = mount( + + ); + + const userValue = userValues[type]; + const fieldUserValue = type === 'array' ? userValue.join(', ') : userValue; + + it('should be able to change value and cancel', async () => { + component.instance().onFieldChange({ target: { value: fieldUserValue } }); + component.update(); + findTestSubject(component, `advancedSetting-cancelEditField-${setting.name}`).simulate('click'); + expect(component.instance().state.unsavedValue === component.instance().state.savedValue).toBe(true); + }); + + it('should be able to change value and save', async () => { + component.instance().onFieldChange({ target: { value: fieldUserValue } }); + component.update(); + findTestSubject(component, `advancedSetting-saveEditField-${setting.name}`).simulate('click'); + expect(save).toBeCalled(); + component.setState({ savedValue: fieldUserValue }); + await component.setProps({ setting: { + ...component.instance().props.setting, + value: userValue, + } }); + component.update(); + }); + + it('should be able to reset to default value', async () => { + findTestSubject(component, `advancedSetting-resetField-${setting.name}`).simulate('click'); + expect(clear).toBeCalled(); + }); + }); + } + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/field/index.js b/src/core_plugins/kibana/public/management/sections/settings/components/field/index.js new file mode 100644 index 0000000000000..497b3a78c2c38 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/field/index.js @@ -0,0 +1 @@ +export { Field } from './field'; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap new file mode 100644 index 0000000000000..627f5d864d1d2 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap @@ -0,0 +1,227 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Form should render no settings message when there are no settings 1`] = ` + + + No settings found + + (Clear search) + + + +`; + +exports[`Form should render normally 1`] = ` + + + + + + + +

+ General +

+
+
+
+ + + +
+
+ +
+ + + + + + +

+ Dashboard +

+
+
+
+ + +
+
+ +
+ + + + + + +

+ X-pack +

+
+ + + Search terms are hiding + 9 + settings + + + (clear search) + + + + +
+
+ + +
+
+ +
+
+`; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/form.js b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.js new file mode 100644 index 0000000000000..188d00c0fb9e3 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.js @@ -0,0 +1,105 @@ +import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { getCategoryName } from '../../lib'; +import { Field } from '../field'; + +export class Form extends PureComponent { + + static propTypes = { + settings: PropTypes.object.isRequired, + categories: PropTypes.array.isRequired, + categoryCounts: PropTypes.object.isRequired, + clearQuery: PropTypes.func.isRequired, + save: PropTypes.func.isRequired, + clear: PropTypes.func.isRequired, + } + + renderClearQueryLink(totalSettings, currentSettings) { + const { clearQuery } = this.props; + + if(totalSettings !== currentSettings) { + return ( + + + Search terms are hiding {totalSettings - currentSettings} settings {( + + (clear search) + + )} + + + ); + } + + return null; + } + + renderCategory(category, settings, totalSettings) { + return ( + + + + + + +

{getCategoryName(category)}

+
+ {this.renderClearQueryLink(totalSettings, settings.length)} +
+
+ + {settings.map(setting => { + return ( + + ); + })} +
+
+ +
+ ); + } + + render() { + const { settings, categories, categoryCounts, clearQuery } = this.props; + const currentCategories = []; + + categories.forEach(category => { + if(settings[category] && settings[category].length) { + currentCategories.push(category); + } + }); + + return ( + + { + currentCategories.length ? currentCategories.map((category) => { + return ( + this.renderCategory(category, settings[category], categoryCounts[category]) // fix this + ); + }) : ( + + No settings found (Clear search) + + ) + } + + ); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js new file mode 100644 index 0000000000000..2a22e0c679d64 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js @@ -0,0 +1,87 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Form } from './form'; + +jest.mock('../field', () => ({ + Field: () => { + return 'field'; + } +})); + +const settings = { + 'dashboard': [ + { + name: 'dashboard:test:setting', + ariaName: 'dashboard test setting', + displayName: 'Dashboard test setting', + category: ['dashboard'], + }, + ], + 'general': [ + { + name: 'general:test:date', + ariaName: 'general test date', + displayName: 'Test date', + description: 'bar', + category: ['general'], + }, + { + name: 'setting:test', + ariaName: 'setting test', + displayName: 'Test setting', + description: 'foo', + category: ['general'], + }, + ], + 'x-pack': [ + { + name: 'xpack:test:setting', + ariaName: 'xpack test setting', + displayName: 'X-Pack test setting', + category: ['x-pack'], + description: 'bar', + }, + ], +}; +const categories = ['general', 'dashboard', 'hiddenCategory', 'x-pack']; +const categoryCounts = { + general: 2, + dashboard: 1, + 'x-pack': 10, +}; +const save = () => {}; +const clear = () => {}; +const clearQuery = () => {}; + +describe('Form', () => { + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render no settings message when there are no settings', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/index.js b/src/core_plugins/kibana/public/management/sections/settings/components/form/index.js new file mode 100644 index 0000000000000..9398c9eb7fd1e --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/index.js @@ -0,0 +1 @@ +export { Form } from './form'; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/search/__snapshots__/search.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/search/__snapshots__/search.test.js.snap new file mode 100644 index 0000000000000..126e311afed78 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/search/__snapshots__/search.test.js.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Search should render normally 1`] = ` + +`; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/search/index.js b/src/core_plugins/kibana/public/management/sections/settings/components/search/index.js new file mode 100644 index 0000000000000..161bbbb640687 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/search/index.js @@ -0,0 +1 @@ +export { Search } from './search'; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/search/search.js b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.js new file mode 100644 index 0000000000000..b5a4b160cb1f3 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.js @@ -0,0 +1,55 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiSearchBar, +} from '@elastic/eui'; + +import { getCategoryName } from '../../lib'; + +export class Search extends PureComponent { + + static propTypes = { + categories: PropTypes.array.isRequired, + query: PropTypes.object.isRequired, + onQueryChange: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + const { categories } = props; + this.categories = categories.map(category => { + return { + value: category, + name: getCategoryName(category), + }; + }); + } + + render() { + const { query, onQueryChange } = this.props; + + const box = { + incremental: true, + }; + + const filters = [ + { + type: 'field_value_selection', + field: 'category', + name: 'Category', + multiSelect: 'or', + options: this.categories, + } + ]; + + return ( + + ); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/search/search.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.test.js new file mode 100644 index 0000000000000..78da77c7937cc --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { Query } from '@elastic/eui'; +import { Search } from './search'; + +const query = Query.parse(''); +const categories = ['general', 'dashboard', 'hiddenCategory', 'x-pack']; + +describe('Search', () => { + it('should render normally', async () => { + const onQueryChange = () => {}; + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should call parent function when query is changed', async () => { + const onQueryChange = jest.fn(); + const component = mount( + + ); + component.find('input').simulate('keyUp'); + component.find('EuiSearchFilters').prop('onChange')(query); + expect(onQueryChange).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/index.html b/src/core_plugins/kibana/public/management/sections/settings/index.html index c7630160df904..e0544029d3079 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/index.html +++ b/src/core_plugins/kibana/public/management/sections/settings/index.html @@ -1,77 +1,5 @@ - - -
-
- - - Caution: You can break stuff here - -
- -
-
- Be careful in here, these settings are for very advanced users only. - Tweaks you make here can break large portions of Kibana. Some of these - settings may be undocumented, unsupported or experimental. If a field has a default value, - blanking the field will reset it to its default which - may be unacceptable given other configuration directives. Deleting a - custom setting will permanently remove it from Kibana's config. -
-
-
- - - -
- - -
- - - - - - - - - - - - - - - -
-
- Name -
-
-
- Value -
-
-
- -
-
+ +
diff --git a/src/core_plugins/kibana/public/management/sections/settings/index.js b/src/core_plugins/kibana/public/management/sections/settings/index.js index f5e9a75027c85..641515ad7a7a0 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/index.js +++ b/src/core_plugins/kibana/public/management/sections/settings/index.js @@ -1,41 +1,56 @@ -import _ from 'lodash'; -import { toEditableConfig } from './lib/to_editable_config'; -import './advanced_row'; import { management } from 'ui/management'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; import indexTemplate from './index.html'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AdvancedSettings } from './advanced_settings'; + +const REACT_ADVANCED_SETTINGS_DOM_ELEMENT_ID = 'reactAdvancedSettings'; + +function updateAdvancedSettings($scope, config, query) { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_ADVANCED_SETTINGS_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + , + node, + ); + }); +} + +function destroyAdvancedSettings() { + const node = document.getElementById(REACT_ADVANCED_SETTINGS_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + uiRoutes - .when('/management/kibana/settings', { + .when('/management/kibana/settings/:setting?', { template: indexTemplate }); uiModules.get('apps/management') - .directive('kbnManagementAdvanced', function (config) { + .directive('kbnManagementAdvanced', function (config, $route) { return { restrict: 'E', link: function ($scope) { - // react to changes of the config values - config.watchAll(changed, $scope); - - // initial config setup - changed(); - - function changed() { - const all = config.getAll(); - const editable = _(all) - .map((def, name) => toEditableConfig({ - def, - name, - value: def.userValue, - isCustom: config.isCustom(name) - })) - .value(); - const writable = _.reject(editable, 'readonly'); - $scope.configs = writable; - } + config.watchAll(() => { + updateAdvancedSettings($scope, config, $route.current.params.setting || ''); + }, $scope); + + $scope.$on('$destory', () => { + destroyAdvancedSettings(); + }); + + $route.updateParams({ setting: null }); } }; }); diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/default_category.test.js b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/default_category.test.js new file mode 100644 index 0000000000000..a8f84e94286e2 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/default_category.test.js @@ -0,0 +1,12 @@ +import expect from 'expect.js'; +import { DEFAULT_CATEGORY } from '../default_category'; + +describe('Settings', function () { + describe('Advanced', function () { + describe('DEFAULT_CATEGORY', function () { + it('should be general', function () { + expect(DEFAULT_CATEGORY).to.be('general'); + }); + }); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_aria_name.js b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_aria_name.test.js similarity index 99% rename from src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_aria_name.js rename to src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_aria_name.test.js index f8c02e6dac00e..85adb7d6db55f 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_aria_name.js +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_aria_name.test.js @@ -1,6 +1,5 @@ - -import { getAriaName } from '../get_aria_name'; import expect from 'expect.js'; +import { getAriaName } from '../get_aria_name'; describe('Settings', function () { describe('Advanced', function () { diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_category_name.test.js b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_category_name.test.js new file mode 100644 index 0000000000000..432c467707047 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_category_name.test.js @@ -0,0 +1,33 @@ +import expect from 'expect.js'; +import { getCategoryName } from '../get_category_name'; + +describe('Settings', function () { + describe('Advanced', function () { + describe('getCategoryName(category)', function () { + it('should be a function', function () { + expect(getCategoryName).to.be.a(Function); + }); + + it('should return correct name for known categories', function () { + expect(getCategoryName('general')).to.be('General'); + expect(getCategoryName('timelion')).to.be('Timelion'); + expect(getCategoryName('notifications')).to.be('Notifications'); + expect(getCategoryName('visualizations')).to.be('Visualizations'); + expect(getCategoryName('discover')).to.be('Discover'); + expect(getCategoryName('dashboard')).to.be('Dashboard'); + expect(getCategoryName('reporting')).to.be('Reporting'); + expect(getCategoryName('search')).to.be('Search'); + }); + + it('should capitalize unknown category', function () { + expect(getCategoryName('elasticsearch')).to.be('Elasticsearch'); + }); + + it('should return empty string for no category', function () { + expect(getCategoryName()).to.be(''); + expect(getCategoryName('')).to.be(''); + expect(getCategoryName(false)).to.be(''); + }); + }); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_editor_type.js b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_editor_type.js deleted file mode 100644 index b1de3c48593fb..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_editor_type.js +++ /dev/null @@ -1,27 +0,0 @@ - -import { getEditorType } from '../get_editor_type'; -import expect from 'expect.js'; - -describe('Settings', function () { - describe('Advanced', function () { - describe('getEditorType(conf)', function () { - describe('when given type has a named editor', function () { - it('returns that named editor', function () { - expect(getEditorType({ type: 'json' })).to.equal('json'); - expect(getEditorType({ type: 'array' })).to.equal('array'); - expect(getEditorType({ type: 'boolean' })).to.equal('boolean'); - expect(getEditorType({ type: 'select' })).to.equal('select'); - }); - }); - - describe('when given a type of number, string, null, or undefined', function () { - it('returns "normal"', function () { - expect(getEditorType({ type: 'number' })).to.equal('normal'); - expect(getEditorType({ type: 'string' })).to.equal('normal'); - expect(getEditorType({ type: 'null' })).to.equal('normal'); - expect(getEditorType({ type: 'undefined' })).to.equal('normal'); - }); - }); - }); - }); -}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_val_type.js b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_val_type.test.js similarity index 99% rename from src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_val_type.js rename to src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_val_type.test.js index 4c3b3102a52f6..38f1ac8ed5400 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_val_type.js +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/get_val_type.test.js @@ -1,6 +1,5 @@ - -import { getValType } from '../get_val_type'; import expect from 'expect.js'; +import { getValType } from '../get_val_type'; describe('Settings', function () { describe('Advanced', function () { diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/is_default_value.test.js b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/is_default_value.test.js new file mode 100644 index 0000000000000..048b2c10db9f4 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/is_default_value.test.js @@ -0,0 +1,55 @@ +import expect from 'expect.js'; +import { isDefaultValue } from '../is_default_value'; + +describe('Settings', function () { + describe('Advanced', function () { + describe('getCategoryName(category)', function () { + it('should be a function', function () { + expect(isDefaultValue).to.be.a(Function); + }); + + describe('when given a setting definition object', function () { + const setting = { + isCustom: false, + value: 'value', + defVal: 'defaultValue', + }; + + describe('that is custom', function () { + it('should return true', function () { + expect(isDefaultValue({ ...setting, isCustom: true })).to.be(true); + }); + }); + + describe('without a value', function () { + it('should return true', function () { + expect(isDefaultValue({ ...setting, value: undefined })).to.be(true); + expect(isDefaultValue({ ...setting, value: '' })).to.be(true); + }); + }); + + describe('with a value that is the same as the default value', function () { + it('should return true', function () { + expect(isDefaultValue({ ...setting, value: 'defaultValue' })).to.be(true); + expect(isDefaultValue({ ...setting, value: [], defVal: [] })).to.be(true); + expect(isDefaultValue({ ...setting, value: '{"foo":"bar"}', defVal: '{"foo":"bar"}' })).to.be(true); + expect(isDefaultValue({ ...setting, value: 123, defVal: 123 })).to.be(true); + expect(isDefaultValue({ ...setting, value: 456, defVal: '456' })).to.be(true); + expect(isDefaultValue({ ...setting, value: false, defVal: false })).to.be(true); + }); + }); + + describe('with a value that is different than the default value', function () { + it('should return false', function () { + expect(isDefaultValue({ ...setting })).to.be(false); + expect(isDefaultValue({ ...setting, value: [1], defVal: [2] })).to.be(false); + expect(isDefaultValue({ ...setting, value: '{"foo":"bar"}', defVal: '{"foo2":"bar2"}' })).to.be(false); + expect(isDefaultValue({ ...setting, value: 123, defVal: 1234 })).to.be(false); + expect(isDefaultValue({ ...setting, value: 456, defVal: '4567' })).to.be(false); + expect(isDefaultValue({ ...setting, value: true, defVal: false })).to.be(false); + }); + }); + }); + }); + }); +}); diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.js b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js similarity index 99% rename from src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.js rename to src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js index 55d16bf1a8aef..cbb14515d297a 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.js +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js @@ -1,6 +1,5 @@ - -import { toEditableConfig } from '../to_editable_config'; import expect from 'expect.js'; +import { toEditableConfig } from '../to_editable_config'; describe('Settings', function () { describe('Advanced', function () { diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/default_category.js b/src/core_plugins/kibana/public/management/sections/settings/lib/default_category.js new file mode 100644 index 0000000000000..e0648173825f9 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/default_category.js @@ -0,0 +1 @@ +export const DEFAULT_CATEGORY = 'general'; diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js b/src/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js new file mode 100644 index 0000000000000..d5795772f60a4 --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js @@ -0,0 +1,16 @@ +import { StringUtils } from 'ui/utils/string_utils'; + +const names = { + 'general': 'General', + 'timelion': 'Timelion', + 'notifications': 'Notifications', + 'visualizations': 'Visualizations', + 'discover': 'Discover', + 'dashboard': 'Dashboard', + 'reporting': 'Reporting', + 'search': 'Search', +}; + +export function getCategoryName(category) { + return category ? names[category] || StringUtils.upperFirst(category) : ''; +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/get_editor_type.js b/src/core_plugins/kibana/public/management/sections/settings/lib/get_editor_type.js deleted file mode 100644 index 2fad54df7b1c8..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/settings/lib/get_editor_type.js +++ /dev/null @@ -1,13 +0,0 @@ -import _ from 'lodash'; - -const NAMED_EDITORS = ['json', 'array', 'boolean', 'select', 'markdown', 'image']; -const NORMAL_EDITOR = ['number', 'string', 'null', 'undefined']; - -/** - * @param {object} advanced setting configuration object - * @returns {string} the editor type to use when editing value - */ -export function getEditorType(conf) { - if (_.contains(NAMED_EDITORS, conf.type)) return conf.type; - if (_.contains(NORMAL_EDITOR, conf.type)) return 'normal'; -} diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/index.js b/src/core_plugins/kibana/public/management/sections/settings/lib/index.js new file mode 100644 index 0000000000000..c88cd2169c61e --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/index.js @@ -0,0 +1,5 @@ +export { isDefaultValue } from './is_default_value'; +export { toEditableConfig } from './to_editable_config'; +export { getCategoryName } from './get_category_name'; +export { DEFAULT_CATEGORY } from './default_category'; +export { getAriaName } from './get_aria_name'; diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.js b/src/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.js new file mode 100644 index 0000000000000..daf595416a08e --- /dev/null +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/is_default_value.js @@ -0,0 +1,3 @@ +export function isDefaultValue(setting) { + return (setting.isCustom || setting.value === undefined || setting.value === '' || String(setting.value) === String(setting.defVal)); +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js b/src/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js index 2e72277a8db88..dc5182594738c 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js @@ -1,6 +1,6 @@ import { getValType } from './get_val_type'; -import { getEditorType } from './get_editor_type'; import { getAriaName } from './get_aria_name'; +import { DEFAULT_CATEGORY } from './default_category'; /** * @param {object} advanced setting definition object @@ -14,26 +14,18 @@ export function toEditableConfig({ def, name, value, isCustom }) { } const conf = { name, + displayName: def.name || name, ariaName: getAriaName(name), value, + category: def.category && def.category.length ? def.category : [DEFAULT_CATEGORY], isCustom, readonly: !!def.readonly, defVal: def.value, type: getValType(def, value), description: def.description, - options: def.options + options: def.options, }; - const editor = getEditorType(conf); - conf.json = editor === 'json'; - conf.select = editor === 'select'; - conf.bool = editor === 'boolean'; - conf.array = editor === 'array'; - conf.markdown = editor === 'markdown'; - conf.image = editor === 'image'; - conf.normal = editor === 'normal'; - conf.tooComplex = !editor; - return conf; } diff --git a/src/core_plugins/kibana/ui_setting_defaults.js b/src/core_plugins/kibana/ui_setting_defaults.js index 88ba519b65137..c696800ac9752 100644 --- a/src/core_plugins/kibana/ui_setting_defaults.js +++ b/src/core_plugins/kibana/ui_setting_defaults.js @@ -14,37 +14,46 @@ export function getUiSettingDefaults() { readonly: true }, 'query:queryString:options': { + name: 'Query string options', value: '{ "analyze_wildcard": true, "default_field": "*" }', description: 'Options for the lucene query string parser', type: 'json' }, 'query:allowLeadingWildcards': { + name: 'Allow leading wildcards in query', value: true, description: `When set, * is allowed as the first character in a query clause. Currently only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in basic lucene queries, use query:queryString:options`, }, 'search:queryLanguage': { + name: 'Query language', value: 'lucene', - description: 'Query language used by the query bar. Kuery is an experimental new language built specifically for Kibana.', + description: `Query language used by the query bar. Kuery is an experimental new language built specifically for Kibana.`, type: 'select', options: ['lucene', 'kuery'] }, 'sort:options': { + name: 'Sort options', value: '{ "unmapped_type": "boolean" }', - description: 'Options for the Elasticsearch sort parameter', + description: `Options for the Elasticsearch sort parameter`, type: 'json' }, 'dateFormat': { + name: 'Date format', value: 'MMMM Do YYYY, HH:mm:ss.SSS', - description: 'When displaying a pretty formatted date, use this format', + description: `When displaying a pretty formatted date, use this format`, }, 'dateFormat:tz': { + name: 'Timezone for date formatting', value: 'Browser', - description: 'Which timezone should be used. "Browser" will use the timezone detected by your browser.', + description: `Which timezone should be used. "Browser" will use the timezone detected by your browser.`, type: 'select', options: ['Browser', ...moment.tz.names()] }, 'dateFormat:scaled': { + name: 'Scaled date format', type: 'json', value: `[ @@ -56,112 +65,139 @@ export function getUiSettingDefaults() { ["P1YT", "YYYY"] ]`, description: ( - 'Values that define the format used in situations where timebased' + - ' data is rendered in order, and formatted timestamps should adapt to the' + - ' interval between measurements. Keys are' + - ' ' + - 'ISO8601 intervals.' + `Values that define the format used in situations where time-based + data is rendered in order, and formatted timestamps should adapt to the + interval between measurements. Keys are + + ISO8601 intervals.` ) }, 'dateFormat:dow': { + name: 'Day of week', value: defaultWeekday, - description: 'What day should weeks start on?', + description: `What day should weeks start on?`, type: 'select', options: weekdays }, 'defaultIndex': { + name: 'Default index', value: null, - description: 'The index to access if no index is set', + description: `The index to access if no index is set`, }, 'defaultColumns': { + name: 'Default columns', value: ['_source'], - description: 'Columns displayed by default in the Discovery tab', + description: `Columns displayed by default in the Discovery tab`, + category: ['discover'], }, 'metaFields': { + name: 'Meta fields', value: ['_source', '_id', '_type', '_index', '_score'], - description: 'Fields that exist outside of _source to merge into our document when displaying it', + description: `Fields that exist outside of _source to merge into our document when displaying it`, }, 'discover:sampleSize': { + name: 'Number of rows', value: 500, - description: 'The number of rows to show in the table', + description: `The number of rows to show in the table`, + category: ['discover'], }, 'discover:aggs:terms:size': { + name: 'Number of terms', value: 20, type: 'number', - description: 'Determines how many terms will be visualized when clicking the "visualize" ' + - 'button, in the field drop downs, in the discover sidebar.' + description: `Determines how many terms will be visualized when clicking the "visualize" + button, in the field drop downs, in the discover sidebar.`, + category: ['discover'], }, 'discover:sort:defaultOrder': { + name: 'Default sort direction', value: 'desc', options: ['desc', 'asc'], type: 'select', - description: 'Controls the default sort direction for time based index patterns in the Discover app.', + description: `Controls the default sort direction for time based index patterns in the Discover app.`, + category: ['discover'], }, 'doc_table:highlight': { + name: 'Highlight results', value: true, - description: 'Highlight results in Discover and Saved Searches Dashboard.' + - 'Highlighting makes requests slow when working on big documents.', + description: `Highlight results in Discover and Saved Searches Dashboard. + Highlighting makes requests slow when working on big documents.`, + category: ['discover'], }, 'courier:maxSegmentCount': { + name: 'Maximum segment count', value: 30, - description: 'Requests in discover are split into segments to prevent massive requests from being sent to ' + - 'elasticsearch. This setting attempts to prevent the list of segments from getting too long, which might ' + - 'cause requests to take much longer to process' + description: `Requests in discover are split into segments to prevent massive requests from being sent to + elasticsearch. This setting attempts to prevent the list of segments from getting too long, which might + cause requests to take much longer to process.`, + category: ['search'], }, 'courier:ignoreFilterIfFieldNotInIndex': { + name: 'Ignore filter(s)', value: false, - description: 'This configuration enhances support for dashboards containing visualizations accessing dissimilar indexes. ' + - 'When set to false, all filters are applied to all visualizations. ' + - 'When set to true, filter(s) will be ignored for a visualization ' + - 'when the visualization\'s index does not contain the filtering field.' + description: `This configuration enhances support for dashboards containing visualizations accessing dissimilar indexes. + When set to false, all filters are applied to all visualizations. + When set to true, filter(s) will be ignored for a visualization + when the visualization's index does not contain the filtering field.`, + category: ['search'], }, 'courier:setRequestPreference': { + name: 'Request preference', value: 'sessionId', options: ['sessionId', 'custom', 'none'], type: 'select', - description: 'Allows you to set which shards handle your search requests. ' + - '
    ' + - '
  • sessionId: restricts operations to execute all search requests on the same shards. ' + - 'This has the benefit of reusing shard caches across requests. ' + - '
  • custom: allows you to define a your own preference. ' + - 'Use courier:customRequestPreference to customize your preference value. ' + - '
  • none: means do not set a preference. ' + - 'This might provide better performance because requests can be spread across all shard copies. ' + - 'However, results might be inconsistent because different shards might be in different refresh states.' + - '
' + description: `Allows you to set which shards handle your search requests. +
    +
  • sessionId: restricts operations to execute all search requests on the same shards. + This has the benefit of reusing shard caches across requests.
  • +
  • custom: allows you to define a your own preference. + Use courier:customRequestPreference to customize your preference value.
  • +
  • none: means do not set a preference. + This might provide better performance because requests can be spread across all shard copies. + However, results might be inconsistent because different shards might be in different refresh states.
  • +
`, + category: ['search'], }, 'courier:customRequestPreference': { + name: 'Custom request preference', value: '_local', type: 'string', - description: 'Request Preference ' + - ' used when courier:setRequestPreference is set to "custom".' + description: `Request Preference + used when courier:setRequestPreference is set to "custom".`, + category: ['search'], }, 'fields:popularLimit': { + name: 'Popular fields limit', value: 10, - description: 'The top N most popular fields to show', + description: `The top N most popular fields to show`, }, 'histogram:barTarget': { + name: 'Target bars', value: 50, - description: 'Attempt to generate around this many bars when using "auto" interval in date histograms', + description: `Attempt to generate around this many bars when using "auto" interval in date histograms`, }, 'histogram:maxBars': { + name: 'Maximum bars', value: 100, - description: 'Never show more than this many bars in date histograms, scale values if needed', + description: `Never show more than this many bars in date histograms, scale values if needed`, }, 'visualize:enableLabs': { + name: 'Enable labs', value: true, - description: 'Enable lab visualizations in Visualize.' + description: `Enable lab visualizations in Visualize.`, + category: ['visualization'], }, 'visualization:tileMap:maxPrecision': { + name: 'Maximum tile map precision', value: 7, - description: 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, ' + - '12 is the max. ' + - '' + - 'Explanation of cell dimensions', + description: `The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the max. + Explanation of cell dimensions`, + category: ['visualization'], }, 'visualization:tileMap:WMSdefaults': { + name: 'Default WMS properties', value: JSON.stringify({ enabled: false, url: undefined, @@ -175,62 +211,79 @@ export function getUiSettingDefaults() { } }, null, 2), type: 'json', - description: 'Default properties for the WMS map server support in the coordinate map' + description: `Default properties for the WMS map server support in the coordinate map`, + category: ['visualization'], }, 'visualization:regionmap:showWarnings': { + name: 'Show region map warning', value: true, - description: 'Whether the region map show a warning when terms cannot be joined to a shape on the map.' + description: `Whether the region map shows a warning when terms cannot be joined to a shape on the map.`, + category: ['visualization'], }, 'visualization:colorMapping': { - type: 'json', + name: 'Color mapping', value: JSON.stringify({ Count: '#00A69B' }), - description: 'Maps values to specified colors within visualizations' + type: 'json', + description: `Maps values to specified colors within visualizations`, + category: ['visualization'], }, 'visualization:loadingDelay': { + name: 'Loading delay', value: '2s', - description: 'Time to wait before dimming visualizations during query' + description: `Time to wait before dimming visualizations during query`, + category: ['visualization'], }, 'visualization:dimmingOpacity': { - type: 'number', + name: 'Dimming opacity', value: 0.5, - description: 'The opacity of the chart items that are dimmed when highlighting another element of the chart. ' + - 'The lower this number, the more the highlighted element will stand out.' + - 'This must be a number between 0 and 1.' + type: 'number', + description: `The opacity of the chart items that are dimmed when highlighting another element of the chart. + The lower this number, the more the highlighted element will stand out. + This must be a number between 0 and 1.`, + category: ['visualization'], }, 'csv:separator': { + name: 'CSV separator', value: ',', - description: 'Separate exported values with this string', + description: `Separate exported values with this string`, }, 'csv:quoteValues': { + name: 'Quote CSV values', value: true, - description: 'Should values be quoted in csv exports?', + description: `Should values be quoted in csv exports?`, }, 'history:limit': { + name: 'History limit', value: 10, - description: 'In fields that have history (e.g. query inputs), show this many recent values', + description: `In fields that have history (e.g. query inputs), show this many recent values`, }, 'shortDots:enable': { + name: 'Shorten fields', value: false, - description: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz', + description: `Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz`, }, 'truncate:maxHeight': { + name: 'Maximum table cell height', value: 115, - description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation' + description: `The maximum height that a cell in a table should occupy. Set to 0 to disable truncation`, }, 'indexPattern:fieldMapping:lookBack': { + name: 'Recent matching patterns', value: 5, - description: 'For index patterns containing timestamps in their names, look for this many recent matching ' + - 'patterns from which to query the field mapping' + description: `For index patterns containing timestamps in their names, look for this many recent matching + patterns from which to query the field mapping` }, 'indexPatterns:warnAboutUnsupportedTimePatterns': { + name: 'Time pattern warning', value: false, - description: 'When an index pattern is using the now unsupported "time pattern" format, a warning will ' + - 'be displayed once per session that is using this pattern. Set this to false to disable that warning.' + description: `When an index pattern is using the now unsupported "time pattern" format, a warning will + be displayed once per session that is using this pattern. Set this to false to disable that warning.` }, 'format:defaultTypeMap': { - type: 'json', + name: 'Field type format name', value: `{ "ip": { "id": "ip", "params": {} }, @@ -240,67 +293,77 @@ export function getUiSettingDefaults() { "_source": { "id": "_source", "params": {} }, "_default_": { "id": "string", "params": {} } }`, - description: 'Map of the format name to use by default for each field type. ' + - '"_default_" is used if the field type is not mentioned explicitly' + type: 'json', + description: `Map of the format name to use by default for each field type. + "_default_" is used if the field type is not mentioned explicitly` }, 'format:number:defaultPattern': { - type: 'string', + name: 'Number format', value: '0,0.[000]', - description: 'Default numeral format for the "number" format' + type: 'string', + description: `Default numeral format for the "number" format` }, 'format:bytes:defaultPattern': { - type: 'string', + name: 'Bytes format', value: '0,0.[000]b', - description: 'Default numeral format for the "bytes" format' + type: 'string', + description: `Default numeral format for the "bytes" format` }, 'format:percent:defaultPattern': { - type: 'string', + name: 'Percent format', value: '0,0.[000]%', - description: 'Default numeral format for the "percent" format' + type: 'string', + description: `Default numeral format for the "percent" format` }, 'format:currency:defaultPattern': { - type: 'string', + name: 'Currency format', value: '($0,0.[00])', - description: 'Default numeral format for the "currency" format' + type: 'string', + description: `Default numeral format for the "currency" format` }, 'format:number:defaultLocale': { + name: 'Formatting locale', value: 'en', type: 'select', options: numeralLanguageIds, - description: 'numeral language' + description: `Numeral language locale` }, 'savedObjects:perPage': { - type: 'number', + name: 'Objects per page', value: 5, - description: 'Number of objects to show per page in the load dialog' + type: 'number', + description: `Number of objects to show per page in the load dialog` }, 'savedObjects:listingLimit': { + name: 'Objects listing limit', type: 'number', value: 1000, - description: 'Number of objects to fetch for the listing pages' + description: `Number of objects to fetch for the listing pages` }, 'timepicker:timeDefaults': { - type: 'json', + name: 'Time picker defaults', value: `{ "from": "now-15m", "to": "now", "mode": "quick" }`, - description: 'The timefilter selection to use when Kibana is started without one' + type: 'json', + description: `The timefilter selection to use when Kibana is started without one` }, 'timepicker:refreshIntervalDefaults': { - type: 'json', + name: 'Time picker refresh interval', value: `{ "display": "Off", "pause": false, "value": 0 }`, - description: 'The timefilter\'s default refresh interval' + type: 'json', + description: `The timefilter's default refresh interval` }, 'timepicker:quickRanges': { - type: 'json', + name: 'Time picker quick ranges', value: JSON.stringify([ { from: 'now/d', to: 'now/d', display: 'Today', section: 0 }, { from: 'now/w', to: 'now/w', display: 'This week', section: 0 }, @@ -328,79 +391,106 @@ export function getUiSettingDefaults() { { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 2 }, ], null, 2), - description: 'The list of ranges to show in the Quick section of the time picker. ' + - 'This should be an array of objects, with each object containing "from", "to" (see ' + - 'accepted formats' + - '), "display" (the title to be displayed), and "section" (which column to put the option in).' + type: 'json', + description: `The list of ranges to show in the Quick section of the time picker. + This should be an array of objects, with each object containing "from", "to" (see + accepted formats), + "display" (the title to be displayed), and "section" (which column to put the option in).` }, 'dashboard:defaultDarkTheme': { + name: 'Dark theme', value: false, - description: 'New dashboards use dark theme by default' + description: `New dashboards use dark theme by default`, + category: ['dashboard'], }, 'filters:pinnedByDefault': { + name: 'Pin filters by default', value: false, - description: 'Whether the filters should have a global state (be pinned) by default' + description: `Whether the filters should have a global state (be pinned) by default` }, 'filterEditor:suggestValues': { + name: 'Filter editor suggest values', value: true, - description: 'Set this property to false to prevent the filter editor from suggesting values for fields.' + description: `Set this property to false to prevent the filter editor from suggesting values for fields.` }, 'notifications:banner': { + name: 'Custom banner notification', + value: '', type: 'markdown', - description: 'A custom banner intended for temporary notices to all users. Markdown supported.', - value: '' + description: `A custom banner intended for temporary notices to all users. + Markdown supported.`, + category: ['notifications'], }, 'notifications:lifetime:banner': { + name: 'Banner notification lifetime', value: 3000000, - description: 'The time in milliseconds which a banner notification ' + - 'will be displayed on-screen for. Setting to Infinity will disable the countdown.', + description: `The time in milliseconds which a banner notification + will be displayed on-screen for. Setting to Infinity will disable the countdown.`, type: 'number', + category: ['notifications'], }, 'notifications:lifetime:error': { + name: 'Error notification lifetime', value: 300000, - description: 'The time in milliseconds which an error notification ' + - 'will be displayed on-screen for. Setting to Infinity will disable.', + description: `The time in milliseconds which an error notification + 'will be displayed on-screen for. Setting to Infinity will disable.`, type: 'number', + category: ['notifications'], }, 'notifications:lifetime:warning': { + name: 'Warning notification lifetime', value: 10000, - description: 'The time in milliseconds which a warning notification ' + - 'will be displayed on-screen for. Setting to Infinity will disable.', + description: `The time in milliseconds which a warning notification + 'will be displayed on-screen for. Setting to Infinity will disable.`, type: 'number', + category: ['notifications'], }, 'notifications:lifetime:info': { + name: 'Info notification lifetime', value: 5000, - description: 'The time in milliseconds which an information notification ' + - 'will be displayed on-screen for. Setting to Infinity will disable.', + description: `The time in milliseconds which an information notification + will be displayed on-screen for. Setting to Infinity will disable.`, type: 'number', + category: ['notifications'], }, 'metrics:max_buckets': { + name: 'Maximum buckets', value: 2000, - description: 'The maximum number of buckets a single datasource can return' + description: `The maximum number of buckets a single datasource can return` }, 'state:storeInSessionStorage': { + name: 'Store URLs in session storage', value: false, - description: 'The URL can sometimes grow to be too large for some browsers to ' + - 'handle. To counter-act this we are testing if storing parts of the URL in ' + - 'sessions storage could help. Please let us know how it goes!' + description: `The URL can sometimes grow to be too large for some browsers to + handle. To counter-act this we are testing if storing parts of the URL in + session storage could help. Please let us know how it goes!` }, 'indexPattern:placeholder': { + name: 'Index pattern placeholder', value: 'logstash-*', - description: 'The placeholder for the field "Index name or pattern" in the "Settings > Indices" tab.', + description: `The placeholder for the field "Index name or pattern" in the "Settings > Indices" tab.`, }, 'context:defaultSize': { + name: 'Context size', value: 5, - description: 'The number of surrounding entries to show in the context view', + description: `The number of surrounding entries to show in the context view`, + category: ['discover'], }, 'context:step': { + name: 'Context size step', value: 5, - description: 'The step size to increment or decrement the context size by', + description: `The step size to increment or decrement the context size by`, + category: ['discover'], }, 'context:tieBreakerFields': { + name: 'Tie breaker fields', value: ['_doc'], - description: 'A comma-separated list of fields to use for tiebreaking between documents ' + - 'that have the same timestamp value. From this list the first field that ' + - 'is present and sortable in the current index pattern is used.', + description: `A comma-separated list of fields to use for tie-breaking between documents + that have the same timestamp value. From this list the first field that + is present and sortable in the current index pattern is used.`, + category: ['discover'], }, }; } diff --git a/src/core_plugins/timelion/index.js b/src/core_plugins/timelion/index.js index 4e1a39004aac7..215ad3f5774fe 100644 --- a/src/core_plugins/timelion/index.js +++ b/src/core_plugins/timelion/index.js @@ -23,44 +23,64 @@ export default function (kibana) { uiSettingDefaults: { 'timelion:showTutorial': { + name: 'Show tutorial', value: false, - description: 'Should I show the tutorial by default when entering the timelion app?' + description: `Should I show the tutorial by default when entering the timelion app?`, + category: ['timelion'], }, 'timelion:es.timefield': { + name: 'Time field', value: '@timestamp', - description: 'Default field containing a timestamp when using .es()' + description: `Default field containing a timestamp when using .es()`, + category: ['timelion'], }, 'timelion:es.default_index': { + name: 'Default index', value: '_all', - description: 'Default elasticsearch index to search with .es()' + description: `Default elasticsearch index to search with .es()`, + category: ['timelion'], }, 'timelion:target_buckets': { + name: 'Target buckets', value: 200, - description: 'The number of buckets to shoot for when using auto intervals' + description: `The number of buckets to shoot for when using auto intervals`, + category: ['timelion'], }, 'timelion:max_buckets': { + name: 'Maximum buckets', value: 2000, - description: 'The maximum number of buckets a single datasource can return' + description: `The maximum number of buckets a single datasource can return`, + category: ['timelion'], }, 'timelion:default_columns': { + name: 'Default columns', value: 2, - description: 'Number of columns on a timelion sheet by default' + description: `Number of columns on a timelion sheet by default`, + category: ['timelion'], }, 'timelion:default_rows': { + name: 'Default rows', value: 2, - description: 'Number of rows on a timelion sheet by default' + description: `Number of rows on a timelion sheet by default`, + category: ['timelion'], }, 'timelion:min_interval': { + name: 'Minimum interval', value: '1ms', - description: 'The smallest interval that will be calculated when using "auto"' + description: `The smallest interval that will be calculated when using "auto"`, + category: ['timelion'], }, 'timelion:graphite.url': { + name: 'Graphite URL', value: 'https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', - description: '[experimental] The URL of your graphite host' + description: `[experimental] The URL of your graphite host`, + category: ['timelion'], }, 'timelion:quandl.key': { + name: 'Quandl key', value: 'someKeyHere', - description: '[experimental] Your API key from www.quandl.com' + description: `[experimental] Your API key from www.quandl.com`, + category: ['timelion'], } } }, diff --git a/src/ui/public/notify/notifier.js b/src/ui/public/notify/notifier.js index fa303df19f892..f84449dee29c3 100644 --- a/src/ui/public/notify/notifier.js +++ b/src/ui/public/notify/notifier.js @@ -326,12 +326,12 @@ Notifier.prototype.warning = function (msg, opts, cb) { /** * Display a banner message - * @param {String} msg - * @param {Function} cb + * @param {String} content + * @param {String} name */ let bannerId; let bannerTimeoutId; -Notifier.prototype.banner = function (content = '') { +Notifier.prototype.banner = function (content = '', name = '') { const BANNER_PRIORITY = 100; const dismissBanner = () => { @@ -355,6 +355,7 @@ Notifier.prototype.banner = function (content = '') { * The notifier relies on `markdown-it` to produce safe and correct HTML. */ dangerouslySetInnerHTML={{ __html: markdownIt.render(content) }} //eslint-disable-line react/no-danger + data-test-subj={name ? `banner-${name}` : null} /> diff --git a/src/ui/public/notify/notify.js b/src/ui/public/notify/notify.js index 2ddc9093ab042..f36134455e0a7 100644 --- a/src/ui/public/notify/notify.js +++ b/src/ui/public/notify/notify.js @@ -62,7 +62,7 @@ function applyConfig(config) { const banner = config.get('notifications:banner'); if (typeof banner === 'string' && banner.trim()) { - notify.banner(banner); + notify.banner(banner, 'notifications:banner'); } } diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index 31b6d8aa7a6ec..d2d1a68270511 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -28,18 +28,11 @@ export default function ({ getService, getPageObjects }) { expect(advancedSetting).to.be('America/Phoenix'); }); - it('should coerce an empty setting of type JSON into an empty object', async function () { - await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.setAdvancedSettingsInput('query:queryString:options', '', 'unsavedValueJsonTextArea'); - const advancedSetting = await PageObjects.settings.getAdvancedSettings('query:queryString:options'); - expect(advancedSetting).to.be.eql('{}'); - }); - describe('state:storeInSessionStorage', () => { it ('defaults to false', async () => { await PageObjects.settings.clickKibanaSettings(); - const storeInSessionStorage = await PageObjects.settings.getAdvancedSettings('state:storeInSessionStorage'); - expect(storeInSessionStorage).to.be('false'); + const storeInSessionStorage = await PageObjects.settings.getAdvancedSettingCheckbox('state:storeInSessionStorage'); + expect(storeInSessionStorage).to.be(false); }); it('when false, dashboard state is unhashed', async function () { @@ -61,8 +54,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.toggleAdvancedSettingCheckbox('state:storeInSessionStorage'); - const storeInSessionStorage = await PageObjects.settings.getAdvancedSettings('state:storeInSessionStorage'); - expect(storeInSessionStorage).to.be('true'); + const storeInSessionStorage = await PageObjects.settings.getAdvancedSettingCheckbox('state:storeInSessionStorage'); + expect(storeInSessionStorage).to.be(true); }); it('when true, dashboard state is hashed', async function () { @@ -87,21 +80,6 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('notifications:banner', () => { - it('Should convert notification banner markdown into HTML', async function () { - await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.setAdvancedSettingsInput('notifications:banner', '# Welcome to Kibana', 'unsavedValueMarkdownTextArea'); - const bannerValue = await PageObjects.settings.getAdvancedSettings('notifications:banner'); - expect(bannerValue).to.equal('Welcome to Kibana'); - }); - - after('navigate to settings page and clear notifications:banner', async () => { - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.clearAdvancedSettings('notifications:banner'); - }); - }); - after(async function () { await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'UTC'); diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index c998e318ad32b..64b1b851993ce 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -38,42 +38,42 @@ export function SettingsPageProvider({ getService, getPageObjects }) { async getAdvancedSettings(propertyName) { log.debug('in getAdvancedSettings'); - return await testSubjects.getVisibleText(`advancedSetting-${propertyName}-currentValue`); + const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); + return await setting.getProperty('value'); + } + + async getAdvancedSettingCheckbox(propertyName) { + log.debug('in getAdvancedSettingCheckbox'); + const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); + return await setting.getProperty('checked'); } async clearAdvancedSettings(propertyName) { - await testSubjects.click(`advancedSetting-${propertyName}-clearButton`); + await testSubjects.click(`advancedSetting-resetField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); } async setAdvancedSettingsSelect(propertyName, propertyValue) { - await testSubjects.click(`advancedSetting-${propertyName}-editButton`); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.common.sleep(1000); await remote.setFindTimeout(defaultFindTimeout) - .findByCssSelector(`option[label="${propertyValue}"]`).click(); + .findByCssSelector(`[data-test-subj="advancedSetting-editField-${propertyName}"] option[value="${propertyValue}"]`).click(); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.click(`advancedSetting-${propertyName}-saveButton`); + await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); } - async setAdvancedSettingsInput(propertyName, propertyValue, inputSelector) { - await testSubjects.click(`advancedSetting-${propertyName}-editButton`); - await PageObjects.header.waitUntilLoadingHasFinished(); - const input = await testSubjects.find(inputSelector); + async setAdvancedSettingsInput(propertyName, propertyValue) { + const input = await testSubjects.find(`advancedSetting-editField-${propertyName}`); await input.clearValue(); await input.type(propertyValue); - await testSubjects.click(`advancedSetting-${propertyName}-saveButton`); + await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); } async toggleAdvancedSettingCheckbox(propertyName) { - await testSubjects.click(`advancedSetting-${propertyName}-editButton`); - await PageObjects.header.waitUntilLoadingHasFinished(); - const checkbox = await testSubjects.find(`advancedSetting-${propertyName}-checkbox`); + const checkbox = await testSubjects.find(`advancedSetting-editField-${propertyName}`); await checkbox.click(); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.click(`advancedSetting-${propertyName}-saveButton`); + await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); } diff --git a/x-pack/plugins/dashboard_mode/index.js b/x-pack/plugins/dashboard_mode/index.js index 729e41b8f4724..e4afe029fc01a 100644 --- a/x-pack/plugins/dashboard_mode/index.js +++ b/x-pack/plugins/dashboard_mode/index.js @@ -27,8 +27,10 @@ export function dashboardMode(kibana) { uiExports: { uiSettingDefaults: { [CONFIG_DASHBOARD_ONLY_MODE_ROLES]: { - description: 'Roles that belong to View Dashboards Only mode', + name: 'Dashboards only roles', + description: `Roles that belong to View Dashboards Only mode`, value: ['kibana_dashboard_only_user'], + category: ['dashboard'], } }, app: { diff --git a/x-pack/plugins/reporting/index.js b/x-pack/plugins/reporting/index.js index 69b01ab68f934..e7dd3c6e3f0d1 100644 --- a/x-pack/plugins/reporting/index.js +++ b/x-pack/plugins/reporting/index.js @@ -47,15 +47,17 @@ export const reporting = (kibana) => { }, uiSettingDefaults: { [UI_SETTINGS_CUSTOM_PDF_LOGO]: { - description: `Custom image to use in the PDF's footer`, + name: 'PDF footer image', value: null, + description: `Custom image to use in the PDF's footer`, type: 'image', options: { maxSize: { length: kbToBase64Length(200), description: '200 kB', } - } + }, + category: ['reporting'], } } }, diff --git a/x-pack/plugins/xpack_main/index.js b/x-pack/plugins/xpack_main/index.js index 4714929cd8840..a4001fcf6b203 100644 --- a/x-pack/plugins/xpack_main/index.js +++ b/x-pack/plugins/xpack_main/index.js @@ -67,12 +67,14 @@ export const xpackMain = (kibana) => { uiExports: { uiSettingDefaults: { [CONFIG_TELEMETRY]: { + name: 'Telemetry opt-in', description: CONFIG_TELEMETRY_DESC, value: false }, [XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING]: { + name: 'Admin email', // TODO: change the description when email address is used for more things? - description: 'Recipient email address for X-Pack admin operations, such as Cluster Alert email notifications from Monitoring.', + description: `Recipient email address for X-Pack admin operations, such as Cluster Alert email notifications from Monitoring.`, type: 'string', // TODO: Any way of ensuring this is a valid email address? value: null }