diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md
new file mode 100644
index 0000000000000..d9c47dec9e9d4
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) > [controlledBy](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md)
+
+## ApplyGlobalFilterActionContext.controlledBy property
+
+Signature:
+
+```typescript
+controlledBy?: string;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md
index 2f844b6844645..01ccd4819d906 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md
@@ -14,6 +14,7 @@ export interface ApplyGlobalFilterActionContext
| Property | Type | Description |
| --- | --- | --- |
+| [controlledBy](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md) | string
| |
| [embeddable](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.embeddable.md) | unknown
| |
| [filters](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.filters.md) | Filter[]
| |
| [timeFieldName](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.timefieldname.md) | string
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
index 742b54e19216e..54b5a33ccf682 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
@@ -33,6 +33,7 @@ esFilters: {
disabled: boolean;
controlledBy?: string | undefined;
index?: string | undefined;
+ isMultiIndex?: boolean | undefined;
type?: string | undefined;
key?: string | undefined;
params?: any;
diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts
index c47dcb245cbf0..87455cf1cb763 100644
--- a/src/plugins/data/common/es_query/filters/meta_filter.ts
+++ b/src/plugins/data/common/es_query/filters/meta_filter.ts
@@ -31,6 +31,7 @@ export type FilterMeta = {
controlledBy?: string;
// index and type are optional only because when you create a new filter, there are no defaults
index?: string;
+ isMultiIndex?: boolean;
type?: string;
key?: string;
params?: any;
diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts
index d4ac72294e257..43445d9448f2c 100644
--- a/src/plugins/data/public/actions/apply_filter_action.ts
+++ b/src/plugins/data/public/actions/apply_filter_action.ts
@@ -21,6 +21,9 @@ export interface ApplyGlobalFilterActionContext {
// Need to make this unknown to prevent circular dependencies.
// Apps using this property will need to cast to `IEmbeddable`.
embeddable?: unknown;
+ // controlledBy is an optional key in filter.meta that identifies the owner of a filter
+ // Pass controlledBy to cleanup an existing filter(s) owned by embeddable prior to adding new filters
+ controlledBy?: string;
}
async function isCompatible(context: ApplyGlobalFilterActionContext) {
@@ -42,7 +45,7 @@ export function createFilterAction(
});
},
isCompatible,
- execute: async ({ filters, timeFieldName }: ApplyGlobalFilterActionContext) => {
+ execute: async ({ filters, timeFieldName, controlledBy }: ApplyGlobalFilterActionContext) => {
if (!filters) {
throw new Error('Applying a filter requires a filter');
}
@@ -85,6 +88,15 @@ export function createFilterAction(
selectedFilters = await filterSelectionPromise;
}
+ // remove existing filters for control prior to adding new filtes for control
+ if (controlledBy) {
+ filterManager.getFilters().forEach((filter) => {
+ if (filter.meta.controlledBy === controlledBy) {
+ filterManager.removeFilter(filter);
+ }
+ });
+ }
+
if (timeFieldName) {
const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
timeFieldName,
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index eaf31b468934a..cfbc0a724c74e 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -492,6 +492,8 @@ export const APPLY_FILTER_TRIGGER = "FILTER_TRIGGER";
//
// @public (undocumented)
export interface ApplyGlobalFilterActionContext {
+ // (undocumented)
+ controlledBy?: string;
// (undocumented)
embeddable?: unknown;
// (undocumented)
@@ -763,6 +765,7 @@ export const esFilters: {
disabled: boolean;
controlledBy?: string | undefined;
index?: string | undefined;
+ isMultiIndex?: boolean | undefined;
type?: string | undefined;
key?: string | undefined;
params?: any;
@@ -2699,8 +2702,8 @@ export interface WaitUntilNextSessionCompletesOptions {
// src/plugins/data/common/es_query/filters/exists_filter.ts:19:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/match_all_filter.ts:17:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/meta_filter.ts:42:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
index 5ad88e6fdf5be..9e5090f945182 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
@@ -286,6 +286,11 @@ export function FilterItem(props: FilterItemProps) {
message: '',
status: FILTER_ITEM_OK,
};
+
+ if (filter.meta?.isMultiIndex) {
+ return label;
+ }
+
if (indexPatternExists === false) {
label.status = FILTER_ITEM_ERROR;
label.title = props.intl.formatMessage({
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 1450f807d0a59..4c70e44134831 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -1519,8 +1519,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// Warnings were encountered during analysis:
//
-// src/plugins/data/common/es_query/filters/meta_filter.ts:42:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:52:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js
index a0908035c1480..c2ca952c3e8c9 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js
+++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js
@@ -396,7 +396,7 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -89,
};
- const filter = createExtentFilter(mapExtent, geoFieldName);
+ const filter = createExtentFilter(mapExtent, [geoFieldName]);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [-89, 39],
@@ -412,7 +412,7 @@ describe('createExtentFilter', () => {
minLat: -100,
minLon: -190,
};
- const filter = createExtentFilter(mapExtent, geoFieldName);
+ const filter = createExtentFilter(mapExtent, [geoFieldName]);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [-180, 89],
@@ -428,7 +428,7 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: 100,
};
- const filter = createExtentFilter(mapExtent, geoFieldName);
+ const filter = createExtentFilter(mapExtent, [geoFieldName]);
const leftLon = filter.geo_bounding_box.location.top_left[0];
const rightLon = filter.geo_bounding_box.location.bottom_right[0];
expect(leftLon).toBeGreaterThan(rightLon);
@@ -447,7 +447,7 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -200,
};
- const filter = createExtentFilter(mapExtent, geoFieldName);
+ const filter = createExtentFilter(mapExtent, [geoFieldName]);
const leftLon = filter.geo_bounding_box.location.top_left[0];
const rightLon = filter.geo_bounding_box.location.bottom_right[0];
expect(leftLon).toBeGreaterThan(rightLon);
@@ -466,7 +466,7 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -191,
};
- const filter = createExtentFilter(mapExtent, geoFieldName);
+ const filter = createExtentFilter(mapExtent, [geoFieldName]);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [-180, 39],
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
index c18a79fa9dcbc..e47afce77f779 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
@@ -349,18 +349,49 @@ export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBo
return esBbox;
}
-export function createExtentFilter(mapExtent: MapExtent, geoFieldName: string): GeoFilter {
- return {
- geo_bounding_box: {
- [geoFieldName]: makeESBbox(mapExtent),
- },
- meta: {
- alias: null,
- disabled: false,
- negate: false,
- key: geoFieldName,
- },
- };
+export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]): GeoFilter {
+ const esBbox = makeESBbox(mapExtent);
+ return geoFieldNames.length === 1
+ ? {
+ geo_bounding_box: {
+ [geoFieldNames[0]]: esBbox,
+ },
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ key: geoFieldNames[0],
+ },
+ }
+ : {
+ query: {
+ bool: {
+ should: geoFieldNames.map((geoFieldName) => {
+ return {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: geoFieldName,
+ },
+ },
+ {
+ geo_bounding_box: {
+ [geoFieldName]: esBbox,
+ },
+ },
+ ],
+ },
+ };
+ }),
+ },
+ },
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ };
}
export function createSpatialFilterWithGeometry({
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index 26ebe53d9e385..1c1e29ca485ff 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -42,6 +42,7 @@ import { DataRequestContext } from '../../actions';
import { IStyle } from '../styles/style';
import { getJoinAggKey } from '../../../common/get_agg_key';
import { LICENSED_FEATURES } from '../../licensed_features';
+import { IESSource } from '../sources/es_source';
export interface ILayer {
getBounds(dataRequestContext: DataRequestContext): Promise;
@@ -101,6 +102,7 @@ export interface ILayer {
getLicensedFeatures(): Promise;
getCustomIconAndTooltipContent(): CustomIconAndTooltipContent;
getDescriptor(): LayerDescriptor;
+ getGeoFieldNames(): string[];
}
export type CustomIconAndTooltipContent = {
@@ -513,4 +515,9 @@ export class AbstractLayer implements ILayer {
async getLicensedFeatures(): Promise {
return [];
}
+
+ getGeoFieldNames(): string[] {
+ const source = this.getSource();
+ return source.isESSource() ? [(source as IESSource).getGeoFieldName()] : [];
+ }
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts
index 8e31ad7855197..749e3d6058266 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts
@@ -213,7 +213,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
typeof searchFilters.geogridPrecision === 'number'
? expandToTileBoundaries(searchFilters.buffer, searchFilters.geogridPrecision)
: searchFilters.buffer;
- const extentFilter = createExtentFilter(buffer, geoField.name);
+ const extentFilter = createExtentFilter(buffer, [geoField.name]);
allFilters.push(extentFilter);
}
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
index 643199dbf3933..65fdbca328542 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import React from 'react';
import { Provider } from 'react-redux';
@@ -27,6 +28,7 @@ import {
Query,
RefreshInterval,
} from '../../../../../src/plugins/data/public';
+import { createExtentFilter } from '../../common/elasticsearch_util';
import {
replaceLayerList,
setMapSettings,
@@ -43,8 +45,11 @@ import {
EventHandlers,
} from '../reducers/non_serializable_instances';
import {
+ getGeoFieldNames,
getMapCenter,
getMapBuffer,
+ getMapExtent,
+ getMapReady,
getMapZoom,
getHiddenLayerIds,
getQueryableUniqueIndexPatternIds,
@@ -64,7 +69,7 @@ import {
getChartsPaletteServiceGetColor,
getSearchService,
} from '../kibana_services';
-import { LayerDescriptor } from '../../common/descriptor_types';
+import { LayerDescriptor, MapExtent } from '../../common/descriptor_types';
import { MapContainer } from '../connected_components/map_container';
import { SavedMap } from '../routes/map_page';
import { getIndexPatternsFromIds } from '../index_pattern_util';
@@ -96,16 +101,19 @@ export class MapEmbeddable
private _savedMap: SavedMap;
private _renderTooltipContent?: RenderToolTipContent;
private _subscription: Subscription;
+ private _prevFilterByMapExtent: boolean;
private _prevIsRestore: boolean = false;
+ private _prevMapExtent?: MapExtent;
private _prevTimeRange?: TimeRange;
private _prevQuery?: Query;
private _prevRefreshConfig?: RefreshInterval;
- private _prevFilters?: Filter[];
+ private _prevFilters: Filter[] = [];
private _prevSyncColors?: boolean;
private _prevSearchSessionId?: string;
private _domNode?: HTMLElement;
private _unsubscribeFromStore?: Unsubscribe;
private _isInitialized = false;
+ private _controlledBy: string;
constructor(config: MapEmbeddableConfig, initialInput: MapEmbeddableInput, parent?: IContainer) {
super(
@@ -122,6 +130,9 @@ export class MapEmbeddable
this._savedMap = new SavedMap({ mapEmbeddableInput: initialInput });
this._initializeSaveMap();
this._subscription = this.getUpdated$().subscribe(() => this.onUpdate());
+ this._controlledBy = `mapEmbeddablePanel${this.id}`;
+ this._prevFilterByMapExtent =
+ this.input.filterByMapExtent === undefined ? false : this.input.filterByMapExtent;
}
private async _initializeSaveMap() {
@@ -221,11 +232,23 @@ export class MapEmbeddable
}
onUpdate() {
+ if (
+ this.input.filterByMapExtent !== undefined &&
+ this._prevFilterByMapExtent !== this.input.filterByMapExtent
+ ) {
+ this._prevFilterByMapExtent = this.input.filterByMapExtent;
+ if (this.input.filterByMapExtent) {
+ this.setMapExtentFilter();
+ } else {
+ this.clearMapExtentFilter();
+ }
+ }
+
if (
!_.isEqual(this.input.timeRange, this._prevTimeRange) ||
!_.isEqual(this.input.query, this._prevQuery) ||
- !esFilters.onlyDisabledFiltersChanged(this.input.filters, this._prevFilters) ||
- this.input.searchSessionId !== this._prevSearchSessionId
+ !esFilters.compareFilters(this._getFilters(), this._prevFilters) ||
+ this._getSearchSessionId() !== this._prevSearchSessionId
) {
this._dispatchSetQuery({
forceRefresh: false,
@@ -240,7 +263,7 @@ export class MapEmbeddable
this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors);
}
- const isRestore = getIsRestore(this.input.searchSessionId);
+ const isRestore = getIsRestore(this._getSearchSessionId());
if (isRestore !== this._prevIsRestore) {
this._prevIsRestore = isRestore;
this._savedMap.getStore().dispatch(
@@ -252,22 +275,38 @@ export class MapEmbeddable
}
}
+ _getFilters() {
+ return this.input.filters
+ ? this.input.filters.filter(
+ (filter) => !filter.meta.disabled && filter.meta.controlledBy !== this._controlledBy
+ )
+ : [];
+ }
+
+ _getSearchSessionId() {
+ // New search session id causes all layers from elasticsearch to refetch data.
+ // Dashboard provides a new search session id anytime filters change.
+ // Thus, filtering embeddable container by map extent causes a new search session id any time the map is moved.
+ // Disabling search session when filtering embeddable container by map extent.
+ // The use case for search sessions (restoring results because of slow responses) does not match the use case of
+ // filtering by map extent (rapid responses as users explore their map).
+ return this.input.filterByMapExtent ? undefined : this.input.searchSessionId;
+ }
+
_dispatchSetQuery({ forceRefresh }: { forceRefresh: boolean }) {
+ const filters = this._getFilters();
this._prevTimeRange = this.input.timeRange;
this._prevQuery = this.input.query;
- this._prevFilters = this.input.filters;
- this._prevSearchSessionId = this.input.searchSessionId;
- const enabledFilters = this.input.filters
- ? this.input.filters.filter((filter) => !filter.meta.disabled)
- : [];
+ this._prevFilters = filters;
+ this._prevSearchSessionId = this._getSearchSessionId();
this._savedMap.getStore().dispatch(
setQuery({
- filters: enabledFilters,
+ filters,
query: this.input.query,
timeFilters: this.input.timeRange,
forceRefresh,
- searchSessionId: this.input.searchSessionId,
- searchSessionMapBuffer: getIsRestore(this.input.searchSessionId)
+ searchSessionId: this._getSearchSessionId(),
+ searchSessionMapBuffer: getIsRestore(this._getSearchSessionId())
? this.input.mapBuffer
: undefined,
})
@@ -403,6 +442,57 @@ export class MapEmbeddable
} as ActionExecutionContext;
};
+ setMapExtentFilter() {
+ const state = this._savedMap.getStore().getState();
+ const mapExtent = getMapExtent(state);
+ const geoFieldNames = getGeoFieldNames(state);
+ const center = getMapCenter(state);
+ const zoom = getMapZoom(state);
+
+ if (center === undefined || mapExtent === undefined || geoFieldNames.length === 0) {
+ return;
+ }
+
+ this._prevMapExtent = mapExtent;
+
+ const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
+ mapExtentFilter.meta.isMultiIndex = true;
+ mapExtentFilter.meta.controlledBy = this._controlledBy;
+ mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', {
+ defaultMessage: 'Map bounds at center: {lat}, {lon}, zoom: {zoom}',
+ values: {
+ lat: center.lat,
+ lon: center.lon,
+ zoom,
+ },
+ });
+
+ const executeContext = {
+ ...this.getActionContext(),
+ filters: [mapExtentFilter],
+ controlledBy: this._controlledBy,
+ };
+ const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER);
+ if (!action) {
+ throw new Error('Unable to apply map extent filter, could not locate action');
+ }
+ action.execute(executeContext);
+ }
+
+ clearMapExtentFilter() {
+ this._prevMapExtent = undefined;
+ const executeContext = {
+ ...this.getActionContext(),
+ filters: [],
+ controlledBy: this._controlledBy,
+ };
+ const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER);
+ if (!action) {
+ throw new Error('Unable to apply map extent filter, could not locate action');
+ }
+ action.execute(executeContext);
+ }
+
destroy() {
super.destroy();
this._isActive = false;
@@ -426,9 +516,15 @@ export class MapEmbeddable
}
_handleStoreChanges() {
- if (!this._isActive) {
+ if (!this._isActive || !getMapReady(this._savedMap.getStore().getState())) {
return;
}
+
+ const mapExtent = getMapExtent(this._savedMap.getStore().getState());
+ if (this.input.filterByMapExtent && !_.isEqual(this._prevMapExtent, mapExtent)) {
+ this.setMapExtentFilter();
+ }
+
const center = getMapCenter(this._savedMap.getStore().getState());
const zoom = getMapZoom(this._savedMap.getStore().getState());
diff --git a/x-pack/plugins/maps/public/embeddable/types.ts b/x-pack/plugins/maps/public/embeddable/types.ts
index 7cd4fa8e1253b..79a70f3786fe6 100644
--- a/x-pack/plugins/maps/public/embeddable/types.ts
+++ b/x-pack/plugins/maps/public/embeddable/types.ts
@@ -35,9 +35,10 @@ interface MapEmbeddableState {
}
export type MapByValueInput = {
attributes: MapSavedObjectAttributes;
-} & EmbeddableInput &
- MapEmbeddableState;
-export type MapByReferenceInput = SavedObjectEmbeddableInput & MapEmbeddableState;
+} & EmbeddableInput & { filterByMapExtent?: boolean } & MapEmbeddableState;
+export type MapByReferenceInput = SavedObjectEmbeddableInput & {
+ filterByMapExtent?: boolean;
+} & MapEmbeddableState;
export type MapEmbeddableInput = MapByValueInput | MapByReferenceInput;
export type MapEmbeddableOutput = EmbeddableOutput & {
diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts
index ad8846bd48b60..740112124a251 100644
--- a/x-pack/plugins/maps/public/plugin.ts
+++ b/x-pack/plugins/maps/public/plugin.ts
@@ -42,8 +42,10 @@ import {
createTileMapUrlGenerator,
} from './url_generator';
import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action';
+import { filterByMapExtentAction } from './trigger_actions/filter_by_map_extent_action';
import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory';
import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
+import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public';
import { MapsXPackConfig, MapsConfigType } from '../config';
import { getAppTitle } from '../common/i18n_getters';
import { lazyLoadMapModules } from './lazy_load_bundle';
@@ -173,6 +175,7 @@ export class MapsPlugin
if (core.application.capabilities.maps.show) {
plugins.uiActions.addTriggerAction(VISUALIZE_GEO_FIELD_TRIGGER, visualizeGeoFieldAction);
}
+ plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, filterByMapExtentAction);
if (!core.application.capabilities.maps.save) {
plugins.visualizations.unRegisterAlias(APP_ID);
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts
index a818cdd2d00f9..4f3bfbe303cb9 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.ts
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts
@@ -401,6 +401,26 @@ export const getQueryableUniqueIndexPatternIds = createSelector(
}
);
+export const getGeoFieldNames = createSelector(
+ getLayerList,
+ getWaitingForMapReadyLayerListRaw,
+ (layerList, waitingForMapReadyLayerList) => {
+ const geoFieldNames: string[] = [];
+
+ if (waitingForMapReadyLayerList.length) {
+ waitingForMapReadyLayerList.forEach((layerDescriptor) => {
+ const layer = createLayerInstance(layerDescriptor);
+ geoFieldNames.push(...layer.getGeoFieldNames());
+ });
+ } else {
+ layerList.forEach((layer) => {
+ geoFieldNames.push(...layer.getGeoFieldNames());
+ });
+ }
+ return _.uniq(geoFieldNames);
+ }
+);
+
export const hasDirtyState = createSelector(getLayerListRaw, (layerListRaw) => {
return layerListRaw.some((layerDescriptor) => {
if (layerDescriptor.__isPreviewLayer) {
diff --git a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.ts b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.ts
new file mode 100644
index 0000000000000..7706704cdd63d
--- /dev/null
+++ b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import {
+ Embeddable,
+ EmbeddableInput,
+ ViewMode,
+} from '../../../../../src/plugins/embeddable/public';
+import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
+import { createAction } from '../../../../../src/plugins/ui_actions/public';
+
+export const FILTER_BY_MAP_EXTENT = 'FILTER_BY_MAP_EXTENT';
+
+interface FilterByMapExtentInput extends EmbeddableInput {
+ filterByMapExtent: boolean;
+}
+
+interface FilterByMapExtentActionContext {
+ embeddable: Embeddable;
+}
+
+export const filterByMapExtentAction = createAction({
+ id: FILTER_BY_MAP_EXTENT,
+ type: FILTER_BY_MAP_EXTENT,
+ order: 20,
+ getDisplayName: ({ embeddable }: FilterByMapExtentActionContext) => {
+ return embeddable.getInput().filterByMapExtent
+ ? i18n.translate('xpack.maps.filterByMapExtentMenuItem.disableDisplayName', {
+ defaultMessage: 'Disable filter by map extent',
+ })
+ : i18n.translate('xpack.maps.filterByMapExtentMenuItem.enableDisplayName', {
+ defaultMessage: 'Enable filter by map extent',
+ });
+ },
+ getIconType: () => {
+ return 'filter';
+ },
+ isCompatible: async ({ embeddable }: FilterByMapExtentActionContext) => {
+ return (
+ embeddable.type === MAP_SAVED_OBJECT_TYPE && embeddable.getInput().viewMode === ViewMode.EDIT
+ );
+ },
+ execute: async ({ embeddable }: FilterByMapExtentActionContext) => {
+ embeddable.updateInput({
+ filterByMapExtent: !embeddable.getInput().filterByMapExtent,
+ });
+ },
+});
diff --git a/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js b/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js
new file mode 100644
index 0000000000000..efe02d2d85156
--- /dev/null
+++ b/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+export default function ({ getPageObjects, getService }) {
+ const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'lens', 'maps']);
+
+ const testSubjects = getService('testSubjects');
+ const dashboardPanelActions = getService('dashboardPanelActions');
+ const security = getService('security');
+
+ describe('filter by map extent', () => {
+ before(async () => {
+ await security.testUser.setRoles(
+ ['test_logstash_reader', 'global_maps_all', 'global_dashboard_all'],
+ false
+ );
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.gotoDashboardEditMode('filter by map extent dashboard');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.dashboard.waitForRenderComplete();
+ });
+
+ after(async () => {
+ await security.testUser.restoreDefaults();
+ });
+
+ it('should not filter dashboard by map extent before "filter by map extent" is enabled', async () => {
+ await PageObjects.lens.assertMetric('Count of records', '6');
+ });
+
+ it('should filter dashboard by map extent when "filter by map extent" is enabled', async () => {
+ const mapPanelHeader = await dashboardPanelActions.getPanelHeading('document example');
+ await dashboardPanelActions.openContextMenuMorePanel(mapPanelHeader);
+ await await testSubjects.click('embeddablePanelAction-FILTER_BY_MAP_EXTENT');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await PageObjects.lens.assertMetric('Count of records', '1');
+ });
+
+ it('should filter dashboard by new map extent when map is moved', async () => {
+ await PageObjects.maps.setView(32.95539, -93.93054, 5);
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.lens.assertMetric('Count of records', '2');
+ });
+
+ it('should remove map extent filter dashboard when "filter by map extent" is disabled', async () => {
+ const mapPanelHeader = await dashboardPanelActions.getPanelHeading('document example');
+ await dashboardPanelActions.openContextMenuMorePanel(mapPanelHeader);
+ await await testSubjects.click('embeddablePanelAction-FILTER_BY_MAP_EXTENT');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.lens.assertMetric('Count of records', '6');
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js
index 552f830e2a379..da5d4b8945da7 100644
--- a/x-pack/test/functional/apps/maps/embeddable/index.js
+++ b/x-pack/test/functional/apps/maps/embeddable/index.js
@@ -13,5 +13,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./embeddable_library'));
loadTestFile(require.resolve('./embeddable_state'));
loadTestFile(require.resolve('./tooltip_filter_actions'));
+ loadTestFile(require.resolve('./filter_by_map_extent'));
});
}
diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json
index 631efb58f9c7b..4a879c20f19ab 100644
--- a/x-pack/test/functional/es_archives/maps/kibana/data.json
+++ b/x-pack/test/functional/es_archives/maps/kibana/data.json
@@ -1149,6 +1149,56 @@
}
}
+{
+ "type": "doc",
+ "value": {
+ "id": "dashboard:42f6f040-b34f-11eb-8c95-dd19591c63df",
+ "index": ".kibana",
+ "source": {
+ "dashboard": {
+ "title" : "filter by map extent dashboard",
+ "hits" : 0,
+ "description" : "",
+ "panelsJSON" : "[{\"version\":\"8.0.0\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":0,\"w\":29,\"h\":21,\"i\":\"24ade730-afe4-42b6-919a-c4e0a98c94f2\"},\"panelIndex\":\"24ade730-afe4-42b6-919a-c4e0a98c94f2\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":38.64679,\"lon\":-120.96481,\"zoom\":7.06},\"mapBuffer\":{\"minLon\":-125.44180499999999,\"minLat\":36.364824999999996,\"maxLon\":-116.603825,\"maxLat\":40.943405},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}},\"panelRefName\":\"panel_24ade730-afe4-42b6-919a-c4e0a98c94f2\"},{\"version\":\"8.0.0\",\"type\":\"lens\",\"gridData\":{\"x\":29,\"y\":0,\"w\":10,\"h\":21,\"i\":\"44eb3c47-f6ad-4da8-993b-13c10997d585\"},\"panelIndex\":\"44eb3c47-f6ad-4da8-993b-13c10997d585\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"3cda3519-055a-4b9c-8759-caa28388298c\":{\"columns\":{\"26acba84-22ca-4625-b2ac-5309945e9b30\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"26acba84-22ca-4625-b2ac-5309945e9b30\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"3cda3519-055a-4b9c-8759-caa28388298c\",\"accessor\":\"26acba84-22ca-4625-b2ac-5309945e9b30\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"name\":\"indexpattern-datasource-layer-3cda3519-055a-4b9c-8759-caa28388298c\"}]},\"enhancements\":{},\"hidePanelTitles\":false},\"title\":\"Count panel\"}]",
+ "optionsJSON" : "{\"hidePanelTitles\":false,\"useMargins\":true}",
+ "version" : 1,
+ "timeRestore" : true,
+ "timeTo" : "2015-09-20T01:00:00.000Z",
+ "timeFrom" : "2015-09-20T00:00:00.000Z",
+ "refreshInterval" : {
+ "pause" : true,
+ "value" : 1000
+ },
+ "kibanaSavedObjectMeta" : {
+ "searchSourceJSON" : "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"
+ }
+ },
+ "type" : "dashboard",
+ "references" : [
+ {
+ "name" : "24ade730-afe4-42b6-919a-c4e0a98c94f2:panel_24ade730-afe4-42b6-919a-c4e0a98c94f2",
+ "type" : "map",
+ "id" : "d2e73f40-e14a-11e8-a35a-370a8516603a"
+ },
+ {
+ "type" : "index-pattern",
+ "id" : "c698b940-e149-11e8-a35a-370a8516603a",
+ "name" : "44eb3c47-f6ad-4da8-993b-13c10997d585:indexpattern-datasource-current-indexpattern"
+ },
+ {
+ "type" : "index-pattern",
+ "id" : "c698b940-e149-11e8-a35a-370a8516603a",
+ "name" : "44eb3c47-f6ad-4da8-993b-13c10997d585:indexpattern-datasource-layer-3cda3519-055a-4b9c-8759-caa28388298c"
+ }
+ ],
+ "migrationVersion" : {
+ "dashboard" : "7.11.0"
+ },
+ "updated_at" : "2021-05-12T18:24:17.228Z"
+ }
+ }
+}
+
{
"type": "doc",
"value": {