Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Maps] Autocomplete for custom color palettes and custom icon palettes #56446

Merged
merged 17 commits into from
Feb 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/maps/public/kibana_services.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions x-pack/legacy/plugins/maps/public/layers/sources/es_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { AbstractVectorSource } from './vector_source';
import {
autocompleteService,
fetchSearchSourceAndRecordWithInspector,
indexPatternService,
SearchSource,
Expand Down Expand Up @@ -344,4 +345,25 @@ export class AbstractESSource extends AbstractVectorSource {

return resp.aggregations;
}

getValueSuggestions = async (fieldName, query) => {
thomasneirynck marked this conversation as resolved.
Show resolved Hide resolved
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 [];
}
};
}
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/maps/public/layers/sources/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,8 @@ export class AbstractSource {
async loadStylePropsMeta() {
throw new Error(`Source#loadStylePropsMeta not implemented`);
}

async getValueSuggestions(/* fieldName, query */) {
return [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export class ColorMapSelect extends Component {
<EuiSpacer size="s" />
<ColorStopsCategorical
colorStops={this.state.customColorMap}
field={this.props.styleProperty.getField()}
getValueSuggestions={this.props.styleProperty.getValueSuggestions}
onChange={this._onCustomColorMapChange}
/>
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -49,34 +48,23 @@ export const ColorStopsCategorical = ({
if (index === 0) {
return (
<EuiFieldText
aria-label={i18n.translate(
'xpack.maps.styles.colorStops.categoricalStop.defaultCategoryAriaLabel',
{
defaultMessage: 'Default stop',
}
)}
value={stopValue}
aria-label={getOtherCategoryLabel()}
placeholder={getOtherCategoryLabel()}
disabled
onChange={onStopChange}
compressed
/>
);
} else {
return (
<EuiFieldText
aria-label={i18n.translate(
'xpack.maps.styles.colorStops.categoricalStop.categoryAriaLabel',
{
defaultMessage: 'Category',
}
)}
value={stopValue}
onChange={onStopChange}
compressed
/>
);
}

return (
<StopInput
key={field.getName()} // force new component instance when field changes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really clever

field={field}
getValueSuggestions={getValueSuggestions}
value={stopValue}
onChange={onStopChange}
/>
);
};

const canDeleteStop = (colorStops, index) => {
Expand All @@ -88,7 +76,6 @@ export const ColorStopsCategorical = ({
onChange={onChange}
colorStops={colorStops}
isStopsInvalid={isCategoricalStopsInvalid}
sanitizeStopInput={sanitizeStopInput}
getStopError={getStopError}
renderStopInput={renderStopInput}
canDeleteStop={canDeleteStop}
Expand All @@ -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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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 (
<EuiFieldNumber
aria-label={i18n.translate('xpack.maps.styles.colorStops.ordinalStop.stopLabel', {
defaultMessage: 'Stop',
})}
value={stop}
onChange={onStopChange}
onChange={handleOnChangeEvent}
compressed
/>
);
Expand All @@ -65,7 +65,6 @@ export const ColorStopsOrdinal = ({
onChange={onChange}
colorStops={colorStops}
isStopsInvalid={isOrdinalStopsInvalid}
sanitizeStopInput={sanitizeStopInput}
getStopError={getStopError}
renderStopInput={renderStopInput}
canDeleteStop={canDeleteStop}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function DynamicColorForm({
color={styleOptions.color}
customColorMap={styleOptions.customColorRamp}
useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)}
compressed
styleProperty={styleProperty}
/>
);
}
Expand All @@ -83,7 +83,7 @@ export function DynamicColorForm({
color={styleOptions.colorCategory}
customColorMap={styleOptions.customColorPalette}
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
compressed
styleProperty={styleProperty}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

thomasneirynck marked this conversation as resolved.
Show resolved Hide resolved
return (
<EuiComboBox
options={suggestionOptions}
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
onChange={this._onChange}
onSearchChange={this._onSearchChange}
onCreateOption={this._onCreateOption}
isClearable={false}
isLoading={this.state.isLoadingSuggestions}
onFocus={this._onFocus}
compressed
/>
);
}

_renderTextInput() {
return (
<EuiFieldText
value={this.state.localFieldTextValue}
onChange={this._onFieldTextChange}
compressed
/>
);
}

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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function DynamicIconForm({
return (
<IconMapSelect
{...styleOptions}
styleProperty={styleProperty}
onChange={onIconMapChange}
isDarkMode={isDarkMode}
symbolOptions={symbolOptions}
Expand Down
Loading