diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.js
index 840d4f2c66922..30b2137399c1e 100644
--- a/x-pack/plugins/maps/public/routing/maps_router.js
+++ b/x-pack/plugins/maps/public/routing/maps_router.js
@@ -17,18 +17,18 @@ import { LoadMapAndRender } from './routes/maps_app/load_map_and_render';
export let goToSpecifiedPath;
export let kbnUrlStateStorage;
-export async function renderApp(context, { appBasePath, element, history }) {
+export async function renderApp(context, { appBasePath, element, history, onAppLeave }) {
goToSpecifiedPath = (path) => history.push(path);
kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history });
- render(, element);
+ render(, element);
return () => {
unmountComponentAtNode(element);
};
}
-const App = ({ history, appBasePath }) => {
+const App = ({ history, appBasePath, onAppLeave }) => {
const store = getStore();
const I18nContext = getCoreI18n().Context;
@@ -37,8 +37,20 @@ const App = ({ history, appBasePath }) => {
-
-
+ (
+
+ )}
+ />
+ }
+ />
// Redirect other routes to list, or if hash-containing, their non-hash equivalents
{
- const isOnMapNow = currentPath.startsWith(`/${MAP_PATH}`);
- const breadCrumbs = isOnMapNow
- ? [
- {
- text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', {
- defaultMessage: 'Maps',
- }),
- onClick: () => {
- if (hasUnsavedChanges(savedMap, initialLayerListConfig)) {
- const navigateAway = window.confirm(
- i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', {
- defaultMessage: `Your unsaved changes might not be saved`,
- })
- );
- if (navigateAway) {
- goToSpecifiedPath('/');
- }
- } else {
- goToSpecifiedPath('/');
- }
- },
- },
- { text: savedMap.title },
- ]
- : [];
- getCoreChrome().setBreadcrumbs(breadCrumbs);
-};
diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js
index 762d61d6d33f9..ac2dec0db59cc 100644
--- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js
+++ b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js
@@ -21,7 +21,6 @@ import {
showSaveModal,
} from '../../../../../../../src/plugins/saved_objects/public';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants';
-import { updateBreadcrumbs } from '../breadcrumbs';
import { goToSpecifiedPath } from '../../maps_router';
export function MapsTopNavMenu({
@@ -35,7 +34,6 @@ export function MapsTopNavMenu({
refreshConfig,
setRefreshConfig,
setRefreshStoreConfig,
- initialLayerListConfig,
indexPatterns,
updateFiltersAndDispatch,
isSaveDisabled,
@@ -44,7 +42,7 @@ export function MapsTopNavMenu({
openMapSettings,
inspectorAdapters,
syncAppAndGlobalState,
- currentPath,
+ setBreadcrumbs,
isOpenSettingsDisabled,
}) {
const { TopNavMenu } = getNavigation().ui;
@@ -64,14 +62,13 @@ export function MapsTopNavMenu({
// Nav settings
const config = getTopNavConfig(
savedMap,
- initialLayerListConfig,
isOpenSettingsDisabled,
isSaveDisabled,
closeFlyout,
enableFullScreen,
openMapSettings,
inspectorAdapters,
- currentPath
+ setBreadcrumbs
);
const submitQuery = function ({ dateRange, query }) {
@@ -121,14 +118,13 @@ export function MapsTopNavMenu({
function getTopNavConfig(
savedMap,
- initialLayerListConfig,
isOpenSettingsDisabled,
isSaveDisabled,
closeFlyout,
enableFullScreen,
openMapSettings,
inspectorAdapters,
- currentPath
+ setBreadcrumbs
) {
return [
{
@@ -210,19 +206,15 @@ function getTopNavConfig(
isTitleDuplicateConfirmed,
onTitleDuplicate,
};
- return doSave(
- savedMap,
- saveOptions,
- initialLayerListConfig,
- closeFlyout,
- currentPath
- ).then((response) => {
- // If the save wasn't successful, put the original values back.
- if (!response.id || response.error) {
- savedMap.title = currentTitle;
+ return doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs).then(
+ (response) => {
+ // If the save wasn't successful, put the original values back.
+ if (!response.id || response.error) {
+ savedMap.title = currentTitle;
+ }
+ return response;
}
- return response;
- });
+ );
};
const saveModal = (
@@ -243,7 +235,7 @@ function getTopNavConfig(
];
}
-async function doSave(savedMap, saveOptions, initialLayerListConfig, closeFlyout, currentPath) {
+async function doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs) {
closeFlyout();
savedMap.syncWithStore();
let id;
@@ -265,7 +257,7 @@ async function doSave(savedMap, saveOptions, initialLayerListConfig, closeFlyout
if (id) {
goToSpecifiedPath(`/map/${id}${window.location.hash}`);
- updateBreadcrumbs(savedMap, initialLayerListConfig, currentPath);
+ setBreadcrumbs();
getToasts().addSuccess({
title: i18n.translate('xpack.maps.mapController.saveSuccessMessage', {
diff --git a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js
index a32bd00dbae51..e9229883d708d 100644
--- a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js
+++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js
@@ -33,7 +33,6 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { addHelpMenuToAppChrome } from '../../../help_menu_util';
import { Link } from 'react-router-dom';
-import { updateBreadcrumbs } from '../../page_elements/breadcrumbs';
import { goToSpecifiedPath } from '../../maps_router';
export const EMPTY_FILTER = '';
@@ -53,17 +52,13 @@ export class MapsListView extends React.Component {
listingLimit: getUiSettings().get('savedObjects:listingLimit'),
};
- UNSAFE_componentWillMount() {
- this._isMounted = true;
- updateBreadcrumbs();
- }
-
componentWillUnmount() {
this._isMounted = false;
this.debouncedFetch.cancel();
}
componentDidMount() {
+ this._isMounted = true;
this.initMapList();
}
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js
index 6b47ac6e0352a..9b0d52e4fe297 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js
@@ -11,6 +11,7 @@ import {
getFilters,
getQueryableUniqueIndexPatternIds,
getRefreshConfig,
+ hasUnsavedChanges,
} from '../../../selectors/map_selectors';
import {
replaceLayerList,
@@ -34,6 +35,9 @@ function mapStateToProps(state = {}) {
flyoutDisplay: getFlyoutDisplay(state),
refreshConfig: getRefreshConfig(state),
filters: getFilters(state),
+ hasUnsavedChanges: (savedMap, initialLayerListConfig) => {
+ return hasUnsavedChanges(state, savedMap, initialLayerListConfig);
+ },
};
}
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js
index a17b83502e048..c87f6eb330531 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js
@@ -27,9 +27,8 @@ export const LoadMapAndRender = class extends React.Component {
}
async _loadSavedMap() {
- const { savedMapId } = this.props.match.params;
try {
- const savedMap = await getMapsSavedObjectLoader().get(savedMapId);
+ const savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId);
if (this._isMounted) {
this.setState({ savedMap });
}
@@ -48,11 +47,11 @@ export const LoadMapAndRender = class extends React.Component {
render() {
const { savedMap, failedToLoad } = this.state;
+
if (failedToLoad) {
return ;
}
- const currentPath = this.props.match.url;
- return savedMap ? : null;
+ return savedMap ? : null;
}
};
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js
index bf92f5a337121..29fbb5f46e29b 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js
@@ -30,12 +30,15 @@ import {
} from '../../state_syncing/global_sync';
import { AppStateManager } from '../../state_syncing/app_state_manager';
import { useAppStateSyncing } from '../../state_syncing/app_sync';
-import { updateBreadcrumbs } from '../../page_elements/breadcrumbs';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { GisMap } from '../../../connected_components/gis_map';
+import { goToSpecifiedPath } from '../../maps_router';
+
+const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', {
+ defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?',
+});
export class MapsAppView extends React.Component {
- _visibleSubscription = null;
_globalSyncUnsubscribe = null;
_globalSyncChangeMonitorSubscription = null;
_appSyncUnsubscribe = null;
@@ -47,16 +50,13 @@ export class MapsAppView extends React.Component {
indexPatterns: [],
prevIndexPatternIds: [],
initialized: false,
- isVisible: true,
savedQuery: '',
- currentPath: '',
initialLayerListConfig: null,
};
}
componentDidMount() {
- const { savedMap, currentPath } = this.props;
- this.setState({ currentPath });
+ const { savedMap } = this.props;
getCoreChrome().docTitle.change(savedMap.title);
getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id);
@@ -77,48 +77,24 @@ export class MapsAppView extends React.Component {
this._updateStateFromSavedQuery(initAppState.savedQuery);
}
- // Monitor visibility
- this._visibleSubscription = getCoreChrome()
- .getIsVisible$()
- .subscribe((isVisible) => this.setState({ isVisible }));
this._initMap();
- }
- _initBreadcrumbUpdater = () => {
- const { initialLayerListConfig, currentPath } = this.state;
- updateBreadcrumbs(this.props.savedMap, initialLayerListConfig, currentPath);
- };
+ this._setBreadcrumbs();
- componentDidUpdate(prevProps, prevState) {
- const { currentPath: prevCurrentPath } = prevState;
- const { currentPath, initialLayerListConfig } = this.state;
- const { savedMap } = this.props;
- if (savedMap && initialLayerListConfig && currentPath !== prevCurrentPath) {
- updateBreadcrumbs(savedMap, initialLayerListConfig, currentPath);
- }
- // TODO: Handle null when converting to TS
- this._handleStoreChanges();
- }
-
- _updateFromGlobalState = ({ changes, state: globalState }) => {
- if (!changes || !globalState) {
- return;
- }
- const newState = {};
- Object.keys(changes).forEach((key) => {
- if (changes[key]) {
- newState[key] = globalState[key];
+ this.props.onAppLeave((actions) => {
+ if (this._hasUnsavedChanges()) {
+ if (!window.confirm(unsavedChangesWarning)) {
+ return;
+ }
}
+ return actions.default();
});
+ }
- this.setState(newState, () => {
- this._appStateManager.setQueryAndFilters({
- filters: getData().query.filterManager.getAppFilters(),
- });
- const { time, filters, refreshInterval } = globalState;
- this.props.dispatchSetQuery(refreshInterval, filters, this.state.query, time);
- });
- };
+ componentDidUpdate() {
+ // TODO: Handle null when converting to TS
+ this._handleStoreChanges();
+ }
componentWillUnmount() {
if (this._globalSyncUnsubscribe) {
@@ -127,9 +103,6 @@ export class MapsAppView extends React.Component {
if (this._appSyncUnsubscribe) {
this._appSyncUnsubscribe();
}
- if (this._visibleSubscription) {
- this._visibleSubscription.unsubscribe();
- }
if (this._globalSyncChangeMonitorSubscription) {
this._globalSyncChangeMonitorSubscription.unsubscribe();
}
@@ -141,8 +114,55 @@ export class MapsAppView extends React.Component {
filterManager.removeFilter(filter);
}
});
+
+ getCoreChrome().setBreadcrumbs([]);
+ }
+
+ _hasUnsavedChanges() {
+ return this.props.hasUnsavedChanges(this.props.savedMap, this.state.initialLayerListConfig);
}
+ _setBreadcrumbs = () => {
+ getCoreChrome().setBreadcrumbs([
+ {
+ text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', {
+ defaultMessage: 'Maps',
+ }),
+ onClick: () => {
+ if (this._hasUnsavedChanges()) {
+ const navigateAway = window.confirm(unsavedChangesWarning);
+ if (navigateAway) {
+ goToSpecifiedPath('/');
+ }
+ } else {
+ goToSpecifiedPath('/');
+ }
+ },
+ },
+ { text: this.props.savedMap.title },
+ ]);
+ };
+
+ _updateFromGlobalState = ({ changes, state: globalState }) => {
+ if (!changes || !globalState) {
+ return;
+ }
+ const newState = {};
+ Object.keys(changes).forEach((key) => {
+ if (changes[key]) {
+ newState[key] = globalState[key];
+ }
+ });
+
+ this.setState(newState, () => {
+ this._appStateManager.setQueryAndFilters({
+ filters: getData().query.filterManager.getAppFilters(),
+ });
+ const { time, filters, refreshInterval } = globalState;
+ this.props.dispatchSetQuery(refreshInterval, filters, this.state.query, time);
+ });
+ };
+
_getInitialLayersFromUrlParam() {
const locationSplit = window.location.href.split('?');
if (locationSplit.length <= 1) {
@@ -301,13 +321,9 @@ export class MapsAppView extends React.Component {
this._getInitialLayersFromUrlParam()
);
this.props.replaceLayerList(layerList);
- this.setState(
- {
- initialLayerListConfig: copyPersistentState(layerList),
- savedMap,
- },
- this._initBreadcrumbUpdater
- );
+ this.setState({
+ initialLayerListConfig: copyPersistentState(layerList),
+ });
}
_updateFiltersAndDispatch = (filters) => {
@@ -407,18 +423,10 @@ export class MapsAppView extends React.Component {
}
_renderTopNav() {
- const {
- query,
- time,
- savedQuery,
- initialLayerListConfig,
- isVisible,
- indexPatterns,
- currentPath,
- } = this.state;
- const { savedMap, refreshConfig } = this.props;
+ const { query, time, savedQuery, indexPatterns } = this.state;
+ const { savedMap, refreshConfig, isFullScreen } = this.props;
- return isVisible ? (
+ return !isFullScreen ? (
{
@@ -448,7 +455,7 @@ export class MapsAppView extends React.Component {
this._updateStateFromSavedQuery(query);
}}
syncAppAndGlobalState={this._syncAppAndGlobalState}
- currentPath={currentPath}
+ setBreadcrumbs={this._setBreadcrumbs}
/>
) : null;
}
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts
index f400e242b697f..fe2cfec3c761c 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.ts
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts
@@ -416,3 +416,23 @@ export const areLayersLoaded = createSelector(
return true;
}
);
+
+export function hasUnsavedChanges(
+ state: MapStoreState,
+ savedMap: unknown,
+ initialLayerListConfig: LayerDescriptor[]
+) {
+ const layerListConfigOnly = copyPersistentState(getLayerListRaw(state));
+
+ // @ts-expect-error
+ const savedLayerList = savedMap.getLayerList();
+
+ return !savedLayerList
+ ? !_.isEqual(layerListConfigOnly, initialLayerListConfig)
+ : // savedMap stores layerList as a JSON string using JSON.stringify.
+ // JSON.stringify removes undefined properties from objects.
+ // savedMap.getLayerList converts the JSON string back into Javascript array of objects.
+ // Need to perform the same process for layerListConfigOnly to compare apples to apples
+ // and avoid undefined properties in layerListConfigOnly triggering unsaved changes.
+ !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), savedLayerList);
+}