[Input controls] Replace react-select with EuiComboBox (#17452)

* replace react-select with EuiComboBox

* remove constructor for ListControl component

* get working with portal version

* remove overflow visible from input_control_vis since its no longer needed

* convert index pattern select to EuiComboBox

* replace react-select with EuiComboBox for field select

* group fields by type

* remove esvm

* remove on-foxus box around input cursor

* fix jest tests

* remove broken jest test

* fix functional tests

* review changes

* remove componentWillMount from field_select

* update snapshot changed from rebasing and getting new EUI version

* use combo box clear and close buttons for clearing and closing

* jsdoc syntax fix
This commit is contained in:
Nathan Reese 2018-05-08 13:48:12 -06:00 committed by GitHub
parent 147d208e2d
commit e3841ea7ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 474 additions and 472 deletions

View file

@ -4,17 +4,18 @@ exports[`parentCandidates 1`] = `
<div> <div>
<IndexPatternSelect <IndexPatternSelect
controlIndex={0} controlIndex={0}
getIndexPattern={[Function]}
getIndexPatterns={[Function]} getIndexPatterns={[Function]}
indexPatternId="indexPattern1"
onChange={[Function]} onChange={[Function]}
value="indexPattern1"
/> />
<FieldSelect <FieldSelect
controlIndex={0} controlIndex={0}
fieldName="keywordField"
filterField={[Function]} filterField={[Function]}
getIndexPattern={[Function]} getIndexPattern={[Function]}
indexPatternId="indexPattern1" indexPatternId="indexPattern1"
onChange={[Function]} onChange={[Function]}
value="keywordField"
/> />
<EuiFormRow <EuiFormRow
describedByIds={Array []} describedByIds={Array []}
@ -83,17 +84,18 @@ exports[`renders ListControlEditor 1`] = `
<div> <div>
<IndexPatternSelect <IndexPatternSelect
controlIndex={0} controlIndex={0}
getIndexPattern={[Function]}
getIndexPatterns={[Function]} getIndexPatterns={[Function]}
indexPatternId="indexPattern1"
onChange={[Function]} onChange={[Function]}
value="indexPattern1"
/> />
<FieldSelect <FieldSelect
controlIndex={0} controlIndex={0}
fieldName="keywordField"
filterField={[Function]} filterField={[Function]}
getIndexPattern={[Function]} getIndexPattern={[Function]}
indexPatternId="indexPattern1" indexPatternId="indexPattern1"
onChange={[Function]} onChange={[Function]}
value="keywordField"
/> />
<EuiFormRow <EuiFormRow
describedByIds={Array []} describedByIds={Array []}

View file

@ -4,17 +4,18 @@ exports[`renders RangeControlEditor 1`] = `
<div> <div>
<IndexPatternSelect <IndexPatternSelect
controlIndex={0} controlIndex={0}
getIndexPattern={[Function]}
getIndexPatterns={[Function]} getIndexPatterns={[Function]}
indexPatternId="indexPattern1"
onChange={[Function]} onChange={[Function]}
value="indexPattern1"
/> />
<FieldSelect <FieldSelect
controlIndex={0} controlIndex={0}
fieldName="numberField"
filterField={[Function]} filterField={[Function]}
getIndexPattern={[Function]} getIndexPattern={[Function]}
indexPatternId="indexPattern1" indexPatternId="indexPattern1"
onChange={[Function]} onChange={[Function]}
value="numberField"
/> />
<EuiFormRow <EuiFormRow
describedByIds={Array []} describedByIds={Array []}

View file

@ -0,0 +1,11 @@
export const getIndexPatternMock = () => {
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 }
]
});
};

View file

@ -0,0 +1,16 @@
export const getIndexPatternsMock = () => {
return Promise.resolve([
{
id: 'indexPattern1',
attributes: {
title: 'indexPattern1'
}
},
{
id: 'indexPattern2',
attributes: {
title: 'indexPattern2'
}
}
]);
};

View file

@ -48,16 +48,16 @@ export class ControlsTab extends Component {
this.setVisParam('controls', setControl(this.props.scope.vis.params.controls, controlIndex, updatedControl)); 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]; const updatedControl = this.props.scope.vis.params.controls[controlIndex];
updatedControl.indexPattern = evt.value; updatedControl.indexPattern = indexPatternId;
updatedControl.fieldName = ''; updatedControl.fieldName = '';
this.setVisParam('controls', setControl(this.props.scope.vis.params.controls, controlIndex, updatedControl)); 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]; 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)); this.setVisParam('controls', setControl(this.props.scope.vis.params.controls, controlIndex, updatedControl));
} }

View file

@ -2,6 +2,7 @@ import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test'; import { findTestSubject } from '@elastic/eui/lib/test';
import { getIndexPatternMock } from './__tests__/get_index_pattern_mock';
import { import {
ControlsTab, ControlsTab,
} from './controls_tab'; } from './controls_tab';
@ -21,14 +22,7 @@ const savedObjectsClientMock = {
} }
}; };
const indexPatternsMock = { const indexPatternsMock = {
get: () => { get: getIndexPatternMock
return Promise.resolve({
fields: [
{ name: 'keywordField', type: 'string', aggregatable: true },
{ name: 'numberField', type: 'number', aggregatable: true }
]
});
}
}; };
const scopeMock = { const scopeMock = {
vis: { vis: {

View file

@ -1,60 +1,102 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Select from 'react-select';
import { import {
EuiFormRow, EuiFormRow,
EuiComboBox,
} from '@elastic/eui'; } from '@elastic/eui';
export class FieldSelect extends Component { export class FieldSelect extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
// not storing activeIndexPatternId in react state this._hasUnmounted = false;
// 1) does not effect rendering
// 2) requires synchronous modification to avoid race condition
this.activeIndexPatternId = props.indexPatternId;
this.state = { this.state = {
fields: [] isLoading: false,
fields: [],
indexPatternId: props.indexPatternId,
}; };
this.filterField = _.get(props, 'filterField', () => { return true; }); this.filterField = _.get(props, 'filterField', () => { return true; });
this.loadFields(props.indexPatternId); }
componentWillUnmount() {
this._hasUnmounted = true;
}
componentDidMount() {
this.loadFields(this.state.indexPatternId);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.indexPatternId !== nextProps.indexPatternId) { if (this.props.indexPatternId !== nextProps.indexPatternId) {
this.activeIndexPatternId = nextProps.indexPatternId;
this.setState({ fields: [] });
this.loadFields(nextProps.indexPatternId); 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) { if (!indexPatternId || indexPatternId.length === 0) {
return; return;
} }
const indexPattern = await this.props.getIndexPattern(indexPatternId); const indexPattern = await this.props.getIndexPattern(indexPatternId);
// props.indexPatternId may be updated before getIndexPattern returns if (this._hasUnmounted) {
// ignore response when fetched index pattern does not match active index pattern
if (indexPattern.id !== this.activeIndexPatternId) {
return; 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) .filter(this.filterField)
.sort((a, b) => { .forEach(field => {
if (a.name < b.name) return -1; if (fieldsByTypeMap.has(field.type)) {
if (a.name > b.name) return 1; const fieldsList = fieldsByTypeMap.get(field.type);
return 0; fieldsList.push(field.name);
}) fieldsByTypeMap.set(field.type, fieldsList);
.map(function (field) { } else {
return { label: field.name, value: field.name }; 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() { render() {
@ -63,19 +105,25 @@ export class FieldSelect extends Component {
} }
const selectId = `fieldSelect-${this.props.controlIndex}`; const selectId = `fieldSelect-${this.props.controlIndex}`;
const selectedOptions = [];
if (this.props.fieldName) {
selectedOptions.push({ value: this.props.fieldName, label: this.props.fieldName });
}
return ( return (
<EuiFormRow <EuiFormRow
id={selectId} id={selectId}
label="Field" label="Field"
> >
<Select <EuiComboBox
className="field-react-select"
placeholder="Select field..." placeholder="Select field..."
value={this.props.value} singleSelection={true}
isLoading={this.state.isLoading}
options={this.state.fields} options={this.state.fields}
onChange={this.props.onChange} selectedOptions={selectedOptions}
resetValue={''} onChange={this.onChange}
inputProps={{ id: selectId }} data-test-subj={selectId}
/> />
</EuiFormRow> </EuiFormRow>
); );
@ -86,7 +134,7 @@ FieldSelect.propTypes = {
getIndexPattern: PropTypes.func.isRequired, getIndexPattern: PropTypes.func.isRequired,
indexPatternId: PropTypes.string, indexPatternId: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
value: PropTypes.string, fieldName: PropTypes.string,
filterField: PropTypes.func, filterField: PropTypes.func,
controlIndex: PropTypes.number.isRequired, controlIndex: PropTypes.number.isRequired,
}; };

View file

@ -1,45 +1,123 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Select from 'react-select';
import { import {
EuiFormRow, EuiFormRow,
EuiComboBox,
} from '@elastic/eui'; } from '@elastic/eui';
export class IndexPatternSelect extends Component { export class IndexPatternSelect extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.loadOptions = this.loadOptions.bind(this); this.state = {
isLoading: false,
options: [],
selectedIndexPattern: undefined,
};
} }
loadOptions(input, callback) { componentWillMount() {
this.props.getIndexPatterns(input).then((indexPatternSavedObjects) => { 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) => { const options = indexPatternSavedObjects.map((indexPatternSavedObject) => {
return { return {
label: indexPatternSavedObject.attributes.title, label: indexPatternSavedObject.attributes.title,
value: indexPatternSavedObject.id 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() { render() {
const selectId = `indexPatternSelect-${this.props.controlIndex}`; const selectId = `indexPatternSelect-${this.props.controlIndex}`;
const selectedOptions = [];
if (this.state.selectedIndexPattern) {
selectedOptions.push(this.state.selectedIndexPattern);
}
return ( return (
<EuiFormRow <EuiFormRow
id={selectId} id={selectId}
label="Index Pattern" label="Index Pattern"
> >
<Select.Async <EuiComboBox
className="index-pattern-react-select"
placeholder="Select index pattern..." placeholder="Select index pattern..."
value={this.props.value} singleSelection={true}
loadOptions={this.loadOptions} isLoading={this.state.isLoading}
onChange={this.props.onChange} onSearchChange={this.fetchOptions}
resetValue={''} options={this.state.options}
inputProps={{ id: selectId }} selectedOptions={selectedOptions}
onChange={this.onChange}
data-test-subj={selectId}
/> />
</EuiFormRow> </EuiFormRow>
); );
@ -48,7 +126,8 @@ export class IndexPatternSelect extends Component {
IndexPatternSelect.propTypes = { IndexPatternSelect.propTypes = {
getIndexPatterns: PropTypes.func.isRequired, getIndexPatterns: PropTypes.func.isRequired,
getIndexPattern: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
value: PropTypes.string, indexPatternId: PropTypes.string,
controlIndex: PropTypes.number.isRequired, controlIndex: PropTypes.number.isRequired,
}; };

View file

@ -52,14 +52,15 @@ export function ListControlEditor(props) {
<div> <div>
<IndexPatternSelect <IndexPatternSelect
value={props.controlParams.indexPattern} indexPatternId={props.controlParams.indexPattern}
onChange={props.handleIndexPatternChange} onChange={props.handleIndexPatternChange}
getIndexPatterns={props.getIndexPatterns} getIndexPatterns={props.getIndexPatterns}
getIndexPattern={props.getIndexPattern}
controlIndex={props.controlIndex} controlIndex={props.controlIndex}
/> />
<FieldSelect <FieldSelect
value={props.controlParams.fieldName} fieldName={props.controlParams.fieldName}
indexPatternId={props.controlParams.indexPattern} indexPatternId={props.controlParams.indexPattern}
filterField={filterField} filterField={filterField}
onChange={props.handleFieldNameChange} onChange={props.handleFieldNameChange}

View file

@ -2,36 +2,13 @@ import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test'; import { findTestSubject } from '@elastic/eui/lib/test';
import { getIndexPatternMock } from './__tests__/get_index_pattern_mock';
import { getIndexPatternsMock } from './__tests__/get_index_patterns_mock';
import { import {
ListControlEditor, ListControlEditor,
} from './list_control_editor'; } from './list_control_editor';
const getIndexPatterns = () => {
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 = { const controlParams = {
id: '1', id: '1',
indexPattern: 'indexPattern1', indexPattern: 'indexPattern1',
@ -58,8 +35,8 @@ beforeEach(() => {
test('renders ListControlEditor', () => { test('renders ListControlEditor', () => {
const component = shallow(<ListControlEditor const component = shallow(<ListControlEditor
getIndexPatterns={getIndexPatterns} getIndexPatterns={getIndexPatternsMock}
getIndexPattern={getIndexPattern} getIndexPattern={getIndexPatternMock}
controlIndex={0} controlIndex={0}
controlParams={controlParams} controlParams={controlParams}
handleFieldNameChange={handleFieldNameChange} handleFieldNameChange={handleFieldNameChange}
@ -78,8 +55,8 @@ test('parentCandidates', () => {
{ value: '2', text: 'fieldB' } { value: '2', text: 'fieldB' }
]; ];
const component = shallow(<ListControlEditor const component = shallow(<ListControlEditor
getIndexPatterns={getIndexPatterns} getIndexPatterns={getIndexPatternsMock}
getIndexPattern={getIndexPattern} getIndexPattern={getIndexPatternMock}
controlIndex={0} controlIndex={0}
controlParams={controlParams} controlParams={controlParams}
handleFieldNameChange={handleFieldNameChange} handleFieldNameChange={handleFieldNameChange}
@ -94,8 +71,8 @@ test('parentCandidates', () => {
test('handleCheckboxOptionChange - multiselect', () => { test('handleCheckboxOptionChange - multiselect', () => {
const component = mount(<ListControlEditor const component = mount(<ListControlEditor
getIndexPatterns={getIndexPatterns} getIndexPatterns={getIndexPatternsMock}
getIndexPattern={getIndexPattern} getIndexPattern={getIndexPatternMock}
controlIndex={0} controlIndex={0}
controlParams={controlParams} controlParams={controlParams}
handleFieldNameChange={handleFieldNameChange} handleFieldNameChange={handleFieldNameChange}
@ -126,8 +103,8 @@ test('handleCheckboxOptionChange - multiselect', () => {
test('handleNumberOptionChange - size', () => { test('handleNumberOptionChange - size', () => {
const component = mount(<ListControlEditor const component = mount(<ListControlEditor
getIndexPatterns={getIndexPatterns} getIndexPatterns={getIndexPatternsMock}
getIndexPattern={getIndexPattern} getIndexPattern={getIndexPatternMock}
controlIndex={0} controlIndex={0}
controlParams={controlParams} controlParams={controlParams}
handleFieldNameChange={handleFieldNameChange} handleFieldNameChange={handleFieldNameChange}

View file

@ -25,14 +25,15 @@ export function RangeControlEditor(props) {
<div> <div>
<IndexPatternSelect <IndexPatternSelect
value={props.controlParams.indexPattern} indexPatternId={props.controlParams.indexPattern}
onChange={props.handleIndexPatternChange} onChange={props.handleIndexPatternChange}
getIndexPatterns={props.getIndexPatterns} getIndexPatterns={props.getIndexPatterns}
getIndexPattern={props.getIndexPattern}
controlIndex={props.controlIndex} controlIndex={props.controlIndex}
/> />
<FieldSelect <FieldSelect
value={props.controlParams.fieldName} fieldName={props.controlParams.fieldName}
indexPatternId={props.controlParams.indexPattern} indexPatternId={props.controlParams.indexPattern}
filterField={filterField} filterField={filterField}
onChange={props.handleFieldNameChange} onChange={props.handleFieldNameChange}

View file

@ -2,36 +2,13 @@ import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test'; import { findTestSubject } from '@elastic/eui/lib/test';
import { getIndexPatternMock } from './__tests__/get_index_pattern_mock';
import { getIndexPatternsMock } from './__tests__/get_index_patterns_mock';
import { import {
RangeControlEditor, RangeControlEditor,
} from './range_control_editor'; } from './range_control_editor';
const getIndexPatterns = () => {
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 = { const controlParams = {
id: '1', id: '1',
indexPattern: 'indexPattern1', indexPattern: 'indexPattern1',
@ -55,8 +32,8 @@ beforeEach(() => {
test('renders RangeControlEditor', () => { test('renders RangeControlEditor', () => {
const component = shallow(<RangeControlEditor const component = shallow(<RangeControlEditor
getIndexPatterns={getIndexPatterns} getIndexPatterns={getIndexPatternsMock}
getIndexPattern={getIndexPattern} getIndexPattern={getIndexPatternMock}
controlIndex={0} controlIndex={0}
controlParams={controlParams} controlParams={controlParams}
handleFieldNameChange={handleFieldNameChange} handleFieldNameChange={handleFieldNameChange}
@ -68,8 +45,8 @@ test('renders RangeControlEditor', () => {
test('handleNumberOptionChange - step', () => { test('handleNumberOptionChange - step', () => {
const component = mount(<RangeControlEditor const component = mount(<RangeControlEditor
getIndexPatterns={getIndexPatterns} getIndexPatterns={getIndexPatternsMock}
getIndexPattern={getIndexPattern} getIndexPattern={getIndexPatternMock}
controlIndex={0} controlIndex={0}
controlParams={controlParams} controlParams={controlParams}
handleFieldNameChange={handleFieldNameChange} handleFieldNameChange={handleFieldNameChange}
@ -95,8 +72,8 @@ test('handleNumberOptionChange - step', () => {
test('handleNumberOptionChange - decimalPlaces', () => { test('handleNumberOptionChange - decimalPlaces', () => {
const component = mount(<RangeControlEditor const component = mount(<RangeControlEditor
getIndexPatterns={getIndexPatterns} getIndexPatterns={getIndexPatternsMock}
getIndexPattern={getIndexPattern} getIndexPattern={getIndexPatternMock}
controlIndex={0} controlIndex={0}
controlParams={controlParams} controlParams={controlParams}
handleFieldNameChange={handleFieldNameChange} handleFieldNameChange={handleFieldNameChange}

View file

@ -25,31 +25,24 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = `
} }
> >
<ListControl <ListControl
control={
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
"type": "terms",
},
"selectOptions": Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
],
"type": "list",
"value": "",
}
}
controlIndex={0} controlIndex={0}
disableMsg={null}
id="mock-list-control"
label="list control"
multiselect={true}
options={
Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
]
}
selectedOptions={Array []}
stageFilter={[Function]} stageFilter={[Function]}
/> />
</EuiFlexItem> </EuiFlexItem>
@ -158,31 +151,24 @@ exports[`Clear btns enabled when there are values 1`] = `
} }
> >
<ListControl <ListControl
control={
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
"type": "terms",
},
"selectOptions": Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
],
"type": "list",
"value": "",
}
}
controlIndex={0} controlIndex={0}
disableMsg={null}
id="mock-list-control"
label="list control"
multiselect={true}
options={
Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
]
}
selectedOptions={Array []}
stageFilter={[Function]} stageFilter={[Function]}
/> />
</EuiFlexItem> </EuiFlexItem>
@ -291,31 +277,24 @@ exports[`Renders list control 1`] = `
} }
> >
<ListControl <ListControl
control={
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
"type": "terms",
},
"selectOptions": Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
],
"type": "list",
"value": "",
}
}
controlIndex={0} controlIndex={0}
disableMsg={null}
id="mock-list-control"
label="list control"
multiselect={true}
options={
Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
]
}
selectedOptions={Array []}
stageFilter={[Function]} stageFilter={[Function]}
/> />
</EuiFlexItem> </EuiFlexItem>

View file

@ -2,98 +2,31 @@
exports[`renders ListControl 1`] = ` exports[`renders ListControl 1`] = `
<FormRow <FormRow
control={
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
"type": "terms",
},
"selectOptions": Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
],
"type": "list",
"value": "",
}
}
controlIndex={0} controlIndex={0}
id="mock-list-control" id="mock-list-control"
label="list control" label="list control"
> >
<Select <EuiComboBox
arrowRenderer={[Function]} data-test-subj="listControlSelect0"
autosize={true} isClearable={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="list-control-react-select"
clearAllText="Clear all"
clearRenderer={[Function]}
clearValueText="Clear value"
clearable={true}
closeOnSelect={true}
deleteRemoves={true}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={
Object {
"id": "mock-list-control",
}
}
isLoading={false}
joinValues={false}
labelKey="label"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={true}
noResultsText="No results found"
onBlurResetsInput={true}
onChange={[Function]} onChange={[Function]}
onCloseResetsInput={true}
onSelectResetsInput={true}
openOnClick={true}
optionComponent={[Function]}
options={ options={
Array [ Array [
Object { Object {
"data-test-subj": "option_choice1",
"label": "choice1", "label": "choice1",
"value": "choice1", "value": "choice1",
}, },
Object { Object {
"data-test-subj": "option_choice2",
"label": "choice2", "label": "choice2",
"value": "choice2", "value": "choice2",
}, },
] ]
} }
pageSize={5}
placeholder="Select..." placeholder="Select..."
removeSelected={true} selectedOptions={Array []}
required={false} singleSelection={false}
rtl={false}
scrollMenuIntoView={true}
searchable={true}
simpleValue={true}
tabSelectsValue={true}
trimFilter={true}
value=""
valueComponent={[Function]}
valueKey="value"
valueRenderer={[Function]}
/> />
</FormRow> </FormRow>
`; `;

View file

@ -2,26 +2,8 @@
exports[`renders RangeControl 1`] = ` exports[`renders RangeControl 1`] = `
<FormRow <FormRow
control={
Object {
"hasValue": [Function],
"id": "mock-range-control",
"isEnabled": [Function],
"label": "range control",
"max": 100,
"min": 0,
"options": Object {
"decimalPlaces": 0,
"step": 1,
},
"type": "range",
"value": Object {
"max": 0,
"min": 0,
},
}
}
controlIndex={0} controlIndex={0}
disableMsg={null}
id="mock-range-control" id="mock-range-control"
label="range control" label="range control"
> >

View file

@ -8,9 +8,9 @@ import {
export function FormRow(props) { export function FormRow(props) {
let control = props.children; let control = props.children;
if (!props.control.isEnabled()) { if (props.disableMsg) {
control = ( control = (
<EuiToolTip placement="top" content={props.control.disabledReason}> <EuiToolTip placement="top" content={props.disableMsg}>
{control} {control}
</EuiToolTip> </EuiToolTip>
); );
@ -32,5 +32,5 @@ FormRow.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
controlIndex: PropTypes.number.isRequired, controlIndex: PropTypes.number.isRequired,
control: PropTypes.object.isRequired, disableMsg: PropTypes.string,
}; };

View file

@ -6,15 +6,10 @@ import {
} from './form_row'; } from './form_row';
test('renders enabled control', () => { test('renders enabled control', () => {
const enabledControl = {
id: 'mock-enabled-control',
isEnabled: () => { return true; },
};
const component = shallow( const component = shallow(
<FormRow <FormRow
label="test control" label="test control"
id="controlId" id="controlId"
control={enabledControl}
controlIndex={0} controlIndex={0}
> >
<div>My Control</div> <div>My Control</div>
@ -24,16 +19,11 @@ test('renders enabled control', () => {
}); });
test('renders disabled control with tooltip', () => { 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( const component = shallow(
<FormRow <FormRow
label="test control" label="test control"
id="controlId" id="controlId"
control={disabledControl} disableMsg="I am disabled for testing purposes"
controlIndex={0} controlIndex={0}
> >
<div>My Control</div> <div>My Control</div>

View file

@ -38,7 +38,12 @@ export class InputControlVis extends Component {
case 'list': case 'list':
controlComponent = ( controlComponent = (
<ListControl <ListControl
control={control} id={control.id}
label={control.label}
options={control.selectOptions}
selectedOptions={control.value}
disableMsg={control.isEnabled() ? null : control.disabledReason}
multiselect={control.options.multiselect}
controlIndex={index} controlIndex={index}
stageFilter={this.props.stageFilter} stageFilter={this.props.stageFilter}
/> />

View file

@ -16,8 +16,7 @@ const mockListControl = {
}, },
type: 'list', type: 'list',
label: 'list control', label: 'list control',
value: '', value: [],
getMultiSelectDelimiter: () => { return ','; },
selectOptions: [ selectOptions: [
{ label: 'choice1', value: 'choice1' }, { label: 'choice1', value: 'choice1' },
{ label: 'choice2', value: 'choice2' } { label: 'choice2', value: 'choice2' }
@ -159,28 +158,3 @@ test('resetControls', () => {
sinon.assert.notCalled(submitFilters); sinon.assert.notCalled(submitFilters);
sinon.assert.notCalled(stageFilter); sinon.assert.notCalled(stageFilter);
}); });
test('stageFilter list control', () => {
const component = mount(<InputControlVis
stageFilter={stageFilter}
submitFilters={submitFilters}
resetControls={resetControls}
clearControls={clearControls}
controls={[mockListControl]}
updateFiltersOnChange={updateFiltersOnChange}
hasChanges={() => { 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
);
});

View file

@ -1,57 +1,44 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Select from 'react-select';
import { FormRow } from './form_row'; import { FormRow } from './form_row';
import { import {
EuiFieldText, EuiFieldText,
EuiComboBox,
} from '@elastic/eui'; } from '@elastic/eui';
export class ListControl extends Component { export class ListControl extends Component {
constructor(props) {
super(props);
this.handleOnChange = this.handleOnChange.bind(this); handleOnChange = (selectedOptions) => {
this.truncate = this.truncate.bind(this); this.props.stageFilter(this.props.controlIndex, selectedOptions);
}
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)}...`;
} }
renderControl() { renderControl() {
if (!this.props.control.isEnabled()) { if (this.props.disableMsg) {
// react-select clobbers the tooltip, so just returning a disabled input instead
return ( return (
<EuiFieldText <EuiFieldText
placeholder="Select..."
disabled={true} disabled={true}
/> />
); );
} }
const options = this.props.options.map(option => {
return {
label: option.label,
value: option.value,
['data-test-subj']: `option_${option.value.replace(' ', '_')}`
};
});
return ( return (
<Select <EuiComboBox
className="list-control-react-select"
placeholder="Select..." placeholder="Select..."
multi={this.props.control.options.multiselect} options={options}
simpleValue={true} selectedOptions={this.props.selectedOptions}
delimiter={this.props.control.getMultiSelectDelimiter()}
value={this.props.control.value}
options={this.props.control.selectOptions}
onChange={this.handleOnChange} onChange={this.handleOnChange}
valueRenderer={this.truncate} singleSelection={!this.props.multiselect}
inputProps={{ id: this.props.control.id }} data-test-subj={`listControlSelect${this.props.controlIndex}`}
/> />
); );
} }
@ -59,10 +46,10 @@ export class ListControl extends Component {
render() { render() {
return ( return (
<FormRow <FormRow
id={this.props.control.id} id={this.props.id}
label={this.props.control.label} label={this.props.label}
controlIndex={this.props.controlIndex} controlIndex={this.props.controlIndex}
control={this.props.control} disableMsg={this.props.disableMsg}
> >
{this.renderControl()} {this.renderControl()}
</FormRow> </FormRow>
@ -70,8 +57,18 @@ export class ListControl extends Component {
} }
} }
const comboBoxOptionShape = PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
});
ListControl.propTypes = { 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, controlIndex: PropTypes.number.isRequired,
stageFilter: PropTypes.func.isRequired stageFilter: PropTypes.func.isRequired
}; };

View file

@ -6,22 +6,11 @@ import {
ListControl, ListControl,
} from './list_control'; } from './list_control';
const control = { const options = [
id: 'mock-list-control', { label: 'choice1', value: 'choice1' },
isEnabled: () => { return true; }, { label: 'choice2', value: 'choice2' }
options: { ];
type: 'terms',
multiselect: true
},
type: 'list',
label: 'list control',
getMultiSelectDelimiter: () => { return ','; },
value: '',
selectOptions: [
{ label: 'choice1', value: 'choice1' },
{ label: 'choice2', value: 'choice2' }
]
};
let stageFilter; let stageFilter;
beforeEach(() => { beforeEach(() => {
@ -30,7 +19,11 @@ beforeEach(() => {
test('renders ListControl', () => { test('renders ListControl', () => {
const component = shallow(<ListControl const component = shallow(<ListControl
control={control} id="mock-list-control"
label="list control"
options={options}
selectedOptions={[]}
multiselect={true}
controlIndex={0} controlIndex={0}
stageFilter={stageFilter} stageFilter={stageFilter}
/>); />);

View file

@ -159,7 +159,7 @@ export class RangeControl extends Component {
id={this.props.control.id} id={this.props.control.id}
label={this.props.control.label} label={this.props.control.label}
controlIndex={this.props.controlIndex} controlIndex={this.props.controlIndex}
control={this.props.control} disableMsg={this.props.control.isEnabled() ? null : this.props.control.disabledReason}
> >
{this.renderControl()} {this.renderControl()}
</FormRow> </FormRow>

View file

@ -3,25 +3,23 @@ import { FilterManager } from './filter_manager.js';
import { buildPhraseFilter } from 'ui/filter_manager/lib/phrase'; import { buildPhraseFilter } from 'ui/filter_manager/lib/phrase';
import { buildPhrasesFilter } from 'ui/filter_manager/lib/phrases'; import { buildPhrasesFilter } from 'ui/filter_manager/lib/phrases';
const EMPTY_VALUE = '';
export class PhraseFilterManager extends FilterManager { export class PhraseFilterManager extends FilterManager {
constructor(controlId, fieldName, indexPattern, queryFilter, delimiter) { constructor(controlId, fieldName, indexPattern, queryFilter) {
super(controlId, fieldName, indexPattern, queryFilter, EMPTY_VALUE); super(controlId, fieldName, indexPattern, queryFilter, []);
this.delimiter = delimiter;
} }
/** /**
* Convert phrases into filter * Convert phrases into filter
* *
* @param {string} react-select value (delimiter-separated string of values) * @param {array}
* @return {object} query filter * @return {object} query filter
* single phrase: match query * single phrase: match query
* multiple phrases: bool query with should containing list of match_phrase queries * multiple phrases: bool query with should containing list of match_phrase queries
*/ */
createFilter(value) { createFilter(selectedOptions) {
const phrases = value.split(this.delimiter); const phrases = selectedOptions.map(phrase => {
return phrase.value;
});
let newFilter; let newFilter;
if (phrases.length === 1) { if (phrases.length === 1) {
newFilter = buildPhraseFilter( newFilter = buildPhraseFilter(
@ -43,14 +41,25 @@ export class PhraseFilterManager extends FilterManager {
if (kbnFilters.length === 0) { if (kbnFilters.length === 0) {
return this.getUnsetValue(); return this.getUnsetValue();
} else { } else {
const values = kbnFilters return kbnFilters
.map((kbnFilter) => { .map((kbnFilter) => {
return this._getValueFromFilter(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.<string>} array of values pulled from filter
*/
_getValueFromFilter(kbnFilter) { _getValueFromFilter(kbnFilter) {
// bool filter - multiple phrase filters // bool filter - multiple phrase filters
if (_.has(kbnFilter, 'query.bool.should')) { if (_.has(kbnFilter, 'query.bool.should')) {
@ -63,8 +72,7 @@ export class PhraseFilterManager extends FilterManager {
return true; return true;
} }
return false; return false;
}) });
.join(this.delimiter);
} }
// scripted field filter // scripted field filter

View file

@ -24,11 +24,11 @@ describe('PhraseFilterManager', function () {
const queryFilterMock = {}; const queryFilterMock = {};
let filterManager; let filterManager;
beforeEach(() => { 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 () { 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).to.have.property('meta');
expect(newFilter.meta.index).to.be(indexPatternId); expect(newFilter.meta.index).to.be(indexPatternId);
expect(newFilter.meta.controlledBy).to.be(controlId); expect(newFilter.meta.controlledBy).to.be(controlId);
@ -37,7 +37,7 @@ describe('PhraseFilterManager', function () {
}); });
test('should create bool filter from multiple values', 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).to.have.property('meta');
expect(newFilter.meta.index).to.be(indexPatternId); expect(newFilter.meta.index).to.be(indexPatternId);
expect(newFilter.meta.controlledBy).to.be(controlId); expect(newFilter.meta.controlledBy).to.be(controlId);
@ -67,7 +67,7 @@ describe('PhraseFilterManager', function () {
this.mockFilters = mockFilters; 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 () { 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 () { 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' }]);
}); });
}); });

View file

@ -32,14 +32,8 @@ const termsAgg = (field, size, direction) => {
}; };
}; };
const listControlDelimiter = '$$kbn_delimiter$$';
class ListControl extends Control { class ListControl extends Control {
getMultiSelectDelimiter() {
return this.filterManager.delimiter;
}
async fetch() { async fetch() {
let ancestorFilters; let ancestorFilters;
if (this.hasAncestors()) { if (this.hasAncestors()) {
@ -99,7 +93,7 @@ export async function listControlFactory(controlParams, kbnApi, useTimeFilter) {
return new ListControl( return new ListControl(
controlParams, controlParams,
new PhraseFilterManager(controlParams.id, controlParams.fieldName, indexPattern, kbnApi.queryFilter, listControlDelimiter), new PhraseFilterManager(controlParams.id, controlParams.fieldName, indexPattern, kbnApi.queryFilter),
kbnApi, kbnApi,
useTimeFilter useTimeFilter
); );

View file

@ -32,11 +32,3 @@
left: 0% !important; left: 0% !important;
} }
} }
visualization.input_control_vis {
overflow: visible;
.vis-container {
overflow: visible;
}
}

View file

@ -705,4 +705,10 @@ style-compile {
display: block; display: block;
} }
.euiComboBox {
input:focus {
box-shadow: none;
}
}
@import "~dragula/dist/dragula.css"; @import "~dragula/dist/dragula.css";

View file

@ -16,9 +16,9 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.setAbsoluteRange('2017-01-01', '2017-01-02'); await PageObjects.header.setAbsoluteRange('2017-01-01', '2017-01-02');
await PageObjects.visualize.clickVisEditorTab('controls'); await PageObjects.visualize.clickVisEditorTab('controls');
await PageObjects.visualize.addInputControl(); 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.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.visualize.clickGo();
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }) {
describe('updateFiltersOnChange is false', () => { describe('updateFiltersOnChange is false', () => {
it('should contain dropdown with terms aggregation results as options', async () => { 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'); 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 () => { 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'); const selectedOptions = await PageObjects.visualize.getComboBoxSelectedOptions('listControlSelect0');
expect(dropdownValue.trim()).to.equal('ios'); expect(selectedOptions[0].trim()).to.equal('ios');
const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios'); const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios');
expect(hasFilter).to.equal(false); 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 () => { it('should replace existing filter pill(s) when new item is selected', async () => {
await PageObjects.visualize.clearReactSelect('.list-control-react-select'); await PageObjects.visualize.clearComboBox('listControlSelect0');
await PageObjects.visualize.setReactSelect('.list-control-react-select', 'osx'); await PageObjects.visualize.setComboBox('listControlSelect0', 'osx');
await testSubjects.click('inputControlSubmitBtn'); await testSubjects.click('inputControlSubmitBtn');
const hasOldFilter = await filterBar.hasFilter(FIELD_NAME, 'ios'); const hasOldFilter = await filterBar.hasFilter(FIELD_NAME, 'ios');
@ -79,18 +79,18 @@ export default function ({ getService, getPageObjects }) {
await filterBar.removeFilter(FIELD_NAME); await filterBar.removeFilter(FIELD_NAME);
await PageObjects.common.sleep(500); // give time for filter to be removed and event handlers to fire 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); expect(hasValue).to.equal(false);
}); });
it('should clear form when Clear button is clicked but not remove filter pill', async () => { 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'); await testSubjects.click('inputControlSubmitBtn');
const hasFilterBeforeClearBtnClicked = await filterBar.hasFilter(FIELD_NAME, 'ios'); const hasFilterBeforeClearBtnClicked = await filterBar.hasFilter(FIELD_NAME, 'ios');
expect(hasFilterBeforeClearBtnClicked).to.equal(true); expect(hasFilterBeforeClearBtnClicked).to.equal(true);
await testSubjects.click('inputControlClearBtn'); 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); expect(hasValue).to.equal(false);
const hasFilterAfterClearBtnClicked = await filterBar.hasFilter(FIELD_NAME, 'ios'); 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 () => { 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'); const selectedOptions = await PageObjects.visualize.getComboBoxSelectedOptions('listControlSelect0');
expect(dropdownValue.trim()).to.equal('ios'); expect(selectedOptions[0].trim()).to.equal('ios');
const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios'); const hasFilter = await filterBar.hasFilter(FIELD_NAME, 'ios');
expect(hasFilter).to.equal(true); expect(hasFilter).to.equal(true);
@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
// Expect control to have values for selected time filter // 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'); 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.clickVisEditorTab('controls');
await PageObjects.visualize.addInputControl(); 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.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.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.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.setSelectByOptionText('parentSelect-1', 'geo.src');
await PageObjects.visualize.clickGo(); 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 () => { 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'); expect(parentControlMenu.trim().split('\n').join()).to.equal('BR,CN,ID,IN,US');
const childControlInput = await find.byCssSelector('[data-test-subj="inputControl1"] input'); 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 () => { 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'); 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 () => { 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'); 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 () => { 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 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'); 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 filterBar.removeFilter('geo.src');
await PageObjects.common.sleep(500); // give time for filter to be removed and event handlers to fire 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); expect(hasValue).to.equal(false);
}); });
}); });

View file

@ -194,41 +194,57 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
await input.type(timeString); await input.type(timeString);
} }
async setReactSelect(className, value) { async setComboBox(comboBoxSelector, value) {
const input = await find.byCssSelector(className + ' * input', 0); const comboBox = await testSubjects.find(comboBoxSelector);
const input = await comboBox.findByTagName('input');
await input.clearValue(); await input.clearValue();
await input.type(value); await input.type(value);
await find.clickByCssSelector('.Select-option'); await find.clickByCssSelector('.euiComboBoxOption');
const stillOpen = await find.existsByCssSelector('.Select-menu-outer', 0); await this.closeComboBoxOptionsList(comboBox);
if (stillOpen) { await remote.pressKeys('\uE004');
await find.clickByCssSelector(className + ' * .Select-arrow-zone');
}
} }
async clearReactSelect(className) { async getComboBoxOptions(comboBoxSelector) {
await find.clickByCssSelector(className + ' * .Select-clear-zone'); await testSubjects.click(comboBoxSelector);
}
async getReactSelectOptions(containerSelector) {
await testSubjects.click(containerSelector);
const menu = await retry.try( const menu = await retry.try(
async () => find.byCssSelector('.Select-menu-outer')); async () => await testSubjects.find('comboBoxOptionsList'));
return await menu.getVisibleText(); const optionsText = await menu.getVisibleText();
const comboBox = await testSubjects.find(comboBoxSelector);
await this.closeComboBoxOptionsList(comboBox);
return optionsText;
} }
async doesReactSelectHaveValue(className) { async doesComboBoxHaveSelectedOptions(comboBoxSelector) {
return await find.existsByCssSelector(className + ' * .Select-value-label', 0); const comboBox = await testSubjects.find(comboBoxSelector);
const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill');
return selectedOptions > 0;
} }
async getReactSelectValue(className) { async getComboBoxSelectedOptions(comboBoxSelector) {
const hasValue = await this.doesReactSelectHaveValue(className); const comboBox = await testSubjects.find(comboBoxSelector);
if (!hasValue) { const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill');
return ''; if (selectedOptions.length === 0) {
return [];
} }
const valueElement = await retry.try( const getOptionValuePromises = selectedOptions.map(async (optionElement) => {
async () => find.byCssSelector(className + ' * .Select-value-label')); return await optionElement.getVisibleText();
return await valueElement.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() { async addInputControl() {