Skip to content

Commit 22fd2a9

Browse files
[Maps] Autocomplete for custom color palettes and custom icon palettes (#56446)
* [Maps] type ahead for stop values for custom color maps and custom icon maps * use Popover to show type ahead suggestions * datalist version * use EuiComboBox * clean up * wire ColorStopsCategorical to use StopInput component for autocomplete * clean up * cast suggestion values to string so boolean fields work * review feedback * fix problem with stall suggestions from previous field Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 3194cd0 commit 22fd2a9

15 files changed

Lines changed: 264 additions & 65 deletions

x-pack/legacy/plugins/maps/public/kibana_services.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { npStart } from 'ui/new_platform';
1414
export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER;
1515
export { SearchSource } from '../../../../../src/plugins/data/public';
1616
export const indexPatternService = npStart.plugins.data.indexPatterns;
17+
export const autocompleteService = npStart.plugins.data.autocomplete;
1718

1819
let licenseId;
1920
export const setLicenseId = latestLicenseId => (licenseId = latestLicenseId);

x-pack/legacy/plugins/maps/public/layers/sources/es_source.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { AbstractVectorSource } from './vector_source';
88
import {
9+
autocompleteService,
910
fetchSearchSourceAndRecordWithInspector,
1011
indexPatternService,
1112
SearchSource,
@@ -344,4 +345,25 @@ export class AbstractESSource extends AbstractVectorSource {
344345

345346
return resp.aggregations;
346347
}
348+
349+
getValueSuggestions = async (fieldName, query) => {
350+
if (!fieldName) {
351+
return [];
352+
}
353+
354+
try {
355+
const indexPattern = await this.getIndexPattern();
356+
const field = indexPattern.fields.getByName(fieldName);
357+
return await autocompleteService.getValueSuggestions({
358+
indexPattern,
359+
field,
360+
query,
361+
});
362+
} catch (error) {
363+
console.warn(
364+
`Unable to fetch suggestions for field: ${fieldName}, query: ${query}, error: ${error.message}`
365+
);
366+
return [];
367+
}
368+
};
347369
}

x-pack/legacy/plugins/maps/public/layers/sources/source.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,8 @@ export class AbstractSource {
139139
async loadStylePropsMeta() {
140140
throw new Error(`Source#loadStylePropsMeta not implemented`);
141141
}
142+
143+
async getValueSuggestions(/* fieldName, query */) {
144+
return [];
145+
}
142146
}

x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export class ColorMapSelect extends Component {
7272
<EuiSpacer size="s" />
7373
<ColorStopsCategorical
7474
colorStops={this.state.customColorMap}
75+
field={this.props.styleProperty.getField()}
76+
getValueSuggestions={this.props.styleProperty.getValueSuggestions}
7577
onChange={this._onCustomColorMapChange}
7678
/>
7779
</Fragment>

x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,26 +59,23 @@ export const ColorStops = ({
5959
onChange,
6060
colorStops,
6161
isStopsInvalid,
62-
sanitizeStopInput,
6362
getStopError,
6463
renderStopInput,
6564
addNewRow,
6665
canDeleteStop,
6766
}) => {
6867
function getStopInput(stop, index) {
69-
const onStopChange = e => {
68+
const onStopChange = newStopValue => {
7069
const newColorStops = _.cloneDeep(colorStops);
71-
newColorStops[index].stop = sanitizeStopInput(e.target.value);
72-
const invalid = isStopsInvalid(newColorStops);
70+
newColorStops[index].stop = newStopValue;
7371
onChange({
7472
colorStops: newColorStops,
75-
isInvalid: invalid,
73+
isInvalid: isStopsInvalid(newColorStops),
7674
});
7775
};
7876

79-
const error = getStopError(stop, index);
8077
return {
81-
stopError: error,
78+
stopError: getStopError(stop, index),
8279
stopInput: renderStopInput(stop, onStopChange, index),
8380
};
8481
}

x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@ import {
1717
import { i18n } from '@kbn/i18n';
1818
import { ColorStops } from './color_stops';
1919
import { getOtherCategoryLabel } from '../../style_util';
20+
import { StopInput } from '../stop_input';
2021

2122
export const ColorStopsCategorical = ({
2223
colorStops = [
2324
{ stop: null, color: DEFAULT_CUSTOM_COLOR }, //first stop is the "other" color
2425
{ stop: '', color: DEFAULT_NEXT_COLOR },
2526
],
27+
field,
2628
onChange,
29+
getValueSuggestions,
2730
}) => {
28-
const sanitizeStopInput = value => {
29-
return value;
30-
};
31-
3231
const getStopError = (stop, index) => {
3332
let count = 0;
3433
for (let i = 1; i < colorStops.length; i++) {
@@ -49,34 +48,23 @@ export const ColorStopsCategorical = ({
4948
if (index === 0) {
5049
return (
5150
<EuiFieldText
52-
aria-label={i18n.translate(
53-
'xpack.maps.styles.colorStops.categoricalStop.defaultCategoryAriaLabel',
54-
{
55-
defaultMessage: 'Default stop',
56-
}
57-
)}
58-
value={stopValue}
51+
aria-label={getOtherCategoryLabel()}
5952
placeholder={getOtherCategoryLabel()}
6053
disabled
61-
onChange={onStopChange}
62-
compressed
63-
/>
64-
);
65-
} else {
66-
return (
67-
<EuiFieldText
68-
aria-label={i18n.translate(
69-
'xpack.maps.styles.colorStops.categoricalStop.categoryAriaLabel',
70-
{
71-
defaultMessage: 'Category',
72-
}
73-
)}
74-
value={stopValue}
75-
onChange={onStopChange}
7654
compressed
7755
/>
7856
);
7957
}
58+
59+
return (
60+
<StopInput
61+
key={field.getName()} // force new component instance when field changes
62+
field={field}
63+
getValueSuggestions={getValueSuggestions}
64+
value={stopValue}
65+
onChange={onStopChange}
66+
/>
67+
);
8068
};
8169

8270
const canDeleteStop = (colorStops, index) => {
@@ -88,7 +76,6 @@ export const ColorStopsCategorical = ({
8876
onChange={onChange}
8977
colorStops={colorStops}
9078
isStopsInvalid={isCategoricalStopsInvalid}
91-
sanitizeStopInput={sanitizeStopInput}
9279
getStopError={getStopError}
9380
renderStopInput={renderStopInput}
9481
canDeleteStop={canDeleteStop}
@@ -114,4 +101,8 @@ ColorStopsCategorical.propTypes = {
114101
* Callback for when the color stops changes. Called with { colorStops, isInvalid }
115102
*/
116103
onChange: PropTypes.func.isRequired,
104+
/**
105+
* Callback for fetching stop value suggestions. Called with query.
106+
*/
107+
getValueSuggestions: PropTypes.func.isRequired,
117108
};

x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ export const ColorStopsOrdinal = ({
2121
colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }],
2222
onChange,
2323
}) => {
24-
const sanitizeStopInput = value => {
25-
const sanitizedValue = parseFloat(value);
26-
return isNaN(sanitizedValue) ? '' : sanitizedValue;
27-
};
28-
2924
const getStopError = (stop, index) => {
3025
let error;
3126
if (isOrdinalStopInvalid(stop)) {
@@ -44,13 +39,18 @@ export const ColorStopsOrdinal = ({
4439
};
4540

4641
const renderStopInput = (stop, onStopChange) => {
42+
function handleOnChangeEvent(event) {
43+
const sanitizedValue = parseFloat(event.target.value);
44+
const newStopValue = isNaN(sanitizedValue) ? '' : sanitizedValue;
45+
onStopChange(newStopValue);
46+
}
4747
return (
4848
<EuiFieldNumber
4949
aria-label={i18n.translate('xpack.maps.styles.colorStops.ordinalStop.stopLabel', {
5050
defaultMessage: 'Stop',
5151
})}
5252
value={stop}
53-
onChange={onStopChange}
53+
onChange={handleOnChangeEvent}
5454
compressed
5555
/>
5656
);
@@ -65,7 +65,6 @@ export const ColorStopsOrdinal = ({
6565
onChange={onChange}
6666
colorStops={colorStops}
6767
isStopsInvalid={isOrdinalStopsInvalid}
68-
sanitizeStopInput={sanitizeStopInput}
6968
getStopError={getStopError}
7069
renderStopInput={renderStopInput}
7170
canDeleteStop={canDeleteStop}

x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function DynamicColorForm({
6767
color={styleOptions.color}
6868
customColorMap={styleOptions.customColorRamp}
6969
useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)}
70-
compressed
70+
styleProperty={styleProperty}
7171
/>
7272
);
7373
}
@@ -83,7 +83,7 @@ export function DynamicColorForm({
8383
color={styleOptions.colorCategory}
8484
customColorMap={styleOptions.customColorPalette}
8585
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
86-
compressed
86+
styleProperty={styleProperty}
8787
/>
8888
);
8989
};
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import _ from 'lodash';
8+
import React, { Component } from 'react';
9+
10+
import { EuiComboBox, EuiFieldText } from '@elastic/eui';
11+
12+
export class StopInput extends Component {
13+
constructor(props) {
14+
super(props);
15+
this.state = {
16+
suggestions: [],
17+
isLoadingSuggestions: false,
18+
hasPrevFocus: false,
19+
fieldDataType: undefined,
20+
localFieldTextValue: props.value,
21+
};
22+
}
23+
24+
componentDidMount() {
25+
this._isMounted = true;
26+
this._loadFieldDataType();
27+
}
28+
29+
componentWillUnmount() {
30+
this._isMounted = false;
31+
this._loadSuggestions.cancel();
32+
}
33+
34+
async _loadFieldDataType() {
35+
const fieldDataType = await this.props.field.getDataType();
36+
if (this._isMounted) {
37+
this.setState({ fieldDataType });
38+
}
39+
}
40+
41+
_onFocus = () => {
42+
if (!this.state.hasPrevFocus) {
43+
this.setState({ hasPrevFocus: true });
44+
this._onSearchChange('');
45+
}
46+
};
47+
48+
_onChange = selectedOptions => {
49+
this.props.onChange(_.get(selectedOptions, '[0].label', ''));
50+
};
51+
52+
_onCreateOption = newValue => {
53+
this.props.onChange(newValue);
54+
};
55+
56+
_onSearchChange = async searchValue => {
57+
this.setState(
58+
{
59+
isLoadingSuggestions: true,
60+
searchValue,
61+
},
62+
() => {
63+
this._loadSuggestions(searchValue);
64+
}
65+
);
66+
};
67+
68+
_loadSuggestions = _.debounce(async searchValue => {
69+
let suggestions = [];
70+
try {
71+
suggestions = await this.props.getValueSuggestions(searchValue);
72+
} catch (error) {
73+
// ignore suggestions error
74+
}
75+
76+
if (this._isMounted && searchValue === this.state.searchValue) {
77+
this.setState({
78+
isLoadingSuggestions: false,
79+
suggestions,
80+
});
81+
}
82+
}, 300);
83+
84+
_onFieldTextChange = event => {
85+
this.setState({ localFieldTextValue: event.target.value });
86+
// onChange can cause UI lag, ensure smooth input typing by debouncing onChange
87+
this._debouncedOnFieldTextChange();
88+
};
89+
90+
_debouncedOnFieldTextChange = _.debounce(() => {
91+
this.props.onChange(this.state.localFieldTextValue);
92+
}, 500);
93+
94+
_renderSuggestionInput() {
95+
const suggestionOptions = this.state.suggestions.map(suggestion => {
96+
return { label: `${suggestion}` };
97+
});
98+
99+
const selectedOptions = [];
100+
if (this.props.value) {
101+
let option = suggestionOptions.find(({ label }) => {
102+
return label === this.props.value;
103+
});
104+
if (!option) {
105+
option = { label: this.props.value };
106+
suggestionOptions.unshift(option);
107+
}
108+
selectedOptions.push(option);
109+
}
110+
111+
return (
112+
<EuiComboBox
113+
options={suggestionOptions}
114+
selectedOptions={selectedOptions}
115+
singleSelection={{ asPlainText: true }}
116+
onChange={this._onChange}
117+
onSearchChange={this._onSearchChange}
118+
onCreateOption={this._onCreateOption}
119+
isClearable={false}
120+
isLoading={this.state.isLoadingSuggestions}
121+
onFocus={this._onFocus}
122+
compressed
123+
/>
124+
);
125+
}
126+
127+
_renderTextInput() {
128+
return (
129+
<EuiFieldText
130+
value={this.state.localFieldTextValue}
131+
onChange={this._onFieldTextChange}
132+
compressed
133+
/>
134+
);
135+
}
136+
137+
render() {
138+
if (!this.state.fieldDataType) {
139+
return null;
140+
}
141+
142+
// autocomplete service can not provide suggestions for non string fields (and boolean) because it uses
143+
// term aggregation include parameter. Include paramerter uses a regular expressions that only supports string type
144+
return this.state.fieldDataType === 'string' || this.state.fieldDataType === 'boolean'
145+
? this._renderSuggestionInput()
146+
: this._renderTextInput();
147+
}
148+
}

x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function DynamicIconForm({
4343
return (
4444
<IconMapSelect
4545
{...styleOptions}
46+
styleProperty={styleProperty}
4647
onChange={onIconMapChange}
4748
isDarkMode={isDarkMode}
4849
symbolOptions={symbolOptions}

0 commit comments

Comments
 (0)