diff --git a/x-pack/plugins/maps/public/shared/components/__snapshots__/layer_toc_actions.test.js.snap b/x-pack/plugins/maps/public/shared/components/__snapshots__/layer_toc_actions.test.js.snap
new file mode 100644
index 0000000000000..a5b56da230301
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/components/__snapshots__/layer_toc_actions.test.js.snap
@@ -0,0 +1,390 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LayerTocActions is rendered 1`] = `
+
+
+ icon mock
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="contextMenu"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ withTitle={true}
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
+ "name": "Hide layer",
+ "onClick": [Function],
+ },
+ ],
+ "title": "Layer actions",
+ },
+ ]
+ }
+ />
+
+`;
+
+exports[`LayerTocActions should disable fit to data when supportsFitToBounds is false 1`] = `
+
+
+ icon mock
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="contextMenu"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ withTitle={true}
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": "Layer does not support fit to data",
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
+ "name": "Hide layer",
+ "onClick": [Function],
+ },
+ ],
+ "title": "Layer actions",
+ },
+ ]
+ }
+ />
+
+`;
+
+exports[`LayerTocActions should display spinner when layer is loading 1`] = `
+
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="contextMenu"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ withTitle={true}
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
+ "name": "Hide layer",
+ "onClick": [Function],
+ },
+ ],
+ "title": "Layer actions",
+ },
+ ]
+ }
+ />
+
+`;
+
+exports[`LayerTocActions should provide feedback when layer is not visible because of current zoom level 1`] = `
+
+
+
+ icon mock
+
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="contextMenu"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ withTitle={true}
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
+ "name": "Hide layer",
+ "onClick": [Function],
+ },
+ ],
+ "title": "Layer actions",
+ },
+ ]
+ }
+ />
+
+`;
+
+exports[`LayerTocActions should show visible toggle when layer is not visible 1`] = `
+
+
+ icon mock
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="contextMenu"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ withTitle={true}
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
+ "name": "Show layer",
+ "onClick": [Function],
+ },
+ ],
+ "title": "Layer actions",
+ },
+ ]
+ }
+ />
+
+`;
+
+exports[`LayerTocActions should show warning when layer has errors 1`] = `
+
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ id="contextMenu"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ withTitle={true}
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
+ "name": "Hide layer",
+ "onClick": [Function],
+ },
+ ],
+ "title": "Layer actions",
+ },
+ ]
+ }
+ />
+
+`;
diff --git a/x-pack/plugins/maps/public/shared/components/layer_toc_actions.js b/x-pack/plugins/maps/public/shared/components/layer_toc_actions.js
index 69bc14fa35bc0..ae71f5db63469 100644
--- a/x-pack/plugins/maps/public/shared/components/layer_toc_actions.js
+++ b/x-pack/plugins/maps/public/shared/components/layer_toc_actions.js
@@ -41,9 +41,26 @@ function cleanDisplayName(displayName) {
export class LayerTocActions extends Component {
state = {
- isPopoverOpen: false
+ isPopoverOpen: false,
+ supportsFitToBounds: false,
};
+ componentDidMount() {
+ this._isMounted = true;
+ this._loadSupportsFitToBounds();
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ async _loadSupportsFitToBounds() {
+ const supportsFitToBounds = await this.props.layer.supportsFitToBounds();
+ if (this._isMounted) {
+ this.setState({ supportsFitToBounds });
+ }
+ }
+
_onClick = () => {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen,
@@ -128,6 +145,9 @@ export class LayerTocActions extends Component {
size="m"
/>
),
+ 'data-test-subj': 'fitToBoundsButton',
+ toolTipContent: this.state.supportsFitToBounds ? null : 'Layer does not support fit to data',
+ disabled: !this.state.supportsFitToBounds,
onClick: () => {
this._closePopover();
this.props.fitToBounds();
diff --git a/x-pack/plugins/maps/public/shared/components/layer_toc_actions.test.js b/x-pack/plugins/maps/public/shared/components/layer_toc_actions.test.js
new file mode 100644
index 0000000000000..118f6b20f23a2
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/components/layer_toc_actions.test.js
@@ -0,0 +1,143 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { LayerTocActions } from './layer_toc_actions';
+
+let supportsFitToBounds;
+let isLayerLoading;
+let isVisible;
+let hasErrors;
+let showAtZoomLevel;
+const layerMock = {
+ supportsFitToBounds: () => { return supportsFitToBounds; },
+ isVisible: () => { return isVisible; },
+ hasErrors: () => { return hasErrors; },
+ getErrors: () => { return 'simulated layer error'; },
+ isLayerLoading: () => { return isLayerLoading; },
+ showAtZoomLevel: () => { return showAtZoomLevel; },
+ getZoomConfig: () => { return { minZoom: 2, maxZoom: 3 }; },
+ getIcon: () => { return (
icon mock
); },
+};
+
+const defaultProps = {
+ displayName: 'layer1',
+ zoom: 0,
+ layer: layerMock,
+};
+
+describe('LayerTocActions', () => {
+ beforeEach(() => {
+ supportsFitToBounds = true;
+ isLayerLoading = false;
+ isVisible = true;
+ hasErrors = false;
+ showAtZoomLevel = true;
+ });
+
+ test('is rendered', async () => {
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should disable fit to data when supportsFitToBounds is false', async () => {
+ supportsFitToBounds = false;
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should display spinner when layer is loading', async () => {
+ isLayerLoading = true;
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should show warning when layer has errors', async () => {
+ hasErrors = true;
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should show visible toggle when layer is not visible', async () => {
+ isVisible = false;
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should provide feedback when layer is not visible because of current zoom level', async () => {
+ showAtZoomLevel = false;
+ const component = shallow(
+
+ );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/maps/public/shared/layers/layer.js b/x-pack/plugins/maps/public/shared/layers/layer.js
index 8d095d17e0be1..945176e6eb04f 100644
--- a/x-pack/plugins/maps/public/shared/layers/layer.js
+++ b/x-pack/plugins/maps/public/shared/layers/layer.js
@@ -53,6 +53,10 @@ export class AbstractLayer {
return this._source.isJoinable();
}
+ async supportsFitToBounds() {
+ return await this._source.supportsFitToBounds();
+ }
+
async getDisplayName() {
if (this._descriptor.label) {
return this._descriptor.label;
diff --git a/x-pack/plugins/maps/public/shared/layers/sources/es_source.js b/x-pack/plugins/maps/public/shared/layers/sources/es_source.js
index 02718133a7efe..a3a4c7356ddad 100644
--- a/x-pack/plugins/maps/public/shared/layers/sources/es_source.js
+++ b/x-pack/plugins/maps/public/shared/layers/sources/es_source.js
@@ -78,7 +78,7 @@ export class AbstractESSource extends AbstractVectorSource {
return searchSource;
}
- async getBoundsForFilters({ query, timeFilters }, layerName) {
+ async getBoundsForFilters({ query, timeFilters }) {
const searchSource = await this._makeSearchSource({ query, timeFilters }, 0);
const geoField = await this._getGeoField();
@@ -96,15 +96,29 @@ export class AbstractESSource extends AbstractVectorSource {
const aggConfigs = new AggConfigs(indexPattern, geoBoundsAgg);
searchSource.setField('aggs', aggConfigs.toDsl());
- const esResp = await this._runEsQuery(layerName, searchSource, 'bounds request');
- const esBounds = _.get(esResp, 'aggregations.1.bounds');
- return (esBounds) ?
- {
- min_lon: esBounds.top_left.lon,
- max_lon: esBounds.bottom_right.lon,
- min_lat: esBounds.bottom_right.lat,
- max_lat: esBounds.top_left.lat
- } : null;
+ let esBounds;
+ try {
+ const esResp = await searchSource.fetch();
+ esBounds = _.get(esResp, 'aggregations.1.bounds');
+ } catch(error) {
+ esBounds = {
+ top_left: {
+ lat: 90,
+ lon: -180
+ },
+ bottom_right: {
+ lat: -90,
+ lon: 180
+ }
+ };
+ }
+
+ return {
+ min_lon: esBounds.top_left.lon,
+ max_lon: esBounds.bottom_right.lon,
+ min_lat: esBounds.bottom_right.lat,
+ max_lat: esBounds.top_left.lat
+ };
}
async isTimeAware() {
@@ -130,6 +144,18 @@ export class AbstractESSource extends AbstractVectorSource {
}
}
+ async supportsFitToBounds() {
+ try {
+ const geoField = await this._getGeoField();
+ // geo_bounds aggregation only supports geo_point
+ // there is currently no backend support for getting bounding box of geo_shape field
+ return geoField.type !== 'geo_shape';
+ } catch (error) {
+ return false;
+ }
+ }
+
+
async _getGeoField() {
const indexPattern = await this._getIndexPattern();
const geoField = indexPattern.fields.byName[this._descriptor.geoField];
diff --git a/x-pack/plugins/maps/public/shared/layers/sources/source.js b/x-pack/plugins/maps/public/shared/layers/sources/source.js
index d163a8dcca8cc..faf9280089e80 100644
--- a/x-pack/plugins/maps/public/shared/layers/sources/source.js
+++ b/x-pack/plugins/maps/public/shared/layers/sources/source.js
@@ -24,6 +24,10 @@ export class AbstractSource {
destroy() {}
+ async supportsFitToBounds() {
+ return true;
+ }
+
/**
* return list of immutable source properties.
* Immutable source properties are properties that can not be edited by the user.
diff --git a/x-pack/test/functional/apps/maps/es_search_source.js b/x-pack/test/functional/apps/maps/es_search_source.js
index 2e1dc53c7bd09..079eb24d86612 100644
--- a/x-pack/test/functional/apps/maps/es_search_source.js
+++ b/x-pack/test/functional/apps/maps/es_search_source.js
@@ -39,9 +39,16 @@ export default function ({ getPageObjects, getService }) {
expect(beforeRefreshTimerTimestamp).not.to.equal(afterRefreshTimerTimestamp);
});
+ describe('inspector', () => {
+ it('should register elasticsearch request in inspector', async () => {
+ const hits = await getHits();
+ expect(hits).to.equal('6');
+ });
+ });
+
describe('query bar', () => {
before(async () => {
- await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"');
+ await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8" OR machine.os.raw : "ios"');
});
after(async () => {
@@ -53,7 +60,7 @@ export default function ({ getPageObjects, getService }) {
const requestStats = await inspector.getTableData();
const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits');
await inspector.close();
- expect(hits).to.equal('1');
+ expect(hits).to.equal('3');
});
it('should re-fetch query when "refresh" is clicked', async () => {
@@ -62,12 +69,15 @@ export default function ({ getPageObjects, getService }) {
const afterQueryRefreshTimestamp = await getRequestTimestamp();
expect(beforeQueryRefreshTimestamp).not.to.equal(afterQueryRefreshTimestamp);
});
- });
- describe('inspector', () => {
- it('should register elasticsearch request in inspector', async () => {
- const hits = await getHits();
- expect(hits).to.equal('6');
+ it('should apply query to fit to bounds', async () => {
+ // Set view to other side of world so no matching results
+ await PageObjects.maps.setView(-15, -100, 6);
+ await PageObjects.maps.clickFitToBounds('logstash');
+ const { lat, lon, zoom } = await PageObjects.maps.getView();
+ expect(Math.round(lat)).to.equal(41);
+ expect(Math.round(lon)).to.equal(-102);
+ expect(Math.round(zoom)).to.equal(5);
});
});
diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json
index b7c6299f73915..610e6ec4a2c0a 100644
--- a/x-pack/test/functional/es_archives/maps/kibana/data.json
+++ b/x-pack/test/functional/es_archives/maps/kibana/data.json
@@ -107,7 +107,7 @@
"title" : "document example",
"description" : "",
"mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}",
- "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{\"alphaValue\":1}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[]},\"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}},\"alphaValue\":1},\"previousStyle\":null},\"type\":\"VECTOR\"}]",
+ "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{\"alphaValue\":1}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[]},\"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}},\"alphaValue\":1},\"previousStyle\":null},\"type\":\"VECTOR\"}]",
"uiStateJSON" : "{\"isDarkMode\":false}",
"bounds" : {
"type" : "envelope",
@@ -390,4 +390,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js
index 43d8fae38f117..cf5bdc1b7e1b3 100644
--- a/x-pack/test/functional/page_objects/gis_page.js
+++ b/x-pack/test/functional/page_objects/gis_page.js
@@ -175,6 +175,13 @@ export function GisPageProvider({ getService, getPageObjects }) {
await testSubjects.click('layerVisibilityToggleButton');
}
+ async clickFitToBounds(layerName) {
+ log.debug(`Fit to bounds, layer: ${layerName}`);
+ await this.openLayerTocActionsPanel(layerName);
+ await testSubjects.click('fitToBoundsButton');
+ await this.waitForLayersToLoad();
+ }
+
async openLayerTocActionsPanel(layerName) {
const cleanLayerName = layerName.split(' ').join('');
const isOpen = await testSubjects.exists(`layerTocActionsPanel${cleanLayerName}`);