[Maps] show field type icons in data driven styling field select (#55166)
* [Maps] show field icons in data driven styling field select * only show origin group label when there is more then one origin * review feedback Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
b78c1b1042
commit
04374e665c
|
@ -14,9 +14,7 @@ import { ColorStopsCategorical } from './color_stops_categorical';
|
||||||
const CUSTOM_COLOR_MAP = 'CUSTOM_COLOR_MAP';
|
const CUSTOM_COLOR_MAP = 'CUSTOM_COLOR_MAP';
|
||||||
|
|
||||||
export class ColorMapSelect extends Component {
|
export class ColorMapSelect extends Component {
|
||||||
state = {
|
state = {};
|
||||||
selected: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
if (nextProps.customColorMap === prevState.prevPropsCustomColorMap) {
|
if (nextProps.customColorMap === prevState.prevPropsCustomColorMap) {
|
||||||
|
@ -41,10 +39,7 @@ export class ColorMapSelect extends Component {
|
||||||
_onCustomColorMapChange = ({ colorStops, isInvalid }) => {
|
_onCustomColorMapChange = ({ colorStops, isInvalid }) => {
|
||||||
// Manage invalid custom color map in local state
|
// Manage invalid custom color map in local state
|
||||||
if (isInvalid) {
|
if (isInvalid) {
|
||||||
const newState = {
|
this.setState({ customColorMap: colorStops });
|
||||||
customColorMap: colorStops,
|
|
||||||
};
|
|
||||||
this.setState(newState);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,35 +51,34 @@ export class ColorMapSelect extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderColorStopsInput() {
|
_renderColorStopsInput() {
|
||||||
let colorStopsInput;
|
if (!this.props.useCustomColorMap) {
|
||||||
if (this.props.useCustomColorMap) {
|
return null;
|
||||||
if (this.props.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
|
|
||||||
colorStopsInput = (
|
|
||||||
<Fragment>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
<ColorStopsOrdinal
|
|
||||||
colorStops={this.state.customColorMap}
|
|
||||||
onChange={this._onCustomColorMapChange}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
} else if (this.props.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) {
|
|
||||||
colorStopsInput = (
|
|
||||||
<Fragment>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
<ColorStopsCategorical
|
|
||||||
colorStops={this.state.customColorMap}
|
|
||||||
onChange={this._onCustomColorMapChange}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return colorStopsInput;
|
|
||||||
|
if (this.props.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<ColorStopsOrdinal
|
||||||
|
colorStops={this.state.customColorMap}
|
||||||
|
onChange={this._onCustomColorMapChange}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<ColorStopsCategorical
|
||||||
|
colorStops={this.state.customColorMap}
|
||||||
|
onChange={this._onCustomColorMapChange}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const colorStopsInput = this._renderColorStopsInput();
|
|
||||||
const colorMapOptionsWithCustom = [
|
const colorMapOptionsWithCustom = [
|
||||||
{
|
{
|
||||||
value: CUSTOM_COLOR_MAP,
|
value: CUSTOM_COLOR_MAP,
|
||||||
|
@ -110,7 +104,7 @@ export class ColorMapSelect extends Component {
|
||||||
valueOfSelected={valueOfSelected}
|
valueOfSelected={valueOfSelected}
|
||||||
hasDividers={true}
|
hasDividers={true}
|
||||||
/>
|
/>
|
||||||
{colorStopsInput}
|
{this._renderColorStopsInput()}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,87 +13,56 @@ import { CATEGORICAL_DATA_TYPES, COLOR_MAP_TYPE } from '../../../../../../common
|
||||||
import { COLOR_GRADIENTS, COLOR_PALETTES } from '../../../color_utils';
|
import { COLOR_GRADIENTS, COLOR_PALETTES } from '../../../color_utils';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
export class DynamicColorForm extends React.Component {
|
export function DynamicColorForm({
|
||||||
state = {
|
fields,
|
||||||
colorMapType: COLOR_MAP_TYPE.ORDINAL,
|
onDynamicStyleChange,
|
||||||
|
staticDynamicSelect,
|
||||||
|
styleProperty,
|
||||||
|
}) {
|
||||||
|
const styleOptions = styleProperty.getOptions();
|
||||||
|
|
||||||
|
const onColorMapSelect = ({ color, customColorMap, type, useCustomColorMap }) => {
|
||||||
|
const newColorOptions = {
|
||||||
|
...styleOptions,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
if (type === COLOR_MAP_TYPE.ORDINAL) {
|
||||||
|
newColorOptions.useCustomColorRamp = useCustomColorMap;
|
||||||
|
newColorOptions.customColorRamp = customColorMap;
|
||||||
|
newColorOptions.color = color;
|
||||||
|
} else {
|
||||||
|
newColorOptions.useCustomColorPalette = useCustomColorMap;
|
||||||
|
newColorOptions.customColorPalette = customColorMap;
|
||||||
|
newColorOptions.colorCategory = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDynamicStyleChange(styleProperty.getStyleName(), newColorOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
const onFieldChange = async ({ field }) => {
|
||||||
super();
|
const { name, origin, type } = field;
|
||||||
this._isMounted = false;
|
onDynamicStyleChange(styleProperty.getStyleName(), {
|
||||||
}
|
...styleOptions,
|
||||||
|
field: { name, origin },
|
||||||
componentWillUnmount() {
|
type: CATEGORICAL_DATA_TYPES.includes(type)
|
||||||
this._isMounted = false;
|
? COLOR_MAP_TYPE.CATEGORICAL
|
||||||
}
|
: COLOR_MAP_TYPE.ORDINAL,
|
||||||
|
});
|
||||||
componentDidMount() {
|
};
|
||||||
this._isMounted = true;
|
|
||||||
this._loadColorMapType();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this._loadColorMapType();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _loadColorMapType() {
|
|
||||||
const field = this.props.styleProperty.getField();
|
|
||||||
if (!field) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dataType = await field.getDataType();
|
|
||||||
const colorMapType = CATEGORICAL_DATA_TYPES.includes(dataType)
|
|
||||||
? COLOR_MAP_TYPE.CATEGORICAL
|
|
||||||
: COLOR_MAP_TYPE.ORDINAL;
|
|
||||||
if (this._isMounted && this.state.colorMapType !== colorMapType) {
|
|
||||||
this.setState({ colorMapType }, () => {
|
|
||||||
const options = this.props.styleProperty.getOptions();
|
|
||||||
this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), {
|
|
||||||
...options,
|
|
||||||
type: colorMapType,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getColorSelector() {
|
|
||||||
const { onDynamicStyleChange, styleProperty } = this.props;
|
|
||||||
const styleOptions = styleProperty.getOptions();
|
|
||||||
|
|
||||||
|
const renderColorMapSelect = () => {
|
||||||
if (!styleOptions.field || !styleOptions.field.name) {
|
if (!styleOptions.field || !styleOptions.field.name) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let colorSelect;
|
if (styleOptions.type === COLOR_MAP_TYPE.ORDINAL) {
|
||||||
const onColorChange = colorOptions => {
|
return (
|
||||||
const newColorOptions = {
|
|
||||||
type: colorOptions.type,
|
|
||||||
};
|
|
||||||
if (colorOptions.type === COLOR_MAP_TYPE.ORDINAL) {
|
|
||||||
newColorOptions.useCustomColorRamp = colorOptions.useCustomColorMap;
|
|
||||||
newColorOptions.customColorRamp = colorOptions.customColorMap;
|
|
||||||
newColorOptions.color = colorOptions.color;
|
|
||||||
} else {
|
|
||||||
newColorOptions.useCustomColorPalette = colorOptions.useCustomColorMap;
|
|
||||||
newColorOptions.customColorPalette = colorOptions.customColorMap;
|
|
||||||
newColorOptions.colorCategory = colorOptions.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDynamicStyleChange(styleProperty.getStyleName(), {
|
|
||||||
...styleOptions,
|
|
||||||
...newColorOptions,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
|
|
||||||
const customOptionLabel = i18n.translate('xpack.maps.style.customColorRampLabel', {
|
|
||||||
defaultMessage: 'Custom color ramp',
|
|
||||||
});
|
|
||||||
colorSelect = (
|
|
||||||
<ColorMapSelect
|
<ColorMapSelect
|
||||||
colorMapOptions={COLOR_GRADIENTS}
|
colorMapOptions={COLOR_GRADIENTS}
|
||||||
customOptionLabel={customOptionLabel}
|
customOptionLabel={i18n.translate('xpack.maps.style.customColorRampLabel', {
|
||||||
onChange={options => onColorChange(options)}
|
defaultMessage: 'Custom color ramp',
|
||||||
|
})}
|
||||||
|
onChange={onColorMapSelect}
|
||||||
colorMapType={COLOR_MAP_TYPE.ORDINAL}
|
colorMapType={COLOR_MAP_TYPE.ORDINAL}
|
||||||
color={styleOptions.color}
|
color={styleOptions.color}
|
||||||
customColorMap={styleOptions.customColorRamp}
|
customColorMap={styleOptions.customColorRamp}
|
||||||
|
@ -101,52 +70,39 @@ export class DynamicColorForm extends React.Component {
|
||||||
compressed
|
compressed
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (this.state.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) {
|
|
||||||
const customOptionLabel = i18n.translate('xpack.maps.style.customColorPaletteLabel', {
|
|
||||||
defaultMessage: 'Custom color palette',
|
|
||||||
});
|
|
||||||
colorSelect = (
|
|
||||||
<ColorMapSelect
|
|
||||||
colorMapOptions={COLOR_PALETTES}
|
|
||||||
customOptionLabel={customOptionLabel}
|
|
||||||
onChange={options => onColorChange(options)}
|
|
||||||
colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
|
|
||||||
color={styleOptions.colorCategory}
|
|
||||||
customColorMap={styleOptions.customColorPalette}
|
|
||||||
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
|
|
||||||
compressed
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return colorSelect;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fields, onDynamicStyleChange, staticDynamicSelect, styleProperty } = this.props;
|
|
||||||
const styleOptions = styleProperty.getOptions();
|
|
||||||
const onFieldChange = options => {
|
|
||||||
const field = options.field;
|
|
||||||
onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorSelect = this._getColorSelector();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<ColorMapSelect
|
||||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
colorMapOptions={COLOR_PALETTES}
|
||||||
<EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
|
customOptionLabel={i18n.translate('xpack.maps.style.customColorPaletteLabel', {
|
||||||
<EuiFlexItem>
|
defaultMessage: 'Custom color palette',
|
||||||
<FieldSelect
|
})}
|
||||||
fields={fields}
|
onChange={onColorMapSelect}
|
||||||
selectedFieldName={_.get(styleOptions, 'field.name')}
|
colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
|
||||||
onChange={onFieldChange}
|
color={styleOptions.colorCategory}
|
||||||
compressed
|
customColorMap={styleOptions.customColorPalette}
|
||||||
/>
|
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
|
||||||
</EuiFlexItem>
|
compressed
|
||||||
</EuiFlexGroup>
|
/>
|
||||||
<EuiSpacer size="s" />
|
|
||||||
{colorSelect}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||||
|
<EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<FieldSelect
|
||||||
|
fields={fields}
|
||||||
|
selectedFieldName={_.get(styleOptions, 'field.name')}
|
||||||
|
onChange={onFieldChange}
|
||||||
|
compressed
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
{renderColorMapSelect()}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,67 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { EuiComboBox } from '@elastic/eui';
|
import { EuiComboBox, EuiHighlight } from '@elastic/eui';
|
||||||
import { SOURCE_DATA_ID_ORIGIN } from '../../../../../common/constants';
|
import { FIELD_ORIGIN } from '../../../../../common/constants';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { FieldIcon } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||||
|
|
||||||
|
function renderOption(option, searchValue, contentClassName) {
|
||||||
|
return (
|
||||||
|
<span className={contentClassName}>
|
||||||
|
<FieldIcon type={option.value.type} size="m" useColor />
|
||||||
|
|
||||||
|
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupFieldsByOrigin(fields) {
|
||||||
|
const fieldsByOriginMap = new Map();
|
||||||
|
fields.forEach(field => {
|
||||||
|
if (fieldsByOriginMap.has(field.origin)) {
|
||||||
|
const fieldsList = fieldsByOriginMap.get(field.origin);
|
||||||
|
fieldsList.push(field);
|
||||||
|
fieldsByOriginMap.set(field.origin, fieldsList);
|
||||||
|
} else {
|
||||||
|
fieldsByOriginMap.set(field.origin, [field]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function fieldsListToOptions(fieldsList) {
|
||||||
|
return fieldsList
|
||||||
|
.map(field => {
|
||||||
|
return { value: field, label: field.label };
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldsByOriginMap.size === 1) {
|
||||||
|
// do not show origin group if all fields are from same origin
|
||||||
|
const onlyOriginKey = fieldsByOriginMap.keys().next().value;
|
||||||
|
const fieldsList = fieldsByOriginMap.get(onlyOriginKey);
|
||||||
|
return fieldsListToOptions(fieldsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionGroups = [];
|
||||||
|
fieldsByOriginMap.forEach((fieldsList, fieldOrigin) => {
|
||||||
|
optionGroups.push({
|
||||||
|
label: i18n.translate('xpack.maps.style.fieldSelect.OriginLabel', {
|
||||||
|
defaultMessage: 'Fields from {fieldOrigin}',
|
||||||
|
values: { fieldOrigin },
|
||||||
|
}),
|
||||||
|
options: fieldsListToOptions(fieldsList),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
optionGroups.sort((a, b) => {
|
||||||
|
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
return optionGroups;
|
||||||
|
}
|
||||||
|
|
||||||
export function FieldSelect({ fields, selectedFieldName, onChange, ...rest }) {
|
export function FieldSelect({ fields, selectedFieldName, onChange, ...rest }) {
|
||||||
const onFieldChange = selectedFields => {
|
const onFieldChange = selectedFields => {
|
||||||
|
@ -18,39 +76,6 @@ export function FieldSelect({ fields, selectedFieldName, onChange, ...rest }) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupFieldsByOrigin = () => {
|
|
||||||
const fieldsByOriginMap = new Map();
|
|
||||||
fields.forEach(field => {
|
|
||||||
if (fieldsByOriginMap.has(field.origin)) {
|
|
||||||
const fieldsList = fieldsByOriginMap.get(field.origin);
|
|
||||||
fieldsList.push(field);
|
|
||||||
fieldsByOriginMap.set(field.origin, fieldsList);
|
|
||||||
} else {
|
|
||||||
fieldsByOriginMap.set(field.origin, [field]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const optionGroups = [];
|
|
||||||
fieldsByOriginMap.forEach((fieldsList, fieldOrigin) => {
|
|
||||||
optionGroups.push({
|
|
||||||
label: fieldOrigin,
|
|
||||||
options: fieldsList
|
|
||||||
.map(field => {
|
|
||||||
return { value: field, label: field.label };
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
optionGroups.sort((a, b) => {
|
|
||||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
|
||||||
});
|
|
||||||
|
|
||||||
return optionGroups;
|
|
||||||
};
|
|
||||||
|
|
||||||
let selectedOption;
|
let selectedOption;
|
||||||
if (selectedFieldName) {
|
if (selectedFieldName) {
|
||||||
selectedOption = fields.find(field => {
|
selectedOption = fields.find(field => {
|
||||||
|
@ -61,7 +86,7 @@ export function FieldSelect({ fields, selectedFieldName, onChange, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<EuiComboBox
|
<EuiComboBox
|
||||||
selectedOptions={selectedOption ? [selectedOption] : []}
|
selectedOptions={selectedOption ? [selectedOption] : []}
|
||||||
options={groupFieldsByOrigin()}
|
options={groupFieldsByOrigin(fields)}
|
||||||
onChange={onFieldChange}
|
onChange={onFieldChange}
|
||||||
singleSelection={{ asPlainText: true }}
|
singleSelection={{ asPlainText: true }}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
|
@ -69,6 +94,7 @@ export function FieldSelect({ fields, selectedFieldName, onChange, ...rest }) {
|
||||||
placeholder={i18n.translate('xpack.maps.styles.vector.selectFieldPlaceholder', {
|
placeholder={i18n.translate('xpack.maps.styles.vector.selectFieldPlaceholder', {
|
||||||
defaultMessage: 'Select a field',
|
defaultMessage: 'Select a field',
|
||||||
})}
|
})}
|
||||||
|
renderOption={renderOption}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -76,7 +102,8 @@ export function FieldSelect({ fields, selectedFieldName, onChange, ...rest }) {
|
||||||
|
|
||||||
export const fieldShape = PropTypes.shape({
|
export const fieldShape = PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
origin: PropTypes.oneOf(['join', SOURCE_DATA_ID_ORIGIN]).isRequired,
|
origin: PropTypes.oneOf(Object.values(FIELD_ORIGIN)).isRequired,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
});
|
});
|
||||||
|
|
||||||
FieldSelect.propTypes = {
|
FieldSelect.propTypes = {
|
||||||
|
|
|
@ -61,6 +61,7 @@ export class VectorStyleEditor extends Component {
|
||||||
label: await field.getLabel(),
|
label: await field.getLabel(),
|
||||||
name: field.getName(),
|
name: field.getName(),
|
||||||
origin: field.getOrigin(),
|
origin: field.getOrigin(),
|
||||||
|
type: await field.getDataType(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue