[Maps] show empty tooltips with actions on click (#99337)
* [Maps] show tooltips with actions on click * clean up * call canShowTooltip in _getTooltipFeatures * move view into action * cleanup * i18n * add comment to clarify if statement * tslint * fix security tslint * fix jest tests * clean up Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b5380697cc
commit
b248472e82
|
@ -44,6 +44,7 @@ storiesOf('app/RumDashboard/VisitorsRegionMap', module)
|
|||
'__kbnjoin__count__3657625d-17b0-41ef-99ba-3a2b2938655c': 439145,
|
||||
'__kbnjoin__avg_of_transaction.duration.us__3657625d-17b0-41ef-99ba-3a2b2938655c': 2041665.6350131081,
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { GeoJsonProperties } from 'geojson';
|
||||
import { Geometry } from 'geojson';
|
||||
import { Query } from '../../../../../src/plugins/data/common';
|
||||
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
|
||||
|
||||
|
@ -41,10 +43,20 @@ export type Goto = {
|
|||
center?: MapCenterAndZoom;
|
||||
};
|
||||
|
||||
export const GEOMETRY_FILTER_ACTION = 'GEOMETRY_FILTER_ACTION';
|
||||
|
||||
export type TooltipFeatureAction = {
|
||||
label: string;
|
||||
id: typeof GEOMETRY_FILTER_ACTION;
|
||||
form: ReactNode;
|
||||
};
|
||||
|
||||
export type TooltipFeature = {
|
||||
id?: number | string;
|
||||
layerId: string;
|
||||
geometry?: Geometry;
|
||||
mbProperties: GeoJsonProperties;
|
||||
actions: TooltipFeatureAction[];
|
||||
};
|
||||
|
||||
export type TooltipState = {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import uuid from 'uuid/v4';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Feature } from 'geojson';
|
||||
import { getOpenTooltips } from '../selectors/map_selectors';
|
||||
|
@ -36,11 +35,7 @@ export function openOnClickTooltip(tooltipState: TooltipState) {
|
|||
);
|
||||
});
|
||||
|
||||
openTooltips.push({
|
||||
...tooltipState,
|
||||
isLocked: true,
|
||||
id: uuid(),
|
||||
});
|
||||
openTooltips.push(tooltipState);
|
||||
|
||||
dispatch({
|
||||
type: SET_OPEN_TOOLTIPS,
|
||||
|
@ -63,13 +58,7 @@ export function closeOnHoverTooltip() {
|
|||
export function openOnHoverTooltip(tooltipState: TooltipState) {
|
||||
return {
|
||||
type: SET_OPEN_TOOLTIPS,
|
||||
openTooltips: [
|
||||
{
|
||||
...tooltipState,
|
||||
isLocked: false,
|
||||
id: uuid(),
|
||||
},
|
||||
],
|
||||
openTooltips: [tooltipState],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,6 @@ export interface ILayer {
|
|||
getMbLayerIds(): string[];
|
||||
ownsMbLayerId(mbLayerId: string): boolean;
|
||||
ownsMbSourceId(mbSourceId: string): boolean;
|
||||
canShowTooltip(): boolean;
|
||||
syncLayerWithMB(mbMap: MbMap): void;
|
||||
getLayerTypeIconName(): string;
|
||||
isInitialDataLoadComplete(): boolean;
|
||||
|
@ -452,10 +451,6 @@ export class AbstractLayer implements ILayer {
|
|||
throw new Error('Should implement AbstractLayer#ownsMbSourceId');
|
||||
}
|
||||
|
||||
canShowTooltip() {
|
||||
return false;
|
||||
}
|
||||
|
||||
syncLayerWithMB(mbMap: MbMap) {
|
||||
throw new Error('Should implement AbstractLayer#syncLayerWithMB');
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ export interface IVectorLayer extends ILayer {
|
|||
getFeatureById(id: string | number): Feature | null;
|
||||
getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
|
||||
hasJoins(): boolean;
|
||||
canShowTooltip(): boolean;
|
||||
}
|
||||
|
||||
export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
||||
|
@ -1033,10 +1034,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
|
||||
canShowTooltip() {
|
||||
return (
|
||||
this.isVisible() &&
|
||||
(this.getSource().canFormatFeatureProperties() || this.getJoins().length > 0)
|
||||
);
|
||||
return this.getSource().hasTooltipProperties() || this.getJoins().length > 0;
|
||||
}
|
||||
|
||||
getFeatureById(id: string | number) {
|
||||
|
|
|
@ -196,7 +196,7 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
|
|||
return fields.map((f) => this.createField({ fieldName: f.name }));
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
hasTooltipProperties() {
|
||||
return this._tooltipFields.length > 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -471,7 +471,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
|
|||
}
|
||||
}
|
||||
|
||||
canFormatFeatureProperties(): boolean {
|
||||
hasTooltipProperties(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -359,7 +359,7 @@ export class ESGeoLineSource extends AbstractESAggSource {
|
|||
return true;
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
hasTooltipProperties() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -226,7 +226,7 @@ export class ESPewPewSource extends AbstractESAggSource {
|
|||
return turfBboxToBounds(turfBbox(multiPoint(corners)));
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
hasTooltipProperties() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -464,7 +464,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
|
|||
};
|
||||
}
|
||||
|
||||
canFormatFeatureProperties(): boolean {
|
||||
hasTooltipProperties(): boolean {
|
||||
return this._tooltipFields.length > 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ export class GeoJsonFileSource extends AbstractVectorSource {
|
|||
return (this._descriptor as GeojsonFileSourceDescriptor).name;
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
hasTooltipProperties() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
|
|||
return this._descriptor.name;
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
hasTooltipProperties() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,10 +30,10 @@ describe('getUrlTemplateWithMeta', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('canFormatFeatureProperties', () => {
|
||||
describe('hasTooltipProperties', () => {
|
||||
it('false if no tooltips', async () => {
|
||||
const source = new MVTSingleLayerVectorSource(descriptor);
|
||||
expect(source.canFormatFeatureProperties()).toEqual(false);
|
||||
expect(source.hasTooltipProperties()).toEqual(false);
|
||||
});
|
||||
it('true if tooltip', async () => {
|
||||
const descriptorWithTooltips = {
|
||||
|
@ -42,7 +42,7 @@ describe('canFormatFeatureProperties', () => {
|
|||
tooltipProperties: ['foobar'],
|
||||
};
|
||||
const source = new MVTSingleLayerVectorSource(descriptorWithTooltips);
|
||||
expect(source.canFormatFeatureProperties()).toEqual(true);
|
||||
expect(source.hasTooltipProperties()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ export class MVTSingleLayerVectorSource
|
|||
return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
|
||||
}
|
||||
|
||||
canFormatFeatureProperties(): boolean {
|
||||
hasTooltipProperties(): boolean {
|
||||
return !!this._tooltipFields.length;
|
||||
}
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@ export class TableSource extends AbstractVectorSource implements ITermJoinSource
|
|||
});
|
||||
}
|
||||
|
||||
canFormatFeatureProperties(): boolean {
|
||||
hasTooltipProperties(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ export interface IVectorSource extends ISource {
|
|||
getSyncMeta(): VectorSourceSyncMeta | null;
|
||||
getFieldNames(): string[];
|
||||
createField({ fieldName }: { fieldName: string }): IField;
|
||||
canFormatFeatureProperties(): boolean;
|
||||
hasTooltipProperties(): boolean;
|
||||
getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]>;
|
||||
isBoundsAware(): boolean;
|
||||
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig;
|
||||
|
@ -115,7 +115,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
|
|||
throw new Error('Should implement VectorSource#getGeoJson');
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
hasTooltipProperties() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,26 +8,21 @@
|
|||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
import { EuiIcon, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
|
||||
import { GeoJsonProperties, Geometry } from 'geojson';
|
||||
import { Filter } from 'src/plugins/data/public';
|
||||
import { FeatureProperties } from './feature_properties';
|
||||
import { GEO_JSON_TYPE, ES_GEO_FIELD_TYPE, RawValue } from '../../../../common/constants';
|
||||
import { FeatureGeometryFilterForm } from './feature_geometry_filter_form';
|
||||
import { RawValue } from '../../../../common/constants';
|
||||
import { Footer } from './footer';
|
||||
import { Header } from './header';
|
||||
import { PreIndexedShape } from '../../../../common/elasticsearch_util';
|
||||
import { GeoFieldWithIndex } from '../../../components/geo_field_with_index';
|
||||
import { TooltipFeature } from '../../../../common/descriptor_types';
|
||||
import { GEOMETRY_FILTER_ACTION, TooltipFeature } from '../../../../common/descriptor_types';
|
||||
import { ITooltipProperty } from '../../../classes/tooltips/tooltip_property';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
|
||||
enum VIEWS {
|
||||
PROPERTIES_VIEW = 'PROPERTIES_VIEW',
|
||||
GEOMETRY_FILTER_VIEW = 'GEOMETRY_FILTER_VIEW',
|
||||
FILTER_ACTIONS_VIEW = 'FILTER_ACTIONS_VIEW',
|
||||
}
|
||||
const PROPERTIES_VIEW = 'PROPERTIES_VIEW';
|
||||
const FILTER_ACTIONS_VIEW = 'FILTER_ACTIONS_VIEW';
|
||||
|
||||
type VIEWS = typeof PROPERTIES_VIEW | typeof FILTER_ACTIONS_VIEW | typeof GEOMETRY_FILTER_ACTION;
|
||||
|
||||
interface Props {
|
||||
addFilters: ((filters: Filter[], actionId: string) => Promise<void>) | null;
|
||||
|
@ -55,14 +50,6 @@ interface Props {
|
|||
}) => Geometry | null;
|
||||
getLayerName: (layerId: string) => Promise<string | null>;
|
||||
findLayerById: (layerId: string) => ILayer | undefined;
|
||||
geoFields: GeoFieldWithIndex[];
|
||||
loadPreIndexedShape: ({
|
||||
layerId,
|
||||
featureId,
|
||||
}: {
|
||||
layerId: string;
|
||||
featureId?: string | number;
|
||||
}) => Promise<PreIndexedShape | null>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -77,14 +64,14 @@ export class FeaturesTooltip extends Component<Props, State> {
|
|||
currentFeature: null,
|
||||
filterView: null,
|
||||
prevFeatures: [],
|
||||
view: VIEWS.PROPERTIES_VIEW,
|
||||
view: PROPERTIES_VIEW,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
if (nextProps.features !== prevState.prevFeatures) {
|
||||
return {
|
||||
currentFeature: nextProps.features ? nextProps.features[0] : null,
|
||||
view: VIEWS.PROPERTIES_VIEW,
|
||||
view: PROPERTIES_VIEW,
|
||||
prevFeatures: nextProps.features,
|
||||
};
|
||||
}
|
||||
|
@ -96,69 +83,37 @@ export class FeaturesTooltip extends Component<Props, State> {
|
|||
this.setState({ currentFeature: feature });
|
||||
};
|
||||
|
||||
_showGeometryFilterView = () => {
|
||||
this.setState({ view: VIEWS.GEOMETRY_FILTER_VIEW });
|
||||
};
|
||||
|
||||
_showPropertiesView = () => {
|
||||
this.setState({ view: VIEWS.PROPERTIES_VIEW, filterView: null });
|
||||
this.setState({ view: PROPERTIES_VIEW, filterView: null });
|
||||
};
|
||||
|
||||
_showFilterActionsView = (filterView: ReactNode) => {
|
||||
this.setState({ view: VIEWS.FILTER_ACTIONS_VIEW, filterView });
|
||||
this.setState({ view: FILTER_ACTIONS_VIEW, filterView });
|
||||
};
|
||||
|
||||
_renderActions(geoFields: GeoFieldWithIndex[]) {
|
||||
if (!this.props.isLocked || geoFields.length === 0) {
|
||||
_renderActions() {
|
||||
if (
|
||||
!this.props.isLocked ||
|
||||
!this.state.currentFeature ||
|
||||
this.state.currentFeature.actions.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiLink className="mapFeatureTooltip_actionLinks" onClick={this._showGeometryFilterView}>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.tooltip.showGeometryFilterViewLinkLabel"
|
||||
defaultMessage="Filter by geometry"
|
||||
/>
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
|
||||
_filterGeoFields(featureGeometry: Geometry | null) {
|
||||
if (!featureGeometry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// line geometry can only create filters for geo_shape fields.
|
||||
if (
|
||||
featureGeometry.type === GEO_JSON_TYPE.LINE_STRING ||
|
||||
featureGeometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING
|
||||
) {
|
||||
return this.props.geoFields.filter(({ geoFieldType }) => {
|
||||
return geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO support geo distance filters for points
|
||||
if (
|
||||
featureGeometry.type === GEO_JSON_TYPE.POINT ||
|
||||
featureGeometry.type === GEO_JSON_TYPE.MULTI_POINT
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.props.geoFields;
|
||||
}
|
||||
|
||||
_loadCurrentFeaturePreIndexedShape = async () => {
|
||||
if (!this.state.currentFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.props.loadPreIndexedShape({
|
||||
layerId: this.state.currentFeature.layerId,
|
||||
featureId: this.state.currentFeature.id,
|
||||
return this.state.currentFeature.actions.map((action) => {
|
||||
return (
|
||||
<EuiLink
|
||||
className="mapFeatureTooltip_actionLinks"
|
||||
onClick={() => {
|
||||
this.setState({ view: action.id });
|
||||
}}
|
||||
key={action.id}
|
||||
>
|
||||
{action.label}
|
||||
</EuiLink>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_renderBackButton(label: string) {
|
||||
return (
|
||||
|
@ -181,38 +136,20 @@ export class FeaturesTooltip extends Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const currentFeatureGeometry = this.props.loadFeatureGeometry({
|
||||
layerId: this.state.currentFeature.layerId,
|
||||
featureId: this.state.currentFeature.id,
|
||||
const action = this.state.currentFeature.actions.find(({ id }) => {
|
||||
return id === this.state.view;
|
||||
});
|
||||
const geoFields = this._filterGeoFields(currentFeatureGeometry);
|
||||
|
||||
if (
|
||||
this.state.view === VIEWS.GEOMETRY_FILTER_VIEW &&
|
||||
currentFeatureGeometry &&
|
||||
this.props.addFilters
|
||||
) {
|
||||
if (action) {
|
||||
return (
|
||||
<Fragment>
|
||||
{this._renderBackButton(
|
||||
i18n.translate('xpack.maps.tooltip.showGeometryFilterViewLinkLabel', {
|
||||
defaultMessage: 'Filter by geometry',
|
||||
})
|
||||
)}
|
||||
<FeatureGeometryFilterForm
|
||||
onClose={this.props.closeTooltip}
|
||||
geometry={currentFeatureGeometry}
|
||||
geoFields={geoFields}
|
||||
addFilters={this.props.addFilters}
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
loadPreIndexedShape={this._loadCurrentFeaturePreIndexedShape}
|
||||
/>
|
||||
{this._renderBackButton(action.label)}
|
||||
{action.form}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.FILTER_ACTIONS_VIEW) {
|
||||
if (this.state.view === FILTER_ACTIONS_VIEW) {
|
||||
return (
|
||||
<Fragment>
|
||||
{this._renderBackButton(
|
||||
|
@ -247,7 +184,7 @@ export class FeaturesTooltip extends Component<Props, State> {
|
|||
onSingleValueTrigger={this.props.onSingleValueTrigger}
|
||||
showFilterActions={this._showFilterActionsView}
|
||||
/>
|
||||
{this._renderActions(geoFields)}
|
||||
{this._renderActions()}
|
||||
<Footer
|
||||
features={this.props.features}
|
||||
isLocked={this.props.isLocked}
|
||||
|
|
|
@ -32,6 +32,7 @@ describe('Footer', () => {
|
|||
id: 'feature1',
|
||||
layerId: 'layer1',
|
||||
mbProperties: {},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
describe('mouseover (unlocked)', () => {
|
||||
|
@ -54,11 +55,13 @@ describe('Footer', () => {
|
|||
id: 'feature1',
|
||||
layerId: 'layer1',
|
||||
mbProperties: {},
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
id: 'feature2',
|
||||
layerId: 'layer1',
|
||||
mbProperties: {},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
describe('mouseover (unlocked)', () => {
|
||||
|
@ -97,16 +100,19 @@ describe('Footer', () => {
|
|||
id: 'feature1',
|
||||
layerId: 'layer1',
|
||||
mbProperties: {},
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
id: 'feature2',
|
||||
layerId: 'layer1',
|
||||
mbProperties: {},
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
id: 'feature1',
|
||||
layerId: 'layer2',
|
||||
mbProperties: {},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
describe('mouseover (unlocked)', () => {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { FeatureGeometryFilterForm } from './feature_geometry_filter_form';
|
||||
export { FeaturesTooltip } from './features_tooltip';
|
|
@ -15,23 +15,10 @@ exports[`TooltipControl render should render hover tooltip 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
geoFields={
|
||||
Array [
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
findLayerById={[Function]}
|
||||
index={0}
|
||||
isLocked={false}
|
||||
key="1"
|
||||
layerList={
|
||||
Array [
|
||||
Object {
|
||||
"canShowTooltip": [Function],
|
||||
"getId": [Function],
|
||||
"getMbLayerIds": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
location={
|
||||
Array [
|
||||
-120,
|
||||
|
@ -62,23 +49,10 @@ exports[`TooltipControl render should render locked tooltip 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
geoFields={
|
||||
Array [
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
findLayerById={[Function]}
|
||||
index={0}
|
||||
isLocked={true}
|
||||
key="2"
|
||||
layerList={
|
||||
Array [
|
||||
Object {
|
||||
"canShowTooltip": [Function],
|
||||
"getId": [Function],
|
||||
"getMbLayerIds": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
location={
|
||||
Array [
|
||||
-120,
|
||||
|
|
|
@ -48,17 +48,9 @@ exports[`TooltipPopover render should render tooltip popover 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
findLayerById={[Function]}
|
||||
geoFields={
|
||||
Array [
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
getLayerName={[Function]}
|
||||
isLocked={false}
|
||||
loadFeatureGeometry={[Function]}
|
||||
loadFeatureProperties={[Function]}
|
||||
loadPreIndexedShape={[Function]}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiPopover>
|
||||
|
@ -106,7 +98,6 @@ exports[`TooltipPopover render should render tooltip popover with custom tooltip
|
|||
}
|
||||
getLayerName={[Function]}
|
||||
isLocked={false}
|
||||
loadFeatureGeometry={[Function]}
|
||||
loadFeatureProperties={[Function]}
|
||||
>
|
||||
Custom tooltip content
|
||||
|
|
|
@ -7,8 +7,17 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import uuid from 'uuid/v4';
|
||||
import {
|
||||
ES_GEO_FIELD_TYPE,
|
||||
FEATURE_ID_PROPERTY_NAME,
|
||||
GEO_JSON_TYPE,
|
||||
LON_INDEX,
|
||||
} from '../../../../common/constants';
|
||||
import { GEOMETRY_FILTER_ACTION } from '../../../../common/descriptor_types';
|
||||
import { TooltipPopover } from './tooltip_popover';
|
||||
import { FeatureGeometryFilterForm } from '../features_tooltip';
|
||||
import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions';
|
||||
|
||||
function justifyAnchorLocation(mbLngLat, targetFeature) {
|
||||
|
@ -48,6 +57,28 @@ export class TooltipControl extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_findLayerById = (layerId) => {
|
||||
return this.props.layerList.find((layer) => {
|
||||
return layer.getId() === layerId;
|
||||
});
|
||||
};
|
||||
|
||||
// Must load original geometry instead of using geometry from mapbox feature.
|
||||
// Mapbox feature geometry is from vector tile and is not the same as the original geometry.
|
||||
_getFeatureGeometry = ({ layerId, featureId }) => {
|
||||
const tooltipLayer = this._findLayerById(layerId);
|
||||
if (!tooltipLayer || featureId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetFeature = tooltipLayer.getFeatureById(featureId);
|
||||
if (!targetFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return targetFeature.geometry;
|
||||
};
|
||||
|
||||
_getLayerByMbLayerId(mbLayerId) {
|
||||
return this.props.layerList.find((layer) => {
|
||||
const mbLayerIds = layer.getMbLayerIds();
|
||||
|
@ -55,7 +86,76 @@ export class TooltipControl extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_getTooltipFeatures(mbFeatures) {
|
||||
_loadPreIndexedShape = async ({ layerId, featureId }) => {
|
||||
const tooltipLayer = this._findLayerById(layerId);
|
||||
if (!tooltipLayer || typeof featureId === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetFeature = tooltipLayer.getFeatureById(featureId);
|
||||
if (!targetFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties);
|
||||
};
|
||||
|
||||
_getFeatureActions({ layerId, featureId, tooltipId }) {
|
||||
const actions = [];
|
||||
|
||||
const geometry = this._getFeatureGeometry({ layerId, featureId });
|
||||
const geoFieldsForFeature = this._filterGeoFieldsByFeatureGeometry(geometry);
|
||||
if (geoFieldsForFeature.length && this.props.addFilters) {
|
||||
actions.push({
|
||||
label: i18n.translate('xpack.maps.tooltip.action.filterByGeometryLabel', {
|
||||
defaultMessage: 'Filter by geometry',
|
||||
}),
|
||||
id: GEOMETRY_FILTER_ACTION,
|
||||
form: (
|
||||
<FeatureGeometryFilterForm
|
||||
onClose={() => {
|
||||
this.props.closeOnClickTooltip(tooltipId);
|
||||
}}
|
||||
geometry={geometry}
|
||||
geoFields={geoFieldsForFeature}
|
||||
addFilters={this.props.addFilters}
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
loadPreIndexedShape={async () => {
|
||||
return this._loadPreIndexedShape({ layerId, featureId });
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
_filterGeoFieldsByFeatureGeometry(geometry) {
|
||||
if (!geometry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// line geometry can only create filters for geo_shape fields.
|
||||
if (
|
||||
geometry.type === GEO_JSON_TYPE.LINE_STRING ||
|
||||
geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING
|
||||
) {
|
||||
return this.props.geoFields.filter(({ geoFieldType }) => {
|
||||
return geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO support geo distance filters for points
|
||||
if (geometry.type === GEO_JSON_TYPE.POINT || geometry.type === GEO_JSON_TYPE.MULTI_POINT) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.props.geoFields;
|
||||
}
|
||||
|
||||
_getTooltipFeatures(mbFeatures, isLocked, tooltipId) {
|
||||
const uniqueFeatures = [];
|
||||
//there may be duplicates in the results from mapbox
|
||||
//this is because mapbox returns the results per tile
|
||||
|
@ -81,12 +181,18 @@ export class TooltipControl extends React.Component {
|
|||
// - As empty object literal
|
||||
// To avoid ambiguity, normalize properties to empty object literal.
|
||||
const mbProperties = mbFeature.properties ? mbFeature.properties : {};
|
||||
//This keeps track of first properties (assuming these will be identical for features in different tiles)
|
||||
uniqueFeatures.push({
|
||||
id: featureId,
|
||||
layerId: layerId,
|
||||
mbProperties,
|
||||
});
|
||||
const actions = isLocked ? this._getFeatureActions({ layerId, featureId, tooltipId }) : [];
|
||||
|
||||
const hasActions = isLocked && actions.length;
|
||||
if (hasActions || layer.canShowTooltip()) {
|
||||
//This keeps track of first feature (assuming these will be identical for features in different tiles)
|
||||
uniqueFeatures.push({
|
||||
id: featureId,
|
||||
layerId: layerId,
|
||||
mbProperties,
|
||||
actions,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return uniqueFeatures;
|
||||
|
@ -109,10 +215,17 @@ export class TooltipControl extends React.Component {
|
|||
const targetMbFeataure = mbFeatures[0];
|
||||
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure);
|
||||
|
||||
const features = this._getTooltipFeatures(mbFeatures);
|
||||
const isLocked = true;
|
||||
const tooltipId = uuid();
|
||||
const features = this._getTooltipFeatures(mbFeatures, isLocked, tooltipId);
|
||||
if (features.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.props.openOnClickTooltip({
|
||||
features,
|
||||
location: popupAnchorLocation,
|
||||
isLocked,
|
||||
id: tooltipId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -129,25 +242,36 @@ export class TooltipControl extends React.Component {
|
|||
}
|
||||
|
||||
const targetMbFeature = mbFeatures[0];
|
||||
if (this.props.openTooltips[0]) {
|
||||
if (this.props.openTooltips[0] && this.props.openTooltips[0].features.length) {
|
||||
const firstFeature = this.props.openTooltips[0].features[0];
|
||||
if (targetMbFeature.properties[FEATURE_ID_PROPERTY_NAME] === firstFeature.id) {
|
||||
// ignore hover events when hover tooltip is all ready opened for feature
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature);
|
||||
const features = this._getTooltipFeatures(mbFeatures);
|
||||
|
||||
const isLocked = false;
|
||||
const tooltipId = uuid();
|
||||
const features = this._getTooltipFeatures(mbFeatures, isLocked, tooltipId);
|
||||
if (features.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.props.openOnHoverTooltip({
|
||||
features: features,
|
||||
location: popupAnchorLocation,
|
||||
isLocked,
|
||||
id: tooltipId,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
_getMbLayerIdsForTooltips() {
|
||||
const mbLayerIds = this.props.layerList.reduce((mbLayerIds, layer) => {
|
||||
return layer.canShowTooltip() ? mbLayerIds.concat(layer.getMbLayerIds()) : mbLayerIds;
|
||||
// tooltips are only supported for vector layers, filter out all other layer types
|
||||
const isVectorLayer = layer.canShowTooltip !== undefined;
|
||||
return layer.isVisible() && isVectorLayer
|
||||
? mbLayerIds.concat(layer.getMbLayerIds())
|
||||
: mbLayerIds;
|
||||
}, []);
|
||||
|
||||
//Ensure that all layers are actually on the map.
|
||||
|
@ -198,13 +322,12 @@ export class TooltipControl extends React.Component {
|
|||
<TooltipPopover
|
||||
key={id}
|
||||
mbMap={this.props.mbMap}
|
||||
layerList={this.props.layerList}
|
||||
findLayerById={this._findLayerById}
|
||||
addFilters={this.props.addFilters}
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
onSingleValueTrigger={this.props.onSingleValueTrigger}
|
||||
renderTooltipContent={this.props.renderTooltipContent}
|
||||
geoFields={this.props.geoFields}
|
||||
features={features}
|
||||
location={location}
|
||||
closeTooltip={closeTooltip}
|
||||
|
|
|
@ -28,9 +28,28 @@ const mockLayer = {
|
|||
getId: () => {
|
||||
return layerId;
|
||||
},
|
||||
isVisible: () => {
|
||||
return true;
|
||||
},
|
||||
canShowTooltip: () => {
|
||||
return true;
|
||||
},
|
||||
getFeatureById: () => {
|
||||
return {
|
||||
geometry: {
|
||||
coordinates: [
|
||||
[
|
||||
[-67.5, 40.9799],
|
||||
[-90, 40.9799],
|
||||
[-90, 21.94305],
|
||||
[-67.5, 21.94305],
|
||||
[-67.5, 40.9799],
|
||||
],
|
||||
],
|
||||
type: 'Polygon',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const mockMbMapHandlers = {};
|
||||
|
@ -236,10 +255,9 @@ describe('TooltipControl', () => {
|
|||
mockMbMapHandlers.click(mockMapMouseEvent);
|
||||
|
||||
sinon.assert.notCalled(closeOnClickTooltipStub);
|
||||
sinon.assert.calledWith(openOnClickTooltipStub, {
|
||||
features: [{ id: 1, layerId: 'tfi3f', mbProperties: { __kbn__feature_id__: 1 } }],
|
||||
location: [100, 30],
|
||||
});
|
||||
const tooltipState = openOnClickTooltipStub.getCalls()[0].args[0];
|
||||
expect(tooltipState.features.length).toBe(1);
|
||||
expect(tooltipState.location).toEqual([100, 30]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import { LAT_INDEX, LON_INDEX } from '../../../../common/constants';
|
||||
import { FeaturesTooltip } from '../features_tooltip/features_tooltip';
|
||||
import { FeaturesTooltip } from '../features_tooltip';
|
||||
import { EuiPopover, EuiText } from '@elastic/eui';
|
||||
|
||||
const noop = () => {};
|
||||
|
@ -55,24 +55,8 @@ export class TooltipPopover extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
// Must load original geometry instead of using geometry from mapbox feature.
|
||||
// Mapbox feature geometry is from vector tile and is not the same as the original geometry.
|
||||
_loadFeatureGeometry = ({ layerId, featureId }) => {
|
||||
const tooltipLayer = this._findLayerById(layerId);
|
||||
if (!tooltipLayer || typeof featureId === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetFeature = tooltipLayer.getFeatureById(featureId);
|
||||
if (!targetFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return targetFeature.geometry;
|
||||
};
|
||||
|
||||
_loadFeatureProperties = async ({ layerId, featureId, mbProperties }) => {
|
||||
const tooltipLayer = this._findLayerById(layerId);
|
||||
const tooltipLayer = this.props.findLayerById(layerId);
|
||||
if (!tooltipLayer) {
|
||||
return [];
|
||||
}
|
||||
|
@ -86,28 +70,8 @@ export class TooltipPopover extends Component {
|
|||
return await tooltipLayer.getPropertiesForTooltip(properties);
|
||||
};
|
||||
|
||||
_loadPreIndexedShape = async ({ layerId, featureId }) => {
|
||||
const tooltipLayer = this._findLayerById(layerId);
|
||||
if (!tooltipLayer || typeof featureId === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetFeature = tooltipLayer.getFeatureById(featureId);
|
||||
if (!targetFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties);
|
||||
};
|
||||
|
||||
_findLayerById = (layerId) => {
|
||||
return this.props.layerList.find((layer) => {
|
||||
return layer.getId() === layerId;
|
||||
});
|
||||
};
|
||||
|
||||
_getLayerName = async (layerId) => {
|
||||
const layer = this._findLayerById(layerId);
|
||||
const layer = this.props.findLayerById(layerId);
|
||||
if (!layer) {
|
||||
return null;
|
||||
}
|
||||
|
@ -125,7 +89,6 @@ export class TooltipPopover extends Component {
|
|||
features: this.props.features,
|
||||
isLocked: this.props.isLocked,
|
||||
loadFeatureProperties: this._loadFeatureProperties,
|
||||
loadFeatureGeometry: this._loadFeatureGeometry,
|
||||
getLayerName: this._getLayerName,
|
||||
};
|
||||
|
||||
|
@ -135,12 +98,7 @@ export class TooltipPopover extends Component {
|
|||
|
||||
return (
|
||||
<EuiText size="xs" style={{ maxWidth: '425px' }}>
|
||||
<FeaturesTooltip
|
||||
{...publicProps}
|
||||
findLayerById={this._findLayerById}
|
||||
geoFields={this.props.geoFields}
|
||||
loadPreIndexedShape={this._loadPreIndexedShape}
|
||||
/>
|
||||
<FeaturesTooltip {...publicProps} findLayerById={this.props.findLayerById} />
|
||||
</EuiText>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ describe('MapToolTip', () => {
|
|||
id: 1,
|
||||
layerId: 'layerId',
|
||||
mbProperties: {},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
const getLayerName = jest.fn();
|
||||
|
|
|
@ -14088,7 +14088,6 @@
|
|||
"xpack.maps.tooltip.loadingMsg": "読み込み中",
|
||||
"xpack.maps.tooltip.pageNumerText": "{total} 個中 {pageNumber} 個の機能",
|
||||
"xpack.maps.tooltip.showAddFilterActionsViewLabel": "フィルターアクション",
|
||||
"xpack.maps.tooltip.showGeometryFilterViewLinkLabel": "ジオメトリでフィルタリング",
|
||||
"xpack.maps.tooltip.toolsControl.cancelDrawButtonLabel": "キャンセル",
|
||||
"xpack.maps.tooltip.unableToLoadContentTitle": "ツールヒントのコンテンツを読み込めません",
|
||||
"xpack.maps.tooltip.viewActionsTitle": "フィルターアクションを表示",
|
||||
|
|
|
@ -14269,7 +14269,6 @@
|
|||
"xpack.maps.tooltip.loadingMsg": "正在加载",
|
||||
"xpack.maps.tooltip.pageNumerText": "第 {pageNumber} 页,共 {total} 页",
|
||||
"xpack.maps.tooltip.showAddFilterActionsViewLabel": "筛选操作",
|
||||
"xpack.maps.tooltip.showGeometryFilterViewLinkLabel": "按几何筛选",
|
||||
"xpack.maps.tooltip.toolsControl.cancelDrawButtonLabel": "取消",
|
||||
"xpack.maps.tooltip.unableToLoadContentTitle": "无法加载工具提示内容",
|
||||
"xpack.maps.tooltip.viewActionsTitle": "查看筛选操作",
|
||||
|
|
Loading…
Reference in a new issue