diff --git a/src/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap b/src/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap
index 8c6ab30f75c3..a0ee8da4407d 100644
--- a/src/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap
+++ b/src/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap
@@ -4,17 +4,18 @@ exports[`parentCandidates 1`] = `
{
+ return Promise.resolve({
+ id: 'mockIndexPattern',
+ title: 'mockIndexPattern',
+ fields: [
+ { name: 'keywordField', type: 'string', aggregatable: true },
+ { name: 'textField', type: 'string', aggregatable: false },
+ { name: 'numberField', type: 'number', aggregatable: true }
+ ]
+ });
+};
diff --git a/src/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.js b/src/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.js
new file mode 100644
index 000000000000..60dbc949c538
--- /dev/null
+++ b/src/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.js
@@ -0,0 +1,16 @@
+export const getIndexPatternsMock = () => {
+ return Promise.resolve([
+ {
+ id: 'indexPattern1',
+ attributes: {
+ title: 'indexPattern1'
+ }
+ },
+ {
+ id: 'indexPattern2',
+ attributes: {
+ title: 'indexPattern2'
+ }
+ }
+ ]);
+};
diff --git a/src/core_plugins/input_control_vis/public/components/editor/controls_tab.js b/src/core_plugins/input_control_vis/public/components/editor/controls_tab.js
index 6ffe115e1ce8..b71307148524 100644
--- a/src/core_plugins/input_control_vis/public/components/editor/controls_tab.js
+++ b/src/core_plugins/input_control_vis/public/components/editor/controls_tab.js
@@ -48,16 +48,16 @@ export class ControlsTab extends Component {
this.setVisParam('controls', setControl(this.props.scope.vis.params.controls, controlIndex, updatedControl));
}
- handleIndexPatternChange = (controlIndex, evt) => {
+ handleIndexPatternChange = (controlIndex, indexPatternId) => {
const updatedControl = this.props.scope.vis.params.controls[controlIndex];
- updatedControl.indexPattern = evt.value;
+ updatedControl.indexPattern = indexPatternId;
updatedControl.fieldName = '';
this.setVisParam('controls', setControl(this.props.scope.vis.params.controls, controlIndex, updatedControl));
}
- handleFieldNameChange = (controlIndex, evt) => {
+ handleFieldNameChange = (controlIndex, fieldName) => {
const updatedControl = this.props.scope.vis.params.controls[controlIndex];
- updatedControl.fieldName = evt.value;
+ updatedControl.fieldName = fieldName;
this.setVisParam('controls', setControl(this.props.scope.vis.params.controls, controlIndex, updatedControl));
}
diff --git a/src/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js
index 0cc5dc7120c9..9f888ac7e70a 100644
--- a/src/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js
+++ b/src/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js
@@ -2,6 +2,7 @@ import React from 'react';
import sinon from 'sinon';
import { mount, shallow } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
+import { getIndexPatternMock } from './__tests__/get_index_pattern_mock';
import {
ControlsTab,
} from './controls_tab';
@@ -21,14 +22,7 @@ const savedObjectsClientMock = {
}
};
const indexPatternsMock = {
- get: () => {
- return Promise.resolve({
- fields: [
- { name: 'keywordField', type: 'string', aggregatable: true },
- { name: 'numberField', type: 'number', aggregatable: true }
- ]
- });
- }
+ get: getIndexPatternMock
};
const scopeMock = {
vis: {
diff --git a/src/core_plugins/input_control_vis/public/components/editor/field_select.js b/src/core_plugins/input_control_vis/public/components/editor/field_select.js
index 182d0f58096c..5a0813ccdc56 100644
--- a/src/core_plugins/input_control_vis/public/components/editor/field_select.js
+++ b/src/core_plugins/input_control_vis/public/components/editor/field_select.js
@@ -1,60 +1,102 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import Select from 'react-select';
import {
EuiFormRow,
+ EuiComboBox,
} from '@elastic/eui';
export class FieldSelect extends Component {
constructor(props) {
super(props);
- // not storing activeIndexPatternId in react state
- // 1) does not effect rendering
- // 2) requires synchronous modification to avoid race condition
- this.activeIndexPatternId = props.indexPatternId;
+ this._hasUnmounted = false;
this.state = {
- fields: []
+ isLoading: false,
+ fields: [],
+ indexPatternId: props.indexPatternId,
};
this.filterField = _.get(props, 'filterField', () => { return true; });
- this.loadFields(props.indexPatternId);
+ }
+
+ componentWillUnmount() {
+ this._hasUnmounted = true;
+ }
+
+ componentDidMount() {
+ this.loadFields(this.state.indexPatternId);
}
componentWillReceiveProps(nextProps) {
if (this.props.indexPatternId !== nextProps.indexPatternId) {
- this.activeIndexPatternId = nextProps.indexPatternId;
- this.setState({ fields: [] });
this.loadFields(nextProps.indexPatternId);
}
}
- async loadFields(indexPatternId) {
+ loadFields = (indexPatternId) => {
+ this.setState({
+ isLoading: true,
+ fields: [],
+ indexPatternId
+ }, this.debouncedLoad.bind(null, indexPatternId));
+ }
+
+ debouncedLoad = _.debounce(async (indexPatternId) => {
if (!indexPatternId || indexPatternId.length === 0) {
return;
}
const indexPattern = await this.props.getIndexPattern(indexPatternId);
- // props.indexPatternId may be updated before getIndexPattern returns
- // ignore response when fetched index pattern does not match active index pattern
- if (indexPattern.id !== this.activeIndexPatternId) {
+ if (this._hasUnmounted) {
return;
}
- const fields = indexPattern.fields
+ // props.indexPatternId may be updated before getIndexPattern returns
+ // ignore response when fetched index pattern does not match active index pattern
+ if (indexPattern.id !== this.state.indexPatternId) {
+ return;
+ }
+
+ const fieldsByTypeMap = new Map();
+ const fields = [];
+ indexPattern.fields
.filter(this.filterField)
- .sort((a, b) => {
- if (a.name < b.name) return -1;
- if (a.name > b.name) return 1;
- return 0;
- })
- .map(function (field) {
- return { label: field.name, value: field.name };
+ .forEach(field => {
+ if (fieldsByTypeMap.has(field.type)) {
+ const fieldsList = fieldsByTypeMap.get(field.type);
+ fieldsList.push(field.name);
+ fieldsByTypeMap.set(field.type, fieldsList);
+ } else {
+ fieldsByTypeMap.set(field.type, [field.name]);
+ }
});
- this.setState({ fields: fields });
+
+ fieldsByTypeMap.forEach((fieldsList, fieldType) => {
+ fields.push({
+ label: fieldType,
+ options: fieldsList.sort().map(fieldName => {
+ return { value: fieldName, label: fieldName };
+ })
+ });
+ });
+
+ fields.sort((a, b) => {
+ if (a.label < b.label) return -1;
+ if (a.label > b.label) return 1;
+ return 0;
+ });
+
+ this.setState({
+ isLoading: false,
+ fields: fields
+ });
+ }, 300);
+
+ onChange = (selectedOptions) => {
+ this.props.onChange(_.get(selectedOptions, '0.value'));
}
render() {
@@ -63,19 +105,25 @@ export class FieldSelect extends Component {
}
const selectId = `fieldSelect-${this.props.controlIndex}`;
+
+ const selectedOptions = [];
+ if (this.props.fieldName) {
+ selectedOptions.push({ value: this.props.fieldName, label: this.props.fieldName });
+ }
+
return (
-
);
@@ -86,7 +134,7 @@ FieldSelect.propTypes = {
getIndexPattern: PropTypes.func.isRequired,
indexPatternId: PropTypes.string,
onChange: PropTypes.func.isRequired,
- value: PropTypes.string,
+ fieldName: PropTypes.string,
filterField: PropTypes.func,
controlIndex: PropTypes.number.isRequired,
};
diff --git a/src/core_plugins/input_control_vis/public/components/editor/index_pattern_select.js b/src/core_plugins/input_control_vis/public/components/editor/index_pattern_select.js
index 668ab56a2cf9..c56518b3b758 100644
--- a/src/core_plugins/input_control_vis/public/components/editor/index_pattern_select.js
+++ b/src/core_plugins/input_control_vis/public/components/editor/index_pattern_select.js
@@ -1,45 +1,123 @@
+import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import Select from 'react-select';
import {
EuiFormRow,
+ EuiComboBox,
} from '@elastic/eui';
export class IndexPatternSelect extends Component {
constructor(props) {
super(props);
- this.loadOptions = this.loadOptions.bind(this);
+ this.state = {
+ isLoading: false,
+ options: [],
+ selectedIndexPattern: undefined,
+ };
}
- loadOptions(input, callback) {
- this.props.getIndexPatterns(input).then((indexPatternSavedObjects) => {
+ componentWillMount() {
+ this._isMounted = true;
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ this.debouncedFetch.cancel();
+ }
+
+ componentDidMount() {
+ this.fetchOptions();
+ this.fetchSelectedIndexPattern(this.props.indexPatternId);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.indexPatternId !== this.props.indexPatternId) {
+ this.fetchSelectedIndexPattern(nextProps.indexPatternId);
+ }
+ }
+
+ fetchSelectedIndexPattern = async (indexPatternId) => {
+ if (!indexPatternId) {
+ this.setState({
+ selectedIndexPattern: undefined
+ });
+ return;
+ }
+
+ const indexPattern = await this.props.getIndexPattern(indexPatternId);
+
+ if (!this._isMounted) {
+ return;
+ }
+
+ if (!indexPattern) {
+ return;
+ }
+
+ this.setState({
+ selectedIndexPattern: {
+ value: indexPattern.id,
+ label: indexPattern.title,
+ }
+ });
+ }
+
+ debouncedFetch = _.debounce(async (searchValue) => {
+ const indexPatternSavedObjects = await this.props.getIndexPatterns(searchValue);
+
+ if (!this._isMounted) {
+ return;
+ }
+
+ // We need this check to handle the case where search results come back in a different
+ // order than they were sent out. Only load results for the most recent search.
+ if (searchValue === this.state.searchValue) {
const options = indexPatternSavedObjects.map((indexPatternSavedObject) => {
return {
label: indexPatternSavedObject.attributes.title,
value: indexPatternSavedObject.id
};
});
- callback(null, { options: options });
- });
+ this.setState({
+ isLoading: false,
+ options,
+ });
+ }
+ }, 300);
+
+ fetchOptions = (searchValue = '') => {
+ this.setState({
+ isLoading: true,
+ searchValue
+ }, this.debouncedFetch.bind(null, searchValue));
+ }
+
+ onChange = (selectedOptions) => {
+ this.props.onChange(_.get(selectedOptions, '0.value'));
}
render() {
const selectId = `indexPatternSelect-${this.props.controlIndex}`;
+ const selectedOptions = [];
+ if (this.state.selectedIndexPattern) {
+ selectedOptions.push(this.state.selectedIndexPattern);
+ }
return (
-
);
@@ -48,7 +126,8 @@ export class IndexPatternSelect extends Component {
IndexPatternSelect.propTypes = {
getIndexPatterns: PropTypes.func.isRequired,
+ getIndexPattern: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
- value: PropTypes.string,
+ indexPatternId: PropTypes.string,
controlIndex: PropTypes.number.isRequired,
};
diff --git a/src/core_plugins/input_control_vis/public/components/editor/list_control_editor.js b/src/core_plugins/input_control_vis/public/components/editor/list_control_editor.js
index 14c9f2726dfa..d490d8d47d41 100644
--- a/src/core_plugins/input_control_vis/public/components/editor/list_control_editor.js
+++ b/src/core_plugins/input_control_vis/public/components/editor/list_control_editor.js
@@ -52,14 +52,15 @@ export function ListControlEditor(props) {
{
- return Promise.resolve([
- {
- id: 'indexPattern1',
- attributes: {
- title: 'indexPattern1'
- }
- },
- {
- id: 'indexPattern2',
- attributes: {
- title: 'indexPattern2'
- }
- }
- ]);
-};
-const getIndexPattern = () => {
- return Promise.resolve({
- fields: [
- { name: 'keywordField', type: 'string', aggregatable: true },
- { name: 'textField', type: 'string', aggregatable: false },
- { name: 'numberField', type: 'number', aggregatable: true }
- ]
- });
-};
const controlParams = {
id: '1',
indexPattern: 'indexPattern1',
@@ -58,8 +35,8 @@ beforeEach(() => {
test('renders ListControlEditor', () => {
const component = shallow( {
{ value: '2', text: 'fieldB' }
];
const component = shallow( {
test('handleCheckboxOptionChange - multiselect', () => {
const component = mount( {
test('handleNumberOptionChange - size', () => {
const component = mount(
{
- return Promise.resolve([
- {
- id: 'indexPattern1',
- attributes: {
- title: 'indexPattern1'
- }
- },
- {
- id: 'indexPattern2',
- attributes: {
- title: 'indexPattern2'
- }
- }
- ]);
-};
-const getIndexPattern = () => {
- return Promise.resolve({
- fields: [
- { name: 'keywordField', type: 'string', aggregatable: true },
- { name: 'textField', type: 'string', aggregatable: false },
- { name: 'numberField', type: 'number', aggregatable: true }
- ]
- });
-};
const controlParams = {
id: '1',
indexPattern: 'indexPattern1',
@@ -55,8 +32,8 @@ beforeEach(() => {
test('renders RangeControlEditor', () => {
const component = shallow( {
test('handleNumberOptionChange - step', () => {
const component = mount( {
test('handleNumberOptionChange - decimalPlaces', () => {
const component = mount(
@@ -158,31 +151,24 @@ exports[`Clear btns enabled when there are values 1`] = `
}
>
@@ -291,31 +277,24 @@ exports[`Renders list control 1`] = `
}
>
diff --git a/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.js.snap b/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.js.snap
index 2f7a39c8b5b0..13844a1b6b47 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.js.snap
+++ b/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.js.snap
@@ -2,98 +2,31 @@
exports[`renders ListControl 1`] = `
-
`;
diff --git a/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap b/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap
index 644f2285bb2d..24fd9d46902f 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap
+++ b/src/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap
@@ -2,26 +2,8 @@
exports[`renders RangeControl 1`] = `
diff --git a/src/core_plugins/input_control_vis/public/components/vis/form_row.js b/src/core_plugins/input_control_vis/public/components/vis/form_row.js
index 126483b09a2d..4e6bb54645c7 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/form_row.js
+++ b/src/core_plugins/input_control_vis/public/components/vis/form_row.js
@@ -8,9 +8,9 @@ import {
export function FormRow(props) {
let control = props.children;
- if (!props.control.isEnabled()) {
+ if (props.disableMsg) {
control = (
-
+
{control}
);
@@ -32,5 +32,5 @@ FormRow.propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
controlIndex: PropTypes.number.isRequired,
- control: PropTypes.object.isRequired,
+ disableMsg: PropTypes.string,
};
diff --git a/src/core_plugins/input_control_vis/public/components/vis/form_row.test.js b/src/core_plugins/input_control_vis/public/components/vis/form_row.test.js
index cd1439dbbcbe..28d5ec8edeec 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/form_row.test.js
+++ b/src/core_plugins/input_control_vis/public/components/vis/form_row.test.js
@@ -6,15 +6,10 @@ import {
} from './form_row';
test('renders enabled control', () => {
- const enabledControl = {
- id: 'mock-enabled-control',
- isEnabled: () => { return true; },
- };
const component = shallow(
My Control
@@ -24,16 +19,11 @@ test('renders enabled control', () => {
});
test('renders disabled control with tooltip', () => {
- const disabledControl = {
- id: 'mock-disabled-control',
- isEnabled: () => { return false; },
- disabledReason: 'I am disabled for testing purposes'
- };
const component = shallow(
My Control
diff --git a/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.js b/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.js
index be5d3921a0b2..044e17dd5135 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.js
+++ b/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.js
@@ -38,7 +38,12 @@ export class InputControlVis extends Component {
case 'list':
controlComponent = (
diff --git a/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js b/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js
index 5eb22ba5a98f..e421ac528ffa 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js
+++ b/src/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js
@@ -16,8 +16,7 @@ const mockListControl = {
},
type: 'list',
label: 'list control',
- value: '',
- getMultiSelectDelimiter: () => { return ','; },
+ value: [],
selectOptions: [
{ label: 'choice1', value: 'choice1' },
{ label: 'choice2', value: 'choice2' }
@@ -159,28 +158,3 @@ test('resetControls', () => {
sinon.assert.notCalled(submitFilters);
sinon.assert.notCalled(stageFilter);
});
-
-test('stageFilter list control', () => {
- const component = mount( { return true; }}
- hasValues={() => { return true; }}
- />);
- const reactSelectInput = component.find(`#${mockListControl.id}`).hostNodes();
- reactSelectInput.simulate('change', { target: { value: 'choice1' } });
- reactSelectInput.simulate('keyDown', { keyCode: 9, key: 'Tab' });
- sinon.assert.notCalled(clearControls);
- sinon.assert.notCalled(submitFilters);
- sinon.assert.notCalled(resetControls);
- const expectedControlIndex = 0;
- const expectedControlValue = 'choice1';
- sinon.assert.calledWith(stageFilter,
- expectedControlIndex,
- expectedControlValue
- );
-});
diff --git a/src/core_plugins/input_control_vis/public/components/vis/list_control.js b/src/core_plugins/input_control_vis/public/components/vis/list_control.js
index 506d9faa332b..8caa4c60bf83 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/list_control.js
+++ b/src/core_plugins/input_control_vis/public/components/vis/list_control.js
@@ -1,57 +1,44 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import Select from 'react-select';
import { FormRow } from './form_row';
import {
EuiFieldText,
+ EuiComboBox,
} from '@elastic/eui';
export class ListControl extends Component {
- constructor(props) {
- super(props);
- this.handleOnChange = this.handleOnChange.bind(this);
- this.truncate = this.truncate.bind(this);
- }
-
- handleOnChange(evt) {
- let newValue = '';
- if (evt) {
- newValue = evt;
- }
- this.props.stageFilter(this.props.controlIndex, newValue);
- }
-
- truncate(selected) {
- if (selected.label.length <= 24) {
- return selected.label;
- }
- return `${selected.label.substring(0, 23)}...`;
+ handleOnChange = (selectedOptions) => {
+ this.props.stageFilter(this.props.controlIndex, selectedOptions);
}
renderControl() {
- if (!this.props.control.isEnabled()) {
- // react-select clobbers the tooltip, so just returning a disabled input instead
+ if (this.props.disableMsg) {
return (
);
}
+ const options = this.props.options.map(option => {
+ return {
+ label: option.label,
+ value: option.value,
+ ['data-test-subj']: `option_${option.value.replace(' ', '_')}`
+ };
+ });
+
return (
-
);
}
@@ -59,10 +46,10 @@ export class ListControl extends Component {
render() {
return (
{this.renderControl()}
@@ -70,8 +57,18 @@ export class ListControl extends Component {
}
}
+const comboBoxOptionShape = PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+});
+
ListControl.propTypes = {
- control: PropTypes.object.isRequired,
+ id: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ selectedOptions: PropTypes.arrayOf(comboBoxOptionShape).isRequired,
+ options: PropTypes.arrayOf(comboBoxOptionShape).isRequired,
+ disableMsg: PropTypes.string,
+ multiselect: PropTypes.bool.isRequired,
controlIndex: PropTypes.number.isRequired,
stageFilter: PropTypes.func.isRequired
};
diff --git a/src/core_plugins/input_control_vis/public/components/vis/list_control.test.js b/src/core_plugins/input_control_vis/public/components/vis/list_control.test.js
index c853693da484..e5ccfde403a8 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/list_control.test.js
+++ b/src/core_plugins/input_control_vis/public/components/vis/list_control.test.js
@@ -6,22 +6,11 @@ import {
ListControl,
} from './list_control';
-const control = {
- id: 'mock-list-control',
- isEnabled: () => { return true; },
- options: {
- type: 'terms',
- multiselect: true
- },
- type: 'list',
- label: 'list control',
- getMultiSelectDelimiter: () => { return ','; },
- value: '',
- selectOptions: [
- { label: 'choice1', value: 'choice1' },
- { label: 'choice2', value: 'choice2' }
- ]
-};
+const options = [
+ { label: 'choice1', value: 'choice1' },
+ { label: 'choice2', value: 'choice2' }
+];
+
let stageFilter;
beforeEach(() => {
@@ -30,7 +19,11 @@ beforeEach(() => {
test('renders ListControl', () => {
const component = shallow();
diff --git a/src/core_plugins/input_control_vis/public/components/vis/range_control.js b/src/core_plugins/input_control_vis/public/components/vis/range_control.js
index 5de1f82c9b80..dd7cea3ba501 100644
--- a/src/core_plugins/input_control_vis/public/components/vis/range_control.js
+++ b/src/core_plugins/input_control_vis/public/components/vis/range_control.js
@@ -159,7 +159,7 @@ export class RangeControl extends Component {
id={this.props.control.id}
label={this.props.control.label}
controlIndex={this.props.controlIndex}
- control={this.props.control}
+ disableMsg={this.props.control.isEnabled() ? null : this.props.control.disabledReason}
>
{this.renderControl()}
diff --git a/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js b/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js
index 9e6b7099e107..493c93f6a712 100644
--- a/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js
+++ b/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js
@@ -3,25 +3,23 @@ import { FilterManager } from './filter_manager.js';
import { buildPhraseFilter } from 'ui/filter_manager/lib/phrase';
import { buildPhrasesFilter } from 'ui/filter_manager/lib/phrases';
-const EMPTY_VALUE = '';
-
export class PhraseFilterManager extends FilterManager {
- constructor(controlId, fieldName, indexPattern, queryFilter, delimiter) {
- super(controlId, fieldName, indexPattern, queryFilter, EMPTY_VALUE);
-
- this.delimiter = delimiter;
+ constructor(controlId, fieldName, indexPattern, queryFilter) {
+ super(controlId, fieldName, indexPattern, queryFilter, []);
}
/**
* Convert phrases into filter
*
- * @param {string} react-select value (delimiter-separated string of values)
+ * @param {array}
* @return {object} query filter
* single phrase: match query
* multiple phrases: bool query with should containing list of match_phrase queries
*/
- createFilter(value) {
- const phrases = value.split(this.delimiter);
+ createFilter(selectedOptions) {
+ const phrases = selectedOptions.map(phrase => {
+ return phrase.value;
+ });
let newFilter;
if (phrases.length === 1) {
newFilter = buildPhraseFilter(
@@ -43,14 +41,25 @@ export class PhraseFilterManager extends FilterManager {
if (kbnFilters.length === 0) {
return this.getUnsetValue();
} else {
- const values = kbnFilters
+ return kbnFilters
.map((kbnFilter) => {
return this._getValueFromFilter(kbnFilter);
+ })
+ .reduce((accumulator, currentValue) => {
+ return accumulator.concat(currentValue);
+ }, [])
+ .map(value => {
+ return { value, label: value };
});
- return values.join(this.delimiter);
}
}
+ /**
+ * Extract filtering value from kibana filters
+ *
+ * @param {object} kbnFilter
+ * @return {Array.} array of values pulled from filter
+ */
_getValueFromFilter(kbnFilter) {
// bool filter - multiple phrase filters
if (_.has(kbnFilter, 'query.bool.should')) {
@@ -63,8 +72,7 @@ export class PhraseFilterManager extends FilterManager {
return true;
}
return false;
- })
- .join(this.delimiter);
+ });
}
// scripted field filter
diff --git a/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js b/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js
index 79bc747275dd..392192d8c605 100644
--- a/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js
+++ b/src/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js
@@ -24,11 +24,11 @@ describe('PhraseFilterManager', function () {
const queryFilterMock = {};
let filterManager;
beforeEach(() => {
- filterManager = new PhraseFilterManager(controlId, 'field1', indexPatternMock, queryFilterMock, '|');
+ filterManager = new PhraseFilterManager(controlId, 'field1', indexPatternMock, queryFilterMock);
});
test('should create match phrase filter from single value', function () {
- const newFilter = filterManager.createFilter('ios');
+ const newFilter = filterManager.createFilter([{ value: 'ios' }]);
expect(newFilter).to.have.property('meta');
expect(newFilter.meta.index).to.be(indexPatternId);
expect(newFilter.meta.controlledBy).to.be(controlId);
@@ -37,7 +37,7 @@ describe('PhraseFilterManager', function () {
});
test('should create bool filter from multiple values', function () {
- const newFilter = filterManager.createFilter('ios|win xp');
+ const newFilter = filterManager.createFilter([{ value: 'ios' }, { value: 'win xp' }]);
expect(newFilter).to.have.property('meta');
expect(newFilter.meta.index).to.be(indexPatternId);
expect(newFilter.meta.controlledBy).to.be(controlId);
@@ -67,7 +67,7 @@ describe('PhraseFilterManager', function () {
this.mockFilters = mockFilters;
}
}
- filterManager = new MockFindFiltersPhraseFilterManager(controlId, 'field1', indexPatternMock, queryFilterMock, '|');
+ filterManager = new MockFindFiltersPhraseFilterManager(controlId, 'field1', indexPatternMock, queryFilterMock);
});
test('should extract value from match phrase filter', function () {
@@ -83,7 +83,33 @@ describe('PhraseFilterManager', function () {
}
}
]);
- expect(filterManager.getValueFromFilterBar()).to.be('ios');
+ expect(filterManager.getValueFromFilterBar()).to.eql([{ value: 'ios', label: 'ios' }]);
+ });
+
+ test('should extract value from multiple filters', function () {
+ filterManager.setMockFilters([
+ {
+ query: {
+ match: {
+ field1: {
+ query: 'ios',
+ type: 'phrase'
+ }
+ }
+ }
+ },
+ {
+ query: {
+ match: {
+ field1: {
+ query: 'win xp',
+ type: 'phrase'
+ }
+ }
+ }
+ },
+ ]);
+ expect(filterManager.getValueFromFilterBar()).to.eql([{ value: 'ios', label: 'ios' }, { value: 'win xp', label: 'win xp' }]);
});
test('should extract value from bool filter', function () {
@@ -107,7 +133,7 @@ describe('PhraseFilterManager', function () {
}
}
]);
- expect(filterManager.getValueFromFilterBar()).to.be('ios|win xp');
+ expect(filterManager.getValueFromFilterBar()).to.eql([{ value: 'ios', label: 'ios' }, { value: 'win xp', label: 'win xp' }]);
});
});
diff --git a/src/core_plugins/input_control_vis/public/control/list_control_factory.js b/src/core_plugins/input_control_vis/public/control/list_control_factory.js
index 40e09a87cd7b..a1c062664137 100644
--- a/src/core_plugins/input_control_vis/public/control/list_control_factory.js
+++ b/src/core_plugins/input_control_vis/public/control/list_control_factory.js
@@ -32,14 +32,8 @@ const termsAgg = (field, size, direction) => {
};
};
-const listControlDelimiter = '$$kbn_delimiter$$';
-
class ListControl extends Control {
- getMultiSelectDelimiter() {
- return this.filterManager.delimiter;
- }
-
async fetch() {
let ancestorFilters;
if (this.hasAncestors()) {
@@ -99,7 +93,7 @@ export async function listControlFactory(controlParams, kbnApi, useTimeFilter) {
return new ListControl(
controlParams,
- new PhraseFilterManager(controlParams.id, controlParams.fieldName, indexPattern, kbnApi.queryFilter, listControlDelimiter),
+ new PhraseFilterManager(controlParams.id, controlParams.fieldName, indexPattern, kbnApi.queryFilter),
kbnApi,
useTimeFilter
);
diff --git a/src/core_plugins/input_control_vis/public/vis.less b/src/core_plugins/input_control_vis/public/vis.less
index e917712484f8..f310248cd6ec 100644
--- a/src/core_plugins/input_control_vis/public/vis.less
+++ b/src/core_plugins/input_control_vis/public/vis.less
@@ -32,11 +32,3 @@
left: 0% !important;
}
}
-
-visualization.input_control_vis {
- overflow: visible;
-
- .vis-container {
- overflow: visible;
- }
-}
diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less
index caf67121b1cd..e7bea8b77f29 100644
--- a/src/ui/public/styles/base.less
+++ b/src/ui/public/styles/base.less
@@ -705,4 +705,10 @@ style-compile {
display: block;
}
+.euiComboBox {
+ input:focus {
+ box-shadow: none;
+ }
+}
+
@import "~dragula/dist/dragula.css";
diff --git a/test/functional/apps/visualize/_input_control_vis.js b/test/functional/apps/visualize/_input_control_vis.js
index aa0d36803117..f48a327545f0 100644
--- a/test/functional/apps/visualize/_input_control_vis.js
+++ b/test/functional/apps/visualize/_input_control_vis.js
@@ -16,9 +16,9 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.setAbsoluteRange('2017-01-01', '2017-01-02');
await PageObjects.visualize.clickVisEditorTab('controls');
await PageObjects.visualize.addInputControl();
- await PageObjects.visualize.setReactSelect('.index-pattern-react-select', 'logstash');
+ await PageObjects.visualize.setComboBox('indexPatternSelect-0', 'logstash');
await PageObjects.common.sleep(1000); // give time for index-pattern to be fetched
- await PageObjects.visualize.setReactSelect('.field-react-select', FIELD_NAME);
+ await PageObjects.visualize.setComboBox('fieldSelect-0', FIELD_NAME);
await PageObjects.visualize.clickGo();
await PageObjects.header.waitUntilLoadingHasFinished();
@@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }) {
describe('updateFiltersOnChange is false', () => {
it('should contain dropdown with terms aggregation results as options', async () => {
- const menu = await PageObjects.visualize.getReactSelectOptions('inputControl0');
+ const menu = await PageObjects.visualize.getComboBoxOptions('listControlSelect0');
expect(menu.trim().split('\n').join()).to.equal('ios,osx,win 7,win 8,win xp');
});
@@ -48,10 +48,10 @@ export default function ({ getService, getPageObjects }) {
});
it('should stage filter when item selected but not create filter pill', async () => {
- await PageObjects.visualize.setReactSelect('.list-control-react-select', 'ios');
+ await PageObjects.visualize.setComboBox('listControlSelect0', 'ios');
- const dropdownValue = await PageObjects.visualize.getReactSelectValue('.list-control-react-select');
- expect(dropdownValue.trim()).to.equal('ios');
+ const selectedOptions = await PageObjects.visualize.getComboBoxSelectedOptions('listControlSelect0');
+ expect(selectedOptions[0].trim()).to.equal('ios');
const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios');
expect(hasFilter).to.equal(false);
@@ -65,8 +65,8 @@ export default function ({ getService, getPageObjects }) {
});
it('should replace existing filter pill(s) when new item is selected', async () => {
- await PageObjects.visualize.clearReactSelect('.list-control-react-select');
- await PageObjects.visualize.setReactSelect('.list-control-react-select', 'osx');
+ await PageObjects.visualize.clearComboBox('listControlSelect0');
+ await PageObjects.visualize.setComboBox('listControlSelect0', 'osx');
await testSubjects.click('inputControlSubmitBtn');
const hasOldFilter = await filterBar.hasFilter(FIELD_NAME, 'ios');
@@ -79,18 +79,18 @@ export default function ({ getService, getPageObjects }) {
await filterBar.removeFilter(FIELD_NAME);
await PageObjects.common.sleep(500); // give time for filter to be removed and event handlers to fire
- const hasValue = await PageObjects.visualize.doesReactSelectHaveValue('.list-control-react-select');
+ const hasValue = await PageObjects.visualize.doesComboBoxHaveSelectedOptions('listControlSelect0');
expect(hasValue).to.equal(false);
});
it('should clear form when Clear button is clicked but not remove filter pill', async () => {
- await PageObjects.visualize.setReactSelect('.list-control-react-select', 'ios');
+ await PageObjects.visualize.setComboBox('listControlSelect0', 'ios');
await testSubjects.click('inputControlSubmitBtn');
const hasFilterBeforeClearBtnClicked = await filterBar.hasFilter(FIELD_NAME, 'ios');
expect(hasFilterBeforeClearBtnClicked).to.equal(true);
await testSubjects.click('inputControlClearBtn');
- const hasValue = await PageObjects.visualize.doesReactSelectHaveValue('.list-control-react-select');
+ const hasValue = await PageObjects.visualize.doesComboBoxHaveSelectedOptions('listControlSelect0');
expect(hasValue).to.equal(false);
const hasFilterAfterClearBtnClicked = await filterBar.hasFilter(FIELD_NAME, 'ios');
@@ -131,10 +131,10 @@ export default function ({ getService, getPageObjects }) {
});
it('should add filter pill when item selected', async () => {
- await PageObjects.visualize.setReactSelect('.list-control-react-select', 'ios');
+ await PageObjects.visualize.setComboBox('listControlSelect0', 'ios');
- const dropdownValue = await PageObjects.visualize.getReactSelectValue('.list-control-react-select');
- expect(dropdownValue.trim()).to.equal('ios');
+ const selectedOptions = await PageObjects.visualize.getComboBoxSelectedOptions('listControlSelect0');
+ expect(selectedOptions[0].trim()).to.equal('ios');
const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios');
expect(hasFilter).to.equal(true);
@@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
// Expect control to have values for selected time filter
- const menu = await PageObjects.visualize.getReactSelectOptions('inputControl0');
+ const menu = await PageObjects.visualize.getComboBoxOptions('listControlSelect0');
expect(menu.trim().split('\n').join()).to.equal('osx,win 7,win 8,win xp');
});
});
@@ -172,14 +172,14 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visualize.clickVisEditorTab('controls');
await PageObjects.visualize.addInputControl();
- await PageObjects.visualize.setReactSelect('#indexPatternSelect-0-row', 'logstash');
+ await PageObjects.visualize.setComboBox('indexPatternSelect-0', 'logstash');
await PageObjects.common.sleep(1000); // give time for index-pattern to be fetched
- await PageObjects.visualize.setReactSelect('#fieldSelect-0-row', 'geo.src');
+ await PageObjects.visualize.setComboBox('fieldSelect-0', 'geo.src');
await PageObjects.visualize.addInputControl();
- await PageObjects.visualize.setReactSelect('#indexPatternSelect-1-row', 'logstash');
+ await PageObjects.visualize.setComboBox('indexPatternSelect-1', 'logstash');
await PageObjects.common.sleep(1000); // give time for index-pattern to be fetched
- await PageObjects.visualize.setReactSelect('#fieldSelect-1-row', 'clientip');
+ await PageObjects.visualize.setComboBox('fieldSelect-1', 'clientip');
await PageObjects.visualize.setSelectByOptionText('parentSelect-1', 'geo.src');
await PageObjects.visualize.clickGo();
@@ -187,7 +187,7 @@ export default function ({ getService, getPageObjects }) {
});
it('should disable child control when parent control is not set', async () => {
- const parentControlMenu = await PageObjects.visualize.getReactSelectOptions('inputControl0');
+ const parentControlMenu = await PageObjects.visualize.getComboBoxOptions('listControlSelect0');
expect(parentControlMenu.trim().split('\n').join()).to.equal('BR,CN,ID,IN,US');
const childControlInput = await find.byCssSelector('[data-test-subj="inputControl1"] input');
@@ -196,14 +196,14 @@ export default function ({ getService, getPageObjects }) {
});
it('should filter child control options by parent control value', async () => {
- await PageObjects.visualize.setReactSelect('[data-test-subj="inputControl0"]', 'BR');
+ await PageObjects.visualize.setComboBox('listControlSelect0', 'BR');
- const childControlMenu = await PageObjects.visualize.getReactSelectOptions('inputControl1');
+ const childControlMenu = await PageObjects.visualize.getComboBoxOptions('listControlSelect1');
expect(childControlMenu.trim().split('\n').join()).to.equal('14.61.182.136,3.174.21.181,6.183.121.70,71.241.97.89,9.69.255.135');
});
it('should create a seperate filter pill for parent control and child control', async () => {
- await PageObjects.visualize.setReactSelect('[data-test-subj="inputControl1"]', '14.61.182.136');
+ await PageObjects.visualize.setComboBox('listControlSelect1', '14.61.182.136');
await testSubjects.click('inputControlSubmitBtn');
@@ -215,7 +215,7 @@ export default function ({ getService, getPageObjects }) {
});
it('should clear child control dropdown when parent control value is removed', async () => {
- await PageObjects.visualize.clearReactSelect('[data-test-subj="inputControl0"]');
+ await PageObjects.visualize.clearComboBox('listControlSelect0');
await PageObjects.common.sleep(500); // give time for filter to be removed and event handlers to fire
const childControlInput = await find.byCssSelector('[data-test-subj="inputControl1"] input');
@@ -229,7 +229,7 @@ export default function ({ getService, getPageObjects }) {
await filterBar.removeFilter('geo.src');
await PageObjects.common.sleep(500); // give time for filter to be removed and event handlers to fire
- const hasValue = await PageObjects.visualize.doesReactSelectHaveValue('[data-test-subj="inputControl1"]');
+ const hasValue = await PageObjects.visualize.doesComboBoxHaveSelectedOptions('listControlSelect0');
expect(hasValue).to.equal(false);
});
});
diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js
index 9beae9a0c7ee..f04610508a2b 100644
--- a/test/functional/page_objects/visualize_page.js
+++ b/test/functional/page_objects/visualize_page.js
@@ -194,41 +194,57 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
await input.type(timeString);
}
- async setReactSelect(className, value) {
- const input = await find.byCssSelector(className + ' * input', 0);
+ async setComboBox(comboBoxSelector, value) {
+ const comboBox = await testSubjects.find(comboBoxSelector);
+ const input = await comboBox.findByTagName('input');
await input.clearValue();
await input.type(value);
- await find.clickByCssSelector('.Select-option');
- const stillOpen = await find.existsByCssSelector('.Select-menu-outer', 0);
- if (stillOpen) {
- await find.clickByCssSelector(className + ' * .Select-arrow-zone');
- }
+ await find.clickByCssSelector('.euiComboBoxOption');
+ await this.closeComboBoxOptionsList(comboBox);
+ await remote.pressKeys('\uE004');
}
- async clearReactSelect(className) {
- await find.clickByCssSelector(className + ' * .Select-clear-zone');
- }
-
- async getReactSelectOptions(containerSelector) {
- await testSubjects.click(containerSelector);
+ async getComboBoxOptions(comboBoxSelector) {
+ await testSubjects.click(comboBoxSelector);
const menu = await retry.try(
- async () => find.byCssSelector('.Select-menu-outer'));
- return await menu.getVisibleText();
+ async () => await testSubjects.find('comboBoxOptionsList'));
+ const optionsText = await menu.getVisibleText();
+ const comboBox = await testSubjects.find(comboBoxSelector);
+ await this.closeComboBoxOptionsList(comboBox);
+ return optionsText;
}
- async doesReactSelectHaveValue(className) {
- return await find.existsByCssSelector(className + ' * .Select-value-label', 0);
+ async doesComboBoxHaveSelectedOptions(comboBoxSelector) {
+ const comboBox = await testSubjects.find(comboBoxSelector);
+ const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill');
+ return selectedOptions > 0;
}
- async getReactSelectValue(className) {
- const hasValue = await this.doesReactSelectHaveValue(className);
- if (!hasValue) {
- return '';
+ async getComboBoxSelectedOptions(comboBoxSelector) {
+ const comboBox = await testSubjects.find(comboBoxSelector);
+ const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill');
+ if (selectedOptions.length === 0) {
+ return [];
}
- const valueElement = await retry.try(
- async () => find.byCssSelector(className + ' * .Select-value-label'));
- return await valueElement.getVisibleText();
+ const getOptionValuePromises = selectedOptions.map(async (optionElement) => {
+ return await optionElement.getVisibleText();
+ });
+ return await Promise.all(getOptionValuePromises);
+ }
+
+ async clearComboBox(comboBoxSelector) {
+ const comboBox = await testSubjects.find(comboBoxSelector);
+ const clearBtn = await comboBox.findByCssSelector('button.euiFormControlLayout__clear');
+ await clearBtn.click();
+ }
+
+ async closeComboBoxOptionsList(comboBoxElement) {
+ const isOptionsListOpen = await testSubjects.exists('comboBoxOptionsList');
+ if (isOptionsListOpen) {
+ const closeBtn = await comboBoxElement.findByCssSelector('button.euiFormControlLayout__icon');
+ await closeBtn.click();
+ }
}
async addInputControl() {