[Maps] split out DrawFilterControl and DrawControl (#95255)

* [Maps] split out DrawFilterControl and DrawControl

* clean up

* update i18n id

* give 'global_index_pattern_management_all' permission to functional test because new check blocks access without it

* revert last change

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2021-03-25 16:22:02 -06:00 committed by GitHub
parent e01f317d9c
commit ba029aa95e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 132 deletions

View file

@ -12,20 +12,10 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw';
// @ts-expect-error
import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
import { Map as MbMap } from 'mapbox-gl';
import { i18n } from '@kbn/i18n';
import { Filter } from 'src/plugins/data/public';
import { Feature, Polygon } from 'geojson';
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../common/constants';
import { DrawState } from '../../../../common/descriptor_types';
import { DrawCircle, DrawCircleProperties } from './draw_circle';
import {
createDistanceFilterWithMeta,
createSpatialFilterWithGeometry,
getBoundingBoxGeometry,
roundCoordinates,
} from '../../../../common/elasticsearch_util';
import { Feature } from 'geojson';
import { DRAW_TYPE } from '../../../../common/constants';
import { DrawCircle } from './draw_circle';
import { DrawTooltip } from './draw_tooltip';
import { getToasts } from '../../../kibana_services';
const DRAW_RECTANGLE = 'draw_rectangle';
const DRAW_CIRCLE = 'draw_circle';
@ -35,10 +25,8 @@ mbDrawModes[DRAW_RECTANGLE] = DrawRectangle;
mbDrawModes[DRAW_CIRCLE] = DrawCircle;
export interface Props {
addFilters: (filters: Filter[], actionId: string) => Promise<void>;
disableDrawState: () => void;
drawState?: DrawState;
isDrawingFilter: boolean;
drawType?: DRAW_TYPE;
onDraw: (event: { features: Feature[] }) => void;
mbMap: MbMap;
}
@ -70,100 +58,26 @@ export class DrawControl extends Component<Props, {}> {
return;
}
if (this.props.isDrawingFilter) {
if (this.props.drawType) {
this._updateDrawControl();
} else {
this._removeDrawControl();
}
}, 0);
_onDraw = async (e: { features: Feature[] }) => {
if (
!e.features.length ||
!this.props.drawState ||
!this.props.drawState.geoFieldName ||
!this.props.drawState.indexPatternId
) {
return;
}
let filter: Filter | undefined;
if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
const circle = e.features[0] as Feature & { properties: DrawCircleProperties };
const distanceKm = _.round(
circle.properties.radiusKm,
circle.properties.radiusKm > 10 ? 0 : 2
);
// Only include as much precision as needed for distance
let precision = 2;
if (distanceKm <= 1) {
precision = 5;
} else if (distanceKm <= 10) {
precision = 4;
} else if (distanceKm <= 100) {
precision = 3;
}
filter = createDistanceFilterWithMeta({
alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '',
distanceKm,
geoFieldName: this.props.drawState.geoFieldName,
indexPatternId: this.props.drawState.indexPatternId,
point: [
_.round(circle.properties.center[0], precision),
_.round(circle.properties.center[1], precision),
],
});
} else {
const geometry = e.features[0].geometry as Polygon;
// MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number
roundCoordinates(geometry.coordinates);
filter = createSpatialFilterWithGeometry({
geometry:
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
? getBoundingBoxGeometry(geometry)
: geometry,
indexPatternId: this.props.drawState.indexPatternId,
geoFieldName: this.props.drawState.geoFieldName,
geoFieldType: this.props.drawState.geoFieldType
? this.props.drawState.geoFieldType
: ES_GEO_FIELD_TYPE.GEO_POINT,
geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '',
relation: this.props.drawState.relation
? this.props.drawState.relation
: ES_SPATIAL_RELATIONS.INTERSECTS,
});
}
try {
await this.props.addFilters([filter!], this.props.drawState.actionId);
} catch (error) {
getToasts().addWarning(
i18n.translate('xpack.maps.drawControl.unableToCreatFilter', {
defaultMessage: `Unable to create filter, error: '{errorMsg}'.`,
values: {
errorMsg: error.message,
},
})
);
} finally {
this.props.disableDrawState();
}
};
_removeDrawControl() {
if (!this._mbDrawControlAdded) {
return;
}
this.props.mbMap.getCanvas().style.cursor = '';
this.props.mbMap.off('draw.create', this._onDraw);
this.props.mbMap.off('draw.create', this.props.onDraw);
this.props.mbMap.removeControl(this._mbDrawControl);
this._mbDrawControlAdded = false;
}
_updateDrawControl() {
if (!this.props.drawState) {
if (!this.props.drawType) {
return;
}
@ -171,27 +85,27 @@ export class DrawControl extends Component<Props, {}> {
this.props.mbMap.addControl(this._mbDrawControl);
this._mbDrawControlAdded = true;
this.props.mbMap.getCanvas().style.cursor = 'crosshair';
this.props.mbMap.on('draw.create', this._onDraw);
this.props.mbMap.on('draw.create', this.props.onDraw);
}
const drawMode = this._mbDrawControl.getMode();
if (drawMode !== DRAW_RECTANGLE && this.props.drawState.drawType === DRAW_TYPE.BOUNDS) {
if (drawMode !== DRAW_RECTANGLE && this.props.drawType === DRAW_TYPE.BOUNDS) {
this._mbDrawControl.changeMode(DRAW_RECTANGLE);
} else if (drawMode !== DRAW_CIRCLE && this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
} else if (drawMode !== DRAW_CIRCLE && this.props.drawType === DRAW_TYPE.DISTANCE) {
this._mbDrawControl.changeMode(DRAW_CIRCLE);
} else if (
drawMode !== this._mbDrawControl.modes.DRAW_POLYGON &&
this.props.drawState.drawType === DRAW_TYPE.POLYGON
this.props.drawType === DRAW_TYPE.POLYGON
) {
this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON);
}
}
render() {
if (!this.props.isDrawingFilter || !this.props.drawState) {
if (!this.props.drawType) {
return null;
}
return <DrawTooltip mbMap={this.props.mbMap} drawState={this.props.drawState} />;
return <DrawTooltip mbMap={this.props.mbMap} drawType={this.props.drawType} />;
}
}

View file

@ -0,0 +1,126 @@
/*
* 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.
*/
import _ from 'lodash';
import React, { Component } from 'react';
import { Map as MbMap } from 'mapbox-gl';
import { i18n } from '@kbn/i18n';
import { Filter } from 'src/plugins/data/public';
import { Feature, Polygon } from 'geojson';
import {
DRAW_TYPE,
ES_GEO_FIELD_TYPE,
ES_SPATIAL_RELATIONS,
} from '../../../../../common/constants';
import { DrawState } from '../../../../../common/descriptor_types';
import {
createDistanceFilterWithMeta,
createSpatialFilterWithGeometry,
getBoundingBoxGeometry,
roundCoordinates,
} from '../../../../../common/elasticsearch_util';
import { getToasts } from '../../../../kibana_services';
import { DrawControl } from '../draw_control';
import { DrawCircleProperties } from '../draw_circle';
export interface Props {
addFilters: (filters: Filter[], actionId: string) => Promise<void>;
disableDrawState: () => void;
drawState?: DrawState;
isDrawingFilter: boolean;
mbMap: MbMap;
}
export class DrawFilterControl extends Component<Props, {}> {
_onDraw = async (e: { features: Feature[] }) => {
if (
!e.features.length ||
!this.props.drawState ||
!this.props.drawState.geoFieldName ||
!this.props.drawState.indexPatternId
) {
return;
}
let filter: Filter | undefined;
if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
const circle = e.features[0] as Feature & { properties: DrawCircleProperties };
const distanceKm = _.round(
circle.properties.radiusKm,
circle.properties.radiusKm > 10 ? 0 : 2
);
// Only include as much precision as needed for distance
let precision = 2;
if (distanceKm <= 1) {
precision = 5;
} else if (distanceKm <= 10) {
precision = 4;
} else if (distanceKm <= 100) {
precision = 3;
}
filter = createDistanceFilterWithMeta({
alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '',
distanceKm,
geoFieldName: this.props.drawState.geoFieldName,
indexPatternId: this.props.drawState.indexPatternId,
point: [
_.round(circle.properties.center[0], precision),
_.round(circle.properties.center[1], precision),
],
});
} else {
const geometry = e.features[0].geometry as Polygon;
// MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number
roundCoordinates(geometry.coordinates);
filter = createSpatialFilterWithGeometry({
geometry:
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
? getBoundingBoxGeometry(geometry)
: geometry,
indexPatternId: this.props.drawState.indexPatternId,
geoFieldName: this.props.drawState.geoFieldName,
geoFieldType: this.props.drawState.geoFieldType
? this.props.drawState.geoFieldType
: ES_GEO_FIELD_TYPE.GEO_POINT,
geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '',
relation: this.props.drawState.relation
? this.props.drawState.relation
: ES_SPATIAL_RELATIONS.INTERSECTS,
});
}
try {
await this.props.addFilters([filter!], this.props.drawState.actionId);
} catch (error) {
getToasts().addWarning(
i18n.translate('xpack.maps.drawFilterControl.unableToCreatFilter', {
defaultMessage: `Unable to create filter, error: '{errorMsg}'.`,
values: {
errorMsg: error.message,
},
})
);
} finally {
this.props.disableDrawState();
}
};
render() {
return (
<DrawControl
drawType={
this.props.isDrawingFilter && this.props.drawState
? this.props.drawState.drawType
: undefined
}
onDraw={this._onDraw}
mbMap={this.props.mbMap}
/>
);
}
}

View file

@ -0,0 +1,32 @@
/*
* 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.
*/
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { connect } from 'react-redux';
import { DrawFilterControl } from './draw_filter_control';
import { updateDrawState } from '../../../../actions';
import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors';
import { MapStoreState } from '../../../../reducers/store';
function mapStateToProps(state: MapStoreState) {
return {
isDrawingFilter: isDrawingFilter(state),
drawState: getDrawState(state),
};
}
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
return {
disableDrawState() {
dispatch(updateDrawState(null));
},
};
}
const connected = connect(mapStateToProps, mapDispatchToProps)(DrawFilterControl);
export { connected as DrawFilterControl };

View file

@ -11,13 +11,12 @@ import { EuiPopover, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Map as MbMap } from 'mapbox-gl';
import { DRAW_TYPE } from '../../../../common/constants';
import { DrawState } from '../../../../common/descriptor_types';
const noop = () => {};
interface Props {
mbMap: MbMap;
drawState: DrawState;
drawType: DRAW_TYPE;
}
interface State {
@ -58,16 +57,16 @@ export class DrawTooltip extends Component<Props, State> {
}
let instructions;
if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) {
if (this.props.drawType === DRAW_TYPE.BOUNDS) {
instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', {
defaultMessage:
'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.',
});
} else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
} else if (this.props.drawType === DRAW_TYPE.DISTANCE) {
instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', {
defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.',
});
} else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) {
} else if (this.props.drawType === DRAW_TYPE.POLYGON) {
instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', {
defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.',
});

View file

@ -5,28 +5,4 @@
* 2.0.
*/
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { connect } from 'react-redux';
import { DrawControl } from './draw_control';
import { updateDrawState } from '../../../actions';
import { getDrawState, isDrawingFilter } from '../../../selectors/map_selectors';
import { MapStoreState } from '../../../reducers/store';
function mapStateToProps(state: MapStoreState) {
return {
isDrawingFilter: isDrawingFilter(state),
drawState: getDrawState(state),
};
}
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
return {
disableDrawState() {
dispatch(updateDrawState(null));
},
};
}
const connected = connect(mapStateToProps, mapDispatchToProps)(DrawControl);
export { connected as DrawControl };
export { DrawFilterControl } from './draw_filter_control';

View file

@ -17,7 +17,7 @@ import sprites2 from '@elastic/maki/dist/sprite@2.png';
import { Adapters } from 'src/plugins/inspector/public';
import { Filter } from 'src/plugins/data/public';
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
import { DrawControl } from './draw_control';
import { DrawFilterControl } from './draw_control';
import { ScaleControl } from './scale_control';
// @ts-expect-error
import { TooltipControl } from './tooltip_control';
@ -418,7 +418,7 @@ export class MBMap extends Component<Props, State> {
let scaleControl;
if (this.state.mbMap) {
drawControl = this.props.addFilters ? (
<DrawControl mbMap={this.state.mbMap} addFilters={this.props.addFilters} />
<DrawFilterControl mbMap={this.state.mbMap} addFilters={this.props.addFilters} />
) : null;
tooltipControl = !this.props.settings.disableTooltipControl ? (
<TooltipControl