diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index fd972219563a8..b1a613e789e85 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -217,3 +217,9 @@ export enum SCALING_TYPES { export const RGBA_0000 = 'rgba(0,0,0,0)'; export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID'; + +export enum INITIAL_LOCATION { + LAST_SAVED_LOCATION = 'LAST_SAVED_LOCATION', + FIXED_LOCATION = 'FIXED_LOCATION', + BROWSER_LOCATION = 'BROWSER_LOCATION', +} diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index 38c56405787eb..413b440279d77 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -72,7 +72,7 @@ export function trackMapSettings(): AnyAction; export function updateMapSetting( settingKey: string, - settingValue: string | boolean | number + settingValue: string | boolean | number | object ): AnyAction; export function cloneLayer(layerId: string): AnyAction; diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/get_initial_view.ts b/x-pack/plugins/maps/public/connected_components/map/mb/get_initial_view.ts new file mode 100644 index 0000000000000..30e3b9b46916b --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map/mb/get_initial_view.ts @@ -0,0 +1,45 @@ +/* + * 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 { INITIAL_LOCATION } from '../../../../common/constants'; +import { Goto, MapCenterAndZoom } from '../../../../common/descriptor_types'; +import { MapSettings } from '../../../reducers/map'; + +export async function getInitialView( + goto: Goto | null, + settings: MapSettings +): Promise { + if (settings.initialLocation === INITIAL_LOCATION.FIXED_LOCATION) { + return { + lat: settings.fixedLocation.lat, + lon: settings.fixedLocation.lon, + zoom: settings.fixedLocation.zoom, + }; + } + + if (settings.initialLocation === INITIAL_LOCATION.BROWSER_LOCATION) { + return await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition( + // success callback + pos => { + resolve({ + lat: pos.coords.latitude, + lon: pos.coords.longitude, + zoom: settings.browserLocation.zoom, + }); + }, + // error callback + () => { + // eslint-disable-next-line no-console + console.warn('Unable to fetch browser location for initial map location'); + resolve(null); + } + ); + }); + } + + return goto && goto.center ? goto.center : null; +} diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 6bb5a4fed6e52..7afb326f42e02 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -24,6 +24,7 @@ import sprites2 from '@elastic/maki/dist/sprite@2.png'; import { DrawControl } from './draw_control'; import { TooltipControl } from './tooltip_control'; import { clampToLatBounds, clampToLonBounds } from '../../../elasticsearch_geo_utils'; +import { getInitialView } from './get_initial_view'; import { getInjectedVarFunc } from '../../../kibana_services'; @@ -112,6 +113,7 @@ export class MBMapContainer extends React.Component { } async _createMbMapInstance() { + const initialView = await getInitialView(this.props.goto, this.props.settings); return new Promise(resolve => { const mbStyle = { version: 8, @@ -133,7 +135,6 @@ export class MBMapContainer extends React.Component { maxZoom: this.props.settings.maxZoom, minZoom: this.props.settings.minZoom, }; - const initialView = _.get(this.props.goto, 'center'); if (initialView) { options.zoom = initialView.zoom; options.center = { diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/navigation_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/navigation_panel.test.tsx.snap new file mode 100644 index 0000000000000..641dd20a1a44a --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/navigation_panel.test.tsx.snap @@ -0,0 +1,292 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + +
+ +
+
+ + + + + +
+`; + +exports[`should render browser location form when initialLocation is BROWSER_LOCATION 1`] = ` + + +
+ +
+
+ + + + + + + + +
+`; + +exports[`should render fixed location form when initialLocation is FIXED_LOCATION 1`] = ` + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts index 329fac28d7d2e..eaa49719059c5 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts @@ -10,13 +10,20 @@ import { FLYOUT_STATE } from '../../reducers/ui'; import { MapStoreState } from '../../reducers/store'; import { MapSettingsPanel } from './map_settings_panel'; import { rollbackMapSettings, updateMapSetting } from '../../actions/map_actions'; -import { getMapSettings, hasMapSettingsChanges } from '../../selectors/map_selectors'; +import { + getMapCenter, + getMapSettings, + getMapZoom, + hasMapSettingsChanges, +} from '../../selectors/map_selectors'; import { updateFlyout } from '../../actions/ui_actions'; function mapStateToProps(state: MapStoreState) { return { - settings: getMapSettings(state), + center: getMapCenter(state), hasMapSettingsChanges: hasMapSettingsChanges(state), + settings: getMapSettings(state), + zoom: getMapZoom(state), }; } @@ -29,7 +36,7 @@ function mapDispatchToProps(dispatch: Dispatch) { keepChanges: () => { dispatch(updateFlyout(FLYOUT_STATE.NONE)); }, - updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => { + updateMapSetting: (settingKey: string, settingValue: string | number | boolean | object) => { dispatch(updateMapSetting(settingKey, settingValue)); }, }; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx index a89f4461fff06..66b979869416d 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -20,21 +20,26 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { MapSettings } from '../../reducers/map'; import { NavigationPanel } from './navigation_panel'; import { SpatialFiltersPanel } from './spatial_filters_panel'; +import { MapCenter } from '../../../common/descriptor_types'; interface Props { cancelChanges: () => void; + center: MapCenter; hasMapSettingsChanges: boolean; keepChanges: () => void; settings: MapSettings; - updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean | object) => void; + zoom: number; } export function MapSettingsPanel({ cancelChanges, + center, hasMapSettingsChanges, keepChanges, settings, updateMapSetting, + zoom, }: Props) { // TODO move common text like Cancel and Close to common i18n translation const closeBtnLabel = hasMapSettingsChanges @@ -60,7 +65,12 @@ export function MapSettingsPanel({
- +
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.test.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.test.tsx new file mode 100644 index 0000000000000..d785a30324e4e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { NavigationPanel } from './navigation_panel'; +import { getDefaultMapSettings } from '../../reducers/default_map_settings'; +import { INITIAL_LOCATION } from '../../../common/constants'; + +const defaultProps = { + center: { lat: 0, lon: 0 }, + settings: getDefaultMapSettings(), + updateMapSetting: () => {}, + zoom: 0, +}; + +test('should render', async () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); + +test('should render fixed location form when initialLocation is FIXED_LOCATION', async () => { + const settings = { + ...defaultProps.settings, + initialLocation: INITIAL_LOCATION.FIXED_LOCATION, + }; + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); + +test('should render browser location form when initialLocation is BROWSER_LOCATION', async () => { + const settings = { + ...defaultProps.settings, + initialLocation: INITIAL_LOCATION.BROWSER_LOCATION, + }; + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx index ed83e838f44f6..0e12f20dd9a7a 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx @@ -4,25 +4,198 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import React, { ChangeEvent } from 'react'; +import { + EuiButtonEmpty, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiRadioGroup, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { MapSettings } from '../../reducers/map'; import { ValidatedDualRange, Value } from '../../../../../../src/plugins/kibana_react/public'; -import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; +import { INITIAL_LOCATION, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; +import { MapCenter } from '../../../common/descriptor_types'; +// @ts-ignore +import { ValidatedRange } from '../../components/validated_range'; interface Props { + center: MapCenter; settings: MapSettings; - updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean | object) => void; + zoom: number; } -export function NavigationPanel({ settings, updateMapSetting }: Props) { +const initialLocationOptions = [ + { + id: INITIAL_LOCATION.LAST_SAVED_LOCATION, + label: i18n.translate('xpack.maps.mapSettingsPanel.lastSavedLocationLabel', { + defaultMessage: 'Map location at save', + }), + }, + { + id: INITIAL_LOCATION.FIXED_LOCATION, + label: i18n.translate('xpack.maps.mapSettingsPanel.fixedLocationLabel', { + defaultMessage: 'Fixed location', + }), + }, + { + id: INITIAL_LOCATION.BROWSER_LOCATION, + label: i18n.translate('xpack.maps.mapSettingsPanel.browserLocationLabel', { + defaultMessage: 'Browser location', + }), + }, +]; + +export function NavigationPanel({ center, settings, updateMapSetting, zoom }: Props) { const onZoomChange = (value: Value) => { - updateMapSetting('minZoom', Math.max(MIN_ZOOM, parseInt(value[0] as string, 10))); - updateMapSetting('maxZoom', Math.min(MAX_ZOOM, parseInt(value[1] as string, 10))); + const minZoom = Math.max(MIN_ZOOM, parseInt(value[0] as string, 10)); + const maxZoom = Math.min(MAX_ZOOM, parseInt(value[1] as string, 10)); + updateMapSetting('minZoom', minZoom); + updateMapSetting('maxZoom', maxZoom); + + // ensure fixed zoom and browser zoom stay within defined min/max + if (settings.fixedLocation.zoom < minZoom) { + onFixedZoomChange(minZoom); + } else if (settings.fixedLocation.zoom > maxZoom) { + onFixedZoomChange(maxZoom); + } + + if (settings.browserLocation.zoom < minZoom) { + onBrowserZoomChange(minZoom); + } else if (settings.browserLocation.zoom > maxZoom) { + onBrowserZoomChange(maxZoom); + } + }; + + const onInitialLocationChange = (optionId: string): void => { + updateMapSetting('initialLocation', optionId); + }; + + const onFixedLatChange = (event: ChangeEvent) => { + let value = parseFloat(event.target.value); + if (isNaN(value)) { + value = 0; + } else if (value < -90) { + value = -90; + } else if (value > 90) { + value = 90; + } + updateMapSetting('fixedLocation', { ...settings.fixedLocation, lat: value }); + }; + + const onFixedLonChange = (event: ChangeEvent) => { + let value = parseFloat(event.target.value); + if (isNaN(value)) { + value = 0; + } else if (value < -180) { + value = -180; + } else if (value > 180) { + value = 180; + } + updateMapSetting('fixedLocation', { ...settings.fixedLocation, lon: value }); + }; + + const onFixedZoomChange = (value: number) => { + updateMapSetting('fixedLocation', { ...settings.fixedLocation, zoom: value }); + }; + + const onBrowserZoomChange = (value: number) => { + updateMapSetting('browserLocation', { zoom: value }); + }; + + const useCurrentView = () => { + updateMapSetting('fixedLocation', { + lat: center.lat, + lon: center.lon, + zoom: Math.round(zoom), + }); }; + function renderInitialLocationInputs() { + if (settings.initialLocation === INITIAL_LOCATION.LAST_SAVED_LOCATION) { + return null; + } + + const zoomFormRow = ( + + + + ); + + if (settings.initialLocation === INITIAL_LOCATION.BROWSER_LOCATION) { + return zoomFormRow; + } + + return ( + <> + + + + + + + {zoomFormRow} + + + + + + + + + ); + } + return ( @@ -50,6 +223,19 @@ export function NavigationPanel({ settings, updateMapSetting }: Props) { allowEmptyRange={false} compressed /> + + + + + {renderInitialLocationInputs()} ); } diff --git a/x-pack/plugins/maps/public/reducers/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts index fe21b37434edd..9c9b814ae6add 100644 --- a/x-pack/plugins/maps/public/reducers/default_map_settings.ts +++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MAX_ZOOM, MIN_ZOOM } from '../../common/constants'; +import { INITIAL_LOCATION, MAX_ZOOM, MIN_ZOOM } from '../../common/constants'; import { MapSettings } from './map'; export function getDefaultMapSettings(): MapSettings { return { + initialLocation: INITIAL_LOCATION.LAST_SAVED_LOCATION, + fixedLocation: { lat: 0, lon: 0, zoom: 2 }, + browserLocation: { zoom: 2 }, maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM, showSpatialFilters: true, diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index be0700d4bdd6d..20e1dc1035e19 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -15,6 +15,7 @@ import { MapRefreshConfig, TooltipState, } from '../../common/descriptor_types'; +import { INITIAL_LOCATION } from '../../common/constants'; import { Filter, TimeRange } from '../../../../../src/plugins/data/public'; export type MapContext = { @@ -40,6 +41,15 @@ export type MapContext = { }; export type MapSettings = { + initialLocation: INITIAL_LOCATION; + fixedLocation: { + lat: number; + lon: number; + zoom: number; + }; + browserLocation: { + zoom: number; + }; maxZoom: number; minZoom: number; showSpatialFilters: boolean;