mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Maps] Add layer edit controls (#99812)
Co-authored-by: miukimiu <elizabet.oliveira@elastic.co>
This commit is contained in:
parent
d57ffce8ec
commit
246e7be3e5
86 changed files with 1213 additions and 235 deletions
|
@ -41,8 +41,9 @@ export const GIS_API_PATH = `api/${APP_ID}`;
|
|||
export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`;
|
||||
export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`;
|
||||
export const INDEX_SOURCE_API_PATH = `${GIS_API_PATH}/docSource`;
|
||||
export const INDEX_FEATURE_PATH = `/${GIS_API_PATH}/feature`;
|
||||
export const API_ROOT_PATH = `/${GIS_API_PATH}`;
|
||||
export const INDEX_FEATURE_PATH = `/${GIS_API_PATH}/feature`;
|
||||
export const GET_MATCHING_INDEXES_PATH = `/${GIS_API_PATH}/getMatchingIndexes`;
|
||||
|
||||
export const MVT_GETTILE_API_PATH = 'mvt/getTile';
|
||||
export const MVT_GETGRIDTILE_API_PATH = 'mvt/getGridTile';
|
||||
|
@ -106,6 +107,7 @@ export const SOURCE_DATA_REQUEST_ID = 'source';
|
|||
export const SOURCE_META_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${META_DATA_REQUEST_ID_SUFFIX}`;
|
||||
export const SOURCE_FORMATTERS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${FORMATTERS_DATA_REQUEST_ID_SUFFIX}`;
|
||||
export const SOURCE_BOUNDS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_bounds`;
|
||||
export const IS_EDITABLE_REQUEST_ID = 'isEditable';
|
||||
|
||||
export const MIN_ZOOM = 0;
|
||||
export const MAX_ZOOM = 24;
|
||||
|
@ -154,10 +156,20 @@ export const EMPTY_FEATURE_COLLECTION: FeatureCollection = {
|
|||
features: [],
|
||||
};
|
||||
|
||||
export enum DRAW_TYPE {
|
||||
export enum DRAW_MODE {
|
||||
DRAW_SHAPES = 'DRAW_SHAPES',
|
||||
DRAW_POINTS = 'DRAW_POINTS',
|
||||
DRAW_FILTERS = 'DRAW_FILTERS',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
|
||||
export enum DRAW_SHAPE {
|
||||
BOUNDS = 'BOUNDS',
|
||||
DISTANCE = 'DISTANCE',
|
||||
POLYGON = 'POLYGON',
|
||||
POINT = 'POINT',
|
||||
LINE = 'LINE',
|
||||
SIMPLE_SELECT = 'SIMPLE_SELECT',
|
||||
}
|
||||
|
||||
export const AGG_DELIMITER = '_of_';
|
||||
|
|
|
@ -29,9 +29,10 @@ export type MapFilters = {
|
|||
timeFilters: TimeRange;
|
||||
timeslice?: Timeslice;
|
||||
zoom: number;
|
||||
isReadOnly: boolean;
|
||||
};
|
||||
|
||||
type ESSearchSourceSyncMeta = {
|
||||
export type ESSearchSourceSyncMeta = {
|
||||
filterByMapBounds: boolean;
|
||||
sortField: string;
|
||||
sortOrder: SortDirection;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ReactNode } from 'react';
|
|||
import { GeoJsonProperties } from 'geojson';
|
||||
import { Geometry } from 'geojson';
|
||||
import { Query } from '../../../../../src/plugins/data/common';
|
||||
import { DRAW_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
|
||||
import { DRAW_SHAPE, ES_SPATIAL_RELATIONS } from '../constants';
|
||||
|
||||
export type MapExtent = {
|
||||
minLon: number;
|
||||
|
@ -63,8 +63,13 @@ export type TooltipState = {
|
|||
|
||||
export type DrawState = {
|
||||
actionId: string;
|
||||
drawType: DRAW_TYPE;
|
||||
drawShape?: DRAW_SHAPE;
|
||||
filterLabel?: string; // point radius filter alias
|
||||
geometryLabel?: string;
|
||||
relation?: ES_SPATIAL_RELATIONS;
|
||||
};
|
||||
|
||||
export type EditState = {
|
||||
layerId: string;
|
||||
drawShape?: DRAW_SHAPE;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,12 @@ export interface CreateDocSourceResp {
|
|||
error?: Error;
|
||||
}
|
||||
|
||||
export interface MatchingIndexesResp {
|
||||
matchingIndexes?: string[];
|
||||
success: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface IndexSourceMappings {
|
||||
_meta?: {
|
||||
created_by: string;
|
||||
|
|
|
@ -54,13 +54,14 @@ import { IVectorStyle } from '../classes/styles/vector/vector_style';
|
|||
const FIT_TO_BOUNDS_SCALE_FACTOR = 0.1;
|
||||
|
||||
export type DataRequestContext = {
|
||||
startLoading(dataId: string, requestToken: symbol, requestMeta: DataMeta): void;
|
||||
startLoading(dataId: string, requestToken: symbol, requestMeta?: DataMeta): void;
|
||||
stopLoading(dataId: string, requestToken: symbol, data: object, resultsMeta?: DataMeta): void;
|
||||
onLoadError(dataId: string, requestToken: symbol, errorMessage: string): void;
|
||||
updateSourceData(newData: unknown): void;
|
||||
isRequestStillActive(dataId: string, requestToken: symbol): boolean;
|
||||
registerCancelCallback(requestToken: symbol, callback: () => void): void;
|
||||
dataFilters: MapFilters;
|
||||
forceRefresh: boolean;
|
||||
};
|
||||
|
||||
export function clearDataRequests(layer: ILayer) {
|
||||
|
@ -113,7 +114,8 @@ export function updateStyleMeta(layerId: string | null) {
|
|||
function getDataRequestContext(
|
||||
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>,
|
||||
getState: () => MapStoreState,
|
||||
layerId: string
|
||||
layerId: string,
|
||||
forceRefresh: boolean = false
|
||||
): DataRequestContext {
|
||||
return {
|
||||
dataFilters: getDataFilters(getState()),
|
||||
|
@ -135,6 +137,7 @@ function getDataRequestContext(
|
|||
},
|
||||
registerCancelCallback: (requestToken: symbol, callback: () => void) =>
|
||||
dispatch(registerCancelCallback(requestToken, callback)),
|
||||
forceRefresh,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -166,9 +169,14 @@ function syncDataForAllJoinLayers() {
|
|||
};
|
||||
}
|
||||
|
||||
export function syncDataForLayer(layer: ILayer) {
|
||||
export function syncDataForLayer(layer: ILayer, forceRefresh: boolean = false) {
|
||||
return async (dispatch: Dispatch, getState: () => MapStoreState) => {
|
||||
const dataRequestContext = getDataRequestContext(dispatch, getState, layer.getId());
|
||||
const dataRequestContext = getDataRequestContext(
|
||||
dispatch,
|
||||
getState,
|
||||
layer.getId(),
|
||||
forceRefresh
|
||||
);
|
||||
if (!layer.isVisible() || !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -10,17 +10,17 @@ import { ThunkDispatch } from 'redux-thunk';
|
|||
import { Query } from 'src/plugins/data/public';
|
||||
import { MapStoreState } from '../reducers/store';
|
||||
import {
|
||||
createLayerInstance,
|
||||
getLayerById,
|
||||
getLayerList,
|
||||
getLayerListRaw,
|
||||
getSelectedLayerId,
|
||||
getMapReady,
|
||||
getMapColors,
|
||||
createLayerInstance,
|
||||
getMapReady,
|
||||
getSelectedLayerId,
|
||||
} from '../selectors/map_selectors';
|
||||
import { FLYOUT_STATE } from '../reducers/ui';
|
||||
import { cancelRequest } from '../reducers/non_serializable_instances';
|
||||
import { updateFlyout } from './ui_actions';
|
||||
import { setDrawMode, updateFlyout } from './ui_actions';
|
||||
import {
|
||||
ADD_LAYER,
|
||||
ADD_WAITING_FOR_MAP_READY_LAYER,
|
||||
|
@ -49,11 +49,12 @@ import {
|
|||
} from '../../common/descriptor_types';
|
||||
import { ILayer } from '../classes/layers/layer';
|
||||
import { IVectorLayer } from '../classes/layers/vector_layer';
|
||||
import { LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants';
|
||||
import { DRAW_MODE, LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants';
|
||||
import { IVectorStyle } from '../classes/styles/vector/vector_style';
|
||||
import { notifyLicensedFeatureUsage } from '../licensed_features';
|
||||
import { IESAggField } from '../classes/fields/agg';
|
||||
import { IField } from '../classes/fields/field';
|
||||
import { getDrawMode } from '../selectors/ui_selectors';
|
||||
|
||||
export function trackCurrentLayerState(layerId: string) {
|
||||
return {
|
||||
|
@ -255,6 +256,9 @@ export function setSelectedLayer(layerId: string | null) {
|
|||
if (layerId) {
|
||||
dispatch(trackCurrentLayerState(layerId));
|
||||
}
|
||||
if (getDrawMode(getState()) !== DRAW_MODE.NONE) {
|
||||
dispatch(setDrawMode(DRAW_MODE.NONE));
|
||||
}
|
||||
dispatch({
|
||||
type: SET_SELECTED_LAYER,
|
||||
selectedLayerId: layerId,
|
||||
|
|
|
@ -36,6 +36,7 @@ export const ROLLBACK_TO_TRACKED_LAYER_STATE = 'ROLLBACK_TO_TRACKED_LAYER_STATE'
|
|||
export const REMOVE_TRACKED_LAYER_STATE = 'REMOVE_TRACKED_LAYER_STATE';
|
||||
export const SET_OPEN_TOOLTIPS = 'SET_OPEN_TOOLTIPS';
|
||||
export const UPDATE_DRAW_STATE = 'UPDATE_DRAW_STATE';
|
||||
export const UPDATE_EDIT_STATE = 'UPDATE_EDIT_STATE';
|
||||
export const SET_SCROLL_ZOOM = 'SET_SCROLL_ZOOM';
|
||||
export const SET_MAP_INIT_ERROR = 'SET_MAP_INIT_ERROR';
|
||||
export const SET_WAITING_FOR_READY_HIDDEN_LAYERS = 'SET_WAITING_FOR_READY_HIDDEN_LAYERS';
|
||||
|
|
|
@ -11,6 +11,8 @@ import { ThunkDispatch } from 'redux-thunk';
|
|||
import turfBboxPolygon from '@turf/bbox-polygon';
|
||||
import turfBooleanContains from '@turf/boolean-contains';
|
||||
import { Filter, Query, TimeRange } from 'src/plugins/data/public';
|
||||
import { Geometry, Position } from 'geojson';
|
||||
import { DRAW_MODE, DRAW_SHAPE } from '../../common/constants';
|
||||
import { MapStoreState } from '../reducers/store';
|
||||
import {
|
||||
getDataFilters,
|
||||
|
@ -23,6 +25,8 @@ import {
|
|||
getLayerList,
|
||||
getSearchSessionId,
|
||||
getSearchSessionMapBuffer,
|
||||
getLayerById,
|
||||
getEditState,
|
||||
} from '../selectors/map_selectors';
|
||||
import {
|
||||
CLEAR_GOTO,
|
||||
|
@ -42,8 +46,9 @@ import {
|
|||
TRACK_MAP_SETTINGS,
|
||||
UPDATE_DRAW_STATE,
|
||||
UPDATE_MAP_SETTING,
|
||||
UPDATE_EDIT_STATE,
|
||||
} from './map_action_constants';
|
||||
import { autoFitToBounds, syncDataForAllLayers } from './data_request_actions';
|
||||
import { autoFitToBounds, syncDataForAllLayers, syncDataForLayer } from './data_request_actions';
|
||||
import { addLayer, addLayerWithoutDataSync } from './layer_actions';
|
||||
import { MapSettings } from '../reducers/map';
|
||||
import {
|
||||
|
@ -56,6 +61,8 @@ import {
|
|||
import { INITIAL_LOCATION } from '../../common/constants';
|
||||
import { scaleBounds } from '../../common/elasticsearch_util';
|
||||
import { cleanTooltipStateForLayer } from './tooltip_actions';
|
||||
import { VectorLayer } from '../classes/layers/vector_layer';
|
||||
import { SET_DRAW_MODE } from './ui_actions';
|
||||
import { expandToTileBoundaries } from '../../common/geo_tile_utils';
|
||||
|
||||
export interface MapExtentState {
|
||||
|
@ -318,3 +325,54 @@ export function updateDrawState(drawState: DrawState | null) {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function updateEditShape(shapeToDraw: DRAW_SHAPE | null) {
|
||||
return (dispatch: Dispatch, getState: () => MapStoreState) => {
|
||||
const editState = getEditState(getState());
|
||||
if (!editState) {
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: UPDATE_EDIT_STATE,
|
||||
editState: {
|
||||
...editState,
|
||||
drawShape: shapeToDraw,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function updateEditLayer(layerId: string | null) {
|
||||
return (dispatch: Dispatch) => {
|
||||
if (layerId !== null) {
|
||||
dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] });
|
||||
}
|
||||
dispatch({
|
||||
type: SET_DRAW_MODE,
|
||||
drawMode: DRAW_MODE.NONE,
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_EDIT_STATE,
|
||||
editState: layerId ? { layerId } : undefined,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addNewFeatureToIndex(geometry: Geometry | Position[]) {
|
||||
return async (
|
||||
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>,
|
||||
getState: () => MapStoreState
|
||||
) => {
|
||||
const editState = getEditState(getState());
|
||||
const layerId = editState ? editState.layerId : undefined;
|
||||
if (!layerId) {
|
||||
return;
|
||||
}
|
||||
const layer = getLayerById(layerId, getState());
|
||||
if (!layer || !(layer instanceof VectorLayer)) {
|
||||
return;
|
||||
}
|
||||
await layer.addFeature(geometry);
|
||||
await dispatch(syncDataForLayer(layer, true));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import { getFlyoutDisplay } from '../selectors/ui_selectors';
|
|||
import { FLYOUT_STATE } from '../reducers/ui';
|
||||
import { setQuery, trackMapSettings } from './map_actions';
|
||||
import { setSelectedLayer } from './layer_actions';
|
||||
import { DRAW_MODE } from '../../common';
|
||||
import { UPDATE_EDIT_STATE } from './map_action_constants';
|
||||
|
||||
export const UPDATE_FLYOUT = 'UPDATE_FLYOUT';
|
||||
export const SET_IS_LAYER_TOC_OPEN = 'SET_IS_LAYER_TOC_OPEN';
|
||||
|
@ -21,6 +23,7 @@ export const SET_READ_ONLY = 'SET_READ_ONLY';
|
|||
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 function exitFullScreen() {
|
||||
return {
|
||||
|
@ -89,6 +92,21 @@ export function hideTOCDetails(layerId: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setDrawMode(drawMode: DRAW_MODE) {
|
||||
return (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => {
|
||||
if (drawMode === DRAW_MODE.NONE) {
|
||||
dispatch({
|
||||
type: UPDATE_EDIT_STATE,
|
||||
editState: undefined,
|
||||
});
|
||||
}
|
||||
dispatch({
|
||||
type: SET_DRAW_MODE,
|
||||
drawMode,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function openTimeslider() {
|
||||
return {
|
||||
type: SET_IS_TIME_SLIDER_OPEN,
|
||||
|
|
|
@ -17,6 +17,7 @@ export class MockSyncContext implements DataRequestContext {
|
|||
startLoading: (dataId: string, requestToken: symbol, meta: DataMeta) => void;
|
||||
stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataMeta) => void;
|
||||
updateSourceData: (newData: unknown) => void;
|
||||
forceRefresh: boolean;
|
||||
|
||||
constructor({ dataFilters }: { dataFilters: Partial<MapFilters> }) {
|
||||
const mapFilters: MapFilters = {
|
||||
|
@ -27,6 +28,7 @@ export class MockSyncContext implements DataRequestContext {
|
|||
mode: 'relative',
|
||||
},
|
||||
zoom: 0,
|
||||
isReadOnly: false,
|
||||
...dataFilters,
|
||||
};
|
||||
|
||||
|
@ -37,5 +39,6 @@ export class MockSyncContext implements DataRequestContext {
|
|||
this.startLoading = sinon.spy();
|
||||
this.stopLoading = sinon.spy();
|
||||
this.updateSourceData = sinon.spy();
|
||||
this.forceRefresh = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,12 +69,14 @@ export async function syncVectorSource({
|
|||
} = syncContext;
|
||||
const dataRequestId = SOURCE_DATA_REQUEST_ID;
|
||||
const requestToken = Symbol(`${layerId}-${dataRequestId}`);
|
||||
const canSkipFetch = await canSkipSourceUpdate({
|
||||
source,
|
||||
prevDataRequest,
|
||||
nextMeta: requestMeta,
|
||||
extentAware: source.isFilterByMapBounds(),
|
||||
});
|
||||
const canSkipFetch = syncContext.forceRefresh
|
||||
? false
|
||||
: await canSkipSourceUpdate({
|
||||
source,
|
||||
prevDataRequest,
|
||||
nextMeta: requestMeta,
|
||||
extentAware: source.isFilterByMapBounds(),
|
||||
});
|
||||
if (canSkipFetch) {
|
||||
return {
|
||||
refreshed: false,
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
AnyLayer as MbLayer,
|
||||
GeoJSONSource as MbGeoJSONSource,
|
||||
} from '@kbn/mapbox-gl';
|
||||
import { Feature, FeatureCollection, GeoJsonProperties } from 'geojson';
|
||||
import { Feature, FeatureCollection, GeoJsonProperties, Geometry, Position } from 'geojson';
|
||||
import _ from 'lodash';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -28,6 +28,7 @@ import {
|
|||
FIELD_ORIGIN,
|
||||
KBN_TOO_MANY_FEATURES_IMAGE_ID,
|
||||
FieldFormatter,
|
||||
IS_EDITABLE_REQUEST_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property';
|
||||
import { DataRequestAbortError } from '../../util/data_request';
|
||||
|
@ -43,7 +44,6 @@ import {
|
|||
getLineFilterExpression,
|
||||
getPointFilterExpression,
|
||||
} from '../../util/mb_filter_expressions';
|
||||
|
||||
import {
|
||||
DynamicStylePropertyOptions,
|
||||
MapFilters,
|
||||
|
@ -93,12 +93,13 @@ export interface IVectorLayer extends ILayer {
|
|||
getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
|
||||
hasJoins(): boolean;
|
||||
canShowTooltip(): boolean;
|
||||
supportsFeatureEditing(): boolean;
|
||||
getLeftJoinFields(): Promise<IField[]>;
|
||||
addFeature(geometry: Geometry | Position[]): Promise<void>;
|
||||
}
|
||||
|
||||
export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
||||
static type = LAYER_TYPE.VECTOR;
|
||||
|
||||
protected readonly _style: IVectorStyle;
|
||||
private readonly _joins: InnerJoin[];
|
||||
|
||||
|
@ -175,6 +176,13 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
});
|
||||
}
|
||||
|
||||
supportsFeatureEditing(): boolean {
|
||||
const dataRequest = this.getDataRequest(IS_EDITABLE_REQUEST_ID);
|
||||
const data = dataRequest?.getData() as { isEditable: boolean } | undefined;
|
||||
|
||||
return data ? data.isEditable : false;
|
||||
}
|
||||
|
||||
hasJoins() {
|
||||
return this.getValidJoins().length > 0;
|
||||
}
|
||||
|
@ -670,6 +678,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
syncContext,
|
||||
source,
|
||||
});
|
||||
await this._syncIsEditable({ syncContext });
|
||||
if (
|
||||
!sourceResult.featureCollection ||
|
||||
!sourceResult.featureCollection.features.length ||
|
||||
|
@ -687,6 +696,27 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
}
|
||||
}
|
||||
|
||||
async _syncIsEditable({ syncContext }: { syncContext: DataRequestContext }) {
|
||||
if (syncContext.dataFilters.isReadOnly) {
|
||||
return;
|
||||
}
|
||||
const { startLoading, stopLoading, onLoadError } = syncContext;
|
||||
const dataRequestId = IS_EDITABLE_REQUEST_ID;
|
||||
const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`);
|
||||
const prevDataRequest = this.getDataRequest(dataRequestId);
|
||||
if (prevDataRequest) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
startLoading(dataRequestId, requestToken);
|
||||
const isEditable = await this.getSource().loadIsEditable();
|
||||
stopLoading(dataRequestId, requestToken, { isEditable });
|
||||
} catch (error) {
|
||||
onLoadError(dataRequestId, requestToken, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
_getSourceFeatureCollection() {
|
||||
const sourceDataRequest = this.getSourceDataRequest();
|
||||
return sourceDataRequest ? (sourceDataRequest.getData() as FeatureCollection) : null;
|
||||
|
@ -1057,4 +1087,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
|||
async getLicensedFeatures() {
|
||||
return await this._source.getLicensedFeatures();
|
||||
}
|
||||
|
||||
async addFeature(geometry: Geometry | Position[]) {
|
||||
const layerSource = this.getSource();
|
||||
await layerSource.addFeature(geometry);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,6 +144,7 @@ describe('ESGeoGridSource', () => {
|
|||
};
|
||||
|
||||
const vectorSourceRequestMeta: VectorSourceRequestMeta = {
|
||||
isReadOnly: false,
|
||||
geogridPrecision: 4,
|
||||
filters: [],
|
||||
timeFilters: {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { GeoIndexPatternSelect } from '../../../components/geo_index_pattern_sel
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { SCALING_TYPES } from '../../../../common/constants';
|
||||
import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
|
||||
import { ScalingForm } from './scaling_form';
|
||||
import { ScalingForm } from './util/scaling_form';
|
||||
import {
|
||||
getGeoFields,
|
||||
getGeoTileAggNotSupportedReason,
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants';
|
||||
|
||||
jest.mock('../../../kibana_services');
|
||||
jest.mock('./load_index_settings');
|
||||
jest.mock('./util/load_index_settings');
|
||||
|
||||
import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services';
|
||||
import { SearchSource } from 'src/plugins/data/public';
|
||||
|
||||
import { loadIndexSettings } from './load_index_settings';
|
||||
import { loadIndexSettings } from './util/load_index_settings';
|
||||
|
||||
import { ESSearchSource } from './es_search_source';
|
||||
import { VectorSourceRequestMeta } from '../../../../common/descriptor_types';
|
||||
|
@ -90,6 +90,7 @@ describe('ESSearchSource', () => {
|
|||
});
|
||||
|
||||
const searchFilters: VectorSourceRequestMeta = {
|
||||
isReadOnly: false,
|
||||
filters: [],
|
||||
zoom: 0,
|
||||
fieldNames: ['tooltipField', 'styleField'],
|
||||
|
|
|
@ -8,12 +8,11 @@
|
|||
import _ from 'lodash';
|
||||
import React, { ReactElement } from 'react';
|
||||
import rison from 'rison-node';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IFieldType, IndexPattern } from 'src/plugins/data/public';
|
||||
import { GeoJsonProperties } from 'geojson';
|
||||
import { GeoJsonProperties, Geometry, Position } from 'geojson';
|
||||
import { AbstractESSource } from '../es_source';
|
||||
import { getHttp, getSearchService } from '../../../kibana_services';
|
||||
import { getHttp, getMapAppConfig, getSearchService } from '../../../kibana_services';
|
||||
import {
|
||||
addFieldToDSL,
|
||||
getField,
|
||||
|
@ -23,7 +22,6 @@ import {
|
|||
} from '../../../../common/elasticsearch_util';
|
||||
// @ts-expect-error
|
||||
import { UpdateSourceEditor } from './update_source_editor';
|
||||
|
||||
import {
|
||||
DEFAULT_MAX_BUCKETS_LIMIT,
|
||||
ES_GEO_FIELD_TYPE,
|
||||
|
@ -38,11 +36,9 @@ import {
|
|||
} from '../../../../common/constants';
|
||||
import { getDataSourceLabel } from '../../../../common/i18n_getters';
|
||||
import { getSourceFields } from '../../../index_pattern_util';
|
||||
import { loadIndexSettings } from './load_index_settings';
|
||||
|
||||
import { loadIndexSettings } from './util/load_index_settings';
|
||||
import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
|
||||
import { ESDocField } from '../../fields/es_doc_field';
|
||||
|
||||
import { registerSource } from '../source_registry';
|
||||
import {
|
||||
ESSearchSourceDescriptor,
|
||||
|
@ -59,8 +55,9 @@ import { DataRequest } from '../../util/data_request';
|
|||
import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common';
|
||||
import { isValidStringConfig } from '../../util/valid_string_config';
|
||||
import { TopHitsUpdateSourceEditor } from './top_hits';
|
||||
import { getDocValueAndSourceFields, ScriptField } from './get_docvalue_source_fields';
|
||||
import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_source_fields';
|
||||
import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source';
|
||||
import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit';
|
||||
|
||||
export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', {
|
||||
defaultMessage: 'Documents',
|
||||
|
@ -392,6 +389,22 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
|
|||
return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField);
|
||||
}
|
||||
|
||||
async loadIsEditable(): Promise<boolean> {
|
||||
if (!getMapAppConfig().enableDrawingFeature) {
|
||||
return false;
|
||||
}
|
||||
await this.getIndexPattern();
|
||||
if (!(this.indexPattern && this.indexPattern.title)) {
|
||||
return false;
|
||||
}
|
||||
const { matchingIndexes } = await getMatchingIndexes(this.indexPattern.title);
|
||||
if (!matchingIndexes) {
|
||||
return false;
|
||||
}
|
||||
// For now we only support 1:1 index-pattern:index matches
|
||||
return matchingIndexes.length === 1;
|
||||
}
|
||||
|
||||
_hasSort(): boolean {
|
||||
const { sortField, sortOrder } = this._descriptor;
|
||||
return !!sortField && !!sortOrder;
|
||||
|
@ -671,6 +684,11 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
|
|||
return MVT_SOURCE_LAYER_NAME;
|
||||
}
|
||||
|
||||
async addFeature(geometry: Geometry | Position[]) {
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
await addFeatureToIndex(indexPattern.title, geometry, this.getGeoFieldName());
|
||||
}
|
||||
|
||||
async getUrlTemplateWithMeta(
|
||||
searchFilters: VectorSourceRequestMeta
|
||||
): Promise<ITiledSingleLayerMvtParams> {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getIndexPatternService } from '../../../../kibana_services';
|
|||
// @ts-expect-error
|
||||
import { ValidatedRange } from '../../../../components/validated_range';
|
||||
import { DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../../../../common/constants';
|
||||
import { loadIndexSettings } from '../load_index_settings';
|
||||
import { loadIndexSettings } from '../util/load_index_settings';
|
||||
import { OnSourceChangeArgs } from '../../source';
|
||||
import { IFieldType, SortDirection } from '../../../../../../../../src/plugins/data/public';
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import { SortDirection, indexPatterns } from '../../../../../../../src/plugins/d
|
|||
import { ESDocField } from '../../fields/es_doc_field';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { ScalingForm } from './scaling_form';
|
||||
import { ScalingForm } from './util/scaling_form';
|
||||
|
||||
export class UpdateSourceEditor extends Component {
|
||||
static propTypes = {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
jest.mock('../../../kibana_services', () => ({}));
|
||||
|
||||
jest.mock('./load_index_settings', () => ({
|
||||
jest.mock('./util/load_index_settings', () => ({
|
||||
loadIndexSettings: async () => {
|
||||
return { maxInnerResultWindow: 100 };
|
||||
},
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { Geometry, Position } from 'geojson';
|
||||
import { set } from '@elastic/safer-lodash-set';
|
||||
import { GET_MATCHING_INDEXES_PATH, INDEX_FEATURE_PATH } from '../../../../../common';
|
||||
import { getHttp } from '../../../../kibana_services';
|
||||
|
||||
export const addFeatureToIndex = async (
|
||||
indexName: string,
|
||||
geometry: Geometry | Position[],
|
||||
path: string
|
||||
) => {
|
||||
const data = set({}, path, geometry);
|
||||
return await getHttp().fetch({
|
||||
path: `${INDEX_FEATURE_PATH}`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
index: indexName,
|
||||
data,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const getMatchingIndexes = async (indexPattern: string) => {
|
||||
return await getHttp().fetch({
|
||||
path: `${GET_MATCHING_INDEXES_PATH}/${indexPattern}`,
|
||||
method: 'GET',
|
||||
});
|
||||
};
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import { getDocValueAndSourceFields } from './get_docvalue_source_fields';
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { IFieldType } from '../../../../../../../src/plugins/data/common/index_patterns/fields';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields';
|
||||
|
||||
function createMockIndexPattern(fields: IFieldType[]): IndexPattern {
|
||||
const indexPattern = {
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { getField } from '../../../../common/elasticsearch_util';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { getField } from '../../../../../common/elasticsearch_util';
|
||||
|
||||
export interface ScriptField {
|
||||
source: string;
|
|
@ -10,8 +10,8 @@ import {
|
|||
DEFAULT_MAX_RESULT_WINDOW,
|
||||
DEFAULT_MAX_INNER_RESULT_WINDOW,
|
||||
INDEX_SETTINGS_API_PATH,
|
||||
} from '../../../../common/constants';
|
||||
import { getHttp, getToasts } from '../../../kibana_services';
|
||||
} from '../../../../../common/constants';
|
||||
import { getHttp, getToasts } from '../../../../kibana_services';
|
||||
|
||||
let toastDisplayed = false;
|
||||
const indexSettings = new Map<string, Promise<INDEX_SETTINGS>>();
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('../../../kibana_services', () => ({}));
|
||||
jest.mock('../../../../kibana_services', () => ({}));
|
||||
|
||||
jest.mock('./load_index_settings', () => ({
|
||||
loadIndexSettings: async () => {
|
||||
|
@ -17,7 +17,7 @@ import React from 'react';
|
|||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ScalingForm } from './scaling_form';
|
||||
import { SCALING_TYPES } from '../../../../common/constants';
|
||||
import { SCALING_TYPES } from '../../../../../common/constants';
|
||||
|
||||
const defaultProps = {
|
||||
filterByMapBounds: true,
|
|
@ -19,10 +19,14 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { getIndexPatternService } from '../../../kibana_services';
|
||||
import { DEFAULT_MAX_RESULT_WINDOW, LAYER_TYPE, SCALING_TYPES } from '../../../../common/constants';
|
||||
import { getIndexPatternService } from '../../../../kibana_services';
|
||||
import {
|
||||
DEFAULT_MAX_RESULT_WINDOW,
|
||||
LAYER_TYPE,
|
||||
SCALING_TYPES,
|
||||
} from '../../../../../common/constants';
|
||||
import { loadIndexSettings } from './load_index_settings';
|
||||
import { OnSourceChangeArgs } from '../source';
|
||||
import { OnSourceChangeArgs } from '../../source';
|
||||
|
||||
interface Props {
|
||||
filterByMapBounds: boolean;
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import uuid from 'uuid/v4';
|
||||
import React from 'react';
|
||||
import { GeoJsonProperties } from 'geojson';
|
||||
import { GeoJsonProperties, Geometry, Position } from 'geojson';
|
||||
import { AbstractSource, ImmutableSourceProperty, SourceEditorArgs } from '../source';
|
||||
import { BoundsFilters, GeoJsonWithMeta } from '../vector_source';
|
||||
import { ITiledSingleLayerVectorSource } from '../tiled_single_layer_vector_source';
|
||||
|
@ -98,6 +98,10 @@ export class MVTSingleLayerVectorSource
|
|||
});
|
||||
}
|
||||
|
||||
addFeature(geometry: Geometry | Position[]): Promise<void> {
|
||||
throw new Error('Does not implement addFeature');
|
||||
}
|
||||
|
||||
getMVTFields(): MVTField[] {
|
||||
return this._descriptor.fields.map((field: MVTFieldDescriptor) => {
|
||||
return new MVTField({
|
||||
|
@ -223,6 +227,10 @@ export class MVTSingleLayerVectorSource
|
|||
}
|
||||
return tooltips;
|
||||
}
|
||||
|
||||
async loadIsEditable(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
registerSource({
|
||||
|
|
|
@ -8,11 +8,9 @@
|
|||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { Adapters } from 'src/plugins/inspector/public';
|
||||
import { GeoJsonProperties } from 'geojson';
|
||||
import { copyPersistentState } from '../../reducers/copy_persistent_state';
|
||||
|
||||
import { IField } from '../fields/field';
|
||||
import { FieldFormatter, LAYER_TYPE, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
|
||||
import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FeatureCollection, GeoJsonProperties } from 'geojson';
|
||||
import { FeatureCollection, GeoJsonProperties, Geometry, Position } from 'geojson';
|
||||
import { Filter, TimeRange } from 'src/plugins/data/public';
|
||||
import { VECTOR_SHAPE_TYPE } from '../../../../common/constants';
|
||||
import { TooltipProperty, ITooltipProperty } from '../../tooltips/tooltip_property';
|
||||
|
@ -66,6 +66,8 @@ export interface IVectorSource extends ISource {
|
|||
getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]>;
|
||||
isBoundsAware(): boolean;
|
||||
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig;
|
||||
loadIsEditable(): Promise<boolean>;
|
||||
addFeature(geometry: Geometry | Position[]): Promise<void>;
|
||||
}
|
||||
|
||||
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
|
||||
|
@ -153,4 +155,12 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
|
|||
getSyncMeta(): VectorSourceSyncMeta | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async addFeature(geometry: Geometry | Position[]) {
|
||||
throw new Error('Should implement VectorSource#addFeature');
|
||||
}
|
||||
|
||||
async loadIsEditable(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import 'action_select';
|
||||
@import 'metrics_editor/metric_editors';
|
||||
@import './geometry_filter';
|
||||
@import 'draw_forms/geometry_filter_form/geometry_filter';
|
||||
@import 'tooltip_selector/tooltip_selector';
|
||||
|
|
|
@ -16,8 +16,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
|
||||
import { ActionSelect } from './action_select';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
|
||||
import { ActionSelect } from '../action_select';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
|
@ -18,22 +17,33 @@ import {
|
|||
EuiFormErrorText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ES_SPATIAL_RELATIONS } from '../../common/constants';
|
||||
import { getEsSpatialRelationLabel } from '../../common/i18n_getters';
|
||||
import { ActionSelect } from './action_select';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
|
||||
import { ES_SPATIAL_RELATIONS } from '../../../../common/constants';
|
||||
import { getEsSpatialRelationLabel } from '../../../../common/i18n_getters';
|
||||
import { ActionSelect } from '../../action_select';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../../src/plugins/data/public';
|
||||
import { Action, ActionExecutionContext } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export class GeometryFilterForm extends Component {
|
||||
static propTypes = {
|
||||
buttonLabel: PropTypes.string.isRequired,
|
||||
getFilterActions: PropTypes.func,
|
||||
getActionContext: PropTypes.func,
|
||||
intitialGeometryLabel: PropTypes.string.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
isFilterGeometryClosed: PropTypes.bool,
|
||||
errorMsg: PropTypes.string,
|
||||
};
|
||||
interface Props {
|
||||
buttonLabel: string;
|
||||
getFilterActions?: () => Promise<Action[]>;
|
||||
getActionContext?: () => ActionExecutionContext;
|
||||
intitialGeometryLabel: string;
|
||||
onSubmit: ({
|
||||
actionId,
|
||||
geometryLabel,
|
||||
relation,
|
||||
}: {
|
||||
actionId: string;
|
||||
geometryLabel: string;
|
||||
relation: ES_SPATIAL_RELATIONS;
|
||||
}) => void;
|
||||
isFilterGeometryClosed?: boolean;
|
||||
errorMsg?: string;
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export class GeometryFilterForm extends Component<Props> {
|
||||
static defaultProps = {
|
||||
isFilterGeometryClosed: true,
|
||||
};
|
||||
|
@ -44,19 +54,19 @@ export class GeometryFilterForm extends Component {
|
|||
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
|
||||
};
|
||||
|
||||
_onGeometryLabelChange = (e) => {
|
||||
_onGeometryLabelChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
geometryLabel: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onRelationChange = (e) => {
|
||||
_onRelationChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.setState({
|
||||
relation: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onActionIdChange = (value) => {
|
||||
_onActionIdChange = (value: string) => {
|
||||
this.setState({ actionId: value });
|
||||
};
|
||||
|
|
@ -13,24 +13,34 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
|||
import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
|
||||
import type { Map as MbMap } from '@kbn/mapbox-gl';
|
||||
import { Feature } from 'geojson';
|
||||
import { DRAW_TYPE } from '../../../../common/constants';
|
||||
import { DRAW_SHAPE } from '../../../../common/constants';
|
||||
import { DrawCircle } from './draw_circle';
|
||||
import { DrawTooltip } from './draw_tooltip';
|
||||
|
||||
const mbModeEquivalencies = new Map<string, DRAW_SHAPE>([
|
||||
['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;
|
||||
mbDrawModes[DRAW_RECTANGLE] = DrawRectangle;
|
||||
mbDrawModes[DRAW_CIRCLE] = DrawCircle;
|
||||
|
||||
export interface Props {
|
||||
drawType?: DRAW_TYPE;
|
||||
onDraw: (event: { features: Feature[] }) => void;
|
||||
drawShape?: DRAW_SHAPE;
|
||||
onDraw: (event: { features: Feature[] }, drawControl?: MapboxDraw) => void;
|
||||
mbMap: MbMap;
|
||||
enable: boolean;
|
||||
updateEditShape: (shapeToDraw: DRAW_SHAPE) => void;
|
||||
}
|
||||
|
||||
export class DrawControl extends Component<Props, {}> {
|
||||
export class DrawControl extends Component<Props> {
|
||||
private _isMounted = false;
|
||||
private _mbDrawControlAdded = false;
|
||||
private _mbDrawControl = new MapboxDraw({
|
||||
|
@ -44,6 +54,7 @@ export class DrawControl extends Component<Props, {}> {
|
|||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._syncDrawControl();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -51,6 +62,10 @@ export class DrawControl extends Component<Props, {}> {
|
|||
this._removeDrawControl();
|
||||
}
|
||||
|
||||
_onDraw = (event: { features: Feature[] }) => {
|
||||
this.props.onDraw(event, this._mbDrawControl);
|
||||
};
|
||||
|
||||
// debounce with zero timeout needed to allow mapbox-draw finish logic to complete
|
||||
// before _removeDrawControl is called
|
||||
_syncDrawControl = _.debounce(() => {
|
||||
|
@ -58,26 +73,33 @@ export class DrawControl extends Component<Props, {}> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.props.drawType) {
|
||||
if (this.props.enable) {
|
||||
this._updateDrawControl();
|
||||
} else {
|
||||
this._removeDrawControl();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
_onModeChange = ({ mode }: { mode: string }) => {
|
||||
if (mbModeEquivalencies.has(mode)) {
|
||||
this.props.updateEditShape(mbModeEquivalencies.get(mode)!);
|
||||
}
|
||||
};
|
||||
|
||||
_removeDrawControl() {
|
||||
if (!this._mbDrawControlAdded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.mbMap.getCanvas().style.cursor = '';
|
||||
this.props.mbMap.off('draw.create', this.props.onDraw);
|
||||
this.props.mbMap.off('draw.modechange', this._onModeChange);
|
||||
this.props.mbMap.off('draw.create', this._onDraw);
|
||||
this.props.mbMap.removeControl(this._mbDrawControl);
|
||||
this._mbDrawControlAdded = false;
|
||||
}
|
||||
|
||||
_updateDrawControl() {
|
||||
if (!this.props.drawType) {
|
||||
if (!this.props.drawShape) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -85,27 +107,32 @@ 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.props.onDraw);
|
||||
this.props.mbMap.on('draw.modechange', this._onModeChange);
|
||||
this.props.mbMap.on('draw.create', this._onDraw);
|
||||
}
|
||||
|
||||
const { DRAW_LINE_STRING, DRAW_POLYGON, DRAW_POINT, SIMPLE_SELECT } = this._mbDrawControl.modes;
|
||||
const drawMode = this._mbDrawControl.getMode();
|
||||
if (drawMode !== DRAW_RECTANGLE && this.props.drawType === DRAW_TYPE.BOUNDS) {
|
||||
if (drawMode !== DRAW_RECTANGLE && this.props.drawShape === DRAW_SHAPE.BOUNDS) {
|
||||
this._mbDrawControl.changeMode(DRAW_RECTANGLE);
|
||||
} else if (drawMode !== DRAW_CIRCLE && this.props.drawType === DRAW_TYPE.DISTANCE) {
|
||||
} else if (drawMode !== DRAW_CIRCLE && this.props.drawShape === DRAW_SHAPE.DISTANCE) {
|
||||
this._mbDrawControl.changeMode(DRAW_CIRCLE);
|
||||
} else if (
|
||||
drawMode !== this._mbDrawControl.modes.DRAW_POLYGON &&
|
||||
this.props.drawType === DRAW_TYPE.POLYGON
|
||||
) {
|
||||
this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON);
|
||||
} else if (drawMode !== DRAW_POLYGON && this.props.drawShape === DRAW_SHAPE.POLYGON) {
|
||||
this._mbDrawControl.changeMode(DRAW_POLYGON);
|
||||
} else if (drawMode !== DRAW_LINE_STRING && this.props.drawShape === DRAW_SHAPE.LINE) {
|
||||
this._mbDrawControl.changeMode(DRAW_LINE_STRING);
|
||||
} else if (drawMode !== DRAW_POINT && this.props.drawShape === DRAW_SHAPE.POINT) {
|
||||
this._mbDrawControl.changeMode(DRAW_POINT);
|
||||
} else if (drawMode !== SIMPLE_SELECT && this.props.drawShape === DRAW_SHAPE.SIMPLE_SELECT) {
|
||||
this._mbDrawControl.changeMode(SIMPLE_SELECT);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.drawType) {
|
||||
if (!this.props.drawShape) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DrawTooltip mbMap={this.props.mbMap} drawType={this.props.drawType} />;
|
||||
return <DrawTooltip mbMap={this.props.mbMap} drawShape={this.props.drawShape} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 React, { Component } from 'react';
|
||||
import { Map as MbMap } from 'mapbox-gl';
|
||||
// @ts-expect-error
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import { Feature, Geometry, Position } from 'geojson';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-expect-error
|
||||
import * as jsts from 'jsts';
|
||||
import { getToasts } from '../../../../kibana_services';
|
||||
import { DrawControl } from '../';
|
||||
import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common';
|
||||
|
||||
const geoJSONReader = new jsts.io.GeoJSONReader();
|
||||
|
||||
export interface ReduxStateProps {
|
||||
drawShape?: DRAW_SHAPE;
|
||||
drawMode: DRAW_MODE;
|
||||
}
|
||||
|
||||
export interface ReduxDispatchProps {
|
||||
addNewFeatureToIndex: (geometry: Geometry | Position[]) => void;
|
||||
disableDrawState: () => void;
|
||||
}
|
||||
|
||||
export interface OwnProps {
|
||||
mbMap: MbMap;
|
||||
}
|
||||
|
||||
type Props = ReduxStateProps & ReduxDispatchProps & OwnProps;
|
||||
|
||||
export class DrawFeatureControl extends Component<Props, {}> {
|
||||
_onDraw = async (e: { features: Feature[] }, mbDrawControl: MapboxDraw) => {
|
||||
try {
|
||||
e.features.forEach((feature: Feature) => {
|
||||
const { geometry } = geoJSONReader.read(feature);
|
||||
if (!geometry.isSimple() || !geometry.isValid()) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.maps.drawFeatureControl.invalidGeometry', {
|
||||
defaultMessage: `Invalid geometry detected`,
|
||||
})
|
||||
);
|
||||
}
|
||||
if ('coordinates' in feature.geometry) {
|
||||
// @ts-ignore /* Single position array only used if point geometry */
|
||||
const featureGeom: Geometry | Position[] =
|
||||
this.props.drawMode === DRAW_MODE.DRAW_POINTS
|
||||
? feature.geometry.coordinates
|
||||
: feature.geometry;
|
||||
this.props.addNewFeatureToIndex(featureGeom);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
getToasts().addWarning(
|
||||
i18n.translate('xpack.maps.drawFeatureControl.unableToCreateFeature', {
|
||||
defaultMessage: `Unable to create feature, error: '{errorMsg}'.`,
|
||||
values: {
|
||||
errorMsg: error.message,
|
||||
},
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
this.props.disableDrawState();
|
||||
try {
|
||||
mbDrawControl.deleteAll();
|
||||
} catch (_e) {
|
||||
// Fail silently. Always works, but sometimes produces an upstream error in the mb draw lib
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DrawControl
|
||||
drawShape={this.props.drawShape}
|
||||
onDraw={this._onDraw}
|
||||
mbMap={this.props.mbMap}
|
||||
enable={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { Geometry, Position } from 'geojson';
|
||||
import {
|
||||
DrawFeatureControl,
|
||||
ReduxDispatchProps,
|
||||
ReduxStateProps,
|
||||
OwnProps,
|
||||
} from './draw_feature_control';
|
||||
import { addNewFeatureToIndex, updateEditShape } from '../../../../actions';
|
||||
import { MapStoreState } from '../../../../reducers/store';
|
||||
import { getEditState } from '../../../../selectors/map_selectors';
|
||||
import { getDrawMode } from '../../../../selectors/ui_selectors';
|
||||
|
||||
function mapStateToProps(state: MapStoreState): ReduxStateProps {
|
||||
const editState = getEditState(state);
|
||||
return {
|
||||
drawShape: editState ? editState.drawShape : undefined,
|
||||
drawMode: getDrawMode(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(
|
||||
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>
|
||||
): ReduxDispatchProps {
|
||||
return {
|
||||
addNewFeatureToIndex(geometry: Geometry | Position[]) {
|
||||
dispatch(addNewFeatureToIndex(geometry));
|
||||
},
|
||||
disableDrawState() {
|
||||
dispatch(updateEditShape(null));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const connected = connect<ReduxStateProps, ReduxDispatchProps, OwnProps, MapStoreState>(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DrawFeatureControl);
|
||||
export { connected as DrawFeatureControl };
|
|
@ -11,7 +11,7 @@ import type { Map as MbMap } from '@kbn/mapbox-gl';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { Filter } from 'src/plugins/data/public';
|
||||
import { Feature, Polygon } from 'geojson';
|
||||
import { DRAW_TYPE, ES_SPATIAL_RELATIONS } from '../../../../../common/constants';
|
||||
import { DRAW_SHAPE, ES_SPATIAL_RELATIONS } from '../../../../../common/constants';
|
||||
import { DrawState } from '../../../../../common/descriptor_types';
|
||||
import {
|
||||
createDistanceFilterWithMeta,
|
||||
|
@ -20,14 +20,14 @@ import {
|
|||
roundCoordinates,
|
||||
} from '../../../../../common/elasticsearch_util';
|
||||
import { getToasts } from '../../../../kibana_services';
|
||||
import { DrawControl } from '../draw_control';
|
||||
import { DrawControl } from '../';
|
||||
import { DrawCircleProperties } from '../draw_circle';
|
||||
|
||||
export interface Props {
|
||||
addFilters: (filters: Filter[], actionId: string) => Promise<void>;
|
||||
disableDrawState: () => void;
|
||||
drawState?: DrawState;
|
||||
isDrawingFilter: boolean;
|
||||
filterModeActive: boolean;
|
||||
mbMap: MbMap;
|
||||
geoFieldNames: string[];
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export class DrawFilterControl extends Component<Props, {}> {
|
|||
}
|
||||
|
||||
let filter: Filter | undefined;
|
||||
if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
|
||||
if (this.props.drawState.drawShape === DRAW_SHAPE.DISTANCE) {
|
||||
const circle = e.features[0] as Feature & { properties: DrawCircleProperties };
|
||||
const distanceKm = _.round(
|
||||
circle.properties.radiusKm,
|
||||
|
@ -70,7 +70,7 @@ export class DrawFilterControl extends Component<Props, {}> {
|
|||
|
||||
filter = createSpatialFilterWithGeometry({
|
||||
geometry:
|
||||
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
|
||||
this.props.drawState.drawShape === DRAW_SHAPE.BOUNDS
|
||||
? getBoundingBoxGeometry(geometry)
|
||||
: geometry,
|
||||
geoFieldNames: this.props.geoFieldNames,
|
||||
|
@ -100,13 +100,14 @@ export class DrawFilterControl extends Component<Props, {}> {
|
|||
render() {
|
||||
return (
|
||||
<DrawControl
|
||||
drawType={
|
||||
this.props.isDrawingFilter && this.props.drawState
|
||||
? this.props.drawState.drawType
|
||||
drawShape={
|
||||
this.props.filterModeActive && this.props.drawState
|
||||
? this.props.drawState.drawShape
|
||||
: undefined
|
||||
}
|
||||
onDraw={this._onDraw}
|
||||
mbMap={this.props.mbMap}
|
||||
enable={this.props.filterModeActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,18 +9,16 @@ 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,
|
||||
getGeoFieldNames,
|
||||
} from '../../../../selectors/map_selectors';
|
||||
import { setDrawMode, updateDrawState } from '../../../../actions';
|
||||
import { getDrawState, getGeoFieldNames } from '../../../../selectors/map_selectors';
|
||||
import { DRAW_MODE } from '../../../../../common';
|
||||
import { MapStoreState } from '../../../../reducers/store';
|
||||
import { getDrawMode } from '../../../../selectors/ui_selectors';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
isDrawingFilter: isDrawingFilter(state),
|
||||
drawState: getDrawState(state),
|
||||
filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS,
|
||||
geoFieldNames: getGeoFieldNames(state),
|
||||
};
|
||||
}
|
||||
|
@ -29,6 +27,7 @@ function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyActi
|
|||
return {
|
||||
disableDrawState() {
|
||||
dispatch(updateDrawState(null));
|
||||
dispatch(setDrawMode(DRAW_MODE.NONE));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ import React, { Component, RefObject } from 'react';
|
|||
import { EuiPopover, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Map as MbMap } from '@kbn/mapbox-gl';
|
||||
import { DRAW_TYPE } from '../../../../common/constants';
|
||||
import { DRAW_SHAPE } from '../../../../common/constants';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
interface Props {
|
||||
mbMap: MbMap;
|
||||
drawType: DRAW_TYPE;
|
||||
drawShape: DRAW_SHAPE;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -27,6 +27,7 @@ interface State {
|
|||
|
||||
export class DrawTooltip extends Component<Props, State> {
|
||||
private readonly _popoverRef: RefObject<EuiPopover> = React.createRef();
|
||||
private _isMounted = false;
|
||||
|
||||
state: State = {
|
||||
x: undefined,
|
||||
|
@ -35,6 +36,7 @@ export class DrawTooltip extends Component<Props, State> {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.props.mbMap.on('mousemove', this._updateTooltipLocation);
|
||||
this.props.mbMap.on('mouseout', this._hideTooltip);
|
||||
}
|
||||
|
@ -46,30 +48,56 @@ export class DrawTooltip extends Component<Props, State> {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
this.props.mbMap.off('mousemove', this._updateTooltipLocation);
|
||||
this.props.mbMap.off('mouseout', this._hideTooltip);
|
||||
this._updateTooltipLocation.cancel();
|
||||
}
|
||||
|
||||
_hideTooltip = () => {
|
||||
this._updateTooltipLocation.cancel();
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
_updateTooltipLocation = _.throttle(({ lngLat }) => {
|
||||
const mouseLocation = this.props.mbMap.project(lngLat);
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
isOpen: true,
|
||||
x: mouseLocation.x,
|
||||
y: mouseLocation.y,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
render() {
|
||||
if (this.state.x === undefined || this.state.y === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let instructions;
|
||||
if (this.props.drawType === DRAW_TYPE.BOUNDS) {
|
||||
if (this.props.drawShape === DRAW_SHAPE.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.drawType === DRAW_TYPE.DISTANCE) {
|
||||
} else if (this.props.drawShape === DRAW_SHAPE.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.drawType === DRAW_TYPE.POLYGON) {
|
||||
} else if (this.props.drawShape === DRAW_SHAPE.POLYGON) {
|
||||
instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', {
|
||||
defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.',
|
||||
});
|
||||
} else if (this.props.drawShape === DRAW_SHAPE.LINE) {
|
||||
instructions = i18n.translate('xpack.maps.drawTooltip.lineInstructions', {
|
||||
defaultMessage: 'Click to start line. Click to add vertex. Double click to finish.',
|
||||
});
|
||||
} else if (this.props.drawShape === DRAW_SHAPE.POINT) {
|
||||
instructions = i18n.translate('xpack.maps.drawTooltip.pointInstructions', {
|
||||
defaultMessage: 'Click to create point.',
|
||||
});
|
||||
} else {
|
||||
// unknown draw type, tooltip not needed
|
||||
return null;
|
||||
|
@ -98,18 +126,4 @@ export class DrawTooltip extends Component<Props, State> {
|
|||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
_hideTooltip = () => {
|
||||
this._updateTooltipLocation.cancel();
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
_updateTooltipLocation = _.throttle(({ lngLat }) => {
|
||||
const mouseLocation = this.props.mbMap.project(lngLat);
|
||||
this.setState({
|
||||
isOpen: true,
|
||||
x: mouseLocation.x,
|
||||
y: mouseLocation.y,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
|
|
@ -5,4 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { DrawFilterControl } from './draw_filter_control';
|
||||
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';
|
||||
|
||||
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
|
||||
return {
|
||||
updateEditShape(shapeToDraw: DRAW_SHAPE) {
|
||||
dispatch(updateEditShape(shapeToDraw));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const connected = connect(null, mapDispatchToProps)(DrawControl);
|
||||
export { connected as DrawControl };
|
||||
|
|
|
@ -10,27 +10,28 @@ import { ThunkDispatch } from 'redux-thunk';
|
|||
import { connect } from 'react-redux';
|
||||
import { MBMap } from './mb_map';
|
||||
import {
|
||||
mapExtentChanged,
|
||||
mapReady,
|
||||
mapDestroyed,
|
||||
setMouseCoordinates,
|
||||
clearMouseCoordinates,
|
||||
clearGoto,
|
||||
setMapInitError,
|
||||
clearMouseCoordinates,
|
||||
mapDestroyed,
|
||||
mapExtentChanged,
|
||||
MapExtentState,
|
||||
mapReady,
|
||||
setAreTilesLoaded,
|
||||
setMapInitError,
|
||||
setMouseCoordinates,
|
||||
} from '../../actions';
|
||||
import {
|
||||
getGoto,
|
||||
getLayerList,
|
||||
getMapReady,
|
||||
getGoto,
|
||||
getMapSettings,
|
||||
getScrollZoom,
|
||||
getSpatialFiltersLayer,
|
||||
getMapSettings,
|
||||
} from '../../selectors/map_selectors';
|
||||
import { getIsFullScreen } from '../../selectors/ui_selectors';
|
||||
import { getDrawMode, getIsFullScreen } from '../../selectors/ui_selectors';
|
||||
import { getInspectorAdapters } from '../../reducers/non_serializable_instances';
|
||||
import { MapStoreState } from '../../reducers/store';
|
||||
import { DRAW_MODE } from '../../../common';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
|
@ -42,6 +43,9 @@ function mapStateToProps(state: MapStoreState) {
|
|||
inspectorAdapters: getInspectorAdapters(state),
|
||||
scrollZoom: getScrollZoom(state),
|
||||
isFullScreen: getIsFullScreen(state),
|
||||
featureModeActive:
|
||||
getDrawMode(state) === DRAW_MODE.DRAW_SHAPES || getDrawMode(state) === DRAW_MODE.DRAW_POINTS,
|
||||
filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,8 @@ 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 { mapboxgl } from '@kbn/mapbox-gl';
|
||||
|
||||
import { DrawFilterControl } from './draw_control';
|
||||
import { DrawFilterControl } from './draw_control/draw_filter_control';
|
||||
import { ScaleControl } from './scale_control';
|
||||
import { TooltipControl } from './tooltip_control';
|
||||
import { clampToLatBounds, clampToLonBounds } from '../../../common/elasticsearch_util';
|
||||
|
@ -46,6 +44,7 @@ import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public
|
|||
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
|
||||
import { MapExtentState } from '../../actions';
|
||||
import { TileStatusTracker } from './tile_status_tracker';
|
||||
import { DrawFeatureControl } from './draw_control/draw_feature_control';
|
||||
|
||||
export interface Props {
|
||||
isMapReady: boolean;
|
||||
|
@ -69,6 +68,8 @@ export interface Props {
|
|||
onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
|
||||
renderTooltipContent?: RenderToolTipContent;
|
||||
setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void;
|
||||
featureModeActive: boolean;
|
||||
filterModeActive: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -417,11 +418,16 @@ export class MBMap extends Component<Props, State> {
|
|||
|
||||
render() {
|
||||
let drawFilterControl;
|
||||
let drawFeatureControl;
|
||||
let tooltipControl;
|
||||
let scaleControl;
|
||||
if (this.state.mbMap) {
|
||||
drawFilterControl = this.props.addFilters ? (
|
||||
<DrawFilterControl mbMap={this.state.mbMap} addFilters={this.props.addFilters} />
|
||||
drawFilterControl =
|
||||
this.props.addFilters && this.props.filterModeActive ? (
|
||||
<DrawFilterControl mbMap={this.state.mbMap} addFilters={this.props.addFilters} />
|
||||
) : null;
|
||||
drawFeatureControl = this.props.featureModeActive ? (
|
||||
<DrawFeatureControl mbMap={this.state.mbMap} />
|
||||
) : null;
|
||||
tooltipControl = !this.props.settings.disableTooltipControl ? (
|
||||
<TooltipControl
|
||||
|
@ -445,6 +451,7 @@ export class MBMap extends Component<Props, State> {
|
|||
data-test-subj="mapContainer"
|
||||
>
|
||||
{drawFilterControl}
|
||||
{drawFeatureControl}
|
||||
{scaleControl}
|
||||
{tooltipControl}
|
||||
</div>
|
||||
|
|
|
@ -19,8 +19,7 @@ import {
|
|||
PreIndexedShape,
|
||||
} from '../../../../../common/elasticsearch_util';
|
||||
import { ES_SPATIAL_RELATIONS, GEO_JSON_TYPE } from '../../../../../common/constants';
|
||||
// @ts-expect-error
|
||||
import { GeometryFilterForm } from '../../../../components/geometry_filter_form';
|
||||
import { GeometryFilterForm } from '../../../../components/draw_forms/geometry_filter_form/geometry_filter_form';
|
||||
|
||||
// over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped.
|
||||
const META_OVERHEAD = 100;
|
||||
|
|
|
@ -21,15 +21,18 @@ import {
|
|||
getOpenTooltips,
|
||||
getHasLockedTooltips,
|
||||
getGeoFieldNames,
|
||||
isDrawingFilter,
|
||||
} from '../../../selectors/map_selectors';
|
||||
import { getDrawMode } from '../../../selectors/ui_selectors';
|
||||
import { DRAW_MODE } from '../../../../common';
|
||||
import { MapStoreState } from '../../../reducers/store';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
layerList: getLayerList(state),
|
||||
hasLockedTooltips: getHasLockedTooltips(state),
|
||||
isDrawingFilter: isDrawingFilter(state),
|
||||
filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS,
|
||||
drawModeActive:
|
||||
getDrawMode(state) === DRAW_MODE.DRAW_SHAPES || getDrawMode(state) === DRAW_MODE.DRAW_POINTS,
|
||||
openTooltips: getOpenTooltips(state),
|
||||
geoFieldNames: getGeoFieldNames(state),
|
||||
};
|
||||
|
|
|
@ -80,6 +80,8 @@ const defaultProps = {
|
|||
geoFieldNames: [],
|
||||
openTooltips: [],
|
||||
hasLockedTooltips: false,
|
||||
filterModeActive: false,
|
||||
drawModeActive: false,
|
||||
};
|
||||
|
||||
const hoverTooltipState = {
|
||||
|
@ -208,7 +210,6 @@ describe('TooltipControl', () => {
|
|||
{...defaultProps}
|
||||
closeOnClickTooltip={closeOnClickTooltipStub}
|
||||
openOnClickTooltip={openOnClickTooltipStub}
|
||||
isDrawingFilter={true}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -70,7 +70,8 @@ export interface Props {
|
|||
getFilterActions?: () => Promise<Action[]>;
|
||||
geoFieldNames: string[];
|
||||
hasLockedTooltips: boolean;
|
||||
isDrawingFilter: boolean;
|
||||
filterModeActive: boolean;
|
||||
drawModeActive: boolean;
|
||||
layerList: ILayer[];
|
||||
mbMap: MbMap;
|
||||
openOnClickTooltip: (tooltipState: TooltipState) => void;
|
||||
|
@ -244,7 +245,7 @@ export class TooltipControl extends Component<Props, {}> {
|
|||
}
|
||||
|
||||
_lockTooltip = (e: MapMouseEvent) => {
|
||||
if (this.props.isDrawingFilter) {
|
||||
if (this.props.filterModeActive || this.props.drawModeActive) {
|
||||
// ignore click events when in draw mode
|
||||
return;
|
||||
}
|
||||
|
@ -275,7 +276,7 @@ export class TooltipControl extends Component<Props, {}> {
|
|||
};
|
||||
|
||||
_updateHoverTooltipState = _.debounce((e: MapMouseEvent) => {
|
||||
if (this.props.isDrawingFilter || this.props.hasLockedTooltips) {
|
||||
if (this.props.filterModeActive || this.props.hasLockedTooltips || this.props.drawModeActive) {
|
||||
// ignore hover events when in draw mode or when there are locked tooltips
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { connect } from 'react-redux';
|
|||
import { LayerControl } from './layer_control';
|
||||
|
||||
import { FLYOUT_STATE } from '../../../reducers/ui';
|
||||
import { setSelectedLayer, updateFlyout, setIsLayerTOCOpen } from '../../../actions';
|
||||
import { setSelectedLayer, updateFlyout, setIsLayerTOCOpen, setDrawMode } from '../../../actions';
|
||||
import {
|
||||
getIsReadOnly,
|
||||
getIsLayerTOCOpen,
|
||||
|
@ -19,6 +19,7 @@ import {
|
|||
} from '../../../selectors/ui_selectors';
|
||||
import { getLayerList } from '../../../selectors/map_selectors';
|
||||
import { MapStoreState } from '../../../reducers/store';
|
||||
import { DRAW_MODE } from '../../../../common';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
|
@ -34,6 +35,7 @@ function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyActi
|
|||
showAddLayerWizard: async () => {
|
||||
await dispatch(setSelectedLayer(null));
|
||||
dispatch(updateFlyout(FLYOUT_STATE.ADD_LAYER_WIZARD));
|
||||
dispatch(setDrawMode(DRAW_MODE.NONE));
|
||||
},
|
||||
closeLayerTOC: () => {
|
||||
dispatch(setIsLayerTOCOpen(false));
|
||||
|
|
|
@ -11,7 +11,6 @@ exports[`TOCEntry is rendered 1`] = `
|
|||
>
|
||||
<Connect(TOCEntryActionsPopover)
|
||||
displayName="layer 1"
|
||||
editLayer={[Function]}
|
||||
escapedDisplayName="layer_1"
|
||||
isEditButtonDisabled={false}
|
||||
layer={
|
||||
|
@ -26,6 +25,7 @@ exports[`TOCEntry is rendered 1`] = `
|
|||
"showAtZoomLevel": [Function],
|
||||
}
|
||||
}
|
||||
openLayerSettings={[Function]}
|
||||
supportsFitToBounds={false}
|
||||
/>
|
||||
<div
|
||||
|
@ -39,12 +39,12 @@ exports[`TOCEntry is rendered 1`] = `
|
|||
title="Hide layer"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit layer"
|
||||
aria-label="Edit layer settings"
|
||||
iconType="pencil"
|
||||
isDisabled={false}
|
||||
key="edit"
|
||||
key="settings"
|
||||
onClick={[Function]}
|
||||
title="Edit layer"
|
||||
title="Edit layer settings"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Reorder layer"
|
||||
|
@ -85,7 +85,6 @@ exports[`TOCEntry props Should shade background when not selected layer 1`] = `
|
|||
>
|
||||
<Connect(TOCEntryActionsPopover)
|
||||
displayName="layer 1"
|
||||
editLayer={[Function]}
|
||||
escapedDisplayName="layer_1"
|
||||
isEditButtonDisabled={false}
|
||||
layer={
|
||||
|
@ -100,6 +99,7 @@ exports[`TOCEntry props Should shade background when not selected layer 1`] = `
|
|||
"showAtZoomLevel": [Function],
|
||||
}
|
||||
}
|
||||
openLayerSettings={[Function]}
|
||||
supportsFitToBounds={false}
|
||||
/>
|
||||
<div
|
||||
|
@ -113,12 +113,12 @@ exports[`TOCEntry props Should shade background when not selected layer 1`] = `
|
|||
title="Hide layer"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit layer"
|
||||
aria-label="Edit layer settings"
|
||||
iconType="pencil"
|
||||
isDisabled={false}
|
||||
key="edit"
|
||||
key="settings"
|
||||
onClick={[Function]}
|
||||
title="Edit layer"
|
||||
title="Edit layer settings"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Reorder layer"
|
||||
|
@ -159,7 +159,6 @@ exports[`TOCEntry props Should shade background when selected layer 1`] = `
|
|||
>
|
||||
<Connect(TOCEntryActionsPopover)
|
||||
displayName="layer 1"
|
||||
editLayer={[Function]}
|
||||
escapedDisplayName="layer_1"
|
||||
isEditButtonDisabled={false}
|
||||
layer={
|
||||
|
@ -174,6 +173,7 @@ exports[`TOCEntry props Should shade background when selected layer 1`] = `
|
|||
"showAtZoomLevel": [Function],
|
||||
}
|
||||
}
|
||||
openLayerSettings={[Function]}
|
||||
supportsFitToBounds={false}
|
||||
/>
|
||||
<div
|
||||
|
@ -187,12 +187,12 @@ exports[`TOCEntry props Should shade background when selected layer 1`] = `
|
|||
title="Hide layer"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit layer"
|
||||
aria-label="Edit layer settings"
|
||||
iconType="pencil"
|
||||
isDisabled={false}
|
||||
key="edit"
|
||||
key="settings"
|
||||
onClick={[Function]}
|
||||
title="Edit layer"
|
||||
title="Edit layer settings"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Reorder layer"
|
||||
|
@ -233,7 +233,6 @@ exports[`TOCEntry props isReadOnly 1`] = `
|
|||
>
|
||||
<Connect(TOCEntryActionsPopover)
|
||||
displayName="layer 1"
|
||||
editLayer={[Function]}
|
||||
escapedDisplayName="layer_1"
|
||||
isEditButtonDisabled={false}
|
||||
layer={
|
||||
|
@ -248,6 +247,7 @@ exports[`TOCEntry props isReadOnly 1`] = `
|
|||
"showAtZoomLevel": [Function],
|
||||
}
|
||||
}
|
||||
openLayerSettings={[Function]}
|
||||
supportsFitToBounds={false}
|
||||
/>
|
||||
<div
|
||||
|
@ -292,7 +292,6 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is
|
|||
>
|
||||
<Connect(TOCEntryActionsPopover)
|
||||
displayName="layer 1"
|
||||
editLayer={[Function]}
|
||||
escapedDisplayName="layer_1"
|
||||
isEditButtonDisabled={false}
|
||||
layer={
|
||||
|
@ -307,6 +306,7 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is
|
|||
"showAtZoomLevel": [Function],
|
||||
}
|
||||
}
|
||||
openLayerSettings={[Function]}
|
||||
supportsFitToBounds={false}
|
||||
/>
|
||||
<div
|
||||
|
@ -320,12 +320,12 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is
|
|||
title="Hide layer"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit layer"
|
||||
aria-label="Edit layer settings"
|
||||
iconType="pencil"
|
||||
isDisabled={false}
|
||||
key="edit"
|
||||
key="settings"
|
||||
onClick={[Function]}
|
||||
title="Edit layer"
|
||||
title="Edit layer settings"
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
aria-label="Reorder layer"
|
||||
|
|
|
@ -30,6 +30,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.mapTocEntry-isInEditingMode {
|
||||
background-color: tintOrShade($euiColorPrimary, 90%, 70%) !important;
|
||||
}
|
||||
|
||||
.mapTocEntry-isDragging {
|
||||
@include euiBottomShadowMedium;
|
||||
}
|
||||
|
|
|
@ -21,10 +21,17 @@ export function getVisibilityToggleLabel(isVisible: boolean) {
|
|||
});
|
||||
}
|
||||
|
||||
export const EDIT_LAYER_LABEL = i18n.translate(
|
||||
'xpack.maps.layerControl.layerTocActions.editButtonLabel',
|
||||
export const EDIT_LAYER_SETTINGS_LABEL = i18n.translate(
|
||||
'xpack.maps.layerControl.layerTocActions.layerSettingsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Edit layer',
|
||||
defaultMessage: 'Edit layer settings',
|
||||
}
|
||||
);
|
||||
|
||||
export const EDIT_FEATURES_LABEL = i18n.translate(
|
||||
'xpack.maps.layerControl.layerTocActions.editFeaturesButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Edit features',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
getMapZoom,
|
||||
hasDirtyState,
|
||||
getSelectedLayer,
|
||||
getEditState,
|
||||
} from '../../../../../selectors/map_selectors';
|
||||
import {
|
||||
getIsReadOnly,
|
||||
|
@ -40,6 +41,7 @@ function mapStateToProps(state: MapStoreState, ownProps: OwnProps): ReduxStatePr
|
|||
isLegendDetailsOpen: getOpenTOCDetails(state).includes(ownProps.layer.getId()),
|
||||
isEditButtonDisabled:
|
||||
flyoutDisplay !== FLYOUT_STATE.NONE && flyoutDisplay !== FLYOUT_STATE.LAYER_PANEL,
|
||||
editModeActiveForLayer: getEditState(state)?.layerId === ownProps.layer.getId(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ const defaultProps = {
|
|||
isEditButtonDisabled: false,
|
||||
hideTOCDetails: () => {},
|
||||
showTOCDetails: () => {},
|
||||
editModeActiveForLayer: false,
|
||||
};
|
||||
|
||||
describe('TOCEntry', () => {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { TOCEntryActionsPopover } from './toc_entry_actions_popover';
|
|||
import {
|
||||
getVisibilityToggleIcon,
|
||||
getVisibilityToggleLabel,
|
||||
EDIT_LAYER_LABEL,
|
||||
EDIT_LAYER_SETTINGS_LABEL,
|
||||
FIT_TO_DATA_LABEL,
|
||||
} from './action_labels';
|
||||
import { ILayer } from '../../../../../classes/layers/layer';
|
||||
|
@ -31,6 +31,7 @@ export interface ReduxStateProps {
|
|||
hasDirtyStateSelector: boolean;
|
||||
isLegendDetailsOpen: boolean;
|
||||
isEditButtonDisabled: boolean;
|
||||
editModeActiveForLayer: boolean;
|
||||
}
|
||||
|
||||
export interface ReduxDispatchProps {
|
||||
|
@ -196,11 +197,11 @@ export class TOCEntry extends Component<Props, State> {
|
|||
if (!this.props.isReadOnly) {
|
||||
quickActions.push(
|
||||
<EuiButtonIcon
|
||||
key="edit"
|
||||
key="settings"
|
||||
isDisabled={this.props.isEditButtonDisabled}
|
||||
iconType="pencil"
|
||||
aria-label={EDIT_LAYER_LABEL}
|
||||
title={EDIT_LAYER_LABEL}
|
||||
aria-label={EDIT_LAYER_SETTINGS_LABEL}
|
||||
title={EDIT_LAYER_SETTINGS_LABEL}
|
||||
onClick={this._openLayerPanelWithCheck}
|
||||
/>
|
||||
);
|
||||
|
@ -277,7 +278,7 @@ export class TOCEntry extends Component<Props, State> {
|
|||
layer={layer}
|
||||
displayName={this.state.displayName}
|
||||
escapedDisplayName={escapeLayerName(this.state.displayName)}
|
||||
editLayer={this._openLayerPanelWithCheck}
|
||||
openLayerSettings={this._openLayerPanelWithCheck}
|
||||
isEditButtonDisabled={this.props.isEditButtonDisabled}
|
||||
supportsFitToBounds={this.state.supportsFitToBounds}
|
||||
/>
|
||||
|
@ -314,6 +315,7 @@ export class TOCEntry extends Component<Props, State> {
|
|||
'mapTocEntry-isSelected':
|
||||
this.props.layer.isPreviewLayer() ||
|
||||
(this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId()),
|
||||
'mapTocEntry-isInEditingMode': this.props.editModeActiveForLayer,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -75,13 +75,13 @@ exports[`TOCEntryActionsPopover is rendered 1`] = `
|
|||
"toolTipContent": null,
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "editLayerButton",
|
||||
"data-test-subj": "layerSettingsButton",
|
||||
"disabled": false,
|
||||
"icon": <EuiIcon
|
||||
size="m"
|
||||
type="pencil"
|
||||
/>,
|
||||
"name": "Edit layer",
|
||||
"name": "Edit layer settings",
|
||||
"onClick": [Function],
|
||||
"toolTipContent": null,
|
||||
},
|
||||
|
@ -190,13 +190,13 @@ exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBou
|
|||
"toolTipContent": null,
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "editLayerButton",
|
||||
"data-test-subj": "layerSettingsButton",
|
||||
"disabled": false,
|
||||
"icon": <EuiIcon
|
||||
size="m"
|
||||
type="pencil"
|
||||
/>,
|
||||
"name": "Edit layer",
|
||||
"name": "Edit layer settings",
|
||||
"onClick": [Function],
|
||||
"toolTipContent": null,
|
||||
},
|
||||
|
@ -306,13 +306,13 @@ exports[`TOCEntryActionsPopover should have "show layer" action when layer is no
|
|||
"toolTipContent": null,
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "editLayerButton",
|
||||
"data-test-subj": "layerSettingsButton",
|
||||
"disabled": false,
|
||||
"icon": <EuiIcon
|
||||
size="m"
|
||||
type="pencil"
|
||||
/>,
|
||||
"name": "Edit layer",
|
||||
"name": "Edit layer settings",
|
||||
"onClick": [Function],
|
||||
"toolTipContent": null,
|
||||
},
|
||||
|
|
|
@ -10,13 +10,16 @@ import { ThunkDispatch } from 'redux-thunk';
|
|||
import { connect } from 'react-redux';
|
||||
import { MapStoreState } from '../../../../../../reducers/store';
|
||||
import {
|
||||
fitToLayerExtent,
|
||||
toggleLayerVisible,
|
||||
cloneLayer,
|
||||
fitToLayerExtent,
|
||||
removeLayer,
|
||||
setDrawMode,
|
||||
toggleLayerVisible,
|
||||
updateEditLayer,
|
||||
} from '../../../../../../actions';
|
||||
import { getIsReadOnly } from '../../../../../../selectors/ui_selectors';
|
||||
import { TOCEntryActionsPopover } from './toc_entry_actions_popover';
|
||||
import { DRAW_MODE } from '../../../../../../../common';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
|
@ -38,6 +41,14 @@ function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyActi
|
|||
toggleVisible: (layerId: string) => {
|
||||
dispatch(toggleLayerVisible(layerId));
|
||||
},
|
||||
enableShapeEditing: (layerId: string) => {
|
||||
dispatch(updateEditLayer(layerId));
|
||||
dispatch(setDrawMode(DRAW_MODE.DRAW_SHAPES));
|
||||
},
|
||||
enablePointEditing: (layerId: string) => {
|
||||
dispatch(updateEditLayer(layerId));
|
||||
dispatch(setDrawMode(DRAW_MODE.DRAW_POINTS));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ class LayerMock extends AbstractLayer implements ILayer {
|
|||
const defaultProps = {
|
||||
cloneLayer: () => {},
|
||||
displayName: 'layer 1',
|
||||
editLayer: () => {},
|
||||
escapedDisplayName: 'layer1',
|
||||
fitToBounds: () => {},
|
||||
isEditButtonDisabled: false,
|
||||
|
@ -46,6 +45,9 @@ const defaultProps = {
|
|||
removeLayer: () => {},
|
||||
toggleVisible: () => {},
|
||||
supportsFitToBounds: true,
|
||||
enableShapeEditing: () => {},
|
||||
enablePointEditing: () => {},
|
||||
openLayerSettings: () => {},
|
||||
};
|
||||
|
||||
describe('TOCEntryActionsPopover', () => {
|
||||
|
|
|
@ -6,22 +6,28 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { EuiPopover, EuiContextMenu, EuiIcon } from '@elastic/eui';
|
||||
import { EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ILayer } from '../../../../../../classes/layers/layer';
|
||||
import { TOCEntryButton } from '../toc_entry_button';
|
||||
import {
|
||||
EDIT_FEATURES_LABEL,
|
||||
EDIT_LAYER_SETTINGS_LABEL,
|
||||
FIT_TO_DATA_LABEL,
|
||||
getVisibilityToggleIcon,
|
||||
getVisibilityToggleLabel,
|
||||
EDIT_LAYER_LABEL,
|
||||
FIT_TO_DATA_LABEL,
|
||||
} from '../action_labels';
|
||||
import { ESSearchSource } from '../../../../../../classes/sources/es_search_source';
|
||||
import { VectorLayer } from '../../../../../../classes/layers/vector_layer';
|
||||
import { SCALING_TYPES, VECTOR_SHAPE_TYPE } from '../../../../../../../common';
|
||||
import { ESSearchSourceSyncMeta } from '../../../../../../../common/descriptor_types';
|
||||
|
||||
export interface Props {
|
||||
cloneLayer: (layerId: string) => void;
|
||||
enableShapeEditing: (layerId: string) => void;
|
||||
enablePointEditing: (layerId: string) => void;
|
||||
displayName: string;
|
||||
editLayer: () => void;
|
||||
openLayerSettings: () => void;
|
||||
escapedDisplayName: string;
|
||||
fitToBounds: (layerId: string) => void;
|
||||
isEditButtonDisabled: boolean;
|
||||
|
@ -34,10 +40,62 @@ export interface Props {
|
|||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
supportsFeatureEditing: boolean;
|
||||
canEditFeatures: boolean;
|
||||
}
|
||||
|
||||
export class TOCEntryActionsPopover extends Component<Props, State> {
|
||||
state: State = { isPopoverOpen: false };
|
||||
state: State = { isPopoverOpen: false, supportsFeatureEditing: false, canEditFeatures: false };
|
||||
private _isMounted = false;
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._checkLayerEditable();
|
||||
}
|
||||
|
||||
async _checkLayerEditable() {
|
||||
if (!(this.props.layer instanceof VectorLayer)) {
|
||||
return;
|
||||
}
|
||||
const supportsFeatureEditing = this.props.layer.supportsFeatureEditing();
|
||||
const canEditFeatures = await this._getCanEditFeatures();
|
||||
if (
|
||||
!this._isMounted ||
|
||||
(supportsFeatureEditing === this.state.supportsFeatureEditing &&
|
||||
canEditFeatures === this.state.canEditFeatures)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.setState({ supportsFeatureEditing, canEditFeatures });
|
||||
}
|
||||
|
||||
async _getCanEditFeatures(): Promise<boolean> {
|
||||
const vectorLayer = this.props.layer as VectorLayer;
|
||||
const layerSource = await this.props.layer.getSource();
|
||||
if (!(layerSource instanceof ESSearchSource)) {
|
||||
return false;
|
||||
}
|
||||
const isClustered =
|
||||
(layerSource?.getSyncMeta() as ESSearchSourceSyncMeta)?.scalingType ===
|
||||
SCALING_TYPES.CLUSTERS;
|
||||
if (
|
||||
isClustered ||
|
||||
(await vectorLayer.isFilteredByGlobalTime()) ||
|
||||
vectorLayer.isPreviewLayer() ||
|
||||
!vectorLayer.isVisible() ||
|
||||
vectorLayer.hasJoins()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
|
@ -97,15 +155,41 @@ export class TOCEntryActionsPopover extends Component<Props, State> {
|
|||
];
|
||||
|
||||
if (!this.props.isReadOnly) {
|
||||
if (this.state.supportsFeatureEditing) {
|
||||
actionItems.push({
|
||||
name: EDIT_FEATURES_LABEL,
|
||||
icon: <EuiIcon type="vector" size="m" />,
|
||||
'data-test-subj': 'editLayerButton',
|
||||
toolTipContent: this.state.canEditFeatures
|
||||
? null
|
||||
: i18n.translate('xpack.maps.layerTocActions.editLayerTooltip', {
|
||||
defaultMessage:
|
||||
'Edit features only supported for document layers without clustering, joins, or time filtering',
|
||||
}),
|
||||
disabled: !this.state.canEditFeatures,
|
||||
onClick: async () => {
|
||||
this._closePopover();
|
||||
const supportedShapeTypes = await (this.props.layer.getSource() as ESSearchSource).getSupportedShapeTypes();
|
||||
const supportsShapes =
|
||||
supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.POLYGON) &&
|
||||
supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.LINE);
|
||||
if (supportsShapes) {
|
||||
this.props.enableShapeEditing(this.props.layer.getId());
|
||||
} else {
|
||||
this.props.enablePointEditing(this.props.layer.getId());
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
actionItems.push({
|
||||
disabled: this.props.isEditButtonDisabled,
|
||||
name: EDIT_LAYER_LABEL,
|
||||
name: EDIT_LAYER_SETTINGS_LABEL,
|
||||
icon: <EuiIcon type="pencil" size="m" />,
|
||||
'data-test-subj': 'editLayerButton',
|
||||
'data-test-subj': 'layerSettingsButton',
|
||||
toolTipContent: null,
|
||||
onClick: () => {
|
||||
this._closePopover();
|
||||
this.props.editLayer();
|
||||
this.props.openLayerSettings();
|
||||
},
|
||||
});
|
||||
actionItems.push({
|
||||
|
|
|
@ -29,10 +29,50 @@ exports[`Should show all controls 1`] = `
|
|||
<Connect(FitToData) />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Connect(ToolsControl) />
|
||||
<Connect(ToolsControl)
|
||||
disableToolsControl={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Connect(TimesliderToggleButton) />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`Should show point layer edit tools 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="flexStart"
|
||||
className="mapToolbarOverlay"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<Connect(SetViewControl) />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Connect(FeatureEditTools)
|
||||
pointsOnly={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`Should show shape layer edit tools 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="flexStart"
|
||||
className="mapToolbarOverlay"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<Connect(SetViewControl) />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Connect(FeatureEditTools)
|
||||
pointsOnly={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.euiButtonIcon:not(.euiButtonIcon--fill) {
|
||||
color: $euiTextColor !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
@ -46,4 +50,10 @@
|
|||
.euiButtonIcon {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mapToolbarOverlay__button__exit {
|
||||
.euiButtonIcon {
|
||||
color: $euiColorDangerText !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.mapDrawControl__geometryFilterForm {
|
||||
padding: $euiSizeS;
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiButtonIcon, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DRAW_SHAPE } from '../../../../../common/constants';
|
||||
import { VectorCircleIcon } from '../../icons/vector_circle_icon';
|
||||
import { VectorLineIcon } from '../../icons/vector_line_icon';
|
||||
import { VectorSquareIcon } from '../../icons/vector_square_icon';
|
||||
|
||||
export interface ReduxStateProps {
|
||||
drawShape?: string;
|
||||
}
|
||||
|
||||
export interface ReduxDispatchProps {
|
||||
setDrawShape: (shapeToDraw: DRAW_SHAPE) => void;
|
||||
cancelEditing: () => void;
|
||||
}
|
||||
|
||||
export interface OwnProps {
|
||||
pointsOnly?: boolean;
|
||||
}
|
||||
|
||||
type Props = ReduxStateProps & ReduxDispatchProps & OwnProps;
|
||||
|
||||
export function FeatureEditTools(props: Props) {
|
||||
const drawLineSelected = props.drawShape === DRAW_SHAPE.LINE;
|
||||
const drawPolygonSelected = props.drawShape === DRAW_SHAPE.POLYGON;
|
||||
const drawCircleSelected = props.drawShape === DRAW_SHAPE.DISTANCE;
|
||||
const drawBBoxSelected = props.drawShape === DRAW_SHAPE.BOUNDS;
|
||||
const drawPointSelected = props.drawShape === DRAW_SHAPE.POINT;
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="none" className="mapToolbarOverlay__buttonGroup">
|
||||
{props.pointsOnly ? null : (
|
||||
<>
|
||||
<EuiButtonIcon
|
||||
key="line"
|
||||
size="s"
|
||||
onClick={() => props.setDrawShape(DRAW_SHAPE.LINE)}
|
||||
iconType={VectorLineIcon}
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawLineLabel', {
|
||||
defaultMessage: 'Draw line',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawLineTitle', {
|
||||
defaultMessage: 'Draw line',
|
||||
})}
|
||||
aria-pressed={drawLineSelected}
|
||||
isSelected={drawLineSelected}
|
||||
display={drawLineSelected ? 'fill' : 'empty'}
|
||||
/>
|
||||
|
||||
<EuiButtonIcon
|
||||
key="polygon"
|
||||
size="s"
|
||||
onClick={() => props.setDrawShape(DRAW_SHAPE.POLYGON)}
|
||||
iconType="node"
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPolygonLabel', {
|
||||
defaultMessage: 'Draw polygon',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPolygonTitle', {
|
||||
defaultMessage: 'Draw polygon',
|
||||
})}
|
||||
aria-pressed={drawPolygonSelected}
|
||||
isSelected={drawPolygonSelected}
|
||||
display={drawPolygonSelected ? 'fill' : 'empty'}
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
key="circle"
|
||||
size="s"
|
||||
onClick={() => props.setDrawShape(DRAW_SHAPE.DISTANCE)}
|
||||
iconType={VectorCircleIcon}
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawCircleLabel', {
|
||||
defaultMessage: 'Draw circle',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawCircleTitle', {
|
||||
defaultMessage: 'Draw circle',
|
||||
})}
|
||||
aria-pressed={drawCircleSelected}
|
||||
isSelected={drawCircleSelected}
|
||||
display={drawCircleSelected ? 'fill' : 'empty'}
|
||||
/>
|
||||
<EuiButtonIcon
|
||||
key="boundingBox"
|
||||
size="s"
|
||||
onClick={() => props.setDrawShape(DRAW_SHAPE.BOUNDS)}
|
||||
iconType={VectorSquareIcon}
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawBBoxLabel', {
|
||||
defaultMessage: 'Draw bounding box',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawBBoxTitle', {
|
||||
defaultMessage: 'Draw bounding box',
|
||||
})}
|
||||
aria-pressed={drawBBoxSelected}
|
||||
isSelected={drawBBoxSelected}
|
||||
display={drawBBoxSelected ? 'fill' : 'empty'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<EuiButtonIcon
|
||||
key="point"
|
||||
size="s"
|
||||
onClick={() => props.setDrawShape(DRAW_SHAPE.POINT)}
|
||||
iconType="dot"
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPointLabel', {
|
||||
defaultMessage: 'Draw point',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPointTitle', {
|
||||
defaultMessage: 'Draw point',
|
||||
})}
|
||||
aria-pressed={drawPointSelected}
|
||||
isSelected={drawPointSelected}
|
||||
display={drawPointSelected ? 'fill' : 'empty'}
|
||||
/>
|
||||
|
||||
<EuiButtonIcon
|
||||
key="exit"
|
||||
size="s"
|
||||
onClick={props.cancelEditing}
|
||||
iconType="exit"
|
||||
aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.cancelDraw', {
|
||||
defaultMessage: 'Exit feature editing',
|
||||
})}
|
||||
title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.cancelDrawTitle', {
|
||||
defaultMessage: 'Exit feature editing',
|
||||
})}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
* 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 {
|
||||
FeatureEditTools,
|
||||
ReduxDispatchProps,
|
||||
ReduxStateProps,
|
||||
OwnProps,
|
||||
} from './feature_edit_tools';
|
||||
import { setDrawMode, updateEditShape } from '../../../../actions';
|
||||
import { MapStoreState } from '../../../../reducers/store';
|
||||
import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common';
|
||||
import { getEditState } from '../../../../selectors/map_selectors';
|
||||
|
||||
function mapStateToProps(state: MapStoreState): ReduxStateProps {
|
||||
const editState = getEditState(state);
|
||||
return {
|
||||
drawShape: editState ? editState.drawShape : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(
|
||||
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>
|
||||
): ReduxDispatchProps {
|
||||
return {
|
||||
setDrawShape: (shapeToDraw: DRAW_SHAPE) => {
|
||||
dispatch(updateEditShape(shapeToDraw));
|
||||
},
|
||||
cancelEditing: () => {
|
||||
dispatch(setDrawMode(DRAW_MODE.NONE));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const connectedFeatureEditControl = connect<
|
||||
ReduxStateProps,
|
||||
ReduxDispatchProps,
|
||||
OwnProps,
|
||||
MapStoreState
|
||||
>(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FeatureEditTools);
|
||||
export { connectedFeatureEditControl as FeatureEditTools };
|
|
@ -6,13 +6,17 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { MapStoreState } from '../../reducers/store';
|
||||
import { getGeoFieldNames } from '../../selectors/map_selectors';
|
||||
import { ToolbarOverlay } from './toolbar_overlay';
|
||||
import { MapStoreState } from '../../reducers/store';
|
||||
import { getDrawMode } from '../../selectors/ui_selectors';
|
||||
import { getGeoFieldNames } from '../../selectors/map_selectors';
|
||||
import { DRAW_MODE } from '../../../common';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
return {
|
||||
showToolsControl: getGeoFieldNames(state).length !== 0,
|
||||
shapeDrawModeActive: getDrawMode(state) === DRAW_MODE.DRAW_SHAPES,
|
||||
pointDrawModeActive: getDrawMode(state) === DRAW_MODE.DRAW_POINTS,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ test('Should only show set view control', async () => {
|
|||
const component = shallow(
|
||||
<ToolbarOverlay
|
||||
showToolsControl={false}
|
||||
shapeDrawModeActive={false}
|
||||
pointDrawModeActive={false}
|
||||
showFitToBoundsButton={false}
|
||||
showTimesliderButton={false}
|
||||
/>
|
||||
|
@ -33,10 +35,38 @@ test('Should only show set view control', async () => {
|
|||
test('Should show all controls', async () => {
|
||||
const component = shallow(
|
||||
<ToolbarOverlay
|
||||
addFilters={async (filters: Filter[], actionId: string) => {}}
|
||||
showToolsControl={true}
|
||||
addFilters={async (filters: Filter[], actionId: string) => {}}
|
||||
showFitToBoundsButton={true}
|
||||
showTimesliderButton={true}
|
||||
shapeDrawModeActive={false}
|
||||
pointDrawModeActive={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should show point layer edit tools', async () => {
|
||||
const component = shallow(
|
||||
<ToolbarOverlay
|
||||
showToolsControl={false}
|
||||
shapeDrawModeActive={false}
|
||||
pointDrawModeActive={true}
|
||||
showFitToBoundsButton={false}
|
||||
showTimesliderButton={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should show shape layer edit tools', async () => {
|
||||
const component = shallow(
|
||||
<ToolbarOverlay
|
||||
showToolsControl={false}
|
||||
shapeDrawModeActive={true}
|
||||
pointDrawModeActive={false}
|
||||
showFitToBoundsButton={false}
|
||||
showTimesliderButton={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Filter } from 'src/plugins/data/public';
|
|||
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
|
||||
import { SetViewControl } from './set_view_control';
|
||||
import { ToolsControl } from './tools_control';
|
||||
import { FeatureEditTools } from './feature_draw_controls/feature_edit_tools';
|
||||
import { FitToData } from './fit_to_data';
|
||||
import { TimesliderToggleButton } from './timeslider_toggle_button';
|
||||
|
||||
|
@ -19,6 +20,8 @@ export interface Props {
|
|||
showToolsControl: boolean;
|
||||
getFilterActions?: () => Promise<Action[]>;
|
||||
getActionContext?: () => ActionExecutionContext;
|
||||
shapeDrawModeActive: boolean;
|
||||
pointDrawModeActive: boolean;
|
||||
showFitToBoundsButton: boolean;
|
||||
showTimesliderButton: boolean;
|
||||
}
|
||||
|
@ -30,6 +33,7 @@ export function ToolbarOverlay(props: Props) {
|
|||
<ToolsControl
|
||||
getFilterActions={props.getFilterActions}
|
||||
getActionContext={props.getActionContext}
|
||||
disableToolsControl={props.pointDrawModeActive || props.shapeDrawModeActive}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
|
@ -46,6 +50,13 @@ export function ToolbarOverlay(props: Props) {
|
|||
</EuiFlexItem>
|
||||
) : null;
|
||||
|
||||
const featureDrawControl =
|
||||
props.shapeDrawModeActive || props.pointDrawModeActive ? (
|
||||
<EuiFlexItem>
|
||||
<FeatureEditTools pointsOnly={props.pointDrawModeActive} />
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className="mapToolbarOverlay"
|
||||
|
@ -63,6 +74,8 @@ export function ToolbarOverlay(props: Props) {
|
|||
{toolsButton}
|
||||
|
||||
{timesliderToogleButon}
|
||||
|
||||
{featureDrawControl}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ exports[`Should render cancel button when drawing 1`] = `
|
|||
aria-label="Tools"
|
||||
color="text"
|
||||
iconType="wrench"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
title="Tools"
|
||||
|
@ -117,6 +118,7 @@ exports[`renders 1`] = `
|
|||
aria-label="Tools"
|
||||
color="text"
|
||||
iconType="wrench"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
title="Tools"
|
||||
|
|
|
@ -9,24 +9,28 @@ import { AnyAction } from 'redux';
|
|||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { connect } from 'react-redux';
|
||||
import { ToolsControl } from './tools_control';
|
||||
import { isDrawingFilter } from '../../../selectors/map_selectors';
|
||||
import { updateDrawState } from '../../../actions';
|
||||
import { setDrawMode, updateDrawState } from '../../../actions';
|
||||
import { MapStoreState } from '../../../reducers/store';
|
||||
import { DrawState } from '../../../../common/descriptor_types';
|
||||
import { DRAW_MODE } from '../../../../common';
|
||||
import { getDrawMode } from '../../../selectors/ui_selectors';
|
||||
|
||||
function mapStateToProps(state: MapStoreState) {
|
||||
const drawMode = getDrawMode(state);
|
||||
return {
|
||||
isDrawingFilter: isDrawingFilter(state),
|
||||
filterModeActive: drawMode === DRAW_MODE.DRAW_FILTERS,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
|
||||
return {
|
||||
initiateDraw: (drawState: DrawState) => {
|
||||
dispatch(updateDrawState(drawState));
|
||||
},
|
||||
cancelDraw: () => {
|
||||
dispatch(updateDrawState(null));
|
||||
dispatch(setDrawMode(DRAW_MODE.NONE));
|
||||
},
|
||||
initiateDraw: (drawState: DrawState) => {
|
||||
dispatch(setDrawMode(DRAW_MODE.DRAW_FILTERS));
|
||||
dispatch(updateDrawState(drawState));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,7 +20,10 @@ const defaultProps = {
|
|||
indexPatternId: '1',
|
||||
},
|
||||
],
|
||||
isDrawingFilter: false,
|
||||
filterModeActive: false,
|
||||
activateDrawFilterMode: () => {},
|
||||
deactivateDrawMode: () => {},
|
||||
disableToolsControl: false,
|
||||
};
|
||||
|
||||
test('renders', async () => {
|
||||
|
@ -30,7 +33,7 @@ test('renders', async () => {
|
|||
});
|
||||
|
||||
test('Should render cancel button when drawing', async () => {
|
||||
const component = shallow(<ToolsControl {...defaultProps} isDrawingFilter />);
|
||||
const component = shallow(<ToolsControl {...defaultProps} filterModeActive={true} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -18,10 +18,11 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
|
||||
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../common/constants';
|
||||
import { DRAW_SHAPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../common/constants';
|
||||
import { GeometryFilterForm } from '../../../components/draw_forms/geometry_filter_form/geometry_filter_form';
|
||||
import { DistanceFilterForm } from '../../../components/draw_forms/distance_filter_form';
|
||||
// @ts-expect-error
|
||||
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
|
||||
import { DistanceFilterForm } from '../../../components/distance_filter_form';
|
||||
import { IndexGeometrySelectPopoverForm } from '../../../components/draw_forms/index_geometry_select_popover_form';
|
||||
import { DrawState } from '../../../../common/descriptor_types';
|
||||
|
||||
const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
|
||||
|
@ -53,10 +54,11 @@ const DRAW_DISTANCE_LABEL_SHORT = i18n.translate(
|
|||
|
||||
export interface Props {
|
||||
cancelDraw: () => void;
|
||||
initiateDraw: (drawState: DrawState) => void;
|
||||
isDrawingFilter: boolean;
|
||||
filterModeActive: boolean;
|
||||
getFilterActions?: () => Promise<Action[]>;
|
||||
getActionContext?: () => ActionExecutionContext;
|
||||
initiateDraw: (drawState: DrawState) => void;
|
||||
disableToolsControl: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -76,18 +78,21 @@ export class ToolsControl extends Component<Props, State> {
|
|||
|
||||
_closePopover = () => {
|
||||
this.setState({ isPopoverOpen: false });
|
||||
if (this.props.filterModeActive) {
|
||||
this.props.cancelDraw();
|
||||
}
|
||||
};
|
||||
|
||||
_initiateShapeDraw = (options: {
|
||||
actionId: string;
|
||||
geometryLabel: string;
|
||||
indexPatternId: string;
|
||||
geoFieldName: string;
|
||||
geoFieldType: ES_GEO_FIELD_TYPE;
|
||||
relation: ES_SPATIAL_RELATIONS;
|
||||
geometryLabel?: string;
|
||||
indexPatternId?: string;
|
||||
geoFieldName?: string;
|
||||
geoFieldType?: ES_GEO_FIELD_TYPE;
|
||||
relation?: ES_SPATIAL_RELATIONS;
|
||||
}) => {
|
||||
this.props.initiateDraw({
|
||||
drawType: DRAW_TYPE.POLYGON,
|
||||
drawShape: DRAW_SHAPE.POLYGON,
|
||||
...options,
|
||||
});
|
||||
this._closePopover();
|
||||
|
@ -95,11 +100,14 @@ export class ToolsControl extends Component<Props, State> {
|
|||
|
||||
_initiateBoundsDraw = (options: {
|
||||
actionId: string;
|
||||
geometryLabel: string;
|
||||
relation: ES_SPATIAL_RELATIONS;
|
||||
geometryLabel?: string;
|
||||
indexPatternId?: string;
|
||||
geoFieldName?: string;
|
||||
geoFieldType?: ES_GEO_FIELD_TYPE;
|
||||
relation?: ES_SPATIAL_RELATIONS;
|
||||
}) => {
|
||||
this.props.initiateDraw({
|
||||
drawType: DRAW_TYPE.BOUNDS,
|
||||
drawShape: DRAW_SHAPE.BOUNDS,
|
||||
...options,
|
||||
});
|
||||
this._closePopover();
|
||||
|
@ -107,7 +115,7 @@ export class ToolsControl extends Component<Props, State> {
|
|||
|
||||
_initiateDistanceDraw = (options: { actionId: string; filterLabel: string }) => {
|
||||
this.props.initiateDraw({
|
||||
drawType: DRAW_TYPE.DISTANCE,
|
||||
drawShape: DRAW_SHAPE.DISTANCE,
|
||||
...options,
|
||||
});
|
||||
this._closePopover();
|
||||
|
@ -205,6 +213,7 @@ export class ToolsControl extends Component<Props, State> {
|
|||
title={i18n.translate('xpack.maps.toolbarOverlay.toolsControlTitle', {
|
||||
defaultMessage: 'Tools',
|
||||
})}
|
||||
isDisabled={this.props.disableToolsControl}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
@ -224,7 +233,7 @@ export class ToolsControl extends Component<Props, State> {
|
|||
</EuiPopover>
|
||||
);
|
||||
|
||||
if (!this.props.isDrawingFilter) {
|
||||
if (!this.props.filterModeActive) {
|
||||
return toolsPopoverButton;
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
ROLLBACK_MAP_SETTINGS,
|
||||
TRACK_MAP_SETTINGS,
|
||||
UPDATE_MAP_SETTING,
|
||||
UPDATE_EDIT_STATE,
|
||||
} from '../../actions';
|
||||
|
||||
import { getDefaultMapSettings } from './default_map_settings';
|
||||
|
@ -76,6 +77,7 @@ export const DEFAULT_MAP_STATE: MapState = {
|
|||
filters: [],
|
||||
refreshTimerLastTriggeredAt: undefined,
|
||||
drawState: undefined,
|
||||
editState: undefined,
|
||||
},
|
||||
selectedLayerId: null,
|
||||
layerList: [],
|
||||
|
@ -94,6 +96,14 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: Record<string,
|
|||
drawState: action.drawState,
|
||||
},
|
||||
};
|
||||
case UPDATE_EDIT_STATE:
|
||||
return {
|
||||
...state,
|
||||
mapState: {
|
||||
...state.mapState,
|
||||
editState: action.editState,
|
||||
},
|
||||
};
|
||||
case REMOVE_TRACKED_LAYER_STATE:
|
||||
return removeTrackedLayerState(state, action.layerId);
|
||||
case TRACK_CURRENT_LAYER_STATE:
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import {
|
||||
DrawState,
|
||||
EditState,
|
||||
Goto,
|
||||
LayerDescriptor,
|
||||
MapCenter,
|
||||
|
@ -36,6 +37,7 @@ export type MapContext = {
|
|||
filters: Filter[];
|
||||
refreshTimerLastTriggeredAt?: string;
|
||||
drawState?: DrawState;
|
||||
editState?: EditState;
|
||||
searchSessionId?: string;
|
||||
searchSessionMapBuffer?: MapExtent;
|
||||
};
|
||||
|
|
|
@ -18,7 +18,9 @@ import {
|
|||
SET_OPEN_TOC_DETAILS,
|
||||
SHOW_TOC_DETAILS,
|
||||
HIDE_TOC_DETAILS,
|
||||
SET_DRAW_MODE,
|
||||
} from '../actions';
|
||||
import { DRAW_MODE } from '../../common';
|
||||
|
||||
export enum FLYOUT_STATE {
|
||||
NONE = 'NONE',
|
||||
|
@ -29,6 +31,7 @@ export enum FLYOUT_STATE {
|
|||
|
||||
export type MapUiState = {
|
||||
flyoutDisplay: FLYOUT_STATE;
|
||||
drawMode: DRAW_MODE;
|
||||
isFullScreen: boolean;
|
||||
isReadOnly: boolean;
|
||||
isLayerTOCOpen: boolean;
|
||||
|
@ -40,6 +43,7 @@ export const DEFAULT_IS_LAYER_TOC_OPEN = true;
|
|||
|
||||
export const DEFAULT_MAP_UI_STATE = {
|
||||
flyoutDisplay: FLYOUT_STATE.NONE,
|
||||
drawMode: DRAW_MODE.NONE,
|
||||
isFullScreen: false,
|
||||
isReadOnly: !getMapsCapabilities().save,
|
||||
isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN,
|
||||
|
@ -54,6 +58,8 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) {
|
|||
switch (action.type) {
|
||||
case UPDATE_FLYOUT:
|
||||
return { ...state, flyoutDisplay: action.display };
|
||||
case SET_DRAW_MODE:
|
||||
return { ...state, drawMode: action.drawMode };
|
||||
case SET_IS_LAYER_TOC_OPEN:
|
||||
return { ...state, isLayerTOCOpen: action.isLayerTOCOpen };
|
||||
case SET_IS_TIME_SLIDER_OPEN:
|
||||
|
|
|
@ -68,6 +68,7 @@ describe('getDataFilters', () => {
|
|||
minLat: -0.25,
|
||||
minLon: -0.25,
|
||||
};
|
||||
const isReadOnly = false;
|
||||
|
||||
test('should set buffer as searchSessionMapBuffer when using searchSessionId', () => {
|
||||
const dataFilters = getDataFilters.resultFunc(
|
||||
|
@ -80,7 +81,8 @@ describe('getDataFilters', () => {
|
|||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
searchSessionMapBuffer
|
||||
searchSessionMapBuffer,
|
||||
isReadOnly
|
||||
);
|
||||
expect(dataFilters.buffer).toEqual(searchSessionMapBuffer);
|
||||
});
|
||||
|
@ -96,7 +98,8 @@ describe('getDataFilters', () => {
|
|||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
undefined
|
||||
undefined,
|
||||
isReadOnly
|
||||
);
|
||||
expect(dataFilters.buffer).toEqual(mapBuffer);
|
||||
});
|
||||
|
|
|
@ -28,9 +28,9 @@ import { getSourceByType } from '../classes/sources/source_registry';
|
|||
import { GeoJsonFileSource } from '../classes/sources/geojson_file_source';
|
||||
import {
|
||||
SOURCE_DATA_REQUEST_ID,
|
||||
SPATIAL_FILTERS_LAYER_ID,
|
||||
STYLE_TYPE,
|
||||
VECTOR_STYLES,
|
||||
SPATIAL_FILTERS_LAYER_ID,
|
||||
} from '../../common/constants';
|
||||
// @ts-ignore
|
||||
import { extractFeaturesFromFilters } from '../../common/elasticsearch_util';
|
||||
|
@ -39,6 +39,7 @@ import {
|
|||
AbstractSourceDescriptor,
|
||||
DataRequestDescriptor,
|
||||
DrawState,
|
||||
EditState,
|
||||
Goto,
|
||||
HeatmapLayerDescriptor,
|
||||
LayerDescriptor,
|
||||
|
@ -55,6 +56,7 @@ import { ITMSSource } from '../classes/sources/tms_source';
|
|||
import { IVectorSource } from '../classes/sources/vector_source';
|
||||
import { ESGeoGridSource } from '../classes/sources/es_geo_grid_source';
|
||||
import { ILayer } from '../classes/layers/layer';
|
||||
import { getIsReadOnly } from './ui_selectors';
|
||||
|
||||
export function createLayerInstance(
|
||||
layerDescriptor: LayerDescriptor,
|
||||
|
@ -196,9 +198,8 @@ export const isUsingSearch = (state: MapStoreState): boolean => {
|
|||
export const getDrawState = ({ map }: MapStoreState): DrawState | undefined =>
|
||||
map.mapState.drawState;
|
||||
|
||||
export const isDrawingFilter = ({ map }: MapStoreState): boolean => {
|
||||
return !!map.mapState.drawState;
|
||||
};
|
||||
export const getEditState = ({ map }: MapStoreState): EditState | undefined =>
|
||||
map.mapState.editState;
|
||||
|
||||
export const getRefreshTimerLastTriggeredAt = ({ map }: MapStoreState): string | undefined =>
|
||||
map.mapState.refreshTimerLastTriggeredAt;
|
||||
|
@ -229,6 +230,7 @@ export const getDataFilters = createSelector(
|
|||
getFilters,
|
||||
getSearchSessionId,
|
||||
getSearchSessionMapBuffer,
|
||||
getIsReadOnly,
|
||||
(
|
||||
mapExtent,
|
||||
mapBuffer,
|
||||
|
@ -239,7 +241,8 @@ export const getDataFilters = createSelector(
|
|||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
searchSessionMapBuffer
|
||||
searchSessionMapBuffer,
|
||||
isReadOnly
|
||||
) => {
|
||||
return {
|
||||
extent: mapExtent,
|
||||
|
@ -251,6 +254,7 @@ export const getDataFilters = createSelector(
|
|||
query,
|
||||
filters,
|
||||
searchSessionId,
|
||||
isReadOnly,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
import { MapStoreState } from '../reducers/store';
|
||||
|
||||
import { FLYOUT_STATE } from '../reducers/ui';
|
||||
import { DRAW_MODE } from '../../common';
|
||||
|
||||
export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay;
|
||||
export const getDrawMode = ({ ui }: MapStoreState): DRAW_MODE => ui.drawMode;
|
||||
export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen;
|
||||
export const getIsTimesliderOpen = ({ ui }: MapStoreState): boolean => ui.isTimesliderOpen;
|
||||
export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails;
|
||||
|
|
|
@ -11,7 +11,6 @@ import { FeatureCollection } from 'geojson';
|
|||
import * as topojson from 'topojson-client';
|
||||
import { GeometryCollection } from 'topojson-specification';
|
||||
import _ from 'lodash';
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import {
|
||||
GIS_API_PATH,
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { IScopedClusterClient } from 'kibana/server';
|
||||
import { MatchingIndexesResp } from '../../common';
|
||||
|
||||
export async function getMatchingIndexes(
|
||||
indexPattern: string,
|
||||
{ asCurrentUser }: IScopedClusterClient
|
||||
): Promise<MatchingIndexesResp> {
|
||||
try {
|
||||
const { body: indexResults } = await asCurrentUser.cat.indices({
|
||||
index: indexPattern,
|
||||
format: 'JSON',
|
||||
});
|
||||
const matchingIndexes = indexResults
|
||||
.map((indexRecord) => indexRecord.index)
|
||||
.filter((indexName) => !!indexName);
|
||||
return {
|
||||
success: true,
|
||||
matchingIndexes: matchingIndexes as string[],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ export async function writeDataToIndex(
|
|||
})
|
||||
);
|
||||
}
|
||||
const settings: WriteSettings = { index, body: data };
|
||||
const settings: WriteSettings = { index, body: data, refresh: true };
|
||||
const { body: resp } = await asCurrentUser.index(settings);
|
||||
if (resp.result === 'Error') {
|
||||
throw resp;
|
||||
|
|
|
@ -12,11 +12,13 @@ import type { DataRequestHandlerContext } from 'src/plugins/data/server';
|
|||
import {
|
||||
INDEX_SOURCE_API_PATH,
|
||||
MAX_DRAWING_SIZE_BYTES,
|
||||
GET_MATCHING_INDEXES_PATH,
|
||||
INDEX_FEATURE_PATH,
|
||||
} from '../../common/constants';
|
||||
import { createDocSource } from './create_doc_source';
|
||||
import { writeDataToIndex } from './index_data';
|
||||
import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server';
|
||||
import { getMatchingIndexes } from './get_indexes_matching_pattern';
|
||||
|
||||
export function initIndexingRoutes({
|
||||
router,
|
||||
|
@ -101,4 +103,22 @@ export function initIndexingRoutes({
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: `${GET_MATCHING_INDEXES_PATH}/{indexPattern}`,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
indexPattern: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const result = await getMatchingIndexes(
|
||||
request.params.indexPattern,
|
||||
context.core.elasticsearch.client
|
||||
);
|
||||
return response.ok({ body: result });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13242,7 +13242,6 @@
|
|||
"xpack.maps.layerControl.addLayerButtonLabel": "レイヤーを追加",
|
||||
"xpack.maps.layerControl.closeLayerTOCButtonAriaLabel": "レイヤーパネルを畳む",
|
||||
"xpack.maps.layerControl.layersTitle": "レイヤー",
|
||||
"xpack.maps.layerControl.layerTocActions.editButtonLabel": "レイヤーを編集",
|
||||
"xpack.maps.layerControl.openLayerTOCButtonAriaLabel": "レイヤーパネルを拡張",
|
||||
"xpack.maps.layerControl.tocEntry.grabButtonAriaLabel": "レイヤーの並べ替え",
|
||||
"xpack.maps.layerControl.tocEntry.grabButtonTitle": "レイヤーの並べ替え",
|
||||
|
|
|
@ -13419,7 +13419,6 @@
|
|||
"xpack.maps.layerControl.addLayerButtonLabel": "添加图层",
|
||||
"xpack.maps.layerControl.closeLayerTOCButtonAriaLabel": "折叠图层面板",
|
||||
"xpack.maps.layerControl.layersTitle": "图层",
|
||||
"xpack.maps.layerControl.layerTocActions.editButtonLabel": "编辑图层",
|
||||
"xpack.maps.layerControl.openLayerTOCButtonAriaLabel": "展开图层面板",
|
||||
"xpack.maps.layerControl.tocEntry.grabButtonAriaLabel": "重新排序图层",
|
||||
"xpack.maps.layerControl.tocEntry.grabButtonTitle": "重新排序图层",
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('get matching index patterns', () => {
|
||||
it('should return an array containing indexes matching pattern', async () => {
|
||||
const resp = await supertest
|
||||
.get(`/api/maps/getMatchingIndexes/geo_shapes`)
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(resp.body.success).to.be(true);
|
||||
expect(resp.body.matchingIndexes.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should return an empty array when no indexes match pattern', async () => {
|
||||
const resp = await supertest
|
||||
.get(`/api/maps/getMatchingIndexes/notAnIndex`)
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(resp.body.success).to.be(false);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -15,6 +15,7 @@ export default function ({ loadTestFile, getService }) {
|
|||
});
|
||||
|
||||
describe('', () => {
|
||||
loadTestFile(require.resolve('./get_indexes_matching_pattern'));
|
||||
loadTestFile(require.resolve('./create_doc_source'));
|
||||
loadTestFile(require.resolve('./index_data'));
|
||||
loadTestFile(require.resolve('./fonts_api'));
|
||||
|
|
|
@ -312,7 +312,7 @@ export class GisPageObject extends FtrService {
|
|||
async openLayerPanel(layerName: string) {
|
||||
this.log.debug(`Open layer panel, layer: ${layerName}`);
|
||||
await this.openLayerTocActionsPanel(layerName);
|
||||
await this.testSubjects.click('editLayerButton');
|
||||
await this.testSubjects.click('layerSettingsButton');
|
||||
}
|
||||
|
||||
async closeLayerPanel() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue