[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:
parent
48d8c0098e
commit
e49db7127d
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
controlledBy?: string;
|
||||
```
|
|
@ -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> | |
|
||||
|
|
|
@ -33,6 +33,7 @@ esFilters: {
|
|||
disabled: boolean;
|
||||
controlledBy?: string | undefined;
|
||||
index?: string | undefined;
|
||||
isMultiIndex?: boolean | undefined;
|
||||
type?: string | undefined;
|
||||
key?: string | undefined;
|
||||
params?: any;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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()] : [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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 & {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue