}
+ getActions={uiActions.getTriggerCompatibleActions}
+ inspector={inspector}
+ actionPredicate={actionPredicate}
+ showShadow={false}
+ showBadges={false}
+ showNotifications={false}
+ />
+ );
+};
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index 98e0198b9d0fa..a82bc0b9c0d3d 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -7,7 +7,7 @@
import { LensPlugin } from './plugin';
-export {
+export type {
EmbeddableComponentProps,
TypedLensByValueInput,
} from './editor_frame_service/embeddable/embeddable_component';
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
index d1b0ec8876feb..83a782b519248 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
@@ -585,7 +585,6 @@ export function FormulaEditor({
- {/* TODO: Replace `bolt` with `wordWrap` icon (after latest EUI is deployed) and hook up button to enable/disable word wrapping. */}
setIsHelpOpen(!isHelpOpen)}
>
-
+
@@ -747,7 +746,7 @@ export function FormulaEditor({
setIsHelpOpen(!isHelpOpen)}
- iconType="help"
+ iconType="documentation"
color="text"
size="s"
aria-label={i18n.translate(
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
index aa08dfbe7ea33..dbdbcf226d9bb 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx
@@ -64,6 +64,88 @@ function FormulaHelp({
items: [],
});
+ helpGroups.push({
+ label: i18n.translate('xpack.lens.formulaFrequentlyUsedHeading', {
+ defaultMessage: 'Common formulas',
+ }),
+ description: i18n.translate('xpack.lens.formulaCommonFormulaDocumentation', {
+ defaultMessage: `The most common formulas are dividing two values to produce a percent. To display accurately, set "value format" to "percent".`,
+ }),
+
+ items: [
+ {
+ label: i18n.translate('xpack.lens.formulaDocumentation.filterRatio', {
+ defaultMessage: 'Filter ratio',
+ }),
+ description: (
+ 400') / count()
+\`\`\`
+ `,
+
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ })}
+ />
+ ),
+ },
+ {
+ label: i18n.translate('xpack.lens.formulaDocumentation.weekOverWeek', {
+ defaultMessage: 'Week over week',
+ }),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate('xpack.lens.formulaDocumentation.percentOfTotal', {
+ defaultMessage: 'Percent of total',
+ }),
+ description: (
+
+ ),
+ },
+ ],
+ });
+
helpGroups.push({
label: i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSection', {
defaultMessage: 'Elasticsearch',
@@ -78,7 +160,7 @@ function FormulaHelp({
const availableFunctions = getPossibleFunctions(indexPattern);
// Es aggs
- helpGroups[1].items.push(
+ helpGroups[2].items.push(
...availableFunctions
.filter(
(key) =>
@@ -104,20 +186,20 @@ function FormulaHelp({
helpGroups.push({
label: i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSection', {
- defaultMessage: 'Column-wise calculation',
+ defaultMessage: 'Column calculations',
}),
description: i18n.translate(
'xpack.lens.formulaDocumentation.columnCalculationSectionDescription',
{
defaultMessage:
- 'These functions will be executed for reach row of the resulting table, using data from cells from other rows as well as the current value.',
+ 'These functions are executed for each row, but are provided with the whole column as context. This is also known as a window function.',
}
),
items: [],
});
// Calculations aggs
- helpGroups[2].items.push(
+ helpGroups[3].items.push(
...availableFunctions
.filter(
(key) =>
@@ -170,7 +252,7 @@ function FormulaHelp({
});
}, [indexPattern]);
- helpGroups[3].items.push(
+ helpGroups[4].items.push(
...tinymathFns.map(({ label, description, examples }) => {
return {
label,
@@ -312,9 +394,9 @@ round(100 * moving_average(
\`\`\`
Elasticsearch functions take a field name, which can be in quotes. \`sum(bytes)\` is the same
-as \`sum("bytes")\`.
+as \`sum('bytes')\`.
-Some functions take named arguments, like moving_average(count(), window=5)
+Some functions take named arguments, like \`moving_average(count(), window=5)\`.
Elasticsearch metrics can be filtered using KQL or Lucene syntax. To add a filter, use the named
parameter \`kql='field: value'\` or \`lucene=''\`. Always use single quotes when writing KQL or Lucene
@@ -325,7 +407,7 @@ Math functions can take positional arguments, like pow(count(), 3) is the same a
Use the symbols +, -, /, and * to perform basic math.
`,
description:
- 'Text is in markdown. Do not translate function names or field names like sum(bytes)',
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
})}
/>
diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
index 2e189e094ef01..b31125a1912ef 100644
--- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
@@ -20,17 +20,22 @@ function sampleArgs() {
l1: {
type: 'datatable',
columns: [
- { id: 'a', name: 'a', meta: { type: 'string' } },
+ // Simulating a calculated column like a formula
+ { id: 'a', name: 'a', meta: { type: 'string', params: { id: 'string' } } },
{ id: 'b', name: 'b', meta: { type: 'string' } },
- { id: 'c', name: 'c', meta: { type: 'number' } },
+ {
+ id: 'c',
+ name: 'c',
+ meta: { type: 'number', params: { id: 'percent', params: { format: '0.000%' } } },
+ },
],
- rows: [{ a: 10110, b: 2, c: 3 }],
+ rows: [{ a: 'last', b: 'last', c: 3 }],
},
},
};
const args: MetricConfig = {
- accessor: 'a',
+ accessor: 'c',
layerId: 'l1',
title: 'My fanci metric chart',
description: 'Fancy chart description',
@@ -39,7 +44,7 @@ function sampleArgs() {
};
const noAttributesArgs: MetricConfig = {
- accessor: 'a',
+ accessor: 'c',
layerId: 'l1',
title: '',
description: '',
@@ -65,11 +70,17 @@ describe('metric_expression', () => {
});
describe('MetricChart component', () => {
- test('it renders the all attributes when passed (title, description, metricTitle, value)', () => {
+ test('it renders all attributes when passed (title, description, metricTitle, value)', () => {
const { data, args } = sampleArgs();
expect(
- shallow( x as IFieldFormat} />)
+ shallow(
+ ({ convert: (x) => x } as IFieldFormat)}
+ />
+ )
).toMatchInlineSnapshot(`
{
}
}
>
- 10110
+ 3
+
+
+ My fanci metric chart
+
+
+
+ `);
+ });
+
+ test('it renders strings', () => {
+ const { data, args } = sampleArgs();
+ args.accessor = 'a';
+
+ expect(
+ shallow(
+ ({ convert: (x) => x } as IFieldFormat)}
+ />
+ )
+ ).toMatchInlineSnapshot(`
+
+
+
+ last
{
x as IFieldFormat}
+ formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
/>
)
).toMatchInlineSnapshot(`
@@ -130,7 +186,7 @@ describe('metric_expression', () => {
}
}
>
- 10110
+ 3
{
x as IFieldFormat}
+ formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
/>
)
).toMatchInlineSnapshot(`
@@ -174,7 +230,7 @@ describe('metric_expression', () => {
}
}
>
- 10110
+ 3
@@ -189,7 +245,7 @@ describe('metric_expression', () => {
x as IFieldFormat}
+ formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
/>
)
).toMatchInlineSnapshot(`
@@ -202,14 +258,14 @@ describe('metric_expression', () => {
test('it renders an EmptyPlaceholder when null value is passed as data', () => {
const { data, noAttributesArgs } = sampleArgs();
- data.tables.l1.rows[0].a = null;
+ data.tables.l1.rows[0].c = null;
expect(
shallow(
x as IFieldFormat}
+ formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
/>
)
).toMatchInlineSnapshot(`
@@ -222,14 +278,14 @@ describe('metric_expression', () => {
test('it renders 0 value', () => {
const { data, noAttributesArgs } = sampleArgs();
- data.tables.l1.rows[0].a = 0;
+ data.tables.l1.rows[0].c = 0;
expect(
shallow(
x as IFieldFormat}
+ formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
/>
)
).toMatchInlineSnapshot(`
@@ -264,5 +320,13 @@ describe('metric_expression', () => {
`);
});
+
+ test('it finds the right column to format', () => {
+ const { data, args } = sampleArgs();
+ const factory = jest.fn(() => ({ convert: (x) => x } as IFieldFormat));
+
+ shallow();
+ expect(factory).toHaveBeenCalledWith({ id: 'percent', params: { format: '0.000%' } });
+ });
});
});
diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx
index 70b2cb17c7fe1..60d9d66bce995 100644
--- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx
@@ -127,7 +127,7 @@ export function MetricChart({
return ;
}
- const column = firstTable.columns[0];
+ const column = firstTable.columns.find(({ id }) => id === accessor)!;
const row = firstTable.rows[0];
// NOTE: Cardinality and Sum never receives "null" as value, but always 0, even for empty dataset.
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index 328bea5def557..ad81413e21345 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -6,6 +6,7 @@
*/
import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
+import type { Start as InspectorStartContract } from 'src/plugins/inspector/public';
import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
@@ -81,6 +82,7 @@ export interface LensPluginStartDependencies {
savedObjectsTagging?: SavedObjectTaggingPluginStart;
presentationUtil: PresentationUtilPluginStart;
indexPatternFieldEditor: IndexPatternFieldEditorStart;
+ inspector: InspectorStartContract;
usageCollection?: UsageCollectionStart;
}
@@ -256,7 +258,7 @@ export class LensPlugin {
);
return {
- EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable),
+ EmbeddableComponent: getEmbeddableComponent(core, startDependencies),
SaveModalComponent: getSaveModalComponent(core, startDependencies, this.attributeService!),
navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => {
// for openInNewTab, we set the time range in url via getEditPath below
diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts
index fa065e701184e..c16579cc142f0 100644
--- a/x-pack/plugins/maps/common/constants.ts
+++ b/x-pack/plugins/maps/common/constants.ts
@@ -169,6 +169,7 @@ export enum DRAW_SHAPE {
POINT = 'POINT',
LINE = 'LINE',
SIMPLE_SELECT = 'SIMPLE_SELECT',
+ DELETE = 'DELETE',
}
export const AGG_DELIMITER = '_of_';
diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts
index 8bd79474e7f71..1d239a75d1499 100644
--- a/x-pack/plugins/maps/public/actions/layer_actions.ts
+++ b/x-pack/plugins/maps/public/actions/layer_actions.ts
@@ -11,6 +11,7 @@ import { Query } from 'src/plugins/data/public';
import { MapStoreState } from '../reducers/store';
import {
createLayerInstance,
+ getEditState,
getLayerById,
getLayerList,
getLayerListRaw,
@@ -481,6 +482,11 @@ function removeLayerFromLayerList(layerId: string) {
type: REMOVE_LAYER,
id: layerId,
});
+ // Clean up draw state if needed
+ const editState = getEditState(getState());
+ if (layerId === editState?.layerId) {
+ dispatch(setDrawMode(DRAW_MODE.NONE));
+ }
};
}
diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts
index 9d0d27496da92..464e4dbc6d5ae 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.ts
+++ b/x-pack/plugins/maps/public/actions/map_actions.ts
@@ -376,3 +376,22 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) {
await dispatch(syncDataForLayer(layer, true));
};
}
+
+export function deleteFeatureFromIndex(featureId: string) {
+ return async (
+ dispatch: ThunkDispatch,
+ 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.deleteFeature(featureId);
+ await dispatch(syncDataForLayer(layer, true));
+ };
+}
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
index 49a0878ef80b2..7a6d91a71db42 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
@@ -99,6 +99,7 @@ export interface IVectorLayer extends ILayer {
supportsFeatureEditing(): boolean;
getLeftJoinFields(): Promise;
addFeature(geometry: Geometry | Position[]): Promise;
+ deleteFeature(featureId: string): Promise;
}
export class VectorLayer extends AbstractLayer implements IVectorLayer {
@@ -1156,4 +1157,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
const layerSource = this.getSource();
await layerSource.addFeature(geometry);
}
+
+ async deleteFeature(featureId: string) {
+ const layerSource = this.getSource();
+ await layerSource.deleteFeature(featureId);
+ }
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
index 9f7bd1260ca22..019c3c1b4943b 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
@@ -66,7 +66,7 @@ import { isValidStringConfig } from '../../util/valid_string_config';
import { TopHitsUpdateSourceEditor } from './top_hits';
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';
+import { addFeatureToIndex, deleteFeatureFromIndex, getMatchingIndexes } from './util/feature_edit';
export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined {
const timeRangeBounds = getTimeFilter().calculateBounds(timerange);
@@ -716,6 +716,11 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
await addFeatureToIndex(indexPattern.title, geometry, this.getGeoFieldName());
}
+ async deleteFeature(featureId: string) {
+ const indexPattern = await this.getIndexPattern();
+ await deleteFeatureFromIndex(indexPattern.title, featureId);
+ }
+
async getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise {
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts
index ac8e2ba42f282..f306a225df69a 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts
@@ -26,6 +26,16 @@ export const addFeatureToIndex = async (
});
};
+export const deleteFeatureFromIndex = async (indexName: string, featureId: string) => {
+ return await getHttp().fetch({
+ path: `${INDEX_FEATURE_PATH}/${featureId}`,
+ method: 'DELETE',
+ body: JSON.stringify({
+ index: indexName,
+ }),
+ });
+};
+
export const getMatchingIndexes = async (indexPattern: string) => {
return await getHttp().fetch({
path: `${GET_MATCHING_INDEXES_PATH}/${indexPattern}`,
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
index 5bf7a2e47cc66..f825a85f50bbd 100644
--- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
@@ -102,6 +102,10 @@ export class MVTSingleLayerVectorSource
throw new Error('Does not implement addFeature');
}
+ deleteFeature(featureId: string): Promise {
+ throw new Error('Does not implement deleteFeature');
+ }
+
getMVTFields(): MVTField[] {
return this._descriptor.fields.map((field: MVTFieldDescriptor) => {
return new MVTField({
diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
index 8f93de705e365..f006fa7fde3a4 100644
--- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
@@ -69,6 +69,7 @@ export interface IVectorSource extends ISource {
getTimesliceMaskFieldName(): Promise;
supportsFeatureEditing(): Promise;
addFeature(geometry: Geometry | Position[]): Promise;
+ deleteFeature(featureId: string): Promise;
}
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
@@ -165,6 +166,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
throw new Error('Should implement VectorSource#addFeature');
}
+ async deleteFeature(featureId: string): Promise {
+ throw new Error('Should implement VectorSource#deleteFeature');
+ }
+
async supportsFeatureEditing(): Promise {
return false;
}
diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
index 6a193216c7c1e..9568ef5c35bb1 100644
--- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
+++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
@@ -20,7 +20,7 @@ export interface TimesliceMaskConfig {
}
export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true];
-const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];
+export const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];
function getFilterExpression(
filters: unknown[],
diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx
index 788094ee1ab5c..0bdf462cca4b3 100644
--- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx
+++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx
@@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
import { Filter } from 'src/plugins/data/public';
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
+import { Observable } from 'rxjs';
import { MBMap } from '../mb_map';
import { RightSideControls } from '../right_side_controls';
import { Timeslider } from '../timeslider';
@@ -47,6 +48,7 @@ export interface Props {
description?: string;
settings: MapSettings;
layerList: ILayer[];
+ waitUntilTimeLayersLoad$: Observable;
}
interface State {
@@ -223,7 +225,7 @@ export class MapContainer extends Component {
-
+
void;
+ onClick?: (event: MapMouseEvent, drawControl?: MapboxDraw) => void;
mbMap: MbMap;
enable: boolean;
updateEditShape: (shapeToDraw: DRAW_SHAPE) => void;
@@ -68,6 +70,12 @@ export class DrawControl extends Component {
this.props.onDraw(event, this._mbDrawControl);
};
+ _onClick = (event: MapMouseEvent) => {
+ if (this.props.onClick) {
+ this.props.onClick(event, this._mbDrawControl);
+ }
+ };
+
// debounce with zero timeout needed to allow mapbox-draw finish logic to complete
// before _removeDrawControl is called
_syncDrawControl = _.debounce(() => {
@@ -96,6 +104,9 @@ export class DrawControl extends Component {
this.props.mbMap.getCanvas().style.cursor = '';
this.props.mbMap.off('draw.modechange', this._onModeChange);
this.props.mbMap.off('draw.create', this._onDraw);
+ if (this.props.onClick) {
+ this.props.mbMap.off('click', this._onClick);
+ }
this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID);
this.props.mbMap.removeControl(this._mbDrawControl);
this._mbDrawControlAdded = false;
@@ -131,6 +142,9 @@ export class DrawControl extends Component {
this.props.mbMap.getCanvas().style.cursor = 'crosshair';
this.props.mbMap.on('draw.modechange', this._onModeChange);
this.props.mbMap.on('draw.create', this._onDraw);
+ if (this.props.onClick) {
+ this.props.mbMap.on('click', this._onClick);
+ }
}
const { DRAW_LINE_STRING, DRAW_POLYGON, DRAW_POINT, SIMPLE_SELECT } = this._mbDrawControl.modes;
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx
index fb595e7804dfe..eb5ea9b5ddba5 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx
@@ -6,26 +6,34 @@
*/
import React, { Component } from 'react';
-import { Map as MbMap } from 'mapbox-gl';
+import { Map as MbMap, Point as MbPoint } 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 { MapMouseEvent } from '@kbn/mapbox-gl';
import { getToasts } from '../../../../kibana_services';
import { DrawControl } from '../';
import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common';
+import { ILayer } from '../../../../classes/layers/layer';
+import {
+ EXCLUDE_CENTROID_FEATURES,
+ EXCLUDE_TOO_MANY_FEATURES_BOX,
+} from '../../../../classes/util/mb_filter_expressions';
const geoJSONReader = new jsts.io.GeoJSONReader();
export interface ReduxStateProps {
drawShape?: DRAW_SHAPE;
drawMode: DRAW_MODE;
+ editLayer: ILayer | undefined;
}
export interface ReduxDispatchProps {
addNewFeatureToIndex: (geometry: Geometry | Position[]) => void;
+ deleteFeatureFromIndex: (featureId: string) => void;
disableDrawState: () => void;
}
@@ -75,11 +83,58 @@ export class DrawFeatureControl extends Component {
}
};
+ _onClick = async (event: MapMouseEvent, drawControl?: MapboxDraw) => {
+ const mbLngLatPoint: MbPoint = event.point;
+ if (!this.props.editLayer) {
+ return;
+ }
+ const mbEditLayerIds = this.props.editLayer
+ .getMbLayerIds()
+ .filter((mbLayerId) => !!this.props.mbMap.getLayer(mbLayerId));
+ const PADDING = 2; // in pixels
+ const mbBbox = [
+ {
+ x: mbLngLatPoint.x - PADDING,
+ y: mbLngLatPoint.y - PADDING,
+ },
+ {
+ x: mbLngLatPoint.x + PADDING,
+ y: mbLngLatPoint.y + PADDING,
+ },
+ ] as [MbPoint, MbPoint];
+ const selectedFeatures = this.props.mbMap.queryRenderedFeatures(mbBbox, {
+ layers: mbEditLayerIds,
+ filter: ['all', EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES],
+ });
+ if (!selectedFeatures.length) {
+ return;
+ }
+ const topMostFeature = selectedFeatures[0];
+
+ try {
+ if (!(topMostFeature.properties && topMostFeature.properties._id)) {
+ throw Error(`Associated Elasticsearch document id not found`);
+ }
+ const docId = topMostFeature.properties._id;
+ this.props.deleteFeatureFromIndex(docId);
+ } catch (error) {
+ getToasts().addWarning(
+ i18n.translate('xpack.maps.drawFeatureControl.unableToDeleteFeature', {
+ defaultMessage: `Unable to delete feature, error: '{errorMsg}'.`,
+ values: {
+ errorMsg: error.message,
+ },
+ })
+ );
+ }
+ };
+
render() {
return (
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts
index 9034e40913e77..e1d703173fc2d 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts
@@ -15,16 +15,18 @@ import {
ReduxStateProps,
OwnProps,
} from './draw_feature_control';
-import { addNewFeatureToIndex, updateEditShape } from '../../../../actions';
+import { addNewFeatureToIndex, deleteFeatureFromIndex, updateEditShape } from '../../../../actions';
import { MapStoreState } from '../../../../reducers/store';
-import { getEditState } from '../../../../selectors/map_selectors';
+import { getEditState, getLayerById } from '../../../../selectors/map_selectors';
import { getDrawMode } from '../../../../selectors/ui_selectors';
function mapStateToProps(state: MapStoreState): ReduxStateProps {
const editState = getEditState(state);
+ const editLayer = editState ? getLayerById(editState.layerId, state) : undefined;
return {
drawShape: editState ? editState.drawShape : undefined,
drawMode: getDrawMode(state),
+ editLayer,
};
}
@@ -35,6 +37,9 @@ function mapDispatchToProps(
addNewFeatureToIndex(geometry: Geometry | Position[]) {
dispatch(addNewFeatureToIndex(geometry));
},
+ deleteFeatureFromIndex(featureId: string) {
+ dispatch(deleteFeatureFromIndex(featureId));
+ },
disableDrawState() {
dispatch(updateEditShape(null));
},
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx
index 5321c30f75245..4122f7ea796d4 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.tsx
@@ -98,6 +98,10 @@ export class DrawTooltip extends Component {
instructions = i18n.translate('xpack.maps.drawTooltip.pointInstructions', {
defaultMessage: 'Click to create point.',
});
+ } else if (this.props.drawShape === DRAW_SHAPE.DELETE) {
+ instructions = i18n.translate('xpack.maps.drawTooltip.deleteInstructions', {
+ defaultMessage: 'Click feature to delete.',
+ });
} else {
// unknown draw type, tooltip not needed
return null;
diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap
index 6310b5507dca5..42618d099ffcf 100644
--- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap
+++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap
@@ -11,6 +11,7 @@ exports[`TOCEntry is rendered 1`] = `
>
{
openLayerSettings={this._openLayerPanelWithCheck}
isEditButtonDisabled={this.props.isEditButtonDisabled}
supportsFitToBounds={this.state.supportsFitToBounds}
+ editModeActiveForLayer={this.props.editModeActiveForLayer}
/>
{this._renderQuickActions()}
diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap
index 5068a5dc1ad71..5d1fc8e28f993 100644
--- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap
+++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap
@@ -115,6 +115,121 @@ exports[`TOCEntryActionsPopover is rendered 1`] = `
`;
+exports[`TOCEntryActionsPopover should disable Edit features when edit mode active for layer 1`] = `
+
+ }
+ className="mapLayTocActions"
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="testLayer"
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="none"
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
+ "name": "Hide layer",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerSettingsButton",
+ "disabled": false,
+ "icon": ,
+ "name": "Edit layer settings",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "cloneLayerButton",
+ "icon": ,
+ "name": "Clone layer",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "removeLayerButton",
+ "icon": ,
+ "name": "Remove layer",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ ],
+ "title": "Layer actions",
+ },
+ ]
+ }
+ size="m"
+ />
+
+`;
+
exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBounds is false 1`] = `
{},
enablePointEditing: () => {},
openLayerSettings: () => {},
+ editModeActiveForLayer: false,
};
describe('TOCEntryActionsPopover', () => {
@@ -100,4 +101,17 @@ describe('TOCEntryActionsPopover', () => {
expect(component).toMatchSnapshot();
});
+
+ test('should disable Edit features when edit mode active for layer', async () => {
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise((resolve) => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component).toMatchSnapshot();
+ });
});
diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx
index ab7a54be37404..83b4d2c2a756b 100644
--- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx
+++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx
@@ -36,6 +36,7 @@ export interface Props {
removeLayer: (layerId: string) => void;
supportsFitToBounds: boolean;
toggleVisible: (layerId: string) => void;
+ editModeActiveForLayer: boolean;
}
interface State {
@@ -170,7 +171,7 @@ export class TOCEntryActionsPopover extends Component {
defaultMessage:
'Edit features only supported for document layers without clustering, joins, or time filtering',
}),
- disabled: !this.state.isFeatureEditingEnabled,
+ disabled: !this.state.isFeatureEditingEnabled || this.props.editModeActiveForLayer,
onClick: async () => {
this._closePopover();
const supportedShapeTypes = await (this.props.layer.getSource() as ESSearchSource).getSupportedShapeTypes();
diff --git a/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx b/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx
index 0b7bcb115eb95..fb3d87b3aa432 100644
--- a/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx
+++ b/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx
@@ -10,6 +10,8 @@ import React, { Component } from 'react';
import { EuiButtonIcon, EuiDualRange, EuiText } from '@elastic/eui';
import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks';
import { i18n } from '@kbn/i18n';
+import { Observable, Subscription } from 'rxjs';
+import { first } from 'rxjs/operators';
import { epochToKbnDateFormat, getInterval, getTicks } from './time_utils';
import { TimeRange } from '../../../../../../src/plugins/data/common';
import { getTimeFilter } from '../../kibana_services';
@@ -20,9 +22,11 @@ export interface Props {
setTimeslice: (timeslice: Timeslice) => void;
isTimesliderOpen: boolean;
timeRange: TimeRange;
+ waitForTimesliceToLoad$: Observable;
}
interface State {
+ isPaused: boolean;
max: number;
min: number;
range: number;
@@ -44,6 +48,8 @@ export function Timeslider(props: Props) {
class KeyedTimeslider extends Component {
private _isMounted: boolean = false;
+ private _timeoutId: number | undefined;
+ private _subscription: Subscription | undefined;
constructor(props: Props) {
super(props);
@@ -59,6 +65,7 @@ class KeyedTimeslider extends Component {
const timeslice: [number, number] = [min, max];
this.state = {
+ isPaused: true,
max,
min,
range: interval,
@@ -68,6 +75,7 @@ class KeyedTimeslider extends Component {
}
componentWillUnmount() {
+ this._onPause();
this._isMounted = false;
}
@@ -118,6 +126,44 @@ class KeyedTimeslider extends Component {
}
}, 300);
+ _onPlay = () => {
+ this.setState({ isPaused: false });
+ this._playNextFrame();
+ };
+
+ _onPause = () => {
+ this.setState({ isPaused: true });
+ if (this._subscription) {
+ this._subscription.unsubscribe();
+ this._subscription = undefined;
+ }
+ if (this._timeoutId) {
+ clearTimeout(this._timeoutId);
+ this._timeoutId = undefined;
+ }
+ };
+
+ _playNextFrame() {
+ // advance to next frame
+ this._onNext();
+
+ // use waitForTimesliceToLoad$ observable to wait until next frame loaded
+ // .pipe(first()) waits until the first value is emitted from an observable and then automatically unsubscribes
+ this._subscription = this.props.waitForTimesliceToLoad$.pipe(first()).subscribe(() => {
+ if (this.state.isPaused) {
+ return;
+ }
+
+ // use timeout to display frame for small time period before moving to next frame
+ this._timeoutId = window.setTimeout(() => {
+ if (this.state.isPaused) {
+ return;
+ }
+ this._playNextFrame();
+ }, 1750);
+ });
+ }
+
render() {
return (
@@ -154,6 +200,20 @@ class KeyedTimeslider extends Component
{
defaultMessage: 'Next time window',
})}
/>
+