mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* 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:
parent
5327c5cd5d
commit
39ca96faac
29 changed files with 474 additions and 472 deletions
|
@ -4,17 +4,18 @@ exports[`parentCandidates 1`] = `
|
|||
<div>
|
||||
<IndexPatternSelect
|
||||
controlIndex={0}
|
||||
getIndexPattern={[Function]}
|
||||
getIndexPatterns={[Function]}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
||||
value="indexPattern1"
|
||||
/>
|
||||
<FieldSelect
|
||||
controlIndex={0}
|
||||
fieldName="keywordField"
|
||||
filterField={[Function]}
|
||||
getIndexPattern={[Function]}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
||||
value="keywordField"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
@ -83,17 +84,18 @@ exports[`renders ListControlEditor 1`] = `
|
|||
<div>
|
||||
<IndexPatternSelect
|
||||
controlIndex={0}
|
||||
getIndexPattern={[Function]}
|
||||
getIndexPatterns={[Function]}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
||||
value="indexPattern1"
|
||||
/>
|
||||
<FieldSelect
|
||||
controlIndex={0}
|
||||
fieldName="keywordField"
|
||||
filterField={[Function]}
|
||||
getIndexPattern={[Function]}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
||||
value="keywordField"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
|
|
@ -4,17 +4,18 @@ exports[`renders RangeControlEditor 1`] = `
|
|||
<div>
|
||||
<IndexPatternSelect
|
||||
controlIndex={0}
|
||||
getIndexPattern={[Function]}
|
||||
getIndexPatterns={[Function]}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
||||
value="indexPattern1"
|
||||
/>
|
||||
<FieldSelect
|
||||
controlIndex={0}
|
||||
fieldName="numberField"
|
||||
filterField={[Function]}
|
||||
getIndexPattern={[Function]}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
||||
value="numberField"
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
|
|
@ -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 }
|
||||
]
|
||||
});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
export const getIndexPatternsMock = () => {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: 'indexPattern1',
|
||||
attributes: {
|
||||
title: 'indexPattern1'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'indexPattern2',
|
||||
attributes: {
|
||||
title: 'indexPattern2'
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 (
|
||||
<EuiFormRow
|
||||
id={selectId}
|
||||
label="Field"
|
||||
>
|
||||
<Select
|
||||
className="field-react-select"
|
||||
<EuiComboBox
|
||||
placeholder="Select field..."
|
||||
value={this.props.value}
|
||||
singleSelection={true}
|
||||
isLoading={this.state.isLoading}
|
||||
options={this.state.fields}
|
||||
onChange={this.props.onChange}
|
||||
resetValue={''}
|
||||
inputProps={{ id: selectId }}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={this.onChange}
|
||||
data-test-subj={selectId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<EuiFormRow
|
||||
id={selectId}
|
||||
label="Index Pattern"
|
||||
>
|
||||
<Select.Async
|
||||
className="index-pattern-react-select"
|
||||
<EuiComboBox
|
||||
placeholder="Select index pattern..."
|
||||
value={this.props.value}
|
||||
loadOptions={this.loadOptions}
|
||||
onChange={this.props.onChange}
|
||||
resetValue={''}
|
||||
inputProps={{ id: selectId }}
|
||||
singleSelection={true}
|
||||
isLoading={this.state.isLoading}
|
||||
onSearchChange={this.fetchOptions}
|
||||
options={this.state.options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={this.onChange}
|
||||
data-test-subj={selectId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -52,14 +52,15 @@ export function ListControlEditor(props) {
|
|||
<div>
|
||||
|
||||
<IndexPatternSelect
|
||||
value={props.controlParams.indexPattern}
|
||||
indexPatternId={props.controlParams.indexPattern}
|
||||
onChange={props.handleIndexPatternChange}
|
||||
getIndexPatterns={props.getIndexPatterns}
|
||||
getIndexPattern={props.getIndexPattern}
|
||||
controlIndex={props.controlIndex}
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
value={props.controlParams.fieldName}
|
||||
fieldName={props.controlParams.fieldName}
|
||||
indexPatternId={props.controlParams.indexPattern}
|
||||
filterField={filterField}
|
||||
onChange={props.handleFieldNameChange}
|
||||
|
|
|
@ -2,36 +2,13 @@ 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 { getIndexPatternsMock } from './__tests__/get_index_patterns_mock';
|
||||
|
||||
import {
|
||||
ListControlEditor,
|
||||
} 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 = {
|
||||
id: '1',
|
||||
indexPattern: 'indexPattern1',
|
||||
|
@ -58,8 +35,8 @@ beforeEach(() => {
|
|||
|
||||
test('renders ListControlEditor', () => {
|
||||
const component = shallow(<ListControlEditor
|
||||
getIndexPatterns={getIndexPatterns}
|
||||
getIndexPattern={getIndexPattern}
|
||||
getIndexPatterns={getIndexPatternsMock}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
|
@ -78,8 +55,8 @@ test('parentCandidates', () => {
|
|||
{ value: '2', text: 'fieldB' }
|
||||
];
|
||||
const component = shallow(<ListControlEditor
|
||||
getIndexPatterns={getIndexPatterns}
|
||||
getIndexPattern={getIndexPattern}
|
||||
getIndexPatterns={getIndexPatternsMock}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
|
@ -94,8 +71,8 @@ test('parentCandidates', () => {
|
|||
|
||||
test('handleCheckboxOptionChange - multiselect', () => {
|
||||
const component = mount(<ListControlEditor
|
||||
getIndexPatterns={getIndexPatterns}
|
||||
getIndexPattern={getIndexPattern}
|
||||
getIndexPatterns={getIndexPatternsMock}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
|
@ -126,8 +103,8 @@ test('handleCheckboxOptionChange - multiselect', () => {
|
|||
|
||||
test('handleNumberOptionChange - size', () => {
|
||||
const component = mount(<ListControlEditor
|
||||
getIndexPatterns={getIndexPatterns}
|
||||
getIndexPattern={getIndexPattern}
|
||||
getIndexPatterns={getIndexPatternsMock}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
|
|
|
@ -25,14 +25,15 @@ export function RangeControlEditor(props) {
|
|||
<div>
|
||||
|
||||
<IndexPatternSelect
|
||||
value={props.controlParams.indexPattern}
|
||||
indexPatternId={props.controlParams.indexPattern}
|
||||
onChange={props.handleIndexPatternChange}
|
||||
getIndexPatterns={props.getIndexPatterns}
|
||||
getIndexPattern={props.getIndexPattern}
|
||||
controlIndex={props.controlIndex}
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
value={props.controlParams.fieldName}
|
||||
fieldName={props.controlParams.fieldName}
|
||||
indexPatternId={props.controlParams.indexPattern}
|
||||
filterField={filterField}
|
||||
onChange={props.handleFieldNameChange}
|
||||
|
|
|
@ -2,36 +2,13 @@ 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 { getIndexPatternsMock } from './__tests__/get_index_patterns_mock';
|
||||
|
||||
import {
|
||||
RangeControlEditor,
|
||||
} 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 = {
|
||||
id: '1',
|
||||
indexPattern: 'indexPattern1',
|
||||
|
@ -55,8 +32,8 @@ beforeEach(() => {
|
|||
|
||||
test('renders RangeControlEditor', () => {
|
||||
const component = shallow(<RangeControlEditor
|
||||
getIndexPatterns={getIndexPatterns}
|
||||
getIndexPattern={getIndexPattern}
|
||||
getIndexPatterns={getIndexPatternsMock}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
|
@ -68,8 +45,8 @@ test('renders RangeControlEditor', () => {
|
|||
|
||||
test('handleNumberOptionChange - step', () => {
|
||||
const component = mount(<RangeControlEditor
|
||||
getIndexPatterns={getIndexPatterns}
|
||||
getIndexPattern={getIndexPattern}
|
||||
getIndexPatterns={getIndexPatternsMock}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
|
@ -95,8 +72,8 @@ test('handleNumberOptionChange - step', () => {
|
|||
|
||||
test('handleNumberOptionChange - decimalPlaces', () => {
|
||||
const component = mount(<RangeControlEditor
|
||||
getIndexPatterns={getIndexPatterns}
|
||||
getIndexPattern={getIndexPattern}
|
||||
getIndexPatterns={getIndexPatternsMock}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
|
|
|
@ -25,31 +25,24 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = `
|
|||
}
|
||||
>
|
||||
<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}
|
||||
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]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -158,31 +151,24 @@ exports[`Clear btns enabled when there are values 1`] = `
|
|||
}
|
||||
>
|
||||
<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}
|
||||
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]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -291,31 +277,24 @@ exports[`Renders list control 1`] = `
|
|||
}
|
||||
>
|
||||
<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}
|
||||
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]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -2,98 +2,31 @@
|
|||
|
||||
exports[`renders ListControl 1`] = `
|
||||
<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}
|
||||
id="mock-list-control"
|
||||
label="list control"
|
||||
>
|
||||
<Select
|
||||
arrowRenderer={[Function]}
|
||||
autosize={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}
|
||||
<EuiComboBox
|
||||
data-test-subj="listControlSelect0"
|
||||
isClearable={true}
|
||||
onChange={[Function]}
|
||||
onCloseResetsInput={true}
|
||||
onSelectResetsInput={true}
|
||||
openOnClick={true}
|
||||
optionComponent={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"data-test-subj": "option_choice1",
|
||||
"label": "choice1",
|
||||
"value": "choice1",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "option_choice2",
|
||||
"label": "choice2",
|
||||
"value": "choice2",
|
||||
},
|
||||
]
|
||||
}
|
||||
pageSize={5}
|
||||
placeholder="Select..."
|
||||
removeSelected={true}
|
||||
required={false}
|
||||
rtl={false}
|
||||
scrollMenuIntoView={true}
|
||||
searchable={true}
|
||||
simpleValue={true}
|
||||
tabSelectsValue={true}
|
||||
trimFilter={true}
|
||||
value=""
|
||||
valueComponent={[Function]}
|
||||
valueKey="value"
|
||||
valueRenderer={[Function]}
|
||||
selectedOptions={Array []}
|
||||
singleSelection={false}
|
||||
/>
|
||||
</FormRow>
|
||||
`;
|
||||
|
|
|
@ -2,26 +2,8 @@
|
|||
|
||||
exports[`renders RangeControl 1`] = `
|
||||
<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}
|
||||
disableMsg={null}
|
||||
id="mock-range-control"
|
||||
label="range control"
|
||||
>
|
||||
|
|
|
@ -8,9 +8,9 @@ import {
|
|||
|
||||
export function FormRow(props) {
|
||||
let control = props.children;
|
||||
if (!props.control.isEnabled()) {
|
||||
if (props.disableMsg) {
|
||||
control = (
|
||||
<EuiToolTip placement="top" content={props.control.disabledReason}>
|
||||
<EuiToolTip placement="top" content={props.disableMsg}>
|
||||
{control}
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -6,15 +6,10 @@ import {
|
|||
} from './form_row';
|
||||
|
||||
test('renders enabled control', () => {
|
||||
const enabledControl = {
|
||||
id: 'mock-enabled-control',
|
||||
isEnabled: () => { return true; },
|
||||
};
|
||||
const component = shallow(
|
||||
<FormRow
|
||||
label="test control"
|
||||
id="controlId"
|
||||
control={enabledControl}
|
||||
controlIndex={0}
|
||||
>
|
||||
<div>My Control</div>
|
||||
|
@ -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(
|
||||
<FormRow
|
||||
label="test control"
|
||||
id="controlId"
|
||||
control={disabledControl}
|
||||
disableMsg="I am disabled for testing purposes"
|
||||
controlIndex={0}
|
||||
>
|
||||
<div>My Control</div>
|
||||
|
|
|
@ -38,7 +38,12 @@ export class InputControlVis extends Component {
|
|||
case 'list':
|
||||
controlComponent = (
|
||||
<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}
|
||||
stageFilter={this.props.stageFilter}
|
||||
/>
|
||||
|
|
|
@ -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(<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
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
<EuiFieldText
|
||||
placeholder="Select..."
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const options = this.props.options.map(option => {
|
||||
return {
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
['data-test-subj']: `option_${option.value.replace(' ', '_')}`
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="list-control-react-select"
|
||||
<EuiComboBox
|
||||
placeholder="Select..."
|
||||
multi={this.props.control.options.multiselect}
|
||||
simpleValue={true}
|
||||
delimiter={this.props.control.getMultiSelectDelimiter()}
|
||||
value={this.props.control.value}
|
||||
options={this.props.control.selectOptions}
|
||||
options={options}
|
||||
selectedOptions={this.props.selectedOptions}
|
||||
onChange={this.handleOnChange}
|
||||
valueRenderer={this.truncate}
|
||||
inputProps={{ id: this.props.control.id }}
|
||||
singleSelection={!this.props.multiselect}
|
||||
data-test-subj={`listControlSelect${this.props.controlIndex}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -59,10 +46,10 @@ export class ListControl extends Component {
|
|||
render() {
|
||||
return (
|
||||
<FormRow
|
||||
id={this.props.control.id}
|
||||
label={this.props.control.label}
|
||||
id={this.props.id}
|
||||
label={this.props.label}
|
||||
controlIndex={this.props.controlIndex}
|
||||
control={this.props.control}
|
||||
disableMsg={this.props.disableMsg}
|
||||
>
|
||||
{this.renderControl()}
|
||||
</FormRow>
|
||||
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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(<ListControl
|
||||
control={control}
|
||||
id="mock-list-control"
|
||||
label="list control"
|
||||
options={options}
|
||||
selectedOptions={[]}
|
||||
multiselect={true}
|
||||
controlIndex={0}
|
||||
stageFilter={stageFilter}
|
||||
/>);
|
||||
|
|
|
@ -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()}
|
||||
</FormRow>
|
||||
|
|
|
@ -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.<string>} 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
|
||||
|
|
|
@ -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' }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -32,11 +32,3 @@
|
|||
left: 0% !important;
|
||||
}
|
||||
}
|
||||
|
||||
visualization.input_control_vis {
|
||||
overflow: visible;
|
||||
|
||||
.vis-container {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -705,4 +705,10 @@ style-compile {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.euiComboBox {
|
||||
input:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@import "~dragula/dist/dragula.css";
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue