[Maps] show dialog to save map when leaving app (#40215)
This commit is contained in:
parent
3789010d47
commit
6ae5b56df6
|
@ -37,6 +37,33 @@ export function CommonPageProvider({ getService, getPageObjects }) {
|
|||
const defaultFindTimeout = config.get('timeouts.find');
|
||||
|
||||
class CommonPage {
|
||||
|
||||
static async navigateToUrlAndHandleAlert(url, shouldAcceptAlert) {
|
||||
log.debug('Navigate to: ' + url);
|
||||
try {
|
||||
await browser.get(url);
|
||||
} catch(navigationError) {
|
||||
log.debug('Error navigating to url');
|
||||
const alert = await browser.getAlert();
|
||||
if (alert && alert.accept) {
|
||||
if (shouldAcceptAlert) {
|
||||
log.debug('Should accept alert');
|
||||
try {
|
||||
await alert.accept();
|
||||
} catch(alertException) {
|
||||
log.debug('Error accepting alert');
|
||||
throw alertException;
|
||||
}
|
||||
} else {
|
||||
log.debug('Will not accept alert');
|
||||
throw navigationError;
|
||||
}
|
||||
} else {
|
||||
throw navigationError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHostPort() {
|
||||
return getUrl.baseUrl(config.get('servers.kibana'));
|
||||
}
|
||||
|
@ -52,7 +79,8 @@ export function CommonPageProvider({ getService, getPageObjects }) {
|
|||
async navigateToUrl(appName, subUrl, {
|
||||
basePath = '',
|
||||
ensureCurrentUrl = true,
|
||||
shouldLoginIfPrompted = true
|
||||
shouldLoginIfPrompted = true,
|
||||
shouldAcceptAlert = true
|
||||
} = {}) {
|
||||
// we onlt use the pathname from the appConfig and use the subUrl as the hash
|
||||
const appConfig = {
|
||||
|
@ -62,9 +90,7 @@ export function CommonPageProvider({ getService, getPageObjects }) {
|
|||
|
||||
const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig);
|
||||
await retry.try(async () => {
|
||||
log.debug(`navigateToUrl ${appUrl}`);
|
||||
await browser.get(appUrl);
|
||||
|
||||
await CommonPage.navigateToUrlAndHandleAlert(appUrl, shouldAcceptAlert);
|
||||
const currentUrl = shouldLoginIfPrompted ? await this.loginIfPrompted(appUrl) : await browser.getCurrentUrl();
|
||||
|
||||
if (ensureCurrentUrl && !currentUrl.includes(appUrl)) {
|
||||
|
@ -123,7 +149,7 @@ export function CommonPageProvider({ getService, getPageObjects }) {
|
|||
return currentUrl;
|
||||
}
|
||||
|
||||
navigateToApp(appName, { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {}) {
|
||||
navigateToApp(appName, { basePath = '', shouldLoginIfPrompted = true, shouldAcceptAlert = true, hash = '' } = {}) {
|
||||
const self = this;
|
||||
const appConfig = config.get(['apps', appName]);
|
||||
const appUrl = getUrl.noAuth(config.get('servers.kibana'), {
|
||||
|
@ -136,9 +162,8 @@ export function CommonPageProvider({ getService, getPageObjects }) {
|
|||
return retry.try(function () {
|
||||
// since we're using hash URLs, always reload first to force re-render
|
||||
return kibanaServer.uiSettings.getDefaultIndex()
|
||||
.then(function () {
|
||||
log.debug('navigate to: ' + url);
|
||||
return browser.get(url);
|
||||
.then(async function () {
|
||||
return await CommonPage.navigateToUrlAndHandleAlert(url, shouldAcceptAlert);
|
||||
})
|
||||
.then(function () {
|
||||
return self.sleep(700);
|
||||
|
|
|
@ -82,6 +82,18 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
|
|||
: (driver as any).actions({ bridge: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get handle for an alert, confirm, or prompt dialog. (if any).
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async getAlert() {
|
||||
try {
|
||||
return await driver.switchTo().alert();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the a rect describing the current top-level window's size and position.
|
||||
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const GIS_API_PATH = 'api/maps';
|
||||
export const EMS_DATA_FILE_PATH = 'ems/file';
|
||||
export const EMS_DATA_TMS_PATH = 'ems/tms';
|
||||
export const EMS_META_PATH = 'ems/meta';
|
||||
|
@ -15,7 +14,10 @@ export const MAP_SAVED_OBJECT_TYPE = 'map';
|
|||
export const APP_ID = 'maps';
|
||||
export const APP_ICON = 'gisApp';
|
||||
|
||||
export const MAP_BASE_URL = `/app/maps#/map`;
|
||||
export const MAP_APP_PATH = `app/${APP_ID}`;
|
||||
export const GIS_API_PATH = `api/${APP_ID}`;
|
||||
|
||||
export const MAP_BASE_URL = `/${MAP_APP_PATH}#/${MAP_SAVED_OBJECT_TYPE}`;
|
||||
|
||||
export function createMapPath(id) {
|
||||
return `${MAP_BASE_URL}/${id}`;
|
||||
|
|
|
@ -35,7 +35,8 @@ import {
|
|||
setOpenTOCDetails,
|
||||
} from '../actions/ui_actions';
|
||||
import { getIsFullScreen } from '../selectors/ui_selectors';
|
||||
import { getQueryableUniqueIndexPatternIds, hasDirtyState } from '../selectors/map_selectors';
|
||||
import { copyPersistentState } from '../reducers/util';
|
||||
import { getQueryableUniqueIndexPatternIds, hasDirtyState, getLayerListRaw } from '../selectors/map_selectors';
|
||||
import { getInspectorAdapters } from '../reducers/non_serializable_instances';
|
||||
import { Inspector } from 'ui/inspector';
|
||||
import { docTitle } from 'ui/doc_title';
|
||||
|
@ -47,17 +48,21 @@ import { getInitialLayers } from './get_initial_layers';
|
|||
import { getInitialQuery } from './get_initial_query';
|
||||
import { getInitialTimeFilters } from './get_initial_time_filters';
|
||||
import { getInitialRefreshConfig } from './get_initial_refresh_config';
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import {
|
||||
MAP_SAVED_OBJECT_TYPE,
|
||||
MAP_APP_PATH
|
||||
} from '../../common/constants';
|
||||
|
||||
const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-maps-root';
|
||||
|
||||
|
||||
const app = uiModules.get('app/maps', []);
|
||||
const app = uiModules.get(MAP_APP_PATH, []);
|
||||
|
||||
app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage, AppState, globalState) => {
|
||||
|
||||
const savedMap = $route.current.locals.map;
|
||||
let unsubscribe;
|
||||
let initialLayerListConfig;
|
||||
|
||||
const store = createMapStore();
|
||||
|
||||
|
@ -123,6 +128,36 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
|
|||
store.dispatch(setRefreshConfig($scope.refreshConfig));
|
||||
};
|
||||
|
||||
|
||||
function hasUnsavedChanges() {
|
||||
|
||||
const state = store.getState();
|
||||
const layerList = getLayerListRaw(state);
|
||||
const layerListConfigOnly = copyPersistentState(layerList);
|
||||
|
||||
const savedLayerList = savedMap.getLayerList();
|
||||
const oldConfig = savedLayerList ? savedLayerList : initialLayerListConfig;
|
||||
|
||||
return !_.isEqual(layerListConfigOnly, oldConfig);
|
||||
}
|
||||
|
||||
function isOnMapNow() {
|
||||
return window.location.hash.startsWith(`#/${MAP_SAVED_OBJECT_TYPE}`);
|
||||
}
|
||||
|
||||
function beforeUnload(event) {
|
||||
if (!isOnMapNow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanged = hasUnsavedChanges();
|
||||
if (hasChanged) {
|
||||
event.preventDefault();
|
||||
event.returnValue = 'foobar';//this is required for Chrome
|
||||
}
|
||||
}
|
||||
window.addEventListener('beforeunload', beforeUnload);
|
||||
|
||||
function renderMap() {
|
||||
// clear old UI state
|
||||
store.dispatch(setSelectedLayer(null));
|
||||
|
@ -152,8 +187,8 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
|
|||
|
||||
const isDarkMode = config.get('theme:darkMode', false);
|
||||
const layerList = getInitialLayers(savedMap.layerListJSON, isDarkMode);
|
||||
initialLayerListConfig = copyPersistentState(layerList);
|
||||
store.dispatch(replaceLayerList(layerList));
|
||||
|
||||
store.dispatch(setRefreshConfig($scope.refreshConfig));
|
||||
store.dispatch(setQuery({ query: $scope.query, timeFilters: $scope.time }));
|
||||
|
||||
|
@ -215,6 +250,9 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
|
|||
}
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnload);
|
||||
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
|
@ -230,7 +268,18 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
|
|||
text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', {
|
||||
defaultMessage: 'Maps'
|
||||
}),
|
||||
href: '#'
|
||||
onClick: () => {
|
||||
if (isOnMapNow() && hasUnsavedChanges()) {
|
||||
const navigateAway = window.confirm(i18n.translate('xpack.maps.mapController.unsavedChangesWarning', {
|
||||
defaultMessage: `Your unsaved changes might not be saved`,
|
||||
}));
|
||||
if (navigateAway) {
|
||||
window.location.hash = '#';
|
||||
}
|
||||
} else {
|
||||
window.location.hash = '#';
|
||||
}
|
||||
}
|
||||
},
|
||||
{ text: savedMap.title }
|
||||
]);
|
||||
|
|
|
@ -87,6 +87,10 @@ module.factory('SavedGisMap', function (Private) {
|
|||
return `/app/maps#map/${this.id}`;
|
||||
};
|
||||
|
||||
SavedGisMap.prototype.getLayerList = function () {
|
||||
return this.layerListJSON ? JSON.parse(this.layerListJSON) : null;
|
||||
};
|
||||
|
||||
SavedGisMap.prototype.syncWithStore = function (state) {
|
||||
const layerList = getLayerListRaw(state);
|
||||
const layerListConfigOnly = copyPersistentState(layerList);
|
||||
|
|
Loading…
Reference in a new issue