[Maps] Add filter actions to tooltips (#33635)
This commit is contained in:
parent
f39339d9d3
commit
ae977f8636
|
@ -174,9 +174,7 @@ export class FilterEditor extends Component {
|
|||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
{this._renderQuery()}
|
||||
|
||||
{this._renderQueryPopover()}
|
||||
</Fragment>
|
||||
);
|
||||
|
|
237
x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap
generated
Normal file
237
x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap
generated
Normal file
|
@ -0,0 +1,237 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FeatureTooltip should not show close button and not show filter button 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FeatureTooltip should show both filter buttons and close button 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
direction="row"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Close tooltip"
|
||||
color="primary"
|
||||
iconSize="m"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
key="0"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={4}
|
||||
>
|
||||
<strong>
|
||||
foo
|
||||
</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={6}
|
||||
>
|
||||
<span
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "bar",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Filter on property"
|
||||
className="mapFeatureTooltipFilterButton"
|
||||
color="primary"
|
||||
iconSize="m"
|
||||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
title="Filter on property"
|
||||
type="button"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
key="1"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={4}
|
||||
>
|
||||
<strong>
|
||||
foo
|
||||
</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={6}
|
||||
>
|
||||
<span
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "bar",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FeatureTooltip should show close button, but not filter button 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
direction="row"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Close tooltip"
|
||||
color="primary"
|
||||
iconSize="m"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FeatureTooltip should show only filter button for filterable properties 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
key="0"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={4}
|
||||
>
|
||||
<strong>
|
||||
foo
|
||||
</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={6}
|
||||
>
|
||||
<span
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "bar",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Filter on property"
|
||||
className="mapFeatureTooltipFilterButton"
|
||||
color="primary"
|
||||
iconSize="m"
|
||||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
title="Filter on property"
|
||||
type="button"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
key="1"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={4}
|
||||
>
|
||||
<strong>
|
||||
foo
|
||||
</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={6}
|
||||
>
|
||||
<span
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "bar",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -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 (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircle"
|
||||
title={i18n.translate('xpack.maps.tooltip.filterOnPropertyTitle', {
|
||||
defaultMessage: 'Filter on property'
|
||||
})}
|
||||
onClick={() => {
|
||||
this.props.closeTooltip();
|
||||
const filterAction = tooltipProperty.getFilterAction();
|
||||
filterAction();
|
||||
}}
|
||||
aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', {
|
||||
defaultMessage: 'Filter on property'
|
||||
})}
|
||||
className="mapFeatureTooltipFilterButton"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
_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 = (<span
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.props.properties[propertyName]
|
||||
__html: tooltipProperty.getHtmlDisplayValue()
|
||||
}}
|
||||
/>);
|
||||
|
||||
return (
|
||||
<div key={propertyName}>
|
||||
<span><strong>{propertyName}</strong></span>
|
||||
<span>{' '}</span>
|
||||
{htmlValue}
|
||||
</div>
|
||||
<EuiFlexGroup key={index}>
|
||||
<EuiFlexItem grow={4}>
|
||||
<strong>{tooltipProperty.getPropertyName()}</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={6}>
|
||||
{htmlValue}
|
||||
</EuiFlexItem>
|
||||
{this._renderFilterButton(tooltipProperty, hasFilters)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_renderCloseButton() {
|
||||
if (!this.props.showCloseButton) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup alignItems="flexEnd" direction="row" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
onClick={this.props.closeTooltip}
|
||||
iconType="cross"
|
||||
aria-label={i18n.translate('xpack.maps.tooltip.closeAriaLabel', {
|
||||
defaultMessage: 'Close tooltip'
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup alignItems="flexEnd" direction="row" justifyContent="spaceBetween">
|
||||
<EuiFlexItem> </EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
onClick={this.props.onCloseClick}
|
||||
iconType="cross"
|
||||
aria-label={i18n.translate('xpack.maps.tooltip.closeAreaLabel', {
|
||||
defaultMessage: 'Close tooltip'
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{this._renderCloseButton()}
|
||||
<EuiFlexItem>
|
||||
{this._renderProperties()}
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{this._renderProperties()}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
|
|
|
@ -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(
|
||||
<FeatureTooltip
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component)
|
||||
.toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show close button, but not filter button', () => {
|
||||
const component = shallowWithIntl(
|
||||
<FeatureTooltip
|
||||
{...defaultProps}
|
||||
showCloseButton={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component)
|
||||
.toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show only filter button for filterable properties', () => {
|
||||
const component = shallowWithIntl(
|
||||
<FeatureTooltip
|
||||
{...defaultProps}
|
||||
showFilterButtons={true}
|
||||
properties={mockTooltipProperties}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component)
|
||||
.toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show both filter buttons and close button', () => {
|
||||
const component = shallowWithIntl(
|
||||
<FeatureTooltip
|
||||
{...defaultProps}
|
||||
showFilterButtons={true}
|
||||
showCloseButton={true}
|
||||
properties={mockTooltipProperties}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component)
|
||||
.toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -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));
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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((<FeatureTooltip properties={content} onCloseClick={this._onTooltipClose}/>), this._tooltipContainer);
|
||||
const isLocked = this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED;
|
||||
ReactDOM.render((
|
||||
<FeatureTooltip
|
||||
properties={content}
|
||||
closeTooltip={this._onTooltipClose}
|
||||
showFilterButtons={this.props.isFilterable && isLocked}
|
||||
showCloseButton={isLocked}
|
||||
/>
|
||||
), 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -67,7 +67,7 @@ export class LeftInnerJoin {
|
|||
return { ...featureCollection };
|
||||
}
|
||||
|
||||
getJoinSource() {
|
||||
getRightJoinSource() {
|
||||
return this._rightSource;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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]);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue