[Maps] initial location map settings (#64336)

* [Maps] initial location map settings

* fix tslint

* add button to set to current view

* move button to bottom of form

* review feedback

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-05-04 13:57:25 -06:00 committed by GitHub
parent 497398e8ff
commit e3b9b946a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 620 additions and 15 deletions

View file

@ -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',
}

View file

@ -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;

View file

@ -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<MapCenterAndZoom | null> {
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;
}

View file

@ -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 = {

View file

@ -0,0 +1,292 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render 1`] = `
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Navigation"
id="xpack.maps.mapSettingsPanel.navigationTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<ValidatedDualRange
allowEmptyRange={false}
compressed={true}
formRowDisplay="columnCompressed"
fullWidth={false}
label="Zoom range"
max={24}
min={0}
onChange={[Function]}
showInput="inputWithPopover"
showLabels={true}
showRange={true}
value={
Array [
0,
24,
]
}
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initial map location"
labelType="label"
>
<EuiRadioGroup
idSelected="LAST_SAVED_LOCATION"
onChange={[Function]}
options={
Array [
Object {
"id": "LAST_SAVED_LOCATION",
"label": "Map location at save",
},
Object {
"id": "FIXED_LOCATION",
"label": "Fixed location",
},
Object {
"id": "BROWSER_LOCATION",
"label": "Browser location",
},
]
}
/>
</EuiFormRow>
</EuiPanel>
`;
exports[`should render browser location form when initialLocation is BROWSER_LOCATION 1`] = `
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Navigation"
id="xpack.maps.mapSettingsPanel.navigationTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<ValidatedDualRange
allowEmptyRange={false}
compressed={true}
formRowDisplay="columnCompressed"
fullWidth={false}
label="Zoom range"
max={24}
min={0}
onChange={[Function]}
showInput="inputWithPopover"
showLabels={true}
showRange={true}
value={
Array [
0,
24,
]
}
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initial map location"
labelType="label"
>
<EuiRadioGroup
idSelected="BROWSER_LOCATION"
onChange={[Function]}
options={
Array [
Object {
"id": "LAST_SAVED_LOCATION",
"label": "Map location at save",
},
Object {
"id": "FIXED_LOCATION",
"label": "Fixed location",
},
Object {
"id": "BROWSER_LOCATION",
"label": "Browser location",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initial zoom"
labelType="label"
>
<ValidatedRange
compressed={true}
max={24}
min={0}
onChange={[Function]}
showInput={true}
showRange={true}
step={1}
value={2}
/>
</EuiFormRow>
</EuiPanel>
`;
exports[`should render fixed location form when initialLocation is FIXED_LOCATION 1`] = `
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Navigation"
id="xpack.maps.mapSettingsPanel.navigationTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<ValidatedDualRange
allowEmptyRange={false}
compressed={true}
formRowDisplay="columnCompressed"
fullWidth={false}
label="Zoom range"
max={24}
min={0}
onChange={[Function]}
showInput="inputWithPopover"
showLabels={true}
showRange={true}
value={
Array [
0,
24,
]
}
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initial map location"
labelType="label"
>
<EuiRadioGroup
idSelected="FIXED_LOCATION"
onChange={[Function]}
options={
Array [
Object {
"id": "LAST_SAVED_LOCATION",
"label": "Map location at save",
},
Object {
"id": "FIXED_LOCATION",
"label": "Fixed location",
},
Object {
"id": "BROWSER_LOCATION",
"label": "Browser location",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initial latitude"
labelType="label"
>
<EuiFieldNumber
compressed={true}
onChange={[Function]}
value={0}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initial longitude"
labelType="label"
>
<EuiFieldNumber
compressed={true}
onChange={[Function]}
value={0}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initial zoom"
labelType="label"
>
<ValidatedRange
compressed={true}
max={24}
min={0}
onChange={[Function]}
showInput={true}
showRange={true}
step={1}
value={2}
/>
</EuiFormRow>
<EuiFlexGroup
justifyContent="flexEnd"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Set to current view"
id="xpack.maps.mapSettingsPanel.useCurrentViewBtnLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
`;

View file

@ -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<AnyAction>) {
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));
},
};

View file

@ -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({
<div className="mapLayerPanel__body">
<div className="mapLayerPanel__bodyOverflow">
<NavigationPanel settings={settings} updateMapSetting={updateMapSetting} />
<NavigationPanel
center={center}
settings={settings}
updateMapSetting={updateMapSetting}
zoom={zoom}
/>
<EuiSpacer size="s" />
<SpatialFiltersPanel settings={settings} updateMapSetting={updateMapSetting} />
</div>

View file

@ -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(<NavigationPanel {...defaultProps} />);
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(<NavigationPanel {...defaultProps} settings={settings} />);
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(<NavigationPanel {...defaultProps} settings={settings} />);
expect(component).toMatchSnapshot();
});

View file

@ -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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 = (
<EuiFormRow
label={i18n.translate('xpack.maps.mapSettingsPanel.initialZoomLabel', {
defaultMessage: 'Initial zoom',
})}
display="columnCompressed"
>
<ValidatedRange
min={settings.minZoom}
max={settings.maxZoom}
step={1}
value={
settings.initialLocation === INITIAL_LOCATION.BROWSER_LOCATION
? settings.browserLocation.zoom
: settings.fixedLocation.zoom
}
onChange={
settings.initialLocation === INITIAL_LOCATION.BROWSER_LOCATION
? onBrowserZoomChange
: onFixedZoomChange
}
showInput
showRange
compressed
/>
</EuiFormRow>
);
if (settings.initialLocation === INITIAL_LOCATION.BROWSER_LOCATION) {
return zoomFormRow;
}
return (
<>
<EuiFormRow
label={i18n.translate('xpack.maps.mapSettingsPanel.initialLatLabel', {
defaultMessage: 'Initial latitude',
})}
display="columnCompressed"
>
<EuiFieldNumber
value={settings.fixedLocation.lat}
onChange={onFixedLatChange}
compressed
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.maps.mapSettingsPanel.initialLonLabel', {
defaultMessage: 'Initial longitude',
})}
display="columnCompressed"
>
<EuiFieldNumber
value={settings.fixedLocation.lon}
onChange={onFixedLonChange}
compressed
/>
</EuiFormRow>
{zoomFormRow}
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={useCurrentView}>
<FormattedMessage
id="xpack.maps.mapSettingsPanel.useCurrentViewBtnLabel"
defaultMessage="Set to current view"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
return (
<EuiPanel>
<EuiTitle size="xs">
@ -50,6 +223,19 @@ export function NavigationPanel({ settings, updateMapSetting }: Props) {
allowEmptyRange={false}
compressed
/>
<EuiFormRow
label={i18n.translate('xpack.maps.source.mapSettingsPanel.initialLocationLabel', {
defaultMessage: 'Initial map location',
})}
>
<EuiRadioGroup
options={initialLocationOptions}
idSelected={settings.initialLocation}
onChange={onInitialLocationChange}
/>
</EuiFormRow>
{renderInitialLocationInputs()}
</EuiPanel>
);
}

View file

@ -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,

View file

@ -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;