mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[maps] clean-up filter by map bounds action (#136045)
* [maps] clean-up filter by map bounds action * modal * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * update docs * fix warning in generated filter pill * fix functional test * fix jest test * review feedback Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e0b6517ae5
commit
63335d8e37
18 changed files with 284 additions and 122 deletions
Binary file not shown.
Before Width: | Height: | Size: 708 KiB |
|
@ -81,22 +81,17 @@ Create filters from your map to focus in on just the data you want. *Maps* provi
|
|||
|
||||
[float]
|
||||
[[maps-map-extent-filter]]
|
||||
==== Filter dashboard by map extent
|
||||
==== Filter dashboard by map bounds
|
||||
|
||||
A map extent shows uniform data across all panels.
|
||||
As you pan and zoom your map, all panels will update to only include data that is visible in your map.
|
||||
To filter your dashboard by your map bounds as you pan and zoom your map:
|
||||
|
||||
To enable filtering your dashboard by map extent:
|
||||
|
||||
* Open the main menu, and then click *Dashboard*.
|
||||
* Select your dashboard from the list or click *Create dashboard*.
|
||||
* If your dashboard does not have a map, add a map panel.
|
||||
* Click the gear icon image:maps/images/gear_icon.png[gear icon] to open the map panel menu.
|
||||
* Select *More* to view all panel options.
|
||||
* Select *Enable filter by map extent*.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/enable_filter_by_map_extent.png[]
|
||||
. Open the main menu, and then click *Dashboard*.
|
||||
. Select your dashboard from the list or click *Create dashboard*.
|
||||
. If your dashboard does not have a map, add a map panel.
|
||||
. Click the gear icon image:maps/images/gear_icon.png[gear icon] to open the map panel menu.
|
||||
. Select *More* to view all panel options.
|
||||
. Select *Filter dashboard by map bounds*.
|
||||
. Select the checkbox for your map panel.
|
||||
|
||||
[float]
|
||||
[[maps-spatial-filters]]
|
||||
|
|
|
@ -30,6 +30,7 @@ describe('createExtentFilter', () => {
|
|||
disabled: false,
|
||||
key: 'location',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -66,6 +67,7 @@ describe('createExtentFilter', () => {
|
|||
disabled: false,
|
||||
key: 'location',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -102,6 +104,7 @@ describe('createExtentFilter', () => {
|
|||
disabled: false,
|
||||
key: 'location',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -138,6 +141,7 @@ describe('createExtentFilter', () => {
|
|||
disabled: false,
|
||||
key: 'location',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -174,6 +178,7 @@ describe('createExtentFilter', () => {
|
|||
disabled: false,
|
||||
key: 'location',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -210,6 +215,7 @@ describe('createExtentFilter', () => {
|
|||
disabled: false,
|
||||
isMultiIndex: true,
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
|
|
@ -89,6 +89,7 @@ export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]
|
|||
}
|
||||
|
||||
const meta: FilterMeta = {
|
||||
type: SPATIAL_FILTER_TYPE,
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
|
|
|
@ -224,6 +224,7 @@ describe('ESGeoGridSource', () => {
|
|||
disabled: false,
|
||||
key: 'bar',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { getGeoFieldsLabel } from './get_geo_fields_label';
|
||||
|
||||
test('single field', () => {
|
||||
expect(getGeoFieldsLabel(['location'])).toEqual('location');
|
||||
});
|
||||
|
||||
test('two fields', () => {
|
||||
expect(getGeoFieldsLabel(['location', 'secondLocation'])).toEqual('location and secondLocation');
|
||||
});
|
||||
|
||||
test('three or more fields', () => {
|
||||
expect(getGeoFieldsLabel(['location', 'secondLocation', 'thirdLocation'])).toEqual(
|
||||
'location, secondLocation, and thirdLocation'
|
||||
);
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function getGeoFieldsLabel(geoFieldNames: string[]) {
|
||||
if (geoFieldNames.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (geoFieldNames.length === 1) {
|
||||
return geoFieldNames[0];
|
||||
}
|
||||
|
||||
const connector = i18n.translate('xpack.maps.embeddable.geoFieldsConnector', {
|
||||
defaultMessage: ' and ',
|
||||
});
|
||||
|
||||
if (geoFieldNames.length === 2) {
|
||||
return geoFieldNames[0] + connector + geoFieldNames[1];
|
||||
}
|
||||
|
||||
return (
|
||||
geoFieldNames.slice(0, geoFieldNames.length - 1).join(', ') +
|
||||
',' +
|
||||
connector +
|
||||
geoFieldNames[geoFieldNames.length - 1]
|
||||
);
|
||||
}
|
|
@ -82,8 +82,8 @@ import { getIndexPatternsFromIds } from '../index_pattern_util';
|
|||
import { getMapAttributeService } from '../map_attribute_service';
|
||||
import { isUrlDrilldown, toValueClickDataFormat } from '../trigger_actions/trigger_utils';
|
||||
import { waitUntilTimeLayersLoad$ } from '../routes/map_page/map_app/wait_until_time_layers_load';
|
||||
import { synchronizeMovement } from './synchronize_movement';
|
||||
import { getFilterByMapExtent } from '../trigger_actions/filter_by_map_extent_action';
|
||||
import { mapEmbeddablesSingleton } from './map_embeddables_singleton';
|
||||
import { getGeoFieldsLabel } from './get_geo_fields_label';
|
||||
|
||||
import {
|
||||
MapByValueInput,
|
||||
|
@ -112,7 +112,6 @@ 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;
|
||||
|
@ -144,7 +143,6 @@ export class MapEmbeddable
|
|||
this._initializeSaveMap();
|
||||
this._subscription = this.getUpdated$().subscribe(() => this.onUpdate());
|
||||
this._controlledBy = `mapEmbeddablePanel${this.id}`;
|
||||
this._prevFilterByMapExtent = getFilterByMapExtent(this.input);
|
||||
}
|
||||
|
||||
public reportsEmbeddableLoad() {
|
||||
|
@ -278,16 +276,6 @@ export class MapEmbeddable
|
|||
}
|
||||
|
||||
onUpdate() {
|
||||
const filterByMapExtent = getFilterByMapExtent(this.input);
|
||||
if (this._prevFilterByMapExtent !== filterByMapExtent) {
|
||||
this._prevFilterByMapExtent = filterByMapExtent;
|
||||
if (filterByMapExtent) {
|
||||
this.setMapExtentFilter();
|
||||
} else {
|
||||
this.clearMapExtentFilter();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!_.isEqual(this.input.timeRange, this._prevTimeRange) ||
|
||||
!_.isEqual(this.input.query, this._prevQuery) ||
|
||||
|
@ -321,8 +309,12 @@ export class MapEmbeddable
|
|||
: this.input.isMovementSynchronized;
|
||||
};
|
||||
|
||||
_getIsFilterByMapExtent = () => {
|
||||
return this.input.filterByMapExtent === undefined ? false : this.input.filterByMapExtent;
|
||||
};
|
||||
|
||||
_gotoSynchronizedLocation() {
|
||||
const syncedLocation = synchronizeMovement.getLocation();
|
||||
const syncedLocation = mapEmbeddablesSingleton.getLocation();
|
||||
if (syncedLocation) {
|
||||
// set map to synchronized view
|
||||
this._mapSyncHandler(syncedLocation.lat, syncedLocation.lon, syncedLocation.zoom);
|
||||
|
@ -334,7 +326,7 @@ export class MapEmbeddable
|
|||
// Use goto because un-rendered map will not have accurate mapCenter and mapZoom.
|
||||
const goto = getGoto(this._savedMap.getStore().getState());
|
||||
if (goto && goto.center) {
|
||||
synchronizeMovement.setLocation(
|
||||
mapEmbeddablesSingleton.setLocation(
|
||||
this.input.id,
|
||||
goto.center.lat,
|
||||
goto.center.lon,
|
||||
|
@ -347,12 +339,12 @@ export class MapEmbeddable
|
|||
// Initialize synchronized view to map's view
|
||||
const center = getMapCenter(this._savedMap.getStore().getState());
|
||||
const zoom = getMapZoom(this._savedMap.getStore().getState());
|
||||
synchronizeMovement.setLocation(this.input.id, center.lat, center.lon, zoom);
|
||||
mapEmbeddablesSingleton.setLocation(this.input.id, center.lat, center.lon, zoom);
|
||||
}
|
||||
|
||||
_propogateMapMovement = (lat: number, lon: number, zoom: number) => {
|
||||
if (this._getIsMovementSynchronized()) {
|
||||
synchronizeMovement.setLocation(this.input.id, lat, lon, zoom);
|
||||
mapEmbeddablesSingleton.setLocation(this.input.id, lat, lon, zoom);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -418,7 +410,7 @@ export class MapEmbeddable
|
|||
return;
|
||||
}
|
||||
|
||||
synchronizeMovement.register(this.input.id, {
|
||||
mapEmbeddablesSingleton.register(this.input.id, {
|
||||
getTitle: () => {
|
||||
const output = this.getOutput();
|
||||
if (output.title) {
|
||||
|
@ -442,6 +434,18 @@ export class MapEmbeddable
|
|||
this._savedMap.getStore().dispatch(setMapSettings({ autoFitToDataBounds: true }));
|
||||
}
|
||||
},
|
||||
getIsFilterByMapExtent: this._getIsFilterByMapExtent,
|
||||
setIsFilterByMapExtent: (isFilterByMapExtent: boolean) => {
|
||||
this.updateInput({ filterByMapExtent: isFilterByMapExtent });
|
||||
if (isFilterByMapExtent) {
|
||||
this._setMapExtentFilter();
|
||||
} else {
|
||||
this._clearMapExtentFilter();
|
||||
}
|
||||
},
|
||||
getGeoFieldNames: () => {
|
||||
return getGeoFieldNames(this._savedMap.getStore().getState());
|
||||
},
|
||||
});
|
||||
if (this._getIsMovementSynchronized()) {
|
||||
this._gotoSynchronizedLocation();
|
||||
|
@ -568,14 +572,21 @@ 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);
|
||||
// Timing bug for dashboard with multiple maps with synchronized movement and filter by map extent enabled
|
||||
// When moving map with filterByMapExtent:false, previous map extent filter(s) does not get removed
|
||||
// Cuased by syncDashboardContainerInput applyContainerChangesToState.
|
||||
// 1) _setMapExtentFilter executes ACTION_GLOBAL_APPLY_FILTER action,
|
||||
// removing previous map extent filter and adding new map extent filter
|
||||
// 2) applyContainerChangesToState then re-adds stale input.filters (which contains previous map extent filter)
|
||||
// Add debounce to fix timing issue.
|
||||
// 1) applyContainerChangesToState now runs first and does its thing
|
||||
// 2) _setMapExtentFilter executes ACTION_GLOBAL_APPLY_FILTER action,
|
||||
// removing previous map extent filter and adding new map extent filter
|
||||
_setMapExtentFilter = _.debounce(() => {
|
||||
const mapExtent = getMapExtent(this._savedMap.getStore().getState());
|
||||
const geoFieldNames = mapEmbeddablesSingleton.getGeoFieldNames();
|
||||
|
||||
if (center === undefined || mapExtent === undefined || geoFieldNames.length === 0) {
|
||||
if (mapExtent === undefined || geoFieldNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -584,12 +595,8 @@ export class MapEmbeddable
|
|||
const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
|
||||
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,
|
||||
},
|
||||
defaultMessage: '{geoFieldsLabel} within map bounds',
|
||||
values: { geoFieldsLabel: getGeoFieldsLabel(geoFieldNames) },
|
||||
});
|
||||
|
||||
const executeContext = {
|
||||
|
@ -602,9 +609,9 @@ export class MapEmbeddable
|
|||
throw new Error('Unable to apply map extent filter, could not locate action');
|
||||
}
|
||||
action.execute(executeContext);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
clearMapExtentFilter() {
|
||||
_clearMapExtentFilter() {
|
||||
this._prevMapExtent = undefined;
|
||||
const executeContext = {
|
||||
...this.getActionContext(),
|
||||
|
@ -620,7 +627,7 @@ export class MapEmbeddable
|
|||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
synchronizeMovement.unregister(this.input.id);
|
||||
mapEmbeddablesSingleton.unregister(this.input.id);
|
||||
this._isActive = false;
|
||||
if (this._unsubscribeFromStore) {
|
||||
this._unsubscribeFromStore();
|
||||
|
@ -665,8 +672,8 @@ export class MapEmbeddable
|
|||
}
|
||||
|
||||
const mapExtent = getMapExtent(this._savedMap.getStore().getState());
|
||||
if (getFilterByMapExtent(this.input) && !_.isEqual(this._prevMapExtent, mapExtent)) {
|
||||
this.setMapExtentFilter();
|
||||
if (this._getIsFilterByMapExtent() && !_.isEqual(this._prevMapExtent, mapExtent)) {
|
||||
this._setMapExtentFilter();
|
||||
}
|
||||
|
||||
const center = getMapCenter(this._savedMap.getStore().getState());
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { MapCenterAndZoom } from '../../common/descriptor_types';
|
||||
|
||||
interface MapPanel {
|
||||
|
@ -12,6 +13,9 @@ interface MapPanel {
|
|||
onLocationChange(lat: number, lon: number, zoom: number): void;
|
||||
getIsMovementSynchronized(): boolean;
|
||||
setIsMovementSynchronized(IsMovementSynchronized: boolean): void;
|
||||
getIsFilterByMapExtent(): boolean;
|
||||
setIsFilterByMapExtent(isFilterByMapExtent: boolean): void;
|
||||
getGeoFieldNames(): string[];
|
||||
}
|
||||
|
||||
const registry: Record<string, MapPanel> = {};
|
||||
|
@ -19,7 +23,14 @@ let location: MapCenterAndZoom | undefined;
|
|||
let primaryPanelId: string | undefined;
|
||||
let primaryPanelTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
export const synchronizeMovement = {
|
||||
export const mapEmbeddablesSingleton = {
|
||||
getGeoFieldNames() {
|
||||
const geoFieldNames: string[] = [];
|
||||
Object.values(registry).forEach((mapPanel) => {
|
||||
geoFieldNames.push(...mapPanel.getGeoFieldNames());
|
||||
});
|
||||
return _.uniq(geoFieldNames);
|
||||
},
|
||||
getLocation() {
|
||||
return location;
|
||||
},
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* 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 '@kbn/embeddable-plugin/public';
|
||||
import { createAction } from '@kbn/ui-actions-plugin/public';
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
|
||||
export const FILTER_BY_MAP_EXTENT = 'FILTER_BY_MAP_EXTENT';
|
||||
|
||||
interface FilterByMapExtentInput extends EmbeddableInput {
|
||||
filterByMapExtent: boolean;
|
||||
}
|
||||
|
||||
interface FilterByMapExtentActionContext {
|
||||
embeddable: Embeddable<FilterByMapExtentInput>;
|
||||
}
|
||||
|
||||
export function getFilterByMapExtent(input: { filterByMapExtent?: boolean }) {
|
||||
return input.filterByMapExtent === undefined ? false : input.filterByMapExtent;
|
||||
}
|
||||
|
||||
export const filterByMapExtentAction = createAction<FilterByMapExtentActionContext>({
|
||||
id: FILTER_BY_MAP_EXTENT,
|
||||
type: FILTER_BY_MAP_EXTENT,
|
||||
order: 20,
|
||||
getDisplayName: ({ embeddable }: FilterByMapExtentActionContext) => {
|
||||
return getFilterByMapExtent(embeddable.getInput())
|
||||
? 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 &&
|
||||
!embeddable.getInput().disableTriggers
|
||||
);
|
||||
},
|
||||
execute: async ({ embeddable }: FilterByMapExtentActionContext) => {
|
||||
embeddable.updateInput({
|
||||
filterByMapExtent: !getFilterByMapExtent(embeddable.getInput()),
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Embeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public';
|
||||
import { createReactOverlays } from '@kbn/kibana-react-plugin/public';
|
||||
import { createAction } from '@kbn/ui-actions-plugin/public';
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import { getCore } from '../kibana_services';
|
||||
|
||||
export const FILTER_BY_MAP_EXTENT = 'FILTER_BY_MAP_EXTENT';
|
||||
|
||||
interface FilterByMapExtentInput extends EmbeddableInput {
|
||||
filterByMapExtent: boolean;
|
||||
}
|
||||
|
||||
interface FilterByMapExtentActionContext {
|
||||
embeddable: Embeddable<FilterByMapExtentInput>;
|
||||
}
|
||||
|
||||
function getContainerLabel(embeddable: Embeddable<FilterByMapExtentInput>) {
|
||||
return embeddable.parent?.type === 'dashboard'
|
||||
? i18n.translate('xpack.maps.filterByMapExtentMenuItem.dashboardLabel', {
|
||||
defaultMessage: 'dashboard',
|
||||
})
|
||||
: i18n.translate('xpack.maps.filterByMapExtentMenuItem.pageLabel', {
|
||||
defaultMessage: 'page',
|
||||
});
|
||||
}
|
||||
|
||||
function getDisplayName(embeddable: Embeddable<FilterByMapExtentInput>) {
|
||||
return i18n.translate('xpack.maps.filterByMapExtentMenuItem.displayName', {
|
||||
defaultMessage: 'Filter {containerLabel} by map bounds',
|
||||
values: { containerLabel: getContainerLabel(embeddable) },
|
||||
});
|
||||
}
|
||||
|
||||
export const filterByMapExtentAction = createAction<FilterByMapExtentActionContext>({
|
||||
id: FILTER_BY_MAP_EXTENT,
|
||||
type: FILTER_BY_MAP_EXTENT,
|
||||
order: 20,
|
||||
getDisplayName: (context: FilterByMapExtentActionContext) => {
|
||||
return getDisplayName(context.embeddable);
|
||||
},
|
||||
getDisplayNameTooltip: (context: FilterByMapExtentActionContext) => {
|
||||
return i18n.translate('xpack.maps.filterByMapExtentMenuItem.displayNameTooltip', {
|
||||
defaultMessage:
|
||||
'As you zoom and pan the map, the {containerLabel} updates to display only the data visible in the map bounds.',
|
||||
values: { containerLabel: getContainerLabel(context.embeddable) },
|
||||
});
|
||||
},
|
||||
getIconType: () => {
|
||||
return 'filter';
|
||||
},
|
||||
isCompatible: async ({ embeddable }: FilterByMapExtentActionContext) => {
|
||||
return embeddable.type === MAP_SAVED_OBJECT_TYPE && !embeddable.getInput().disableTriggers;
|
||||
},
|
||||
execute: async (context: FilterByMapExtentActionContext) => {
|
||||
const { FilterByMapExtentModal } = await import('./filter_by_map_extent_modal');
|
||||
const { openModal } = createReactOverlays(getCore());
|
||||
const modalSession = openModal(
|
||||
<FilterByMapExtentModal
|
||||
onClose={() => modalSession.close()}
|
||||
title={getDisplayName(context.embeddable)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
import { mapEmbeddablesSingleton } from '../embeddable/map_embeddables_singleton';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export class FilterByMapExtentModal extends Component<Props> {
|
||||
_renderSwitches() {
|
||||
return mapEmbeddablesSingleton.getMapPanels().map((mapPanel) => {
|
||||
return (
|
||||
<EuiFormRow display="columnCompressedSwitch" key={mapPanel.id}>
|
||||
<EuiSwitch
|
||||
label={mapPanel.getTitle()}
|
||||
checked={mapPanel.getIsFilterByMapExtent()}
|
||||
onChange={(event: EuiSwitchEvent) => {
|
||||
const isChecked = event.target.checked;
|
||||
mapPanel.setIsFilterByMapExtent(isChecked);
|
||||
|
||||
// only a single map can create map bound filter at a time
|
||||
// disable all other map panels from creating map bound filter
|
||||
if (isChecked) {
|
||||
mapEmbeddablesSingleton.getMapPanels().forEach((it) => {
|
||||
if (it.id !== mapPanel.id && it.getIsFilterByMapExtent()) {
|
||||
it.setIsFilterByMapExtent(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.forceUpdate();
|
||||
}}
|
||||
compressed
|
||||
data-test-subj={`filterByMapExtentSwitch${mapPanel.id}`}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{this.props.title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>{this._renderSwitches()}</EuiModalBody>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -39,8 +39,8 @@ export const synchronizeMovementAction = createAction<SynchronizeMovementActionC
|
|||
return 'crosshairs';
|
||||
},
|
||||
isCompatible: async ({ embeddable }: SynchronizeMovementActionContext) => {
|
||||
const { synchronizeMovement } = await import('../embeddable/synchronize_movement');
|
||||
if (!synchronizeMovement.hasMultipleMaps()) {
|
||||
const { mapEmbeddablesSingleton } = await import('../embeddable/map_embeddables_singleton');
|
||||
if (!mapEmbeddablesSingleton.hasMultipleMaps()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
import { synchronizeMovement } from '../embeddable/synchronize_movement';
|
||||
import { mapEmbeddablesSingleton } from '../embeddable/map_embeddables_singleton';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
@ -23,7 +23,7 @@ interface Props {
|
|||
|
||||
export class SynchronizeMovementModal extends Component<Props> {
|
||||
_renderSwitches() {
|
||||
const mapPanels = synchronizeMovement.getMapPanels();
|
||||
const mapPanels = mapEmbeddablesSingleton.getMapPanels();
|
||||
|
||||
const synchronizedPanels = mapPanels.filter((mapPanel) => {
|
||||
return mapPanel.getIsMovementSynchronized();
|
||||
|
|
|
@ -17409,7 +17409,6 @@
|
|||
"xpack.maps.drawTooltip.lineInstructions": "Cliquez pour commencer la ligne. Cliquez pour ajouter le vertex. Double-cliquez pour terminer.",
|
||||
"xpack.maps.drawTooltip.pointInstructions": "Cliquez pour créer un point.",
|
||||
"xpack.maps.drawTooltip.polygonInstructions": "Cliquez pour commencer la forme. Cliquez pour ajouter le vertex. Double-cliquez pour terminer.",
|
||||
"xpack.maps.embeddable.boundsFilterLabel": "Limites de carte au centre : {lat}, {lon}, zoom : {zoom}",
|
||||
"xpack.maps.embeddableDisplayName": "carte",
|
||||
"xpack.maps.emsFileSelect.selectPlaceholder": "Sélectionner les limites EMS",
|
||||
"xpack.maps.emsSource.tooltipsTitle": "Champs d'infobulle",
|
||||
|
@ -17439,8 +17438,6 @@
|
|||
"xpack.maps.fileUploadWizard.disabledDesc": "Impossible de charger les fichiers, vous ne disposez pas du privilège Kibana de gestion des vues de données.",
|
||||
"xpack.maps.fileUploadWizard.title": "Charger le fichier",
|
||||
"xpack.maps.fileUploadWizard.uploadLabel": "Importation du fichier",
|
||||
"xpack.maps.filterByMapExtentMenuItem.disableDisplayName": "Désactiver le filtre par étendue de carte",
|
||||
"xpack.maps.filterByMapExtentMenuItem.enableDisplayName": "Activer le filtre par étendue de carte",
|
||||
"xpack.maps.filterEditor.applyForceRefreshLabel": "Appliquer une actualisation globale aux données de calque",
|
||||
"xpack.maps.filterEditor.applyForceRefreshTooltip": "Lorsque cette option est activée, le calque récupère à nouveau les données au moment de l’actualisation automatique ainsi que chaque fois que le bouton \"Actualiser\" est sélectionné.",
|
||||
"xpack.maps.filterEditor.applyGlobalFilterHelp": "Lorsque cette option est activée, les résultats sont affinés par recherche globale.",
|
||||
|
|
|
@ -17399,7 +17399,6 @@
|
|||
"xpack.maps.drawTooltip.lineInstructions": "クリックして行を開始します。クリックして頂点を追加します。ダブルクリックして終了します。",
|
||||
"xpack.maps.drawTooltip.pointInstructions": "クリックして点を作成します。",
|
||||
"xpack.maps.drawTooltip.polygonInstructions": "クリックしてシェイプを開始します。クリックして頂点を追加します。ダブルクリックして終了します。",
|
||||
"xpack.maps.embeddable.boundsFilterLabel": "中央のマップの境界:{lat}、{lon}、ズーム:{zoom}",
|
||||
"xpack.maps.embeddableDisplayName": "マップ",
|
||||
"xpack.maps.emsFileSelect.selectPlaceholder": "EMS境界を選択",
|
||||
"xpack.maps.emsSource.tooltipsTitle": "ツールチップフィールド",
|
||||
|
@ -17429,8 +17428,6 @@
|
|||
"xpack.maps.fileUploadWizard.disabledDesc": "ファイルをアップロードできません。Kibana「データビュー管理」権限がありません。",
|
||||
"xpack.maps.fileUploadWizard.title": "ファイルをアップロード",
|
||||
"xpack.maps.fileUploadWizard.uploadLabel": "ファイルをインポートしています",
|
||||
"xpack.maps.filterByMapExtentMenuItem.disableDisplayName": "マップ範囲でのフィルターを無効にする",
|
||||
"xpack.maps.filterByMapExtentMenuItem.enableDisplayName": "マップ範囲でのフィルターを有効にする",
|
||||
"xpack.maps.filterEditor.applyForceRefreshLabel": "レイヤーデータにグローバル更新を適用",
|
||||
"xpack.maps.filterEditor.applyForceRefreshTooltip": "有効にすると、自動更新が実行されるときと、[更新]をクリックしたときに、レイヤーでデータが再取り込みされます。",
|
||||
"xpack.maps.filterEditor.applyGlobalFilterHelp": "有効にすると、結果がグローバル検索で絞り込まれます",
|
||||
|
|
|
@ -17417,7 +17417,6 @@
|
|||
"xpack.maps.drawTooltip.lineInstructions": "单击以开始绘制线条。单击以添加顶点。双击以完成。",
|
||||
"xpack.maps.drawTooltip.pointInstructions": "单击以创建点。",
|
||||
"xpack.maps.drawTooltip.polygonInstructions": "单击以开始绘制形状。单击以添加顶点。双击以完成。",
|
||||
"xpack.maps.embeddable.boundsFilterLabel": "位于中心的地图边界:{lat}, {lon},缩放:{zoom}",
|
||||
"xpack.maps.embeddableDisplayName": "地图",
|
||||
"xpack.maps.emsFileSelect.selectPlaceholder": "选择 EMS 边界",
|
||||
"xpack.maps.emsSource.tooltipsTitle": "工具提示字段",
|
||||
|
@ -17447,8 +17446,6 @@
|
|||
"xpack.maps.fileUploadWizard.disabledDesc": "无法上传文件,您缺少对“数据视图管理”的 Kibana 权限。",
|
||||
"xpack.maps.fileUploadWizard.title": "上传文件",
|
||||
"xpack.maps.fileUploadWizard.uploadLabel": "正在导入文件",
|
||||
"xpack.maps.filterByMapExtentMenuItem.disableDisplayName": "按地图范围禁用筛选",
|
||||
"xpack.maps.filterByMapExtentMenuItem.enableDisplayName": "按地图范围启用筛选",
|
||||
"xpack.maps.filterEditor.applyForceRefreshLabel": "将全局刷新应用于图层数据",
|
||||
"xpack.maps.filterEditor.applyForceRefreshTooltip": "启用后,图层将在触发自动刷新和单击“刷新”时重新提取数据。",
|
||||
"xpack.maps.filterEditor.applyGlobalFilterHelp": "启用后,全局搜索会缩减结果",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export default function ({ getPageObjects, getService }) {
|
||||
const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'lens', 'maps']);
|
||||
|
||||
const browser = getService('browser');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const security = getService('security');
|
||||
|
@ -35,7 +36,12 @@ export default function ({ getPageObjects, getService }) {
|
|||
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 testSubjects.click('embeddablePanelAction-FILTER_BY_MAP_EXTENT');
|
||||
await testSubjects.setEuiSwitch(
|
||||
'filterByMapExtentSwitch24ade730-afe4-42b6-919a-c4e0a98c94f2',
|
||||
'check'
|
||||
);
|
||||
await browser.pressKeys(browser.keys.ESCAPE);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await PageObjects.lens.assertMetric('Count of records', '1');
|
||||
|
@ -50,7 +56,12 @@ export default function ({ getPageObjects, getService }) {
|
|||
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 testSubjects.click('embeddablePanelAction-FILTER_BY_MAP_EXTENT');
|
||||
await testSubjects.setEuiSwitch(
|
||||
'filterByMapExtentSwitch24ade730-afe4-42b6-919a-c4e0a98c94f2',
|
||||
'uncheck'
|
||||
);
|
||||
await browser.pressKeys(browser.keys.ESCAPE);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.lens.assertMetric('Count of records', '6');
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue