diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 60fda398b4f3e..a1b1c9ec1518e 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -14,6 +14,7 @@ import { npStart } from 'ui/new_platform'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; export { SearchSource } from '../../../../../src/plugins/data/public'; export const indexPatternService = npStart.plugins.data.indexPatterns; +export const autocompleteService = npStart.plugins.data.autocomplete; let licenseId; export const setLicenseId = latestLicenseId => (licenseId = latestLicenseId); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 26cc7ece66753..d78d3038f870d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -6,6 +6,7 @@ import { AbstractVectorSource } from './vector_source'; import { + autocompleteService, fetchSearchSourceAndRecordWithInspector, indexPatternService, SearchSource, @@ -344,4 +345,25 @@ export class AbstractESSource extends AbstractVectorSource { return resp.aggregations; } + + getValueSuggestions = async (fieldName, query) => { + if (!fieldName) { + return []; + } + + try { + const indexPattern = await this.getIndexPattern(); + const field = indexPattern.fields.getByName(fieldName); + return await autocompleteService.getValueSuggestions({ + indexPattern, + field, + query, + }); + } catch (error) { + console.warn( + `Unable to fetch suggestions for field: ${fieldName}, query: ${query}, error: ${error.message}` + ); + return []; + } + }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index cc5d62bbdfeef..3c6ddb74bedeb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -139,4 +139,8 @@ export class AbstractSource { async loadStylePropsMeta() { throw new Error(`Source#loadStylePropsMeta not implemented`); } + + async getValueSuggestions(/* fieldName, query */) { + return []; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js index fde088ab4475e..e8d5754ef4206 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js @@ -72,6 +72,8 @@ export class ColorMapSelect extends Component { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js index 6b403ff61532d..47c2d037e0c79 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js @@ -59,26 +59,23 @@ export const ColorStops = ({ onChange, colorStops, isStopsInvalid, - sanitizeStopInput, getStopError, renderStopInput, addNewRow, canDeleteStop, }) => { function getStopInput(stop, index) { - const onStopChange = e => { + const onStopChange = newStopValue => { const newColorStops = _.cloneDeep(colorStops); - newColorStops[index].stop = sanitizeStopInput(e.target.value); - const invalid = isStopsInvalid(newColorStops); + newColorStops[index].stop = newStopValue; onChange({ colorStops: newColorStops, - isInvalid: invalid, + isInvalid: isStopsInvalid(newColorStops), }); }; - const error = getStopError(stop, index); return { - stopError: error, + stopError: getStopError(stop, index), stopInput: renderStopInput(stop, onStopChange, index), }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js index d52c3dbcfa1df..124c2bf0cff55 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js @@ -17,18 +17,17 @@ import { import { i18n } from '@kbn/i18n'; import { ColorStops } from './color_stops'; import { getOtherCategoryLabel } from '../../style_util'; +import { StopInput } from '../stop_input'; export const ColorStopsCategorical = ({ colorStops = [ { stop: null, color: DEFAULT_CUSTOM_COLOR }, //first stop is the "other" color { stop: '', color: DEFAULT_NEXT_COLOR }, ], + field, onChange, + getValueSuggestions, }) => { - const sanitizeStopInput = value => { - return value; - }; - const getStopError = (stop, index) => { let count = 0; for (let i = 1; i < colorStops.length; i++) { @@ -49,34 +48,23 @@ export const ColorStopsCategorical = ({ if (index === 0) { return ( - ); - } else { - return ( - ); } + + return ( + + ); }; const canDeleteStop = (colorStops, index) => { @@ -88,7 +76,6 @@ export const ColorStopsCategorical = ({ onChange={onChange} colorStops={colorStops} isStopsInvalid={isCategoricalStopsInvalid} - sanitizeStopInput={sanitizeStopInput} getStopError={getStopError} renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} @@ -114,4 +101,8 @@ ColorStopsCategorical.propTypes = { * Callback for when the color stops changes. Called with { colorStops, isInvalid } */ onChange: PropTypes.func.isRequired, + /** + * Callback for fetching stop value suggestions. Called with query. + */ + getValueSuggestions: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js index 61fbb376ad601..0f6a0583d3dbc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js @@ -21,11 +21,6 @@ export const ColorStopsOrdinal = ({ colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }], onChange, }) => { - const sanitizeStopInput = value => { - const sanitizedValue = parseFloat(value); - return isNaN(sanitizedValue) ? '' : sanitizedValue; - }; - const getStopError = (stop, index) => { let error; if (isOrdinalStopInvalid(stop)) { @@ -44,13 +39,18 @@ export const ColorStopsOrdinal = ({ }; const renderStopInput = (stop, onStopChange) => { + function handleOnChangeEvent(event) { + const sanitizedValue = parseFloat(event.target.value); + const newStopValue = isNaN(sanitizedValue) ? '' : sanitizedValue; + onStopChange(newStopValue); + } return ( ); @@ -65,7 +65,6 @@ export const ColorStopsOrdinal = ({ onChange={onChange} colorStops={colorStops} isStopsInvalid={isOrdinalStopsInvalid} - sanitizeStopInput={sanitizeStopInput} getStopError={getStopError} renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index 5491d5d567f84..af5e5b37f5467 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -67,7 +67,7 @@ export function DynamicColorForm({ color={styleOptions.color} customColorMap={styleOptions.customColorRamp} useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)} - compressed + styleProperty={styleProperty} /> ); } @@ -83,7 +83,7 @@ export function DynamicColorForm({ color={styleOptions.colorCategory} customColorMap={styleOptions.customColorPalette} useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)} - compressed + styleProperty={styleProperty} /> ); }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js new file mode 100644 index 0000000000000..d12a3d77d0b29 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import React, { Component } from 'react'; + +import { EuiComboBox, EuiFieldText } from '@elastic/eui'; + +export class StopInput extends Component { + constructor(props) { + super(props); + this.state = { + suggestions: [], + isLoadingSuggestions: false, + hasPrevFocus: false, + fieldDataType: undefined, + localFieldTextValue: props.value, + }; + } + + componentDidMount() { + this._isMounted = true; + this._loadFieldDataType(); + } + + componentWillUnmount() { + this._isMounted = false; + this._loadSuggestions.cancel(); + } + + async _loadFieldDataType() { + const fieldDataType = await this.props.field.getDataType(); + if (this._isMounted) { + this.setState({ fieldDataType }); + } + } + + _onFocus = () => { + if (!this.state.hasPrevFocus) { + this.setState({ hasPrevFocus: true }); + this._onSearchChange(''); + } + }; + + _onChange = selectedOptions => { + this.props.onChange(_.get(selectedOptions, '[0].label', '')); + }; + + _onCreateOption = newValue => { + this.props.onChange(newValue); + }; + + _onSearchChange = async searchValue => { + this.setState( + { + isLoadingSuggestions: true, + searchValue, + }, + () => { + this._loadSuggestions(searchValue); + } + ); + }; + + _loadSuggestions = _.debounce(async searchValue => { + let suggestions = []; + try { + suggestions = await this.props.getValueSuggestions(searchValue); + } catch (error) { + // ignore suggestions error + } + + if (this._isMounted && searchValue === this.state.searchValue) { + this.setState({ + isLoadingSuggestions: false, + suggestions, + }); + } + }, 300); + + _onFieldTextChange = event => { + this.setState({ localFieldTextValue: event.target.value }); + // onChange can cause UI lag, ensure smooth input typing by debouncing onChange + this._debouncedOnFieldTextChange(); + }; + + _debouncedOnFieldTextChange = _.debounce(() => { + this.props.onChange(this.state.localFieldTextValue); + }, 500); + + _renderSuggestionInput() { + const suggestionOptions = this.state.suggestions.map(suggestion => { + return { label: `${suggestion}` }; + }); + + const selectedOptions = []; + if (this.props.value) { + let option = suggestionOptions.find(({ label }) => { + return label === this.props.value; + }); + if (!option) { + option = { label: this.props.value }; + suggestionOptions.unshift(option); + } + selectedOptions.push(option); + } + + return ( + + ); + } + + _renderTextInput() { + return ( + + ); + } + + render() { + if (!this.state.fieldDataType) { + return null; + } + + // autocomplete service can not provide suggestions for non string fields (and boolean) because it uses + // term aggregation include parameter. Include paramerter uses a regular expressions that only supports string type + return this.state.fieldDataType === 'string' || this.state.fieldDataType === 'boolean' + ? this._renderSuggestionInput() + : this._renderTextInput(); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js index 9a0d73cef616c..afa11daf45217 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js @@ -43,6 +43,7 @@ export function DynamicIconForm({ return ( { @@ -23,7 +24,14 @@ const DEFAULT_ICON_STOPS = [ { stop: '', icon: DEFAULT_ICON }, ]; -export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange, symbolOptions }) { +export function IconStops({ + field, + getValueSuggestions, + iconStops = DEFAULT_ICON_STOPS, + isDarkMode, + onChange, + symbolOptions, +}) { return iconStops.map(({ stop, icon }, index) => { const onIconSelect = selectedIconId => { const newIconStops = [...iconStops]; @@ -33,8 +41,7 @@ export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange }; onChange({ customMapStops: newIconStops }); }; - const onStopChange = e => { - const newStopValue = e.target.value; + const onStopChange = newStopValue => { const newIconStops = [...iconStops]; newIconStops[index] = { ...iconStops[index], @@ -83,7 +90,24 @@ export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange const errors = []; // TODO check for duplicate values and add error messages here - const isOtherCategoryRow = index === 0; + const stopInput = + index === 0 ? ( + + ) : ( + + ); + return (
- - - + {stopInput} { + const fieldName = this.getFieldName(); + return this._source && fieldName ? this._source.getValueSuggestions(fieldName, query) : []; + }; + getFieldMeta() { return this._getFieldMeta && this._field ? this._getFieldMeta(this._field.getName()) : null; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 97259a908f1e4..1f96c37c9d286 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -612,6 +612,7 @@ export class VectorStyle extends AbstractStyle { field, this._getFieldMeta, this._getFieldFormatter, + this._source, isSymbolizedAsIcon ); } else { @@ -631,7 +632,8 @@ export class VectorStyle extends AbstractStyle { styleName, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); @@ -663,7 +665,8 @@ export class VectorStyle extends AbstractStyle { VECTOR_STYLES.LABEL_TEXT, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); @@ -682,7 +685,8 @@ export class VectorStyle extends AbstractStyle { VECTOR_STYLES.ICON, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`);