diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 6b9f32b92b9c19..1720f0ebdb5582 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -144,6 +144,7 @@ export enum DRAW_SHAPE { LINE = 'LINE', SIMPLE_SELECT = 'SIMPLE_SELECT', DELETE = 'DELETE', + WAIT = 'WAIT', } export const AGG_DELIMITER = '_of_'; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 2abbfc0d076a1e..cccb49f3606226 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -14,6 +14,7 @@ import turfBooleanContains from '@turf/boolean-contains'; import { Filter } from '@kbn/es-query'; import { Query, TimeRange } from 'src/plugins/data/public'; import { Geometry, Position } from 'geojson'; +import { asyncForEach } from '@kbn/std'; import { DRAW_MODE, DRAW_SHAPE } from '../../common/constants'; import type { MapExtentState, MapViewContext } from '../reducers/map/types'; import { MapStoreState } from '../reducers/store'; @@ -63,9 +64,10 @@ import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/ import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; -import { SET_DRAW_MODE } from './ui_actions'; +import { SET_DRAW_MODE, pushDeletedFeatureId, clearDeletedFeatureIds } from './ui_actions'; import { expandToTileBoundaries } from '../classes/util/geo_tile_utils'; import { getToasts } from '../kibana_services'; +import { getDeletedFeatureIds } from '../selectors/ui_selectors'; export function setMapInitError(errorMessage: string) { return { @@ -321,6 +323,10 @@ export function updateEditShape(shapeToDraw: DRAW_SHAPE | null) { drawShape: shapeToDraw, }, }); + + if (shapeToDraw !== DRAW_SHAPE.DELETE) { + dispatch(clearDeletedFeatureIds()); + } }; } @@ -353,7 +359,7 @@ export function updateEditLayer(layerId: string | null) { }; } -export function addNewFeatureToIndex(geometry: Geometry | Position[]) { +export function addNewFeatureToIndex(geometries: Array) { return async ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -369,7 +375,10 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { } try { - await (layer as IVectorLayer).addFeature(geometry); + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); + await asyncForEach(geometries, async (geometry) => { + await (layer as IVectorLayer).addFeature(geometry); + }); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -378,6 +387,7 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.SIMPLE_SELECT)); }; } @@ -386,6 +396,12 @@ export function deleteFeatureFromIndex(featureId: string) { dispatch: ThunkDispatch, getState: () => MapStoreState ) => { + // There is a race condition where users can click on a previously deleted feature before layer has re-rendered after feature delete. + // Check ensures delete requests for previously deleted features are aborted. + if (getDeletedFeatureIds(getState()).includes(featureId)) { + return; + } + const editState = getEditState(getState()); const layerId = editState ? editState.layerId : undefined; if (!layerId) { @@ -395,8 +411,11 @@ export function deleteFeatureFromIndex(featureId: string) { if (!layer || !isVectorLayer(layer)) { return; } + try { + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); await (layer as IVectorLayer).deleteFeature(featureId); + dispatch(pushDeletedFeatureId(featureId)); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -405,5 +424,6 @@ export function deleteFeatureFromIndex(featureId: string) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.DELETE)); }; } diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts index 70e24283ef48f7..1ffcf416f6f8fa 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.ts @@ -24,6 +24,8 @@ export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; export const SET_DRAW_MODE = 'SET_DRAW_MODE'; +export const PUSH_DELETED_FEATURE_ID = 'PUSH_DELETED_FEATURE_ID'; +export const CLEAR_DELETED_FEATURE_IDS = 'CLEAR_DELETED_FEATURE_IDS'; export function exitFullScreen() { return { @@ -123,3 +125,16 @@ export function closeTimeslider() { dispatch(setQuery({ clearTimeslice: true })); }; } + +export function pushDeletedFeatureId(featureId: string) { + return { + type: PUSH_DELETED_FEATURE_ID, + featureId, + }; +} + +export function clearDeletedFeatureIds() { + return { + type: CLEAR_DELETED_FEATURE_IDS, + }; +} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index a404db91a942ef..8cbfcd3a41e80e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -20,15 +20,6 @@ import { DRAW_SHAPE } from '../../../../common/constants'; import { DrawCircle, DRAW_CIRCLE_RADIUS_LABEL_STYLE } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; -const mbModeEquivalencies = new Map([ - ['simple_select', DRAW_SHAPE.SIMPLE_SELECT], - ['draw_rectangle', DRAW_SHAPE.BOUNDS], - ['draw_circle', DRAW_SHAPE.DISTANCE], - ['draw_polygon', DRAW_SHAPE.POLYGON], - ['draw_line_string', DRAW_SHAPE.LINE], - ['draw_point', DRAW_SHAPE.POINT], -]); - const DRAW_RECTANGLE = 'draw_rectangle'; const DRAW_CIRCLE = 'draw_circle'; const mbDrawModes = MapboxDraw.modes; @@ -41,7 +32,6 @@ export interface Props { onClick?: (event: MapMouseEvent, drawControl?: MapboxDraw) => void; mbMap: MbMap; enable: boolean; - updateEditShape: (shapeToDraw: DRAW_SHAPE) => void; } export class DrawControl extends Component { @@ -91,12 +81,6 @@ export class DrawControl extends Component { } }, 0); - _onModeChange = ({ mode }: { mode: string }) => { - if (mbModeEquivalencies.has(mode)) { - this.props.updateEditShape(mbModeEquivalencies.get(mode)!); - } - }; - _removeDrawControl() { // Do not remove draw control after mbMap.remove is called, causes execeptions and mbMap.remove cleans up all map resources. const isMapRemoved = !this.props.mbMap.loaded(); @@ -105,7 +89,6 @@ export class DrawControl extends Component { } this.props.mbMap.getCanvas().style.cursor = ''; - this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); if (this.props.onClick) { this.props.mbMap.off('click', this._onClick); @@ -118,7 +101,6 @@ export class DrawControl extends Component { if (!this._mbDrawControlAdded) { this.props.mbMap.addControl(this._mbDrawControl); this._mbDrawControlAdded = true; - this.props.mbMap.on('draw.modechange', this._onModeChange); this.props.mbMap.on('draw.create', this._onDraw); if (this.props.onClick) { @@ -144,6 +126,9 @@ export class DrawControl extends Component { this._mbDrawControl.changeMode(DRAW_POINT); } else if (this.props.drawShape === DRAW_SHAPE.DELETE) { this._mbDrawControl.changeMode(SIMPLE_SELECT); + } else if (this.props.drawShape === DRAW_SHAPE.WAIT) { + this.props.mbMap.getCanvas().style.cursor = 'wait'; + this._mbDrawControl.changeMode(SIMPLE_SELECT); } else { this._mbDrawControl.changeMode(SIMPLE_SELECT); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx index 6c7fe9f0ad213a..b6ffacc491030e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import * as jsts from 'jsts'; import { MapMouseEvent } from '@kbn/mapbox-gl'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common/constants'; import { ILayer } from '../../../../classes/layers/layer'; import { EXCLUDE_CENTROID_FEATURES } from '../../../../classes/util/mb_filter_expressions'; @@ -29,9 +29,8 @@ export interface ReduxStateProps { } export interface ReduxDispatchProps { - addNewFeatureToIndex: (geometry: Geometry | Position[]) => void; + addNewFeatureToIndex: (geometries: Array) => void; deleteFeatureFromIndex: (featureId: string) => void; - disableDrawState: () => void; } export interface OwnProps { @@ -43,6 +42,7 @@ type Props = ReduxStateProps & ReduxDispatchProps & OwnProps; export class DrawFeatureControl extends Component { _onDraw = async (e: { features: Feature[] }, mbDrawControl: MapboxDraw) => { try { + const geometries: Array = []; e.features.forEach((feature: Feature) => { const { geometry } = geoJSONReader.read(feature); if (!geometry.isSimple() || !geometry.isValid()) { @@ -58,9 +58,13 @@ export class DrawFeatureControl extends Component { this.props.drawMode === DRAW_MODE.DRAW_POINTS ? feature.geometry.coordinates : feature.geometry; - this.props.addNewFeatureToIndex(featureGeom); + geometries.push(featureGeom); } }); + + if (geometries.length) { + this.props.addNewFeatureToIndex(geometries); + } } catch (error) { getToasts().addWarning( i18n.translate('xpack.maps.drawFeatureControl.unableToCreateFeature', { @@ -71,7 +75,6 @@ export class DrawFeatureControl extends Component { }) ); } finally { - this.props.disableDrawState(); try { mbDrawControl.deleteAll(); } catch (_e) { @@ -86,6 +89,7 @@ export class DrawFeatureControl extends Component { if (!this.props.editLayer || this.props.drawShape !== DRAW_SHAPE.DELETE) { return; } + const mbEditLayerIds = this.props.editLayer .getMbLayerIds() .filter((mbLayerId) => !!this.props.mbMap.getLayer(mbLayerId)); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts index e1d703173fc2da..d2c369b4bd50a2 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts @@ -15,7 +15,7 @@ import { ReduxStateProps, OwnProps, } from './draw_feature_control'; -import { addNewFeatureToIndex, deleteFeatureFromIndex, updateEditShape } from '../../../../actions'; +import { addNewFeatureToIndex, deleteFeatureFromIndex } from '../../../../actions'; import { MapStoreState } from '../../../../reducers/store'; import { getEditState, getLayerById } from '../../../../selectors/map_selectors'; import { getDrawMode } from '../../../../selectors/ui_selectors'; @@ -34,15 +34,12 @@ function mapDispatchToProps( dispatch: ThunkDispatch ): ReduxDispatchProps { return { - addNewFeatureToIndex(geometry: Geometry | Position[]) { - dispatch(addNewFeatureToIndex(geometry)); + addNewFeatureToIndex(geometries: Array) { + dispatch(addNewFeatureToIndex(geometries)); }, deleteFeatureFromIndex(featureId: string) { dispatch(deleteFeatureFromIndex(featureId)); }, - disableDrawState() { - dispatch(updateEditShape(null)); - }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx index 2f652506857d2a..98d88d43fc65f9 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx @@ -20,7 +20,7 @@ import { roundCoordinates, } from '../../../../../common/elasticsearch_util'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DrawCircleProperties } from '../draw_circle'; export interface Props { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts deleted file mode 100644 index b0f1941caec08c..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { ThunkDispatch } from 'redux-thunk'; -import { AnyAction } from 'redux'; -import { connect } from 'react-redux'; -import { updateEditShape } from '../../../actions'; -import { MapStoreState } from '../../../reducers/store'; -import { DrawControl } from './draw_control'; -import { DRAW_SHAPE } from '../../../../common/constants'; - -function mapDispatchToProps(dispatch: ThunkDispatch) { - return { - updateEditShape(shapeToDraw: DRAW_SHAPE) { - dispatch(updateEditShape(shapeToDraw)); - }, - }; -} - -const connected = connect(null, mapDispatchToProps)(DrawControl); -export { connected as DrawControl }; diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index f3f948bb96508a..f0f22c5a8c4a93 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -19,6 +19,8 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, SET_DRAW_MODE, + PUSH_DELETED_FEATURE_ID, + CLEAR_DELETED_FEATURE_IDS, } from '../actions'; import { DRAW_MODE } from '../../common/constants'; @@ -37,6 +39,7 @@ export type MapUiState = { isLayerTOCOpen: boolean; isTimesliderOpen: boolean; openTOCDetails: string[]; + deletedFeatureIds: string[]; }; export const DEFAULT_IS_LAYER_TOC_OPEN = true; @@ -51,6 +54,7 @@ export const DEFAULT_MAP_UI_STATE = { // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], + deletedFeatureIds: [], }; // Reducer @@ -82,6 +86,16 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) { return layerId !== action.layerId; }), }; + case PUSH_DELETED_FEATURE_ID: + return { + ...state, + deletedFeatureIds: [...state.deletedFeatureIds, action.featureId], + }; + case CLEAR_DELETED_FEATURE_IDS: + return { + ...state, + deletedFeatureIds: [], + }; default: return state; } diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts index 942a5190691a15..6bdf5a35679a73 100644 --- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -17,3 +17,4 @@ export const getIsTimesliderOpen = ({ ui }: MapStoreState): boolean => ui.isTime export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getDeletedFeatureIds = ({ ui }: MapStoreState): string[] => ui.deletedFeatureIds;