[Maps] make EMS tooltips configurable (#34325)

- users can now select which fields to use in EMS tooltips
- extracts tooltip-selection to its own component
- use human readable names for field names of EMS fields instead of the id when they are displayed in combo-box and tooltip
This commit is contained in:
Thomas Neirynck 2019-06-25 14:53:54 -04:00 committed by GitHub
parent 36378ed3a3
commit b57f808cdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 240 additions and 49 deletions

View file

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TooltipSelector should create eui row component 1`] = `
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Fields to display in tooltip"
labelType="label"
>
<MultiFieldSelect
fields={Array []}
filterField={[Function]}
onChange={[Function]}
placeholder="Select fields"
value={Array []}
/>
</EuiFormRow>
`;

View file

@ -23,6 +23,7 @@ export function MultiFieldSelect({
placeholder,
...rest
}) {
if (!fields) {
return null;
}
@ -34,11 +35,18 @@ export function MultiFieldSelect({
onChange(fieldNamesArray);
};
const selectedOptions = value
? value.map(fieldName => {
return { value: fieldName, label: fieldName };
})
: [];
let selectedOptions;
if (value) {
selectedOptions = value.map(fieldName => {
const matchingField = fields.find(field => {
return field.name === fieldName;
});
const labelValue = matchingField && matchingField.label ? matchingField.label : fieldName;
return { value: fieldName, label: labelValue };
});
} else {
selectedOptions = [];
}
return (
<EuiComboBox

View file

@ -12,41 +12,51 @@ import {
EuiComboBox,
} from '@elastic/eui';
const sortByLabel = (a, b) => {
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
return 0;
};
// Creates grouped options by grouping fields by field type
export const getGroupedFieldOptions = (fields, filterField) => {
if (!fields) {
return undefined;
return;
}
const fieldsByTypeMap = new Map();
const groupedFieldOptions = [];
fields
.filter(filterField)
.forEach(field => {
const fieldLabel = 'label' in field ? field.label : field.name;
if (fieldsByTypeMap.has(field.type)) {
const fieldsList = fieldsByTypeMap.get(field.type);
fieldsList.push(field.name);
fieldsList.push({ value: field.name, label: fieldLabel });
fieldsByTypeMap.set(field.type, fieldsList);
} else {
fieldsByTypeMap.set(field.type, [field.name]);
fieldsByTypeMap.set(field.type, [{ value: field.name, label: fieldLabel }]);
}
});
const groupedFieldOptions = [];
fieldsByTypeMap.forEach((fieldsList, fieldType) => {
const sortedOptions = fieldsList
.sort(sortByLabel)
.map(({ value, label }) => {
return { value: value, label: label };
});
groupedFieldOptions.push({
label: fieldType,
options: fieldsList.sort().map(fieldName => {
return { value: fieldName, label: fieldName };
})
options: sortedOptions
});
});
groupedFieldOptions.sort((a, b) => {
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
return 0;
});
groupedFieldOptions.sort(sortByLabel);
return groupedFieldOptions;
};

View file

@ -0,0 +1,36 @@
/*
* 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 React from 'react';
import {
EuiFormRow
} from '@elastic/eui';
import { MultiFieldSelect } from './multi_field_select';
import { i18n } from '@kbn/i18n';
export class TooltipSelector extends React.Component {
render() {
return (
<EuiFormRow
label={
i18n.translate('xpack.maps.tooltipSelectorLabel', {
defaultMessage: `Fields to display in tooltip`
})
}
>
<MultiFieldSelect
placeholder={i18n.translate('xpack.maps.tooltipSelectorPlaceholder', {
defaultMessage: `Select fields`
})
}
value={this.props.value}
onChange={this.props.onChange}
fields={this.props.fields}
/>
</EuiFormRow>
);
}
}

View file

@ -0,0 +1,31 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { TooltipSelector } from './tooltip_selector';
const defaultProps = {
value: [],
onChange: (()=>{}),
fields: []
};
describe('TooltipSelector', () => {
test('should create eui row component', async () => {
const component = shallow(
<TooltipSelector
{...defaultProps}
/>
);
expect(component)
.toMatchSnapshot();
});
});

View file

@ -12,6 +12,7 @@ import { getEmsVectorFilesMeta } from '../../../../meta';
import { EMSFileCreateSourceEditor } from './create_source_editor';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../../common/i18n_getters';
import { UpdateSourceEditor } from './update_source_editor';
export class EMSFileSource extends AbstractVectorSource {
@ -24,22 +25,37 @@ export class EMSFileSource extends AbstractVectorSource {
});
static icon = 'emsApp';
static createDescriptor(id) {
static createDescriptor({ id, tooltipProperties = [] }) {
return {
type: EMSFileSource.type,
id: id
id,
tooltipProperties
};
}
static renderEditor({ onPreviewSource, inspectorAdapters }) {
const onChange = (selectedId) => {
const emsFileSourceDescriptor = EMSFileSource.createDescriptor(selectedId);
const emsFileSourceDescriptor = EMSFileSource.createDescriptor({ id: selectedId });
const emsFileSource = new EMSFileSource(emsFileSourceDescriptor, inspectorAdapters);
onPreviewSource(emsFileSource);
};
return <EMSFileCreateSourceEditor onChange={onChange}/>;
}
constructor(descriptor, inspectorAdapters) {
super(EMSFileSource.createDescriptor(descriptor), inspectorAdapters);
}
renderSourceSettingsEditor({ onChange }) {
return (
<UpdateSourceEditor
onChange={onChange}
tooltipProperties={this._descriptor.tooltipProperties}
layerId={this._descriptor.id}
/>
);
}
async _getEmsVectorFileMeta() {
const emsFiles = await getEmsVectorFilesMeta();
const meta = emsFiles.find((source => source.id === this._descriptor.id));
@ -114,7 +130,25 @@ export class EMSFileSource extends AbstractVectorSource {
}
canFormatFeatureProperties() {
return true;
return this._descriptor.tooltipProperties.length;
}
async filterAndFormatPropertiesToHtml(properties) {
const newProperties = {};
const meta = await this._getEmsVectorFileMeta();
for (const key in properties) {
if (properties.hasOwnProperty(key) && this._descriptor.tooltipProperties.indexOf(key) > -1) {
let newFieldName = key;
for (let i = 0; i < meta.fields.length; i++) {
if (meta.fields[i].name === key) {
newFieldName = meta.fields[i].description;
break;
}
}
newProperties[newFieldName] = properties[key];
}
}
return super.filterAndFormatPropertiesToHtml(newProperties);
}
async getSupportedShapeTypes() {

View file

@ -0,0 +1,66 @@
/*
* 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 React, { Component } from 'react';
import PropTypes from 'prop-types';
import { TooltipSelector } from '../../../components/tooltip_selector';
import { getEmsVectorFilesMeta } from '../../../../meta';
export class UpdateSourceEditor extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired
};
state = {
fields: null,
};
componentDidMount() {
this._isMounted = true;
this.loadFields();
}
componentWillUnmount() {
this._isMounted = false;
}
async loadFields() {
let fields;
try {
const emsFiles = await getEmsVectorFilesMeta();
const meta = emsFiles.find((source => source.id === this.props.layerId));
fields = meta.fields.map(field => {
return {
type: 'string',
name: field.name,
label: field.description
};
});
} catch(e) {
//swallow this error. when a matching EMS-config cannot be found, the source already will have thrown errors during the data request. This will propagate to the vector-layer and be displayed in the UX
fields = [];
}
if (this._isMounted) {
this.setState({ fields: fields });
}
}
_onTooltipPropertiesSelect = (propertyNames) => {
this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
};
render() {
return (
<TooltipSelector
value={this.props.tooltipProperties}
onChange={this._onTooltipPropertiesSelect}
fields={this.state.fields}
/>
);
}
}

View file

@ -10,8 +10,8 @@ import {
EuiFormRow,
EuiSwitch,
} from '@elastic/eui';
import { MultiFieldSelect } from '../../../components/multi_field_select';
import { SingleFieldSelect } from '../../../components/single_field_select';
import { TooltipSelector } from '../../../components/tooltip_selector';
import { indexPatternService } from '../../../../kibana_services';
import { i18n } from '@kbn/i18n';
@ -94,12 +94,11 @@ export class UpdateSourceEditor extends Component {
}
}
}
onTooltipPropertiesSelect = (propertyNames) => {
_onTooltipPropertiesChange = (propertyNames) => {
this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
};
onFilterByMapBoundsChange = event => {
_onFilterByMapBoundsChange = event => {
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
};
@ -194,23 +193,11 @@ export class UpdateSourceEditor extends Component {
render() {
return (
<Fragment>
<EuiFormRow
label={
i18n.translate('xpack.maps.source.esSearch.fieldsLabel', {
defaultMessage: `Fields to display in tooltip`
})
}
>
<MultiFieldSelect
placeholder={i18n.translate('xpack.maps.source.esSearch.fieldsPlaceholder', {
defaultMessage: `Select fields`
})
}
value={this.props.tooltipProperties}
onChange={this.onTooltipPropertiesSelect}
fields={this.state.tooltipFields}
/>
</EuiFormRow>
<TooltipSelector
value={this.props.tooltipProperties}
onChange={this._onTooltipPropertiesChange}
fields={this.state.tooltipFields}
/>
<EuiFormRow>
<EuiSwitch
@ -221,7 +208,7 @@ export class UpdateSourceEditor extends Component {
}
checked={this.props.filterByMapBounds}
onChange={this.onFilterByMapBoundsChange}
onChange={this._onFilterByMapBoundsChange}
/>
</EuiFormRow>

View file

@ -623,7 +623,7 @@ export class VectorLayer extends AbstractLayer {
}
canShowTooltip() {
return this.isVisible() && this._source.canFormatFeatureProperties();
return this.isVisible() && (this._source.canFormatFeatureProperties() || this._joins.length);
}
getFeatureById(id) {

File diff suppressed because one or more lines are too long

View file

@ -40,7 +40,7 @@ export const getWebLogsSavedObjects = () => {
}),
'description': '',
'mapStateJSON': '{"zoom":3.64,"center":{"lon":-88.92107,"lat":42.16337},"timeFilters":{"from":"now-7d","to":"now"},"refreshConfig":{"isPaused":true,"interval":0},"query":{"language":"kuery","query":""}}',
'layerListJSON': '[{"id":"0hmz5","alpha":1,"sourceDescriptor":{"type":"EMS_TMS","id":"road_map"},"visible":true,"style":{"type":"TILE","properties":{}},"type":"TILE","minZoom":0,"maxZoom":24},{"id":"edh66","label":"Total Requests by Country","minZoom":0,"maxZoom":24,"alpha":0.5,"sourceDescriptor":{"type":"EMS_FILE","id":"world_countries"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"count of kibana_sample_data_logs:geo.src","name":"__kbnjoin__count_groupby_kibana_sample_data_logs.geo.src","origin":"join"},"color":"Greys"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}}}},"type":"VECTOR","joins":[{"leftField":"iso2","right":{"id":"673ff994-fc75-4c67-909b-69fcb0e1060e","indexPatternTitle":"kibana_sample_data_logs","term":"geo.src","indexPatternRefName":"layer_1_join_0_index_pattern"}}]},{"id":"gaxya","label":"Actual Requests","minZoom":9,"maxZoom":24,"alpha":1,"sourceDescriptor":{"id":"b7486535-171b-4d3b-bb2e-33c1a0a2854c","type":"ES_SEARCH","geoField":"geo.coordinates","limit":2048,"filterByMapBounds":true,"tooltipProperties":["clientip","timestamp","host","request","response","machine.os","agent","bytes"],"indexPatternRefName":"layer_2_source_index_pattern"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"STATIC","options":{"color":"#2200ff"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":2}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"bytes","name":"bytes","origin":"source"},"minSize":1,"maxSize":23}}}},"type":"VECTOR"},{"id":"tfi3f","label":"Total Requests and Bytes","minZoom":0,"maxZoom":9,"alpha":1,"sourceDescriptor":{"type":"ES_GEO_GRID","resolution":"COARSE","id":"8aaa65b5-a4e9-448b-9560-c98cb1c5ac5b","geoField":"geo.coordinates","requestType":"point","metrics":[{"type":"count"},{"type":"sum","field":"bytes"}],"indexPatternRefName":"layer_3_source_index_pattern"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"Count","name":"doc_count","origin":"source"},"color":"Blues"}},"lineColor":{"type":"STATIC","options":{"color":"#cccccc"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"sum of bytes","name":"sum_of_bytes","origin":"source"},"minSize":1,"maxSize":25}}}},"type":"VECTOR"}]',
'layerListJSON': '[{"id":"0hmz5","alpha":1,"sourceDescriptor":{"type":"EMS_TMS","id":"road_map"},"visible":true,"style":{"type":"TILE","properties":{}},"type":"TILE","minZoom":0,"maxZoom":24},{"id":"edh66","label":"Total Requests by Country","minZoom":0,"maxZoom":24,"alpha":0.5,"sourceDescriptor":{"type":"EMS_FILE","id":"world_countries", "tooltipProperties":["name"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"count of kibana_sample_data_logs:geo.src","name":"__kbnjoin__count_groupby_kibana_sample_data_logs.geo.src","origin":"join"},"color":"Greys"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}}}},"type":"VECTOR","joins":[{"leftField":"iso2","right":{"id":"673ff994-fc75-4c67-909b-69fcb0e1060e","indexPatternTitle":"kibana_sample_data_logs","term":"geo.src","indexPatternRefName":"layer_1_join_0_index_pattern"}}]},{"id":"gaxya","label":"Actual Requests","minZoom":9,"maxZoom":24,"alpha":1,"sourceDescriptor":{"id":"b7486535-171b-4d3b-bb2e-33c1a0a2854c","type":"ES_SEARCH","geoField":"geo.coordinates","limit":2048,"filterByMapBounds":true,"tooltipProperties":["clientip","timestamp","host","request","response","machine.os","agent","bytes"],"indexPatternRefName":"layer_2_source_index_pattern"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"STATIC","options":{"color":"#2200ff"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":2}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"bytes","name":"bytes","origin":"source"},"minSize":1,"maxSize":23}}}},"type":"VECTOR"},{"id":"tfi3f","label":"Total Requests and Bytes","minZoom":0,"maxZoom":9,"alpha":1,"sourceDescriptor":{"type":"ES_GEO_GRID","resolution":"COARSE","id":"8aaa65b5-a4e9-448b-9560-c98cb1c5ac5b","geoField":"geo.coordinates","requestType":"point","metrics":[{"type":"count"},{"type":"sum","field":"bytes"}],"indexPatternRefName":"layer_3_source_index_pattern"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"Count","name":"doc_count","origin":"source"},"color":"Blues"}},"lineColor":{"type":"STATIC","options":{"color":"#cccccc"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"sum of bytes","name":"sum_of_bytes","origin":"source"},"minSize":1,"maxSize":25}}}},"type":"VECTOR"}]',
'uiStateJSON': '{"isDarkMode":false}',
'bounds': { 'type': 'envelope', 'coordinates': [[-124.45342, 54.91445], [-53.38872, 26.21461]] }
}

View file

@ -5352,8 +5352,8 @@
"xpack.maps.source.esSearch.disableFilterByMapBoundsTitle": "ダイナミックデータフィルターは無効です",
"xpack.maps.source.esSearch.disableFilterByMapBoundsTurnOnMsg": "ドキュメント数が増えると思われる場合はダイナミックフィルターをオンにしてください。",
"xpack.maps.source.esSearch.extentFilterLabel": "マップの表示範囲でデータを動的にフィルタリングします。",
"xpack.maps.source.esSearch.fieldsLabel": "ツールヒントに表示するフィールド",
"xpack.maps.source.esSearch.fieldsPlaceholder": "フィールドを選択",
"xpack.maps.tooltipSelectorLabel": "ツールヒントに表示するフィールド",
"xpack.maps.tooltipSelectorPlaceholder": "フィールドを選択",
"xpack.maps.source.esSearch.geofieldLabel": "地理空間フィールド",
"xpack.maps.source.esSearch.geoFieldLabel": "地理空間フィールド",
"xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空間フィールドタイプ",
@ -9895,4 +9895,4 @@
"xpack.watcher.watchActionsTitle": "条件が満たされた際に {watchActionsCount, plural, one{# アクション} other {# アクション}} を実行します",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}