[Maps] filter dashboard by map extent (#99860)

* [Maps] filter dashboard by map extent

* clean up

* remove warning from filter pill

* tslint

* API doc updates, i18n fixes, tslint

* only show context menu option in edit mode

* add functional test

* review feedback

* do not use search session when filtering by map bounds

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2021-05-26 08:51:39 -06:00 committed by GitHub
parent 48d8c0098e
commit e49db7127d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 394 additions and 40 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) &gt; [controlledBy](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md)
## ApplyGlobalFilterActionContext.controlledBy property
<b>Signature:</b>
```typescript
controlledBy?: string;
```

View file

@ -14,6 +14,7 @@ export interface ApplyGlobalFilterActionContext
| Property | Type | Description |
| --- | --- | --- |
| [controlledBy](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md) | <code>string</code> | |
| [embeddable](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.embeddable.md) | <code>unknown</code> | |
| [filters](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.filters.md) | <code>Filter[]</code> | |
| [timeFieldName](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.timefieldname.md) | <code>string</code> | |

View file

@ -33,6 +33,7 @@ esFilters: {
disabled: boolean;
controlledBy?: string | undefined;
index?: string | undefined;
isMultiIndex?: boolean | undefined;
type?: string | undefined;
key?: string | undefined;
params?: any;

View file

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

View file

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

View file

@ -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;
@ -2689,8 +2692,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

View file

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

View file

@ -1511,8 +1511,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

View file

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

View file

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

View file

@ -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<MapExtent | null>;
@ -101,6 +102,7 @@ export interface ILayer {
getLicensedFeatures(): Promise<LICENSED_FEATURES[]>;
getCustomIconAndTooltipContent(): CustomIconAndTooltipContent;
getDescriptor(): LayerDescriptor;
getGeoFieldNames(): string[];
}
export type CustomIconAndTooltipContent = {
@ -513,4 +515,9 @@ export class AbstractLayer implements ILayer {
async getLicensedFeatures(): Promise<LICENSED_FEATURES[]> {
return [];
}
getGeoFieldNames(): string[] {
const source = this.getSource();
return source.isESSource() ? [(source as IESSource).getGeoFieldName()] : [];
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<FilterByMapExtentInput>;
}
export const filterByMapExtentAction = createAction<FilterByMapExtentActionContext>({
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,
});
},
});

View file

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

View file

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

View file

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