[Maps] add initial location option that fits to data bounds (#74583)

* [Maps] add initial location option that fits to data bounds

* update navigation_panel snapshot

* add functional test to ensure sync is called when auto fit to bounds with no data

* add functional test for auto fit to bounds on map load

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-08-11 10:18:36 -06:00 committed by GitHub
parent 0ef17f92a6
commit 75b8a3cb71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 132 additions and 29 deletions

View file

@ -234,6 +234,7 @@ export enum INITIAL_LOCATION {
LAST_SAVED_LOCATION = 'LAST_SAVED_LOCATION',
FIXED_LOCATION = 'FIXED_LOCATION',
BROWSER_LOCATION = 'BROWSER_LOCATION',
AUTO_FIT_TO_BOUNDS = 'AUTO_FIT_TO_BOUNDS',
}
export enum LAYER_WIZARD_CATEGORY {

View file

@ -7,6 +7,7 @@
import { Dispatch } from 'redux';
import bbox from '@turf/bbox';
import uuid from 'uuid/v4';
import { multiPoint } from '@turf/helpers';
import { FeatureCollection } from 'geojson';
import { MapStoreState } from '../reducers/store';
@ -133,7 +134,7 @@ export function syncDataForAllLayers() {
};
}
export function syncDataForAllJoinLayers() {
function syncDataForAllJoinLayers() {
return async (dispatch: Dispatch, getState: () => MapStoreState) => {
const syncPromises = getLayerList(getState())
.filter((layer) => {
@ -318,7 +319,7 @@ export function fitToLayerExtent(layerId: string) {
};
}
export function fitToDataBounds() {
export function fitToDataBounds(onNoBounds?: () => void) {
return async (dispatch: Dispatch, getState: () => MapStoreState) => {
const layerList = getFittableLayers(getState());
@ -365,6 +366,9 @@ export function fitToDataBounds() {
}
if (!corners.length) {
if (onNoBounds) {
onNoBounds();
}
return;
}
@ -374,6 +378,32 @@ export function fitToDataBounds() {
};
}
let lastSetQueryCallId: string = '';
export function autoFitToBounds() {
return async (dispatch: Dispatch) => {
// Method can be triggered before async actions complete
// Use localSetQueryCallId to only continue execution path if method has not been re-triggered.
const localSetQueryCallId = uuid();
lastSetQueryCallId = localSetQueryCallId;
// Joins are performed on the client.
// As a result, bounds for join layers must also be performed on the client.
// Therefore join layers need to fetch data prior to auto fitting bounds.
await dispatch<any>(syncDataForAllJoinLayers());
if (localSetQueryCallId === lastSetQueryCallId) {
// In cases where there are no bounds, such as no matching documents, fitToDataBounds does not trigger setGotoWithBounds.
// Ensure layer syncing occurs when setGotoWithBounds is not triggered.
function onNoBounds() {
if (localSetQueryCallId === lastSetQueryCallId) {
dispatch<any>(syncDataForAllLayers());
}
}
dispatch<any>(fitToDataBounds(onNoBounds));
}
};
}
function setGotoWithBounds(bounds: MapExtent) {
return {
type: SET_GOTO,

View file

@ -125,8 +125,6 @@ export function addLayer(layerDescriptor: LayerDescriptor) {
};
}
// Do not use when rendering a map. Method exists to enable selectors for getLayerList when
// rendering is not needed.
export function addLayerWithoutDataSync(layerDescriptor: LayerDescriptor) {
return {
type: ADD_LAYER,

View file

@ -6,7 +6,6 @@
import { Dispatch } from 'redux';
import turfBboxPolygon from '@turf/bbox-polygon';
import turfBooleanContains from '@turf/boolean-contains';
import uuid from 'uuid/v4';
import { Filter, Query, TimeRange } from 'src/plugins/data/public';
import { MapStoreState } from '../reducers/store';
@ -44,12 +43,8 @@ import {
UPDATE_DRAW_STATE,
UPDATE_MAP_SETTING,
} from './map_action_constants';
import {
fitToDataBounds,
syncDataForAllJoinLayers,
syncDataForAllLayers,
} from './data_request_actions';
import { addLayer } from './layer_actions';
import { autoFitToBounds, syncDataForAllLayers } from './data_request_actions';
import { addLayer, addLayerWithoutDataSync } from './layer_actions';
import { MapSettings } from '../reducers/map';
import {
DrawState,
@ -57,6 +52,7 @@ import {
MapExtent,
MapRefreshConfig,
} from '../../common/descriptor_types';
import { INITIAL_LOCATION } from '../../common/constants';
import { scaleBounds } from '../elasticsearch_geo_utils';
export function setMapInitError(errorMessage: string) {
@ -98,13 +94,21 @@ export function mapReady() {
type: MAP_READY,
});
getWaitingForMapReadyLayerListRaw(getState()).forEach((layerDescriptor) => {
dispatch<any>(addLayer(layerDescriptor));
});
const waitingForMapReadyLayerList = getWaitingForMapReadyLayerListRaw(getState());
dispatch({
type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST,
});
if (getMapSettings(getState()).initialLocation === INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS) {
waitingForMapReadyLayerList.forEach((layerDescriptor) => {
dispatch<any>(addLayerWithoutDataSync(layerDescriptor));
});
dispatch<any>(autoFitToBounds());
} else {
waitingForMapReadyLayerList.forEach((layerDescriptor) => {
dispatch<any>(addLayer(layerDescriptor));
});
}
};
}
@ -196,7 +200,6 @@ function generateQueryTimestamp() {
return new Date().toISOString();
}
let lastSetQueryCallId: string = '';
export function setQuery({
query,
timeFilters,
@ -227,18 +230,7 @@ export function setQuery({
});
if (getMapSettings(getState()).autoFitToDataBounds) {
// Joins are performed on the client.
// As a result, bounds for join layers must also be performed on the client.
// Therefore join layers need to fetch data prior to auto fitting bounds.
const localSetQueryCallId = uuid();
lastSetQueryCallId = localSetQueryCallId;
await dispatch<any>(syncDataForAllJoinLayers());
// setQuery can be triggered before async data fetching completes
// Only continue execution path if setQuery has not been re-triggered.
if (localSetQueryCallId === lastSetQueryCallId) {
dispatch<any>(fitToDataBounds());
}
dispatch<any>(autoFitToBounds());
} else {
await dispatch<any>(syncDataForAllLayers());
}

View file

@ -41,5 +41,10 @@ export async function getInitialView(
});
}
if (settings.initialLocation === INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS) {
// map bounds pulled from data sources. Just use default map location
return null;
}
return goto && goto.center ? goto.center : null;
}

View file

@ -75,6 +75,10 @@ exports[`should render 1`] = `
"id": "LAST_SAVED_LOCATION",
"label": "Map location at save",
},
Object {
"id": "AUTO_FIT_TO_BOUNDS",
"label": "Auto fit map to data bounds",
},
Object {
"id": "FIXED_LOCATION",
"label": "Fixed location",
@ -165,6 +169,10 @@ exports[`should render browser location form when initialLocation is BROWSER_LOC
"id": "LAST_SAVED_LOCATION",
"label": "Map location at save",
},
Object {
"id": "AUTO_FIT_TO_BOUNDS",
"label": "Auto fit map to data bounds",
},
Object {
"id": "FIXED_LOCATION",
"label": "Fixed location",
@ -275,6 +283,10 @@ exports[`should render fixed location form when initialLocation is FIXED_LOCATIO
"id": "LAST_SAVED_LOCATION",
"label": "Map location at save",
},
Object {
"id": "AUTO_FIT_TO_BOUNDS",
"label": "Auto fit map to data bounds",
},
Object {
"id": "FIXED_LOCATION",
"label": "Fixed location",

View file

@ -41,6 +41,12 @@ const initialLocationOptions = [
defaultMessage: 'Map location at save',
}),
},
{
id: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS,
label: i18n.translate('xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel', {
defaultMessage: 'Auto fit map to data bounds',
}),
},
{
id: INITIAL_LOCATION.FIXED_LOCATION,
label: i18n.translate('xpack.maps.mapSettingsPanel.fixedLocationLabel', {
@ -125,7 +131,10 @@ export function NavigationPanel({ center, settings, updateMapSetting, zoom }: Pr
};
function renderInitialLocationInputs() {
if (settings.initialLocation === INITIAL_LOCATION.LAST_SAVED_LOCATION) {
if (
settings.initialLocation === INITIAL_LOCATION.LAST_SAVED_LOCATION ||
settings.initialLocation === INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS
) {
return null;
}

View file

@ -10,6 +10,23 @@ export default function ({ getPageObjects }) {
const PageObjects = getPageObjects(['maps']);
describe('auto fit map to bounds', () => {
describe('initial location', () => {
before(async () => {
await PageObjects.maps.loadSavedMap(
'document example - auto fit to bounds for initial location'
);
});
it('should automatically fit to bounds on initial map load', async () => {
const hits = await PageObjects.maps.getHits();
expect(hits).to.equal('6');
const { lat, lon } = await PageObjects.maps.getView();
expect(Math.round(lat)).to.equal(41);
expect(Math.round(lon)).to.equal(-99);
});
});
describe('without joins', () => {
before(async () => {
await PageObjects.maps.loadSavedMap('document example');
@ -25,10 +42,20 @@ export default function ({ getPageObjects }) {
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "ios"');
await PageObjects.maps.waitForMapPanAndZoom(origView);
const hits = await PageObjects.maps.getHits();
expect(hits).to.equal('2');
const { lat, lon } = await PageObjects.maps.getView();
expect(Math.round(lat)).to.equal(43);
expect(Math.round(lon)).to.equal(-102);
});
it('should sync layers even when there is not data', async () => {
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "fake_os_with_no_matches"');
const hits = await PageObjects.maps.getHits();
expect(hits).to.equal('0');
});
});
describe('with joins', () => {

View file

@ -979,6 +979,35 @@
}
}
{
"type": "doc",
"value": {
"id": "map:13776f20-db37-11ea-8fbb-3da39bb9bff2",
"index": ".kibana",
"source": {
"map" : {
"title" : "document example - auto fit to bounds for initial location",
"description" : "",
"mapStateJSON" : "{\"zoom\":5.2,\"center\":{\"lon\":-67.80052,\"lat\":-55.25331},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"AUTO_FIT_TO_BOUNDS\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
"layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]",
"uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
},
"type" : "map",
"references" : [
{
"name" : "layer_1_source_index_pattern",
"type" : "index-pattern",
"id" : "c698b940-e149-11e8-a35a-370a8516603a"
}
],
"migrationVersion" : {
"map" : "7.9.0"
},
"updated_at" : "2020-08-10T18:27:39.805Z"
}
}
}
{
"type": "doc",
"value": {