[Maps] Add layer edit controls (#99812)

Co-authored-by: miukimiu <elizabet.oliveira@elastic.co>
This commit is contained in:
Aaron Caldwell 2021-06-17 07:21:45 -06:00 committed by GitHub
parent d57ffce8ec
commit 246e7be3e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 1213 additions and 235 deletions

View file

@ -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_';

View file

@ -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;

View file

@ -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;
};

View file

@ -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;

View file

@ -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;
}

View file

@ -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,

View file

@ -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';

View file

@ -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));
};
}

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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);
}
}

View file

@ -144,6 +144,7 @@ describe('ESGeoGridSource', () => {
};
const vectorSourceRequestMeta: VectorSourceRequestMeta = {
isReadOnly: false,
geogridPrecision: 4,
filters: [],
timeFilters: {

View file

@ -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,

View file

@ -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'],

View file

@ -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> {

View file

@ -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';

View file

@ -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 = {

View file

@ -7,7 +7,7 @@
jest.mock('../../../kibana_services', () => ({}));
jest.mock('./load_index_settings', () => ({
jest.mock('./util/load_index_settings', () => ({
loadIndexSettings: async () => {
return { maxInnerResultWindow: 100 };
},

View file

@ -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',
});
};

View file

@ -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 = {

View file

@ -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;

View file

@ -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>>();

View file

@ -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,

View file

@ -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;

View file

@ -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({

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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;

View file

@ -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 });
};

View file

@ -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} />;
}
}

View file

@ -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}
/>
);
}
}

View file

@ -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 };

View file

@ -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}
/>
);
}

View file

@ -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));
},
};
}

View file

@ -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);
}

View file

@ -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 };

View file

@ -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,
};
}

View file

@ -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>

View file

@ -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;

View file

@ -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),
};

View file

@ -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}
/>
);

View file

@ -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;
}

View file

@ -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));

View file

@ -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"

View file

@ -30,6 +30,10 @@
}
}
.mapTocEntry-isInEditingMode {
background-color: tintOrShade($euiColorPrimary, 90%, 70%) !important;
}
.mapTocEntry-isDragging {
@include euiBottomShadowMedium;
}

View file

@ -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',
}
);

View file

@ -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(),
};
}

View file

@ -62,6 +62,7 @@ const defaultProps = {
isEditButtonDisabled: false,
hideTOCDetails: () => {},
showTOCDetails: () => {},
editModeActiveForLayer: false,
};
describe('TOCEntry', () => {

View file

@ -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 (

View file

@ -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,
},

View file

@ -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));
},
};
}

View file

@ -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', () => {

View file

@ -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({

View file

@ -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>
`;

View file

@ -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;
}
}

View file

@ -0,0 +1,3 @@
.mapDrawControl__geometryFilterForm {
padding: $euiSizeS;
}

View file

@ -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>
);
}

View file

@ -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 };

View file

@ -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,
};
}

View file

@ -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();

View file

@ -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>
);
}

View file

@ -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"

View file

@ -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));
},
};
}

View file

@ -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();
});

View file

@ -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;
}

View file

@ -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:

View file

@ -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;
};

View file

@ -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:

View file

@ -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);
});

View file

@ -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,
};
}
);

View file

@ -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;

View file

@ -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,

View file

@ -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,
};
}
}

View file

@ -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;

View file

@ -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 });
}
);
}

View file

@ -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": "レイヤーの並べ替え",

View file

@ -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": "重新排序图层",

View file

@ -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);
});
});
}

View file

@ -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'));

View file

@ -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() {