From 74686729d6e22c97fe615e1524865682eea228a1 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 8 Apr 2019 13:03:56 -0400 Subject: [PATCH] [Maps] Add filter actions to tooltips (#33635) --- .../filter_editor/filter_editor.js | 2 - .../feature_tooltip.test.js.snap | 237 ++++++++++++++++++ .../public/components/map/feature_tooltip.js | 91 +++++-- .../components/map/feature_tooltip.test.js | 101 ++++++++ .../maps/public/components/map/mb/index.js | 3 +- .../maps/public/components/map/mb/view.js | 13 +- .../maps/public/embeddable/map_embeddable.js | 5 +- .../embeddable/map_embeddable_factory.js | 4 +- x-pack/plugins/maps/public/kibana_services.js | 5 +- .../shared/layers/joins/left_inner_join.js | 2 +- .../shared/layers/sources/es_join_source.js | 17 ++ .../es_search_source/es_search_source.js | 21 +- .../public/shared/layers/sources/es_source.js | 29 +-- .../shared/layers/sources/vector_source.js | 8 +- .../tooltips/es_aggmetric_tooltip_property.js | 39 +++ .../layers/tooltips/es_tooltip_property.js | 51 ++++ .../layers/tooltips/join_tooltip_property.js | 56 +++++ .../layers/tooltips/tooltip_property.js | 35 +++ .../maps/public/shared/layers/vector_layer.js | 31 ++- x-pack/plugins/maps/public/store/ui.js | 20 +- 20 files changed, 686 insertions(+), 84 deletions(-) create mode 100644 x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap create mode 100644 x-pack/plugins/maps/public/components/map/feature_tooltip.test.js create mode 100644 x-pack/plugins/maps/public/shared/layers/tooltips/es_aggmetric_tooltip_property.js create mode 100644 x-pack/plugins/maps/public/shared/layers/tooltips/es_tooltip_property.js create mode 100644 x-pack/plugins/maps/public/shared/layers/tooltips/join_tooltip_property.js create mode 100644 x-pack/plugins/maps/public/shared/layers/tooltips/tooltip_property.js diff --git a/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js index c260b67d7de71..6d5cfe28c490a 100644 --- a/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js @@ -174,9 +174,7 @@ export class FilterEditor extends Component { /> - {this._renderQuery()} - {this._renderQueryPopover()} ); diff --git a/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap b/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap new file mode 100644 index 0000000000000..2ac044bf0fa14 --- /dev/null +++ b/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap @@ -0,0 +1,237 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FeatureTooltip should not show close button and not show filter button 1`] = ` + + + + + + + +`; + +exports[`FeatureTooltip should show both filter buttons and close button 1`] = ` + + + + + + + + + + + + + + + + + foo + + + + + + + + + + + + + foo + + + + + + + + + + +`; + +exports[`FeatureTooltip should show close button, but not filter button 1`] = ` + + + + + + + + + + + + + + + + +`; + +exports[`FeatureTooltip should show only filter button for filterable properties 1`] = ` + + + + + + + + foo + + + + + + + + + + + + + foo + + + + + + + + + + +`; diff --git a/x-pack/plugins/maps/public/components/map/feature_tooltip.js b/x-pack/plugins/maps/public/components/map/feature_tooltip.js index 9410800e48c2f..0b0a902e845e9 100644 --- a/x-pack/plugins/maps/public/components/map/feature_tooltip.js +++ b/x-pack/plugins/maps/public/components/map/feature_tooltip.js @@ -11,52 +11,93 @@ import { i18n } from '@kbn/i18n'; export class FeatureTooltip extends React.Component { + _renderFilterButton(tooltipProperty) { + if (!this.props.showFilterButtons || !tooltipProperty.isFilterable()) { + return null; + } - _renderProperties() { - return Object.keys(this.props.properties).map(propertyName => { + return ( + + { + this.props.closeTooltip(); + const filterAction = tooltipProperty.getFilterAction(); + filterAction(); + }} + aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { + defaultMessage: 'Filter on property' + })} + className="mapFeatureTooltipFilterButton" + /> + + ); + } + + _renderProperties(hasFilters) { + return this.props.properties.map((tooltipProperty, index) => { /* * Justification for dangerouslySetInnerHTML: - * Propery value contains value generated by Field formatter + * Property value contains value generated by Field formatter * Since these formatters produce raw HTML, this component needs to be able to render them as-is, relying * on the field formatter to only produce safe HTML. */ - const htmlValue = (); + return ( -
- {propertyName} - {' '} - {htmlValue} -
+ + + {tooltipProperty.getPropertyName()} + + + {htmlValue} + + {this._renderFilterButton(tooltipProperty, hasFilters)} + ); }); } + _renderCloseButton() { + if (!this.props.showCloseButton) { + return null; + } + return ( + + + + + + + + + + ); + } + render() { return ( - - -   - - - - - + {this._renderCloseButton()} - {this._renderProperties()} + + {this._renderProperties()} + diff --git a/x-pack/plugins/maps/public/components/map/feature_tooltip.test.js b/x-pack/plugins/maps/public/components/map/feature_tooltip.test.js new file mode 100644 index 0000000000000..324798fa1c2d8 --- /dev/null +++ b/x-pack/plugins/maps/public/components/map/feature_tooltip.test.js @@ -0,0 +1,101 @@ +/* + * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { FeatureTooltip } from './feature_tooltip'; + +class MockTooltipProperty { + constructor(key, value, isFilterable) { + this._key = key; + this._value = value; + this._isFilterable = isFilterable; + } + + isFilterable() { + return this._isFilterable; + } + + getFilterAction() { + return () => {}; + } + + getHtmlDisplayValue() { + return this._value; + } + + getPropertyName() { + return this._key; + } +} + +const defaultProps = { + properties: [], + closeTooltip: () => {}, + showFilterButtons: false, + showCloseButton: false +}; + + +const mockTooltipProperties = [ + new MockTooltipProperty('foo', 'bar', true), + new MockTooltipProperty('foo', 'bar', false) +]; + +describe('FeatureTooltip', () => { + + test('should not show close button and not show filter button', () => { + const component = shallowWithIntl( + + ); + + expect(component) + .toMatchSnapshot(); + }); + + test('should show close button, but not filter button', () => { + const component = shallowWithIntl( + + ); + + expect(component) + .toMatchSnapshot(); + }); + + test('should show only filter button for filterable properties', () => { + const component = shallowWithIntl( + + ); + + expect(component) + .toMatchSnapshot(); + }); + + test('should show both filter buttons and close button', () => { + const component = shallowWithIntl( + + ); + + expect(component) + .toMatchSnapshot(); + }); + + +}); diff --git a/x-pack/plugins/maps/public/components/map/mb/index.js b/x-pack/plugins/maps/public/components/map/mb/index.js index 7e57eaa7ee336..b356eb015f4bf 100644 --- a/x-pack/plugins/maps/public/components/map/mb/index.js +++ b/x-pack/plugins/maps/public/components/map/mb/index.js @@ -16,10 +16,12 @@ import { setTooltipState } from '../../../actions/store_actions'; import { getTooltipState, getLayerList, getMapReady, getGoto } from '../../../selectors/map_selectors'; +import { getIsFilterable } from '../../../store/ui'; import { getInspectorAdapters } from '../../../store/non_serializable_instances'; function mapStateToProps(state = {}) { return { + isFilterable: getIsFilterable(state), isMapReady: getMapReady(state), layerList: getLayerList(state), goto: getGoto(state), @@ -53,7 +55,6 @@ function mapDispatchToProps(dispatch) { setTooltipState(tooltipState) { dispatch(setTooltipState(tooltipState)); } - }; } diff --git a/x-pack/plugins/maps/public/components/map/mb/view.js b/x-pack/plugins/maps/public/components/map/mb/view.js index 53d6473cbc69b..0e129ac6f9aee 100644 --- a/x-pack/plugins/maps/public/components/map/mb/view.js +++ b/x-pack/plugins/maps/public/components/map/mb/view.js @@ -62,7 +62,6 @@ export class MBMapContainer extends React.Component { featureId: targetFeature.properties[FEATURE_ID_PROPERTY_NAME], location: popupAnchorLocation }); - }; _updateHoverTooltipState = _.debounce((e) => { @@ -251,7 +250,15 @@ export class MBMapContainer extends React.Component { if (!this._isMounted) { return; } - ReactDOM.render((), this._tooltipContainer); + const isLocked = this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED; + ReactDOM.render(( + + ), this._tooltipContainer); this._mbPopup.setLngLat(location) .setDOMContent(this._tooltipContainer) @@ -270,8 +277,10 @@ export class MBMapContainer extends React.Component { _syncTooltipState() { if (this.props.tooltipState) { + this._mbMap.getCanvas().style.cursor = 'pointer'; this._showTooltip(); } else { + this._mbMap.getCanvas().style.cursor = ''; this._hideTooltip(); } } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/plugins/maps/public/embeddable/map_embeddable.js index e3d9c52ab4740..5227b53f05273 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.js +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.js @@ -20,9 +20,9 @@ import { setGotoWithCenter, replaceLayerList, setQuery, - setRefreshConfig, + setRefreshConfig } from '../actions/store_actions'; -import { setReadOnly } from '../store/ui'; +import { setReadOnly, setFilterable } from '../store/ui'; import { getInspectorAdapters } from '../store/non_serializable_instances'; import { getMapCenter, getMapZoom } from '../selectors/map_selectors'; @@ -82,6 +82,7 @@ export class MapEmbeddable extends Embeddable { */ render(domNode, containerState) { this._store.dispatch(setReadOnly(true)); + this._store.dispatch(setFilterable(true)); if (this._embeddableConfig.mapCenter) { this._store.dispatch(setGotoWithCenter({ diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js index 8ab8efde42d97..50e5ce35d6eb6 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js @@ -10,7 +10,7 @@ import { EmbeddableFactory } from 'ui/embeddable'; import { MapEmbeddable } from './map_embeddable'; import { indexPatternService } from '../kibana_services'; import { i18n } from '@kbn/i18n'; -import { createMapPath, MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; +import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; export class MapEmbeddableFactory extends EmbeddableFactory { @@ -22,7 +22,7 @@ export class MapEmbeddableFactory extends EmbeddableFactory { defaultMessage: 'Map', }), type: MAP_SAVED_OBJECT_TYPE, - getIconForSavedObject: () => 'gisApp', + getIconForSavedObject: () => APP_ICON }, }); this._savedObjectLoader = gisMapSavedObjectLoader; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index d8d395ef414b1..6c80774383b2b 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -6,12 +6,12 @@ import { uiModules } from 'ui/modules'; import { SearchSourceProvider } from 'ui/courier'; -import { timefilter } from 'ui/timefilter/timefilter'; +import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; -export const timeService = timefilter; export let indexPatternService; export let SearchSource; +export let filterBarQueryFilter; export async function fetchSearchSourceAndRecordWithInspector({ searchSource, requestId, requestName, requestDesc, inspectorAdapters }) { const inspectorRequest = inspectorAdapters.requests.start( @@ -39,4 +39,5 @@ uiModules.get('app/maps').run(($injector) => { indexPatternService = $injector.get('indexPatterns'); const Private = $injector.get('Private'); SearchSource = Private(SearchSourceProvider); + filterBarQueryFilter = Private(FilterBarQueryFilterProvider); }); diff --git a/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js b/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js index d64b4806d0123..7034612335291 100644 --- a/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js +++ b/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js @@ -67,7 +67,7 @@ export class LeftInnerJoin { return { ...featureCollection }; } - getJoinSource() { + getRightJoinSource() { return this._rightSource; } diff --git a/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js b/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js index de911fdb8dda4..c141ab28e0f54 100644 --- a/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js +++ b/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js @@ -10,6 +10,7 @@ import { AbstractESSource } from './es_source'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggConfigs } from 'ui/vis/agg_configs'; import { i18n } from '@kbn/i18n'; +import { ESTooltipProperty } from '../tooltips/es_tooltip_property'; const TERMS_AGG_NAME = 'join'; @@ -74,6 +75,10 @@ export class ESJoinSource extends AbstractESSource { return [this._descriptor.indexPatternId]; } + getTerm() { + return this._descriptor.term; + } + _formatMetricKey(metric) { const metricKey = metric.type !== 'count' ? `${metric.type}_of_${metric.field}` : metric.type; return `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`; @@ -180,4 +185,16 @@ export class ESJoinSource extends AbstractESSource { async filterAndFormatPropertiesToHtml(properties) { return await this.filterAndFormatPropertiesToHtmlForMetricFields(properties); } + + async createESTooltipProperty(propertyName, rawValue) { + try { + const indexPattern = await this._getIndexPattern(); + if (!indexPattern) { + return null; + } + return new ESTooltipProperty(propertyName, rawValue, indexPattern); + } catch (e) { + return null; + } + } } diff --git a/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js index e49520fdb029d..03d598c029de9 100644 --- a/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js @@ -15,6 +15,8 @@ import { UpdateSourceEditor } from './update_source_editor'; import { ES_SEARCH } from '../../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../../common/i18n_getters'; +import { ESTooltipProperty } from '../../tooltips/es_tooltip_property'; + import { DEFAULT_ES_DOC_LIMIT, DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; export class ESSearchSource extends AbstractESSource { @@ -167,30 +169,19 @@ export class ESSearchSource extends AbstractESSource { } async filterAndFormatPropertiesToHtml(properties) { - const filteredProperties = {}; - this._descriptor.tooltipProperties.forEach(propertyName => { - filteredProperties[propertyName] = _.get(properties, propertyName, '-'); - }); - + const tooltipProps = []; let indexPattern; try { indexPattern = await this._getIndexPattern(); } catch(error) { console.warn(`Unable to find Index pattern ${this._descriptor.indexPatternId}, values are not formatted`); - return filteredProperties; + return []; } this._descriptor.tooltipProperties.forEach(propertyName => { - const field = indexPattern.fields.byName[propertyName]; - if (!field) { - return; - } - const htmlConverter = field.format.getConverterFor('html'); - filteredProperties[propertyName] = (htmlConverter) ? htmlConverter(filteredProperties[propertyName]) : - field.format.convert(filteredProperties[propertyName]); + tooltipProps.push(new ESTooltipProperty(propertyName, properties[propertyName], indexPattern)); }); - - return filteredProperties; + return tooltipProps; } isFilterByMapBounds() { diff --git a/x-pack/plugins/maps/public/shared/layers/sources/es_source.js b/x-pack/plugins/maps/public/shared/layers/sources/es_source.js index 54a83360fea3a..c348d22c33fb0 100644 --- a/x-pack/plugins/maps/public/shared/layers/sources/es_source.js +++ b/x-pack/plugins/maps/public/shared/layers/sources/es_source.js @@ -15,6 +15,8 @@ import { timefilter } from 'ui/timefilter/timefilter'; import _ from 'lodash'; import { AggConfigs } from 'ui/vis/agg_configs'; import { i18n } from '@kbn/i18n'; +import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; + import uuid from 'uuid/v4'; import { copyPersistentState } from '../../../store/util'; @@ -99,35 +101,24 @@ export class AbstractESSource extends AbstractVectorSource { return properties; } - function formatMetricValue(metricField, propertyValue) { - if (metricField.type === 'count') { - return propertyValue; - } - - const indexPatternField = indexPattern.fields.byName[metricField.field]; - if (!indexPatternField) { - return propertyValue; - } - - const htmlConverter = indexPatternField.format.getConverterFor('html'); - return (htmlConverter) - ? htmlConverter(propertyValue) - : indexPatternField.format.convert(propertyValue); - } const metricFields = this.getMetricFields(); - const tooltipProps = {}; + const tooltipProperties = []; metricFields.forEach((metricField) => { let value; for (const key in properties) { if (properties.hasOwnProperty(key) && metricField.propertyKey === key) { - value = formatMetricValue(metricField, properties[key]); + value = properties[key]; break; } } - tooltipProps[metricField.propertyLabel] = (typeof value === 'undefined') ? '-' : value; + + const tooltipProperty = new ESAggMetricTooltipProperty(metricField.propertyLabel, value, indexPattern, metricField); + tooltipProperties.push(tooltipProperty); }); - return tooltipProps; + + return tooltipProperties; + } diff --git a/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js b/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js index 5b2467a9da23e..7c1e8f14072f7 100644 --- a/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js +++ b/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js @@ -6,6 +6,7 @@ import { VectorLayer } from '../vector_layer'; +import { TooltipProperty } from '../tooltips/tooltip_property'; import { VectorStyle } from '../styles/vector_style'; import { AbstractSource } from './source'; import * as topojson from 'topojson-client'; @@ -96,15 +97,14 @@ export class AbstractVectorSource extends AbstractSource { // Allow source to filter and format feature properties before displaying to user async filterAndFormatPropertiesToHtml(properties) { - //todo :this is quick hack... should revise (should model proeprties explicitly in vector_layer - const props = {}; + const tooltipProperties = []; for (const key in properties) { if (key.startsWith('__kbn')) {//these are system properties and should be ignored continue; } - props[key] = _.escape(properties[key]); + tooltipProperties.push(new TooltipProperty(key, properties[key])); } - return props; + return tooltipProperties; } async isTimeAware() { diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/es_aggmetric_tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/es_aggmetric_tooltip_property.js new file mode 100644 index 0000000000000..ba8131f349542 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/tooltips/es_aggmetric_tooltip_property.js @@ -0,0 +1,39 @@ +/* + * 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 { ESTooltipProperty } from './es_tooltip_property'; + +export class ESAggMetricTooltipProperty extends ESTooltipProperty { + + constructor(propertyName, rawValue, indexPattern, metricField) { + super(propertyName, rawValue, indexPattern); + this._metricField = metricField; + } + isFilterable() { + return false; + } + + getHtmlDisplayValue() { + if (typeof this._rawValue === 'undefined') { + return '-'; + } + if (this._metricField.type === 'count') { + return this._rawValue; + } + const indexPatternField = this._indexPattern.fields.byName[this._metricField.field]; + if (!indexPatternField) { + return this._rawValue; + } + const htmlConverter = indexPatternField.format.getConverterFor('html'); + + return (htmlConverter) + ? htmlConverter(this._rawValue) + : indexPatternField.format.convert(this._rawValue); + + } + +} diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/es_tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/es_tooltip_property.js new file mode 100644 index 0000000000000..f3df3a198c0b0 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/tooltips/es_tooltip_property.js @@ -0,0 +1,51 @@ +/* + * 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 { buildPhraseFilter } from '@kbn/es-query'; +import { filterBarQueryFilter } from '../../../kibana_services'; +import { TooltipProperty } from './tooltip_property'; +import _ from 'lodash'; + +export class ESTooltipProperty extends TooltipProperty { + + constructor(propertyName, rawValue, indexPattern) { + super(propertyName, rawValue); + this._indexPattern = indexPattern; + } + + getHtmlDisplayValue() { + + if (typeof this._rawValue === 'undefined') { + return '-'; + } + + const field = this._indexPattern.fields.byName[this._propertyName]; + if (!field) { + return _.escape(this._rawValue); + } + const htmlConverter = field.format.getConverterFor('html'); + return htmlConverter ? htmlConverter(this._rawValue) : field.format.convert(this._rawValue); + } + + isFilterable() { + const field = this._indexPattern.fields.byName[this._propertyName]; + return field && (field.type === 'string' || field.type === 'date' || field.type === 'ip' || field.type === 'number'); + } + + getESFilter() { + return buildPhraseFilter( + this._indexPattern.fields.byName[this._propertyName], + this._rawValue, + this._indexPattern); + } + + getFilterAction() { + return () => { + const phraseFilter = this.getESFilter(); + filterBarQueryFilter.addFilters([phraseFilter]); + }; + } +} diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/join_tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/join_tooltip_property.js new file mode 100644 index 0000000000000..db48c94c1616d --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/tooltips/join_tooltip_property.js @@ -0,0 +1,56 @@ +/* + * 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 { filterBarQueryFilter } from '../../../kibana_services'; +import { TooltipProperty } from './tooltip_property'; + +export class JoinTooltipProperty extends TooltipProperty { + + constructor(tooltipProperty, leftInnerJoins) { + super(); + this._tooltipProperty = tooltipProperty; + this._leftInnerJoins = leftInnerJoins; + } + + isFilterable() { + return true; + } + + getPropertyName() { + return this._tooltipProperty.getPropertyName(); + } + + getHtmlDisplayValue() { + return this._tooltipProperty.getHtmlDisplayValue(); + } + + getFilterAction() { + //dispatch all the filter actions to the query bar + //this relies on the de-duping of filterBarQueryFilter + return async () => { + const esFilters = []; + if (this._tooltipProperty.isFilterable()) { + esFilters.push(this._tooltipProperty.getESFilter()); + } + + for (let i = 0; i < this._leftInnerJoins.length; i++) { + const rightSource = this._leftInnerJoins[i].getRightJoinSource(); + const esTooltipProperty = await rightSource.createESTooltipProperty( + rightSource.getTerm(), + this._tooltipProperty.getRawValue() + ); + if (esTooltipProperty) { + const filter = esTooltipProperty.getESFilter(); + esFilters.push(filter); + } + } + filterBarQueryFilter.addFilters(esFilters); + }; + } + + +} diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/tooltip_property.js new file mode 100644 index 0000000000000..3f3a9ca2829bf --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/tooltips/tooltip_property.js @@ -0,0 +1,35 @@ +/* + * 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'; + +export class TooltipProperty { + + constructor(propertyName, rawValue) { + this._propertyName = propertyName; + this._rawValue = rawValue; + } + + getPropertyName() { + return this._propertyName; + } + + getHtmlDisplayValue() { + return _.escape(this._rawValue); + } + + getRawValue() { + return this._rawValue; + } + + isFilterable() { + return false; + } + + getFilterAction() { + throw new Error('This property is not filterable'); + } +} diff --git a/x-pack/plugins/maps/public/shared/layers/vector_layer.js b/x-pack/plugins/maps/public/shared/layers/vector_layer.js index abd9e02e6a36d..f96ccda3d5457 100644 --- a/x-pack/plugins/maps/public/shared/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/shared/layers/vector_layer.js @@ -10,6 +10,7 @@ import { VectorStyle } from './styles/vector_style'; import { LeftInnerJoin } from './joins/left_inner_join'; import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN } from '../../../common/constants'; import _ from 'lodash'; +import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; const EMPTY_FEATURE_COLLECTION = { type: 'FeatureCollection', @@ -159,6 +160,7 @@ export class VectorLayer extends AbstractLayer { } async _canSkipSourceUpdate(source, sourceDataId, searchFilters) { + const timeAware = await source.isTimeAware(); const refreshTimerAware = await source.isRefreshTimerAware(); const extentAware = source.isFilterByMapBounds(); @@ -230,7 +232,7 @@ export class VectorLayer extends AbstractLayer { async _syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters }) { - const joinSource = join.getJoinSource(); + const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); const requestToken = Symbol(`layer-join-refresh:${ this.getId()} - ${sourceDataId}`); @@ -517,13 +519,34 @@ export class VectorLayer extends AbstractLayer { return [this._getMbPointLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId()]; } + + _addJoinsToSourceTooltips(tooltipsFromSource) { + for (let i = 0; i < tooltipsFromSource.length; i++) { + const tooltipProperty = tooltipsFromSource[i]; + const matchingJoins = []; + for (let j = 0; j < this._joins.length; j++) { + if (this._joins[j].getLeftFieldName() === tooltipProperty.getPropertyName()) { + matchingJoins.push(this._joins[j]); + } + } + if (matchingJoins.length) { + tooltipsFromSource[i] = new JoinTooltipProperty(tooltipProperty, matchingJoins); + } + } + } + + async getPropertiesForTooltip(properties) { - const tooltipsFromSource = await this._source.filterAndFormatPropertiesToHtml(properties); + + let allTooltips = await this._source.filterAndFormatPropertiesToHtml(properties); + this._addJoinsToSourceTooltips(allTooltips); + + for (let i = 0; i < this._joins.length; i++) { const propsFromJoin = await this._joins[i].filterAndFormatPropertiesForTooltip(properties); - Object.assign(tooltipsFromSource, propsFromJoin); + allTooltips = [...allTooltips, ...propsFromJoin]; } - return tooltipsFromSource; + return allTooltips; } canShowTooltip() { diff --git a/x-pack/plugins/maps/public/store/ui.js b/x-pack/plugins/maps/public/store/ui.js index 7804fe26b6fe5..6334d49b0a320 100644 --- a/x-pack/plugins/maps/public/store/ui.js +++ b/x-pack/plugins/maps/public/store/ui.js @@ -3,13 +3,12 @@ * 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'; - export const UPDATE_FLYOUT = 'UPDATE_FLYOUT'; export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW'; export const OPEN_SET_VIEW = 'OPEN_SET_VIEW'; export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; export const SET_READ_ONLY = 'SET_READ_ONLY'; +export const SET_FILTERABLE = 'IS_FILTERABLE'; export const FLYOUT_STATE = { NONE: 'NONE', LAYER_PANEL: 'LAYER_PANEL', @@ -20,6 +19,7 @@ const INITIAL_STATE = { flyoutDisplay: FLYOUT_STATE.NONE, isFullScreen: false, isReadOnly: false, + isFilterable: false }; // Reducer @@ -35,6 +35,8 @@ export function ui(state = INITIAL_STATE, action) { return { ...state, isFullScreen: action.isFullScreen }; case SET_READ_ONLY: return { ...state, isReadOnly: action.isReadOnly }; + case SET_FILTERABLE: + return { ...state, isFilterable: action.isFilterable }; default: return state; } @@ -76,9 +78,17 @@ export function setReadOnly(isReadOnly) { }; } +export function setFilterable(isFilterable) { + return { + type: SET_FILTERABLE, + isFilterable + }; +} + // Selectors export const getFlyoutDisplay = ({ ui }) => ui && ui.flyoutDisplay || INITIAL_STATE.flyoutDisplay; -export const getIsSetViewOpen = ({ ui }) => _.get(ui, 'isSetViewOpen', false); -export const getIsFullScreen = ({ ui }) => _.get(ui, 'isFullScreen', false); -export const getIsReadOnly = ({ ui }) => _.get(ui, 'isReadOnly', true); +export const getIsSetViewOpen = ({ ui }) => ui.isSetViewOpen; +export const getIsFullScreen = ({ ui }) => ui.isFullScreen; +export const getIsReadOnly = ({ ui }) => ui.isReadOnly; +export const getIsFilterable = ({ ui }) => ui.isFilterable;