[Maps] Revert layer-state correctly on cancel (#30451) (#30993)

This commit is contained in:
Thomas Neirynck 2019-02-13 15:10:04 -05:00 committed by GitHub
parent 81f86bbc86
commit 828146510c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 315 additions and 116 deletions

View file

@ -6,15 +6,13 @@
import turf from 'turf';
import turfBooleanContains from '@turf/boolean-contains';
import { GIS_API_PATH } from '../../common/constants';
import {
getLayerList,
getLayerListRaw,
getDataFilters,
getSelectedLayer,
getSelectedLayerId,
getMapReady,
getWaitingForMapReadyLayerListRaw,
getWaitingForMapReadyLayerListRaw
} from '../selectors/map_selectors';
export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER';
@ -25,7 +23,6 @@ export const ADD_WAITING_FOR_MAP_READY_LAYER = 'ADD_WAITING_FOR_MAP_READY_LAYER'
export const CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST = 'CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST';
export const REMOVE_LAYER = 'REMOVE_LAYER';
export const PROMOTE_TEMPORARY_LAYERS = 'PROMOTE_TEMPORARY_LAYERS';
export const SET_META = 'SET_META';
export const TOGGLE_LAYER_VISIBLE = 'TOGGLE_LAYER_VISIBLE';
export const MAP_EXTENT_CHANGED = 'MAP_EXTENT_CHANGED';
export const MAP_READY = 'MAP_READY';
@ -38,8 +35,6 @@ export const SET_QUERY = 'SET_QUERY';
export const TRIGGER_REFRESH_TIMER = 'TRIGGER_REFRESH_TIMER';
export const UPDATE_LAYER_PROP = 'UPDATE_LAYER_PROP';
export const UPDATE_LAYER_STYLE = 'UPDATE_LAYER_STYLE';
export const PROMOTE_TEMPORARY_STYLES = 'PROMOTE_TEMPORARY_STYLES';
export const CLEAR_TEMPORARY_STYLES = 'CLEAR_TEMPORARY_STYLES';
export const TOUCH_LAYER = 'TOUCH_LAYER';
export const UPDATE_SOURCE_PROP = 'UPDATE_SOURCE_PROP';
export const SET_REFRESH_CONFIG = 'SET_REFRESH_CONFIG';
@ -47,8 +42,9 @@ export const SET_MOUSE_COORDINATES = 'SET_MOUSE_COORDINATES';
export const CLEAR_MOUSE_COORDINATES = 'CLEAR_MOUSE_COORDINATES';
export const SET_GOTO = 'SET_GOTO';
export const CLEAR_GOTO = 'CLEAR_GOTO';
const GIS_API_RELATIVE = `../${GIS_API_PATH}`;
export const TRACK_CURRENT_LAYER_STATE = 'TRACK_CURRENT_LAYER_STATE';
export const ROLLBACK_TO_TRACKED_LAYER_STATE = 'ROLLBACK_TO_TRACKED_LAYER_STATE';
export const REMOVE_TRACKED_LAYER_STATE = 'REMOVE_TRACKED_LAYER_STATE';
function getLayerLoadingCallbacks(dispatch, layerId) {
return {
@ -74,6 +70,34 @@ async function syncDataForAllLayers(getState, dispatch, dataFilters) {
await Promise.all(syncs);
}
export function trackCurrentLayerState(layerId) {
return {
type: TRACK_CURRENT_LAYER_STATE,
layerId: layerId
};
}
export function rollbackToTrackedLayerStateForSelectedLayer() {
return async (dispatch, getState) => {
const layerId = getSelectedLayerId(getState());
await dispatch({
type: ROLLBACK_TO_TRACKED_LAYER_STATE,
layerId: layerId
});
dispatch(syncDataForLayer(layerId));
};
}
export function removeTrackedLayerStateForSelectedLayer() {
return (dispatch, getState) => {
const layerId = getSelectedLayerId(getState());
dispatch({
type: REMOVE_TRACKED_LAYER_STATE,
layerId: layerId
});
};
}
export function replaceLayerList(newLayerList) {
return (dispatch, getState) => {
getLayerListRaw(getState()).forEach(({ id }) => {
@ -140,9 +164,19 @@ export function toggleLayerVisible(layerId) {
}
export function setSelectedLayer(layerId) {
return {
type: SET_SELECTED_LAYER,
selectedLayerId: layerId
return async (dispatch, getState) => {
const oldSelectedLayer = getSelectedLayerId(getState());
if (oldSelectedLayer) {
await dispatch(rollbackToTrackedLayerStateForSelectedLayer());
}
if (layerId) {
dispatch(trackCurrentLayerState(layerId));
}
dispatch({
type: SET_SELECTED_LAYER,
selectedLayerId: layerId
});
};
}
@ -153,24 +187,12 @@ export function updateLayerOrder(newLayerOrder) {
};
}
export function promoteTemporaryStyles() {
return {
type: PROMOTE_TEMPORARY_STYLES
};
}
export function promoteTemporaryLayers() {
return {
type: PROMOTE_TEMPORARY_LAYERS
};
}
export function clearTemporaryStyles() {
return {
type: CLEAR_TEMPORARY_STYLES
};
}
export function clearTemporaryLayers() {
return (dispatch, getState) => {
getLayerListRaw(getState()).forEach(({ temporary, id }) => {
@ -413,8 +435,8 @@ export function updateLayerAlpha(id, alpha) {
export function removeSelectedLayer() {
return (dispatch, getState) => {
const state = getState();
const layer = getSelectedLayer(state);
dispatch(removeLayer(layer.getId()));
const layerId = getSelectedLayerId(state);
dispatch(removeLayer(layerId));
};
}
@ -434,13 +456,6 @@ export function removeLayer(id) {
};
}
export function setMeta(metaJson) {
return {
type: SET_META,
meta: metaJson
};
}
export function setQuery({ query, timeFilters }) {
return async (dispatch, getState) => {
dispatch({
@ -516,11 +531,11 @@ export function updateLayerStyle(layerId, styleDescriptor, temporary = true) {
export function updateLayerStyleForSelectedLayer(styleDescriptor, temporary = true) {
return (dispatch, getState) => {
const selectedLayer = getSelectedLayer(getState());
if (!selectedLayer) {
const selectedLayerId = getSelectedLayerId(getState());
if (!selectedLayerId) {
return;
}
dispatch(updateLayerStyle(selectedLayer.getId(), styleDescriptor, temporary));
dispatch(updateLayerStyle(selectedLayerId, styleDescriptor, temporary));
};
}
@ -536,9 +551,3 @@ export function setJoinsForLayer(layer, joins) {
dispatch(syncDataForLayer(layer.getId()));
};
}
export async function loadMetaResources(dispatch) {
const meta = await fetch(`${GIS_API_RELATIVE}/meta`);
const metaJson = await meta.json();
await dispatch(setMeta(metaJson));
}

View file

@ -18,6 +18,7 @@ import {
getQuery,
} from '../../selectors/map_selectors';
import { convertMapExtentToPolygon } from '../../elasticsearch_geo_utils';
import { copyPersistentState } from '../../store/util';
const module = uiModules.get('app/maps');
@ -86,16 +87,3 @@ module.factory('SavedGisMap', function (Private) {
return SavedGisMap;
});
function copyPersistentState(input) {
if (typeof input !== 'object' && input !== null) {//primitive
return input;
}
const copyInput = Array.isArray(input) ? [] : {};
for(const key in input) {
if (!key.startsWith('__')) {
copyInput[key] = copyPersistentState(input[key]);
}
}
return copyInput;
}

View file

@ -7,8 +7,14 @@
import { connect } from 'react-redux';
import { FlyoutFooter } from './view';
import { updateFlyout, FLYOUT_STATE } from '../../../store/ui';
import { promoteTemporaryStyles, clearTemporaryStyles, clearTemporaryLayers,
setSelectedLayer, removeSelectedLayer, promoteTemporaryLayers } from '../../../actions/store_actions';
import {
clearTemporaryLayers,
setSelectedLayer,
removeSelectedLayer,
promoteTemporaryLayers,
rollbackToTrackedLayerStateForSelectedLayer,
removeTrackedLayerStateForSelectedLayer
} from '../../../actions/store_actions';
import { getSelectedLayer } from '../../../selectors/map_selectors';
const mapStateToProps = state => {
@ -18,19 +24,20 @@ const mapStateToProps = state => {
};
};
const mapDispatchToProps = dispatch => {
const mapDispatchToProps = (dispatch) => {
return {
cancelLayerPanel: () => {
dispatch(updateFlyout(FLYOUT_STATE.NONE));
dispatch(clearTemporaryStyles());
dispatch(clearTemporaryLayers());
cancelLayerPanel: async () => {
await dispatch(updateFlyout(FLYOUT_STATE.NONE));
await dispatch(clearTemporaryLayers());
await dispatch(rollbackToTrackedLayerStateForSelectedLayer());
await dispatch(setSelectedLayer(null));
},
saveLayerEdits: isNewLayer => {
dispatch(updateFlyout(FLYOUT_STATE.NONE));
dispatch(promoteTemporaryStyles());
if (isNewLayer) {
dispatch(promoteTemporaryLayers());
}
dispatch(removeTrackedLayerStateForSelectedLayer());
dispatch(setSelectedLayer(null));
},
removeLayer: () => {

View file

@ -6,14 +6,13 @@
import { connect } from 'react-redux';
import { StyleTabs } from './view';
import { updateLayerStyleForSelectedLayer, clearTemporaryStyles } from '../../../actions/store_actions';
import { updateLayerStyleForSelectedLayer } from '../../../actions/store_actions';
function mapDispatchToProps(dispatch) {
return {
updateStyle: styleDescriptor => {
dispatch(updateLayerStyleForSelectedLayer(styleDescriptor));
},
reset: () => dispatch(clearTemporaryStyles())
}
};
}

View file

@ -13,7 +13,7 @@ import {
EuiText
} from '@elastic/eui';
export function StyleTabs({ layer, reset, updateStyle }) {
export function StyleTabs({ layer, updateStyle }) {
return layer.getSupportedStyles().map((Style, index) => {
let description;
if (Style.description) {
@ -29,8 +29,7 @@ export function StyleTabs({ layer, reset, updateStyle }) {
handleStyleChange: (styleDescriptor) => {
updateStyle(styleDescriptor);
},
style: (Style.canEdit(currentStyle)) ? currentStyle : null,
resetStyle: () => reset()
style: (Style.canEdit(currentStyle)) ? currentStyle : null
});
if (!styleEditor) {

View file

@ -10,9 +10,17 @@ import { TOCEntry } from './view';
import { updateFlyout, FLYOUT_STATE } from '../../../../../store/ui';
import { fitToLayerExtent, setSelectedLayer, toggleLayerVisible } from '../../../../../actions/store_actions';
import { hasDirtyState, getSelectedLayer } from '../../../../../selectors/map_selectors';
function mapStateToProps(state = {}) {
return {
zoom: _.get(state, 'map.mapState.zoom', 0)
zoom: _.get(state, 'map.mapState.zoom', 0),
getSelectedLayerSelector: () => {
return getSelectedLayer(state);
},
hasDirtyStateSelector: () => {
return hasDirtyState(state);
}
};
}

View file

@ -9,15 +9,22 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer
EuiSpacer,
EuiOverlayMask,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
import { LayerTocActions } from '../../../../../shared/components/layer_toc_actions';
export class TOCEntry extends React.Component {
state = {
displayName: null
}
displayName: null,
shouldShowModal: false
};
componentDidMount() {
this._isMounted = true;
@ -43,6 +50,51 @@ export class TOCEntry extends React.Component {
this._updateDisplayName();
}
_renderCancelModal() {
if (!this.state.shouldShowModal) {
return null;
}
const closeModal = () => {
this.setState({
shouldShowModal: false
});
};
const openPanel = () => {
closeModal();
this.props.openLayerPanel(this.props.layer.getId());
};
return (
<EuiOverlayMask>
<EuiModal
onClose={closeModal}
>
<EuiModalBody>
There are unsaved changes to your layer. Are you sure you want to proceed?
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
onClick={closeModal}
>
Do not proceed
</EuiButtonEmpty>
<EuiButton
onClick={openPanel}
fill
>
Proceed and discard changes
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
}
render() {
const { layer, openLayerPanel, zoom, toggleVisible, fitToBounds } = this.props;
@ -69,12 +121,29 @@ export class TOCEntry extends React.Component {
);
}
const cancelModal = this._renderCancelModal();
const openLayerPanelWithCheck = () => {
const selectedLayer = this.props.getSelectedLayerSelector();
if (selectedLayer && selectedLayer.getId() === this.props.layer.getId()) {
return;
}
if (this.props.hasDirtyStateSelector()) {
this.setState({
shouldShowModal: true
});
} else {
openLayerPanel(layer.getId());
}
};
return (
<div
className="mapTocEntry"
id={layer.getId()}
data-layerid={layer.getId()}
>
{cancelModal}
<EuiFlexGroup
gutterSize="none"
alignItems="center"
@ -89,7 +158,7 @@ export class TOCEntry extends React.Component {
</EuiFlexItem>
<EuiFlexItem>
<button
onClick={() => openLayerPanel(layer.getId())}
onClick={openLayerPanelWithCheck}
data-test-subj={`mapOpenLayerButton${this.state.displayName}`}
>
<div style={{ width: 180 }} className="eui-textTruncate eui-textLeft">

View file

@ -15,6 +15,8 @@ import { HeatmapStyle } from '../shared/layers/styles/heatmap_style';
import { TileStyle } from '../shared/layers/styles/tile_style';
import { timefilter } from 'ui/timefilter';
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../store/util';
function createLayerInstance(layerDescriptor) {
const source = createSourceInstance(layerDescriptor.sourceDescriptor);
const style = createStyleInstance(layerDescriptor.style);
@ -63,7 +65,7 @@ export const getMapReady = ({ map }) => map && map.ready;
export const getGoto = ({ map }) => map && map.goto;
const getSelectedLayerId = ({ map }) => {
export const getSelectedLayerId = ({ map }) => {
return (!map.selectedLayerId || !map.layerList) ? null : map.selectedLayerId;
};
@ -161,3 +163,11 @@ export const getUniqueIndexPatternIds = createSelector(
);
export const getTemporaryLayers = createSelector(getLayerList, (layerList) => layerList.filter(layer => layer.isTemporary()));
export const hasDirtyState = createSelector(getLayerListRaw, (layerListRaw) => {
return layerListRaw.some(layerDescriptor => {
const currentState = copyPersistentState(layerDescriptor);
const trackedState = layerDescriptor[TRACKED_LAYER_DESCRIPTOR];
return (trackedState) ? !_.isEqual(currentState, trackedState) : false;
});
});

View file

@ -48,7 +48,7 @@ export class HeatmapLayer extends AbstractLayer {
syncLayerWithMB(mbMap) {
const mbSource = mbMap.getSource(this.getId());
const heatmapLayerId = this.getId() + '_heatmap';
const mbLayerId = this.getId() + '_heatmap';
if (!mbSource) {
mbMap.addSource(this.getId(), {
@ -58,7 +58,7 @@ export class HeatmapLayer extends AbstractLayer {
mbMap.addLayer({
id: heatmapLayerId,
id: mbLayerId,
type: 'heatmap',
source: this.getId(),
paint: {}
@ -86,15 +86,15 @@ export class HeatmapLayer extends AbstractLayer {
mbSourceAfter.setData(featureCollection);
}
mbMap.setLayoutProperty(heatmapLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
this._style.setMBPaintProperties({
mbMap,
layerId: heatmapLayerId,
layerId: mbLayerId,
propertyName: SCALED_PROPERTY_NAME,
alpha: this.getAlpha(),
resolution: this._source.getGridResolution()
});
mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
mbMap.setPaintProperty(mbLayerId, 'heatmap-opacity', this.getAlpha());
mbMap.setLayerZoomRange(mbLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
}
async getBounds(filters) {

View file

@ -41,7 +41,7 @@ export class HeatmapStyle extends AbstractStyle {
return null;
}
setMBPaintProperties({ alpha, mbMap, layerId, propertyName, resolution }) {
setMBPaintProperties({ mbMap, layerId, propertyName, resolution }) {
let radius;
if (resolution === GRID_RESOLUTION.COARSE) {
radius = 64;
@ -57,7 +57,6 @@ export class HeatmapStyle extends AbstractStyle {
type: 'identity',
property: propertyName
});
mbMap.setPaintProperty(layerId, 'heatmap-opacity', alpha);
}
}

View file

@ -31,8 +31,4 @@ export class TileStyle extends AbstractStyle {
static getDisplayName() {
return 'Tile style';
}
setMBPaintProperties({ alpha, mbMap, layerId }) {
mbMap.setPaintProperty(layerId, 'raster-opacity', alpha);
}
}

View file

@ -90,11 +90,7 @@ export class TileLayer extends AbstractLayer {
_setTileLayerProperties(mbMap, mbLayerId) {
mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
mbMap.setLayerZoomRange(mbLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
this._style && this._style.setMBPaintProperties({
alpha: this.getAlpha(),
mbMap,
layerId: mbLayerId,
});
mbMap.setPaintProperty(mbLayerId, 'raster-opacity', this.getAlpha());
}
getLayerTypeIconName() {

View file

@ -475,7 +475,7 @@ export class VectorLayer extends AbstractLayer {
if (!mbSource) {
mbMap.addSource(this.getId(), {
type: 'geojson',
data: { 'type': 'FeatureCollection', 'features': [] }
data: EMPTY_FEATURE_COLLECTION
});
}
}
@ -486,8 +486,8 @@ export class VectorLayer extends AbstractLayer {
this._syncStylePropertiesWithMb(mbMap);
}
renderStyleEditor(style, options) {
return style.renderEditor({
renderStyleEditor(Style, options) {
return Style.renderEditor({
layer: this,
...options
});
@ -497,7 +497,6 @@ export class VectorLayer extends AbstractLayer {
return this._source.canFormatFeatureProperties();
}
async _getPropertiesForTooltip(feature) {
const tooltipsFromSource = await this._source.filterAndFormatProperties(feature.properties);

View file

@ -23,8 +23,6 @@ import {
SET_QUERY,
UPDATE_LAYER_PROP,
UPDATE_LAYER_STYLE,
PROMOTE_TEMPORARY_STYLES,
CLEAR_TEMPORARY_STYLES,
SET_JOINS,
TOUCH_LAYER,
UPDATE_SOURCE_PROP,
@ -34,13 +32,21 @@ import {
CLEAR_MOUSE_COORDINATES,
SET_GOTO,
CLEAR_GOTO,
TRACK_CURRENT_LAYER_STATE,
ROLLBACK_TO_TRACKED_LAYER_STATE,
REMOVE_TRACKED_LAYER_STATE
} from "../actions/store_actions";
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util';
const getLayerIndex = (list, layerId) => list.findIndex(({ id }) => layerId === id);
const updateLayerInList = (state, id, attribute, newValue) => {
const updateLayerInList = (state, layerId, attribute, newValue) => {
if (!layerId) {
return state;
}
const { layerList } = state;
const layerIdx = getLayerIndex(layerList, id);
const layerIdx = getLayerIndex(layerList, layerId);
const updatedLayer = {
...layerList[layerIdx],
// Update layer w/ new value. If no value provided, toggle boolean value
@ -94,8 +100,16 @@ const INITIAL_STATE = {
waitingForMapReadyLayerList: [],
};
export function map(state = INITIAL_STATE, action) {
switch (action.type) {
case REMOVE_TRACKED_LAYER_STATE:
return removeTrackedLayerState(state, action.layerId);
case TRACK_CURRENT_LAYER_STATE:
return trackCurrentLayerState(state, action.layerId);
case ROLLBACK_TO_TRACKED_LAYER_STATE:
return rollbackTrackedLayerState(state, action.layerId);
case SET_MOUSE_COORDINATES:
return {
...state,
@ -216,7 +230,6 @@ export function map(state = INITIAL_STATE, action) {
case UPDATE_SOURCE_PROP:
return updateLayerSourceDescriptorProp(state, action.layerId, action.propName, action.value);
case SET_JOINS:
console.warn('when setting joins, must remove all corresponding datarequests as well');
const layerDescriptor = state.layerList.find(descriptor => descriptor.id === action.layer.getId());
if (layerDescriptor) {
const newLayerDescriptor = { ...layerDescriptor, joins: action.joins.slice() };
@ -263,22 +276,8 @@ export function map(state = INITIAL_STATE, action) {
return updateLayerInList(state, action.layerId, 'visible');
case UPDATE_LAYER_STYLE:
const styleLayerId = action.layerId;
const styleLayerIdx = getLayerIndex(state.layerList, styleLayerId);
const layerStyle = state.layerList[styleLayerIdx].style;
const layerPrevStyle = layerStyle.__previousStyle || layerStyle;
return updateLayerInList(state, styleLayerId, 'style',
{ ...action.style, __previousStyle: { ...layerPrevStyle } });
case PROMOTE_TEMPORARY_STYLES:
const stylePromoteIdx = getLayerIndex(state.layerList, state.selectedLayerId);
const styleToSet = {
...state.layerList[stylePromoteIdx].style,
__previousStyle: null
};
return updateLayerInList(state, state.selectedLayerId, 'style', styleToSet);
case CLEAR_TEMPORARY_STYLES:
const styleClearIdx = getLayerIndex(state.layerList, state.selectedLayerId);
const prevStyleToLoad = state.layerList[styleClearIdx].style.__previousStyle || state.layerList[styleClearIdx].style || {};
return updateLayerInList(state, state.selectedLayerId, 'style', prevStyleToLoad);
{ ...action.style });
default:
return state;
}
@ -359,3 +358,50 @@ function getValidDataRequest(state, action, checkRequestToken = true) {
function findLayerById(state, id) {
return state.layerList.find(layer => layer.id === id);
}
function trackCurrentLayerState(state, layerId) {
const layer = findLayerById(state, layerId);
const layerCopy = copyPersistentState(layer);
return updateLayerInList(state, layerId, TRACKED_LAYER_DESCRIPTOR, layerCopy);
}
function removeTrackedLayerState(state, layerId) {
const layer = findLayerById(state, layerId);
if (!layer) {
return state;
}
const copyLayer = { ...layer };
delete copyLayer[TRACKED_LAYER_DESCRIPTOR];
return {
...state,
layerList: replaceInLayerList(state.layerList, layerId, copyLayer)
};
}
function rollbackTrackedLayerState(state, layerId) {
const layer = findLayerById(state, layerId);
if (!layer) {
return state;
}
const trackedLayerDescriptor = layer[TRACKED_LAYER_DESCRIPTOR];
//this assumes that any nested temp-state in the layer-descriptor (e.g. of styles), is not relevant and can be recovered easily (e.g. this is not the case for __dataRequests)
//That assumption is true in the context of this app, but not generalizable.
//consider rewriting copyPersistentState to only strip the first level of temp state.
const rolledbackLayer = { ...layer, ...trackedLayerDescriptor };
delete rolledbackLayer[TRACKED_LAYER_DESCRIPTOR];
return {
...state,
layerList: replaceInLayerList(state.layerList, layerId, rolledbackLayer)
};
}
function replaceInLayerList(layerList, layerId, newLayerDescriptor) {
const layerIndex = getLayerIndex(layerList, layerId);
const newLayerList = [...layerList];
newLayerList[layerIndex] = newLayerDescriptor;
return newLayerList;
}

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
export const TRACKED_LAYER_DESCRIPTOR = '__trackedLayerDescriptor';
export function copyPersistentState(input) {
if (typeof input !== 'object' || input === null) {//primitive
return input;
}
const copyInput = Array.isArray(input) ? [] : {};
for(const key in input) {
if (!key.startsWith('__')) {
copyInput[key] = copyPersistentState(input[key]);
}
}
return copyInput;
}

View file

@ -0,0 +1,53 @@
/*
* 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 { copyPersistentState } from './util';
describe('store/util', () => {
describe('copyPersistentState', () => {
it('should ignore state preceded by double underscores', async () => {
const copy = copyPersistentState({
foo: 'bar',
nested: {
bar: 'foo',
__bar: 'foo__'
}
});
expect(copy).toEqual({
foo: 'bar',
nested: {
bar: 'foo'
}
});
});
it('should copy null value correctly', async () => {
const copy = copyPersistentState({
foo: 'bar',
nested: {
nullval: null,
bar: 'foo',
__bar: 'foo__'
}
});
expect(copy).toEqual({
foo: 'bar',
nested: {
nullval: null,
bar: 'foo'
}
});
});
});
});