mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* add onAppLeave to AppMountParameters * adapt legacy shims of app mount * update generated doc * returns properly typed AppLeaveAction from leave handler instead of raw strings * add openConfirm to modal service and use it instead of window.confirm * fix unit test * update querystringinput snapshots * add integration tests * nits and review comments * add functional tests
This commit is contained in:
parent
f6185106a8
commit
6f9f68c13b
53 changed files with 2535 additions and 1169 deletions
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveAction](./kibana-plugin-public.appleaveaction.md)
|
||||
|
||||
## AppLeaveAction type
|
||||
|
||||
Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)
|
||||
|
||||
See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction;
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md)
|
||||
|
||||
## AppLeaveActionType enum
|
||||
|
||||
Possible type of actions on application leave.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare enum AppLeaveActionType
|
||||
```
|
||||
|
||||
## Enumeration Members
|
||||
|
||||
| Member | Value | Description |
|
||||
| --- | --- | --- |
|
||||
| confirm | <code>"confirm"</code> | |
|
||||
| default | <code>"default"</code> | |
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md)
|
||||
|
||||
## AppLeaveConfirmAction interface
|
||||
|
||||
Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.
|
||||
|
||||
See
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface AppLeaveConfirmAction
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [text](./kibana-plugin-public.appleaveconfirmaction.text.md) | <code>string</code> | |
|
||||
| [title](./kibana-plugin-public.appleaveconfirmaction.title.md) | <code>string</code> | |
|
||||
| [type](./kibana-plugin-public.appleaveconfirmaction.type.md) | <code>AppLeaveActionType.confirm</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) > [text](./kibana-plugin-public.appleaveconfirmaction.text.md)
|
||||
|
||||
## AppLeaveConfirmAction.text property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
text: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) > [title](./kibana-plugin-public.appleaveconfirmaction.title.md)
|
||||
|
||||
## AppLeaveConfirmAction.title property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
title?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) > [type](./kibana-plugin-public.appleaveconfirmaction.type.md)
|
||||
|
||||
## AppLeaveConfirmAction.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: AppLeaveActionType.confirm;
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md)
|
||||
|
||||
## AppLeaveDefaultAction interface
|
||||
|
||||
Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.
|
||||
|
||||
See
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface AppLeaveDefaultAction
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [type](./kibana-plugin-public.appleavedefaultaction.type.md) | <code>AppLeaveActionType.default</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) > [type](./kibana-plugin-public.appleavedefaultaction.type.md)
|
||||
|
||||
## AppLeaveDefaultAction.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: AppLeaveActionType.default;
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)
|
||||
|
||||
## AppLeaveHandler type
|
||||
|
||||
A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return `confirm` to to prompt a message to the user before leaving the page, or `default` to keep the default behavior (doing nothing).
|
||||
|
||||
See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction;
|
||||
```
|
|
@ -22,6 +22,6 @@ export interface ApplicationStart
|
|||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. |
|
||||
| [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigiate to a given app |
|
||||
| [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigate to a given app |
|
||||
| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->. |
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## ApplicationStart.navigateToApp() method
|
||||
|
||||
Navigiate to a given app
|
||||
Navigate to a given app
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
@ -12,7 +12,7 @@ Navigiate to a given app
|
|||
navigateToApp(appId: string, options?: {
|
||||
path?: string;
|
||||
state?: any;
|
||||
}): void;
|
||||
}): Promise<void>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -24,5 +24,5 @@ navigateToApp(appId: string, options?: {
|
|||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
`Promise<void>`
|
||||
|
||||
|
|
|
@ -17,4 +17,5 @@ export interface AppMountParameters
|
|||
| --- | --- | --- |
|
||||
| [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md) | <code>string</code> | The route path for configuring navigation to the application. This string should not include the base path from HTTP. |
|
||||
| [element](./kibana-plugin-public.appmountparameters.element.md) | <code>HTMLElement</code> | The container element to render the application into. |
|
||||
| [onAppLeave](./kibana-plugin-public.appmountparameters.onappleave.md) | <code>(handler: AppLeaveHandler) => void</code> | A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.<!-- -->This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. |
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [onAppLeave](./kibana-plugin-public.appmountparameters.onappleave.md)
|
||||
|
||||
## AppMountParameters.onAppLeave property
|
||||
|
||||
A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.
|
||||
|
||||
This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
```ts
|
||||
// application.tsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
|
||||
import { CoreStart, AppMountParams } from 'src/core/public';
|
||||
import { MyPluginDepsStart } from './plugin';
|
||||
|
||||
export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => {
|
||||
const { renderApp, hasUnsavedChanges } = await import('./application');
|
||||
onAppLeave(actions => {
|
||||
if(hasUnsavedChanges()) {
|
||||
return actions.confirm('Some changes were not saved. Are you sure you want to leave?');
|
||||
}
|
||||
return actions.default();
|
||||
});
|
||||
return renderApp(params);
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -24,7 +24,7 @@ export interface ChromeNavLink
|
|||
| [id](./kibana-plugin-public.chromenavlink.id.md) | <code>string</code> | A unique identifier for looking up links. |
|
||||
| [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) | <code>boolean</code> | Whether or not the subUrl feature should be enabled. |
|
||||
| [order](./kibana-plugin-public.chromenavlink.order.md) | <code>number</code> | An ordinal used to sort nav links relative to one another for display. |
|
||||
| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | <code>string</code> | A url base that legacy apps can set to match deep URLs to an applcation. |
|
||||
| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | <code>string</code> | A url base that legacy apps can set to match deep URLs to an application. |
|
||||
| [title](./kibana-plugin-public.chromenavlink.title.md) | <code>string</code> | The title of the application. |
|
||||
| [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) | <code>string</code> | A tooltip shown when hovering over an app link. |
|
||||
| [url](./kibana-plugin-public.chromenavlink.url.md) | <code>string</code> | A url that legacy apps can set to deep link into their applications. |
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
>
|
||||
|
||||
A url base that legacy apps can set to match deep URLs to an applcation.
|
||||
A url base that legacy apps can set to match deep URLs to an application.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -18,12 +18,20 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md)<!-- -->.<!-- -->It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. |
|
||||
| [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. |
|
||||
|
||||
## Enumerations
|
||||
|
||||
| Enumeration | Description |
|
||||
| --- | --- |
|
||||
| [AppLeaveActionType](./kibana-plugin-public.appleaveactiontype.md) | Possible type of actions on application leave. |
|
||||
|
||||
## Interfaces
|
||||
|
||||
| Interface | Description |
|
||||
| --- | --- |
|
||||
| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. |
|
||||
| [AppBase](./kibana-plugin-public.appbase.md) | |
|
||||
| [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.<!-- -->See |
|
||||
| [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.<!-- -->See |
|
||||
| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |
|
||||
| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | |
|
||||
| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->. |
|
||||
|
@ -105,6 +113,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
|
||||
| Type Alias | Description |
|
||||
| --- | --- |
|
||||
| [AppLeaveAction](./kibana-plugin-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md)<!-- -->See [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) |
|
||||
| [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return <code>confirm</code> to to prompt a message to the user before leaving the page, or <code>default</code> to keep the default behavior (doing nothing).<!-- -->See [AppMountParameters](./kibana-plugin-public.appmountparameters.md) for detailed usage examples. |
|
||||
| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. |
|
||||
| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. |
|
||||
| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. |
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface OverlayStart
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [banners](./kibana-plugin-public.overlaystart.banners.md) | <code>OverlayBannersStart</code> | [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) |
|
||||
| [openConfirm](./kibana-plugin-public.overlaystart.openconfirm.md) | <code>OverlayModalStart['openConfirm']</code> | |
|
||||
| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | <code>OverlayFlyoutStart['open']</code> | |
|
||||
| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | <code>OverlayModalStart['open']</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayStart](./kibana-plugin-public.overlaystart.md) > [openConfirm](./kibana-plugin-public.overlaystart.openconfirm.md)
|
||||
|
||||
## OverlayStart.openConfirm property
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
openConfirm: OverlayModalStart['openConfirm'];
|
||||
```
|
49
src/core/public/application/application_leave.test.ts
Normal file
49
src/core/public/application/application_leave.test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { isConfirmAction, getLeaveAction } from './application_leave';
|
||||
import { AppLeaveActionType } from './types';
|
||||
|
||||
describe('isConfirmAction', () => {
|
||||
it('returns true if action is confirm', () => {
|
||||
expect(isConfirmAction({ type: AppLeaveActionType.confirm, text: 'message' })).toEqual(true);
|
||||
});
|
||||
it('returns false if action is default', () => {
|
||||
expect(isConfirmAction({ type: AppLeaveActionType.default })).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeaveAction', () => {
|
||||
it('returns the default action provided by the handler', () => {
|
||||
expect(getLeaveAction(actions => actions.default())).toEqual({
|
||||
type: AppLeaveActionType.default,
|
||||
});
|
||||
});
|
||||
it('returns the confirm action provided by the handler', () => {
|
||||
expect(getLeaveAction(actions => actions.confirm('some message'))).toEqual({
|
||||
type: AppLeaveActionType.confirm,
|
||||
text: 'some message',
|
||||
});
|
||||
expect(getLeaveAction(actions => actions.confirm('another message', 'a title'))).toEqual({
|
||||
type: AppLeaveActionType.confirm,
|
||||
text: 'another message',
|
||||
title: 'a title',
|
||||
});
|
||||
});
|
||||
});
|
46
src/core/public/application/application_leave.tsx
Normal file
46
src/core/public/application/application_leave.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AppLeaveActionFactory,
|
||||
AppLeaveActionType,
|
||||
AppLeaveAction,
|
||||
AppLeaveConfirmAction,
|
||||
AppLeaveHandler,
|
||||
} from './types';
|
||||
|
||||
const appLeaveActionFactory: AppLeaveActionFactory = {
|
||||
confirm(text: string, title?: string) {
|
||||
return { type: AppLeaveActionType.confirm, text, title };
|
||||
},
|
||||
default() {
|
||||
return { type: AppLeaveActionType.default };
|
||||
},
|
||||
};
|
||||
|
||||
export function isConfirmAction(action: AppLeaveAction): action is AppLeaveConfirmAction {
|
||||
return action.type === AppLeaveActionType.confirm;
|
||||
}
|
||||
|
||||
export function getLeaveAction(handler?: AppLeaveHandler): AppLeaveAction {
|
||||
if (!handler) {
|
||||
return appLeaveActionFactory.default();
|
||||
}
|
||||
return handler(appLeaveActionFactory);
|
||||
}
|
|
@ -25,17 +25,18 @@ import { shallow } from 'enzyme';
|
|||
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
|
||||
import { contextServiceMock } from '../context/context_service.mock';
|
||||
import { httpServiceMock } from '../http/http_service.mock';
|
||||
import { overlayServiceMock } from '../overlays/overlay_service.mock';
|
||||
import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks';
|
||||
import { MockLifecycle } from './test_types';
|
||||
import { ApplicationService } from './application_service';
|
||||
|
||||
function mount() {}
|
||||
|
||||
describe('#setup()', () => {
|
||||
let setupDeps: MockLifecycle<'setup'>;
|
||||
let startDeps: MockLifecycle<'start'>;
|
||||
let service: ApplicationService;
|
||||
let setupDeps: MockLifecycle<'setup'>;
|
||||
let startDeps: MockLifecycle<'start'>;
|
||||
let service: ApplicationService;
|
||||
|
||||
describe('#setup()', () => {
|
||||
beforeEach(() => {
|
||||
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
|
||||
setupDeps = {
|
||||
|
@ -44,7 +45,7 @@ describe('#setup()', () => {
|
|||
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
|
||||
};
|
||||
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
|
||||
startDeps = { http, injectedMetadata: setupDeps.injectedMetadata };
|
||||
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
||||
|
@ -146,12 +147,9 @@ describe('#setup()', () => {
|
|||
});
|
||||
|
||||
describe('#start()', () => {
|
||||
let setupDeps: MockLifecycle<'setup'>;
|
||||
let startDeps: MockLifecycle<'start'>;
|
||||
let service: ApplicationService;
|
||||
|
||||
beforeEach(() => {
|
||||
MockHistory.push.mockReset();
|
||||
|
||||
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
|
||||
setupDeps = {
|
||||
http,
|
||||
|
@ -159,7 +157,7 @@ describe('#start()', () => {
|
|||
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
|
||||
};
|
||||
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
|
||||
startDeps = { http, injectedMetadata: setupDeps.injectedMetadata };
|
||||
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
||||
|
@ -264,6 +262,7 @@ describe('#start()', () => {
|
|||
}
|
||||
}
|
||||
mounters={Map {}}
|
||||
setAppLeaveHandler={[Function]}
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
@ -320,10 +319,10 @@ describe('#start()', () => {
|
|||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
navigateToApp('myTestApp');
|
||||
await navigateToApp('myTestApp');
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined);
|
||||
|
||||
navigateToApp('myOtherApp');
|
||||
await navigateToApp('myOtherApp');
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined);
|
||||
});
|
||||
|
||||
|
@ -334,10 +333,10 @@ describe('#start()', () => {
|
|||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
navigateToApp('myTestApp');
|
||||
await navigateToApp('myTestApp');
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined);
|
||||
|
||||
navigateToApp('app2');
|
||||
await navigateToApp('app2');
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/custom/path', undefined);
|
||||
});
|
||||
|
||||
|
@ -348,13 +347,13 @@ describe('#start()', () => {
|
|||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
navigateToApp('myTestApp', { path: 'deep/link/to/location/2' });
|
||||
await navigateToApp('myTestApp', { path: 'deep/link/to/location/2' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith(
|
||||
'/app/myTestApp/deep/link/to/location/2',
|
||||
undefined
|
||||
);
|
||||
|
||||
navigateToApp('app2', { path: 'deep/link/to/location/2' });
|
||||
await navigateToApp('app2', { path: 'deep/link/to/location/2' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith(
|
||||
'/custom/path/deep/link/to/location/2',
|
||||
undefined
|
||||
|
@ -368,10 +367,10 @@ describe('#start()', () => {
|
|||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
navigateToApp('myTestApp', { state: 'my-state' });
|
||||
await navigateToApp('myTestApp', { state: 'my-state' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', 'my-state');
|
||||
|
||||
navigateToApp('app2', { state: 'my-state' });
|
||||
await navigateToApp('app2', { state: 'my-state' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/custom/path', 'my-state');
|
||||
});
|
||||
|
||||
|
@ -382,7 +381,7 @@ describe('#start()', () => {
|
|||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
navigateToApp('myTestApp');
|
||||
await navigateToApp('myTestApp');
|
||||
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/myTestApp');
|
||||
});
|
||||
|
||||
|
@ -439,3 +438,39 @@ describe('#start()', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop()', () => {
|
||||
let addListenerSpy: jest.SpyInstance;
|
||||
let removeListenerSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
addListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
removeListenerSpy = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
MockHistory.push.mockReset();
|
||||
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
|
||||
setupDeps = {
|
||||
http,
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
|
||||
};
|
||||
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
|
||||
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('removes the beforeunload listener', async () => {
|
||||
service.setup(setupDeps);
|
||||
await service.start(startDeps);
|
||||
expect(addListenerSpy).toHaveBeenCalledTimes(1);
|
||||
expect(addListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
|
||||
const handler = addListenerSpy.mock.calls[0][1];
|
||||
service.stop();
|
||||
expect(removeListenerSpy).toHaveBeenCalledTimes(1);
|
||||
expect(removeListenerSpy).toHaveBeenCalledWith('beforeunload', handler);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,13 +22,15 @@ import { BehaviorSubject, Subject } from 'rxjs';
|
|||
import { takeUntil } from 'rxjs/operators';
|
||||
import { createBrowserHistory, History } from 'history';
|
||||
|
||||
import { InjectedMetadataSetup, InjectedMetadataStart } from '../injected_metadata';
|
||||
import { InjectedMetadataSetup } from '../injected_metadata';
|
||||
import { HttpSetup, HttpStart } from '../http';
|
||||
import { OverlayStart } from '../overlays';
|
||||
import { ContextSetup, IContextContainer } from '../context';
|
||||
import { AppRouter } from './ui';
|
||||
import { CapabilitiesService, Capabilities } from './capabilities';
|
||||
import {
|
||||
App,
|
||||
AppLeaveHandler,
|
||||
LegacyApp,
|
||||
AppMount,
|
||||
AppMountDeprecated,
|
||||
|
@ -38,11 +40,13 @@ import {
|
|||
InternalApplicationSetup,
|
||||
InternalApplicationStart,
|
||||
} from './types';
|
||||
import { getLeaveAction, isConfirmAction } from './application_leave';
|
||||
|
||||
interface SetupDeps {
|
||||
context: ContextSetup;
|
||||
http: HttpSetup;
|
||||
injectedMetadata: InjectedMetadataSetup;
|
||||
history?: History<any>;
|
||||
/**
|
||||
* Only necessary for redirecting to legacy apps
|
||||
* @deprecated
|
||||
|
@ -51,8 +55,8 @@ interface SetupDeps {
|
|||
}
|
||||
|
||||
interface StartDeps {
|
||||
injectedMetadata: InjectedMetadataStart;
|
||||
http: HttpStart;
|
||||
overlays: OverlayStart;
|
||||
}
|
||||
|
||||
// Mount functions with two arguments are assumed to expect deprecated `context` object.
|
||||
|
@ -80,6 +84,7 @@ export class ApplicationService {
|
|||
private readonly legacyApps = new Map<string, LegacyApp>();
|
||||
private readonly mounters = new Map<string, Mounter>();
|
||||
private readonly capabilities = new CapabilitiesService();
|
||||
private readonly appLeaveHandlers = new Map<string, AppLeaveHandler>();
|
||||
private currentAppId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
private stop$ = new Subject();
|
||||
private registrationClosed = false;
|
||||
|
@ -92,11 +97,12 @@ export class ApplicationService {
|
|||
http: { basePath },
|
||||
injectedMetadata,
|
||||
redirectTo = (path: string) => (window.location.href = path),
|
||||
history,
|
||||
}: SetupDeps): InternalApplicationSetup {
|
||||
const basename = basePath.get();
|
||||
// Only setup history if we're not in legacy mode
|
||||
if (!injectedMetadata.getLegacyMode()) {
|
||||
this.history = createBrowserHistory({ basename });
|
||||
this.history = history || createBrowserHistory({ basename });
|
||||
}
|
||||
|
||||
// If we do not have history available, use redirectTo to do a full page refresh.
|
||||
|
@ -171,12 +177,14 @@ export class ApplicationService {
|
|||
};
|
||||
}
|
||||
|
||||
public async start({ injectedMetadata, http }: StartDeps): Promise<InternalApplicationStart> {
|
||||
public async start({ http, overlays }: StartDeps): Promise<InternalApplicationStart> {
|
||||
if (!this.mountContext) {
|
||||
throw new Error('ApplicationService#setup() must be invoked before start.');
|
||||
}
|
||||
|
||||
this.registrationClosed = true;
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload);
|
||||
|
||||
const { capabilities } = await this.capabilities.start({
|
||||
appIds: [...this.mounters.keys()],
|
||||
http,
|
||||
|
@ -191,17 +199,66 @@ export class ApplicationService {
|
|||
registerMountContext: this.mountContext.registerContext,
|
||||
getUrlForApp: (appId, { path }: { path?: string } = {}) =>
|
||||
getAppUrl(availableMounters, appId, path),
|
||||
navigateToApp: (appId, { path, state }: { path?: string; state?: any } = {}) => {
|
||||
this.navigate!(getAppUrl(availableMounters, appId, path), state);
|
||||
this.currentAppId$.next(appId);
|
||||
navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => {
|
||||
if (await this.shouldNavigate(overlays)) {
|
||||
this.appLeaveHandlers.delete(this.currentAppId$.value!);
|
||||
this.navigate!(getAppUrl(availableMounters, appId, path), state);
|
||||
this.currentAppId$.next(appId);
|
||||
}
|
||||
},
|
||||
getComponent: () => {
|
||||
if (!this.history) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AppRouter
|
||||
history={this.history}
|
||||
mounters={availableMounters}
|
||||
setAppLeaveHandler={this.setAppLeaveHandler}
|
||||
/>
|
||||
);
|
||||
},
|
||||
getComponent: () =>
|
||||
this.history ? <AppRouter history={this.history} mounters={availableMounters} /> : null,
|
||||
};
|
||||
}
|
||||
|
||||
private setAppLeaveHandler = (appId: string, handler: AppLeaveHandler) => {
|
||||
this.appLeaveHandlers.set(appId, handler);
|
||||
};
|
||||
|
||||
private async shouldNavigate(overlays: OverlayStart): Promise<boolean> {
|
||||
const currentAppId = this.currentAppId$.value;
|
||||
if (currentAppId === undefined) {
|
||||
return true;
|
||||
}
|
||||
const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId));
|
||||
if (isConfirmAction(action)) {
|
||||
const confirmed = await overlays.openConfirm(action.text, {
|
||||
title: action.title,
|
||||
'data-test-subj': 'appLeaveConfirmModal',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private onBeforeUnload = (event: Event) => {
|
||||
const currentAppId = this.currentAppId$.value;
|
||||
if (currentAppId === undefined) {
|
||||
return;
|
||||
}
|
||||
const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId));
|
||||
if (isConfirmAction(action)) {
|
||||
event.preventDefault();
|
||||
// some browsers accept a string return value being the message displayed
|
||||
event.returnValue = action.text as any;
|
||||
}
|
||||
};
|
||||
|
||||
public stop() {
|
||||
this.stop$.next();
|
||||
this.currentAppId$.complete();
|
||||
window.removeEventListener('beforeunload', this.onBeforeUnload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@ export {
|
|||
AppMountParameters,
|
||||
ApplicationSetup,
|
||||
ApplicationStart,
|
||||
AppLeaveHandler,
|
||||
AppLeaveActionType,
|
||||
AppLeaveAction,
|
||||
AppLeaveDefaultAction,
|
||||
AppLeaveConfirmAction,
|
||||
// Internal types
|
||||
InternalApplicationStart,
|
||||
LegacyApp,
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createRenderer } from './utils';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import { ApplicationService } from '../application_service';
|
||||
import { httpServiceMock } from '../../http/http_service.mock';
|
||||
import { contextServiceMock } from '../../context/context_service.mock';
|
||||
import { injectedMetadataServiceMock } from '../../injected_metadata/injected_metadata_service.mock';
|
||||
import { MockLifecycle } from '../test_types';
|
||||
import { overlayServiceMock } from '../../overlays/overlay_service.mock';
|
||||
import { AppMountParameters } from '../types';
|
||||
|
||||
describe('ApplicationService', () => {
|
||||
let setupDeps: MockLifecycle<'setup'>;
|
||||
let startDeps: MockLifecycle<'start'>;
|
||||
let service: ApplicationService;
|
||||
let history: MemoryHistory<any>;
|
||||
let update: ReturnType<typeof createRenderer>;
|
||||
|
||||
const navigate = (path: string) => {
|
||||
history.push(path);
|
||||
return update();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory();
|
||||
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
|
||||
|
||||
http.post.mockResolvedValue({ navLinks: {} });
|
||||
|
||||
setupDeps = {
|
||||
http,
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
|
||||
history: history as any,
|
||||
};
|
||||
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
|
||||
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
||||
describe('leaving an application that registered an app leave handler', () => {
|
||||
it('navigates to the new app if action is default', async () => {
|
||||
startDeps.overlays.openConfirm.mockResolvedValue(true);
|
||||
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(Symbol(), {
|
||||
id: 'app1',
|
||||
title: 'App1',
|
||||
mount: ({ onAppLeave }: AppMountParameters) => {
|
||||
onAppLeave(actions => actions.default());
|
||||
return () => undefined;
|
||||
},
|
||||
});
|
||||
register(Symbol(), {
|
||||
id: 'app2',
|
||||
title: 'App2',
|
||||
mount: ({}: AppMountParameters) => {
|
||||
return () => undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const { navigateToApp, getComponent } = await service.start(startDeps);
|
||||
|
||||
update = createRenderer(getComponent());
|
||||
|
||||
await navigate('/app/app1');
|
||||
await navigateToApp('app2');
|
||||
|
||||
expect(startDeps.overlays.openConfirm).not.toHaveBeenCalled();
|
||||
expect(history.entries.length).toEqual(3);
|
||||
expect(history.entries[2].pathname).toEqual('/app/app2');
|
||||
});
|
||||
|
||||
it('navigates to the new app if action is confirm and user accepted', async () => {
|
||||
startDeps.overlays.openConfirm.mockResolvedValue(true);
|
||||
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(Symbol(), {
|
||||
id: 'app1',
|
||||
title: 'App1',
|
||||
mount: ({ onAppLeave }: AppMountParameters) => {
|
||||
onAppLeave(actions => actions.confirm('confirmation-message', 'confirmation-title'));
|
||||
return () => undefined;
|
||||
},
|
||||
});
|
||||
register(Symbol(), {
|
||||
id: 'app2',
|
||||
title: 'App2',
|
||||
mount: ({}: AppMountParameters) => {
|
||||
return () => undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const { navigateToApp, getComponent } = await service.start(startDeps);
|
||||
|
||||
update = createRenderer(getComponent());
|
||||
|
||||
await navigate('/app/app1');
|
||||
await navigateToApp('app2');
|
||||
|
||||
expect(startDeps.overlays.openConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(startDeps.overlays.openConfirm).toHaveBeenCalledWith(
|
||||
'confirmation-message',
|
||||
expect.objectContaining({ title: 'confirmation-title' })
|
||||
);
|
||||
expect(history.entries.length).toEqual(3);
|
||||
expect(history.entries[2].pathname).toEqual('/app/app2');
|
||||
});
|
||||
|
||||
it('blocks navigation to the new app if action is confirm and user declined', async () => {
|
||||
startDeps.overlays.openConfirm.mockResolvedValue(false);
|
||||
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(Symbol(), {
|
||||
id: 'app1',
|
||||
title: 'App1',
|
||||
mount: ({ onAppLeave }: AppMountParameters) => {
|
||||
onAppLeave(actions => actions.confirm('confirmation-message', 'confirmation-title'));
|
||||
return () => undefined;
|
||||
},
|
||||
});
|
||||
register(Symbol(), {
|
||||
id: 'app2',
|
||||
title: 'App2',
|
||||
mount: ({}: AppMountParameters) => {
|
||||
return () => undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const { navigateToApp, getComponent } = await service.start(startDeps);
|
||||
|
||||
update = createRenderer(getComponent());
|
||||
|
||||
await navigate('/app/app1');
|
||||
await navigateToApp('app2');
|
||||
|
||||
expect(startDeps.overlays.openConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(startDeps.overlays.openConfirm).toHaveBeenCalledWith(
|
||||
'confirmation-message',
|
||||
expect.objectContaining({ title: 'confirmation-title' })
|
||||
);
|
||||
expect(history.entries.length).toEqual(2);
|
||||
expect(history.entries[1].pathname).toEqual('/app/app1');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -36,6 +36,7 @@ describe('AppContainer', () => {
|
|||
|
||||
const mockMountersToMounters = () =>
|
||||
new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter]));
|
||||
const setAppLeaveHandlerMock = () => undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
mounters = new Map([
|
||||
|
@ -46,7 +47,13 @@ describe('AppContainer', () => {
|
|||
createAppMounter('app3', '<div>App 3</div>', '/custom/path'),
|
||||
] as Array<MockedMounterTuple<EitherApp>>);
|
||||
history = createMemoryHistory();
|
||||
update = createRenderer(<AppRouter history={history} mounters={mockMountersToMounters()} />);
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
mounters={mockMountersToMounters()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('calls mount handler and returned unmount function when navigating between apps', async () => {
|
||||
|
@ -78,7 +85,13 @@ describe('AppContainer', () => {
|
|||
mounters.set(...createAppMounter('spaces', '<div>Custom Space</div>', '/spaces/fake-login'));
|
||||
mounters.set(...createAppMounter('login', '<div>Login Page</div>', '/fake-login'));
|
||||
history = createMemoryHistory();
|
||||
update = createRenderer(<AppRouter history={history} mounters={mockMountersToMounters()} />);
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
mounters={mockMountersToMounters()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
/>
|
||||
);
|
||||
|
||||
await navigate('/fake-login');
|
||||
|
||||
|
@ -90,7 +103,13 @@ describe('AppContainer', () => {
|
|||
mounters.set(...createAppMounter('login', '<div>Login Page</div>', '/fake-login'));
|
||||
mounters.set(...createAppMounter('spaces', '<div>Custom Space</div>', '/spaces/fake-login'));
|
||||
history = createMemoryHistory();
|
||||
update = createRenderer(<AppRouter history={history} mounters={mockMountersToMounters()} />);
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
mounters={mockMountersToMounters()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
/>
|
||||
);
|
||||
|
||||
await navigate('/spaces/fake-login');
|
||||
|
||||
|
@ -124,7 +143,13 @@ describe('AppContainer', () => {
|
|||
|
||||
it('should not remount when when changing pages within app using hash history', async () => {
|
||||
history = createHashHistory();
|
||||
update = createRenderer(<AppRouter history={history} mounters={mockMountersToMounters()} />);
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
mounters={mockMountersToMounters()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
/>
|
||||
);
|
||||
|
||||
const { mounter, unmount } = mounters.get('app1')!;
|
||||
await navigate('/app/app1/page1');
|
||||
|
@ -153,6 +178,7 @@ describe('AppContainer', () => {
|
|||
Object {
|
||||
"appBasePath": "/app/legacyApp1",
|
||||
"element": <div />,
|
||||
"onAppLeave": [Function],
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -165,6 +191,7 @@ describe('AppContainer', () => {
|
|||
Object {
|
||||
"appBasePath": "/app/baseApp",
|
||||
"element": <div />,
|
||||
"onAppLeave": [Function],
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -230,6 +230,117 @@ export interface AppMountParameters {
|
|||
* ```
|
||||
*/
|
||||
appBasePath: string;
|
||||
|
||||
/**
|
||||
* A function that can be used to register a handler that will be called
|
||||
* when the user is leaving the current application, allowing to
|
||||
* prompt a confirmation message before actually changing the page.
|
||||
*
|
||||
* This will be called either when the user goes to another application, or when
|
||||
* trying to close the tab or manually changing the url.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // application.tsx
|
||||
* import React from 'react';
|
||||
* import ReactDOM from 'react-dom';
|
||||
* import { BrowserRouter, Route } from 'react-router-dom';
|
||||
*
|
||||
* import { CoreStart, AppMountParams } from 'src/core/public';
|
||||
* import { MyPluginDepsStart } from './plugin';
|
||||
*
|
||||
* export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => {
|
||||
* const { renderApp, hasUnsavedChanges } = await import('./application');
|
||||
* onAppLeave(actions => {
|
||||
* if(hasUnsavedChanges()) {
|
||||
* return actions.confirm('Some changes were not saved. Are you sure you want to leave?');
|
||||
* }
|
||||
* return actions.default();
|
||||
* });
|
||||
* return renderApp(params);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler that will be executed before leaving the application, either when
|
||||
* going to another application or when closing the browser tab or manually changing
|
||||
* the url.
|
||||
* Should return `confirm` to to prompt a message to the user before leaving the page, or `default`
|
||||
* to keep the default behavior (doing nothing).
|
||||
*
|
||||
* See {@link AppMountParameters} for detailed usage examples.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction;
|
||||
|
||||
/**
|
||||
* Possible type of actions on application leave.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum AppLeaveActionType {
|
||||
confirm = 'confirm',
|
||||
default = 'default',
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to return from a {@link AppLeaveHandler} to execute the default
|
||||
* behaviour when leaving the application.
|
||||
*
|
||||
* See {@link AppLeaveActionFactory}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface AppLeaveDefaultAction {
|
||||
type: AppLeaveActionType.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to return from a {@link AppLeaveHandler} to show a confirmation
|
||||
* message when trying to leave an application.
|
||||
*
|
||||
* See {@link AppLeaveActionFactory}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface AppLeaveConfirmAction {
|
||||
type: AppLeaveActionType.confirm;
|
||||
text: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible actions to return from a {@link AppLeaveHandler}
|
||||
*
|
||||
* See {@link AppLeaveConfirmAction} and {@link AppLeaveDefaultAction}
|
||||
*
|
||||
* @public
|
||||
* */
|
||||
export type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction;
|
||||
|
||||
/**
|
||||
* Factory provided when invoking a {@link AppLeaveHandler} to retrieve the {@link AppLeaveAction} to execute.
|
||||
*/
|
||||
export interface AppLeaveActionFactory {
|
||||
/**
|
||||
* Returns a confirm action, resulting on prompting a message to the user before leaving the
|
||||
* application, allowing him to choose if he wants to stay on the app or confirm that he
|
||||
* wants to leave.
|
||||
*
|
||||
* @param text The text to display in the confirmation message
|
||||
* @param title (optional) title to display in the confirmation message
|
||||
*/
|
||||
confirm(text: string, title?: string): AppLeaveConfirmAction;
|
||||
/**
|
||||
* Returns a default action, resulting on executing the default behavior when
|
||||
* the user tries to leave an application
|
||||
*/
|
||||
default(): AppLeaveDefaultAction;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,13 +428,13 @@ export interface ApplicationStart {
|
|||
capabilities: RecursiveReadonly<Capabilities>;
|
||||
|
||||
/**
|
||||
* Navigiate to a given app
|
||||
* Navigate to a given app
|
||||
*
|
||||
* @param appId
|
||||
* @param options.path - optional path inside application to deep link to
|
||||
* @param options.state - optional state to forward to the application
|
||||
*/
|
||||
navigateToApp(appId: string, options?: { path?: string; state?: any }): void;
|
||||
navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns a relative URL to a given app, including the global base path.
|
||||
|
|
|
@ -26,15 +26,20 @@ import React, {
|
|||
MutableRefObject,
|
||||
} from 'react';
|
||||
|
||||
import { AppUnmount, Mounter } from '../types';
|
||||
import { AppUnmount, Mounter, AppLeaveHandler } from '../types';
|
||||
import { AppNotFound } from './app_not_found_screen';
|
||||
|
||||
interface Props {
|
||||
appId: string;
|
||||
mounter?: Mounter;
|
||||
setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void;
|
||||
}
|
||||
|
||||
export const AppContainer: FunctionComponent<Props> = ({ mounter, appId }: Props) => {
|
||||
export const AppContainer: FunctionComponent<Props> = ({
|
||||
mounter,
|
||||
appId,
|
||||
setAppLeaveHandler,
|
||||
}: Props) => {
|
||||
const [appNotFound, setAppNotFound] = useState(false);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const unmountRef: MutableRefObject<AppUnmount | null> = useRef<AppUnmount>(null);
|
||||
|
@ -59,13 +64,14 @@ export const AppContainer: FunctionComponent<Props> = ({ mounter, appId }: Props
|
|||
(await mounter.mount({
|
||||
appBasePath: mounter.appBasePath,
|
||||
element: elementRef.current!,
|
||||
onAppLeave: handler => setAppLeaveHandler(appId, handler),
|
||||
})) || null;
|
||||
setAppNotFound(false);
|
||||
};
|
||||
|
||||
mount();
|
||||
return unmount;
|
||||
}, [mounter]);
|
||||
}, [appId, mounter, setAppLeaveHandler]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
|
|
@ -21,19 +21,20 @@ import React, { FunctionComponent } from 'react';
|
|||
import { History } from 'history';
|
||||
import { Router, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
|
||||
import { Mounter } from '../types';
|
||||
import { Mounter, AppLeaveHandler } from '../types';
|
||||
import { AppContainer } from './app_container';
|
||||
|
||||
interface Props {
|
||||
mounters: Map<string, Mounter>;
|
||||
history: History;
|
||||
setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
appId: string;
|
||||
}
|
||||
|
||||
export const AppRouter: FunctionComponent<Props> = ({ history, mounters }) => (
|
||||
export const AppRouter: FunctionComponent<Props> = ({ history, mounters, setAppLeaveHandler }) => (
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
{[...mounters].flatMap(([appId, mounter]) =>
|
||||
|
@ -45,7 +46,13 @@ export const AppRouter: FunctionComponent<Props> = ({ history, mounters }) => (
|
|||
<Route
|
||||
key={mounter.appRoute}
|
||||
path={mounter.appRoute}
|
||||
render={() => <AppContainer mounter={mounter} appId={appId} />}
|
||||
render={() => (
|
||||
<AppContainer
|
||||
mounter={mounter}
|
||||
appId={appId}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
]
|
||||
)}
|
||||
|
@ -61,7 +68,9 @@ export const AppRouter: FunctionComponent<Props> = ({ history, mounters }) => (
|
|||
? [appId, mounters.get(appId)]
|
||||
: [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? [];
|
||||
|
||||
return <AppContainer mounter={mounter} appId={id} />;
|
||||
return (
|
||||
<AppContainer mounter={mounter} appId={id} setAppLeaveHandler={setAppLeaveHandler} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
|
|
|
@ -63,7 +63,7 @@ export interface ChromeNavLink {
|
|||
/** LEGACY FIELDS */
|
||||
|
||||
/**
|
||||
* A url base that legacy apps can set to match deep URLs to an applcation.
|
||||
* A url base that legacy apps can set to match deep URLs to an application.
|
||||
*
|
||||
* @internalRemarks
|
||||
* This should be removed once legacy apps are gone.
|
||||
|
|
|
@ -214,7 +214,6 @@ export class CoreSystem {
|
|||
const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup! });
|
||||
const savedObjects = await this.savedObjects.start({ http });
|
||||
const i18n = await this.i18n.start();
|
||||
const application = await this.application.start({ http, injectedMetadata });
|
||||
await this.integrations.start({ uiSettings });
|
||||
|
||||
const coreUiTargetDomElement = document.createElement('div');
|
||||
|
@ -239,6 +238,7 @@ export class CoreSystem {
|
|||
overlays,
|
||||
targetDomElement: notificationsTargetDomElement,
|
||||
});
|
||||
const application = await this.application.start({ http, overlays });
|
||||
const chrome = await this.chrome.start({
|
||||
application,
|
||||
docLinks,
|
||||
|
|
|
@ -90,6 +90,11 @@ export {
|
|||
AppUnmount,
|
||||
AppMountContext,
|
||||
AppMountParameters,
|
||||
AppLeaveHandler,
|
||||
AppLeaveActionType,
|
||||
AppLeaveAction,
|
||||
AppLeaveDefaultAction,
|
||||
AppLeaveConfirmAction,
|
||||
} from './application';
|
||||
|
||||
export {
|
||||
|
|
|
@ -8,6 +8,129 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openConfirm() renders a mountpoint confirm message 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"<div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div class=\\"euiModal euiModal--maxWidth-default euiModal--confirmation\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiModal__closeIcon\\" type=\\"button\\" aria-label=\\"Closes this modal window\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"></svg></button><div class=\\"euiModal__flex\\"><div class=\\"euiModalBody\\"><div class=\\"euiModalBody__overflow\\"><div class=\\"euiText euiText--medium\\" data-test-subj=\\"confirmModalBodyText\\"><div class=\\"kbnOverlayMountWrapper\\"><span>Modal content</span></div></div></div></div><div class=\\"euiModalFooter\\"><button class=\\"euiButtonEmpty euiButtonEmpty--primary\\" type=\\"button\\" data-test-subj=\\"confirmModalCancelButton\\"><span class=\\"euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\">Cancel</span></span></button><button class=\\"euiButton euiButton--primary euiButton--fill\\" type=\\"button\\" data-test-subj=\\"confirmModalConfirmButton\\"><span class=\\"euiButton__content\\"><span class=\\"euiButton__text\\">Confirm</span></span></button></div></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div>"`;
|
||||
|
||||
exports[`ModalService openConfirm() renders a string confirm message 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
Some message
|
||||
</EuiConfirmModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openConfirm() renders a string confirm message 2`] = `"<div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div class=\\"euiModal euiModal--maxWidth-default euiModal--confirmation\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiModal__closeIcon\\" type=\\"button\\" aria-label=\\"Closes this modal window\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"></svg></button><div class=\\"euiModal__flex\\"><div class=\\"euiModalBody\\"><div class=\\"euiModalBody__overflow\\"><div class=\\"euiText euiText--medium\\" data-test-subj=\\"confirmModalBodyText\\"><p>Some message</p></div></div></div><div class=\\"euiModalFooter\\"><button class=\\"euiButtonEmpty euiButtonEmpty--primary\\" type=\\"button\\" data-test-subj=\\"confirmModalCancelButton\\"><span class=\\"euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\">Cancel</span></span></button><button class=\\"euiButton euiButton--primary euiButton--fill\\" type=\\"button\\" data-test-subj=\\"confirmModalConfirmButton\\"><span class=\\"euiButton__content\\"><span class=\\"euiButton__text\\">Confirm</span></span></button></div></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div>"`;
|
||||
|
||||
exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
confirm 1
|
||||
</EuiConfirmModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
some confirm
|
||||
</EuiConfirmModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openConfirm() with a currently active modal replaces the current modal with the new confirm 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
some confirm
|
||||
</EuiConfirmModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openModal() renders a modal to the DOM 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
|
@ -31,6 +154,43 @@ Array [
|
|||
|
||||
exports[`ModalService openModal() renders a modal to the DOM 2`] = `"<div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div class=\\"euiModal euiModal--maxWidth-default\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiModal__closeIcon\\" type=\\"button\\" aria-label=\\"Closes this modal window\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"></svg></button><div class=\\"euiModal__flex\\"><div class=\\"kbnOverlayMountWrapper\\"><span>Modal content</span></div></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div>"`;
|
||||
|
||||
exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
confirm 1
|
||||
</EuiConfirmModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
some confirm
|
||||
</EuiConfirmModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
|
|
|
@ -25,6 +25,7 @@ const createStartContractMock = () => {
|
|||
close: jest.fn(),
|
||||
onClose: Promise.resolve(),
|
||||
}),
|
||||
openConfirm: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
|
|
@ -80,6 +80,91 @@ describe('ModalService', () => {
|
|||
expect(onCloseComplete).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a currently active confirm', () => {
|
||||
let confirm1: Promise<boolean>;
|
||||
|
||||
beforeEach(() => {
|
||||
confirm1 = modals.openConfirm('confirm 1');
|
||||
});
|
||||
|
||||
it('replaces the current confirm with the new one', () => {
|
||||
modals.openConfirm('some confirm');
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('resolves the previous confirm promise', async () => {
|
||||
modals.open(mountReactNode(<span>Flyout content 2</span>));
|
||||
expect(await confirm1).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('openConfirm()', () => {
|
||||
it('renders a mountpoint confirm message', () => {
|
||||
expect(mockReactDomRender).not.toHaveBeenCalled();
|
||||
modals.openConfirm(container => {
|
||||
const content = document.createElement('span');
|
||||
content.textContent = 'Modal content';
|
||||
container.append(content);
|
||||
return () => {};
|
||||
});
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
const modalContent = mount(mockReactDomRender.mock.calls[0][0]);
|
||||
expect(modalContent.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a string confirm message', () => {
|
||||
expect(mockReactDomRender).not.toHaveBeenCalled();
|
||||
modals.openConfirm('Some message');
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
const modalContent = mount(mockReactDomRender.mock.calls[0][0]);
|
||||
expect(modalContent.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('with a currently active modal', () => {
|
||||
let ref1: OverlayRef;
|
||||
|
||||
beforeEach(() => {
|
||||
ref1 = modals.open(mountReactNode(<span>Modal content 1</span>));
|
||||
});
|
||||
|
||||
it('replaces the current modal with the new confirm', () => {
|
||||
modals.openConfirm('some confirm');
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
expect(() => ref1.close()).not.toThrowError();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('resolves onClose on the previous ref', async () => {
|
||||
const onCloseComplete = jest.fn();
|
||||
ref1.onClose.then(onCloseComplete);
|
||||
modals.openConfirm('some confirm');
|
||||
await ref1.onClose;
|
||||
expect(onCloseComplete).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a currently active confirm', () => {
|
||||
let confirm1: Promise<boolean>;
|
||||
|
||||
beforeEach(() => {
|
||||
confirm1 = modals.openConfirm('confirm 1');
|
||||
});
|
||||
|
||||
it('replaces the current confirm with the new one', () => {
|
||||
modals.openConfirm('some confirm');
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('resolves the previous confirm promise', async () => {
|
||||
modals.openConfirm('some confirm');
|
||||
expect(await confirm1).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModalRef#close()', () => {
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import { i18n as t } from '@kbn/i18n';
|
||||
import { EuiModal, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
|
@ -57,6 +58,18 @@ class ModalRef implements OverlayRef {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface OverlayModalConfirmOptions {
|
||||
title?: string;
|
||||
cancelButtonText?: string;
|
||||
confirmButtonText?: string;
|
||||
className?: string;
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* APIs to open and manage modal dialogs.
|
||||
*
|
||||
|
@ -72,6 +85,14 @@ export interface OverlayModalStart {
|
|||
* @return {@link OverlayRef} A reference to the opened modal.
|
||||
*/
|
||||
open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef;
|
||||
/**
|
||||
* Opens a confirmation modal with the given text or mountpoint as a message.
|
||||
* Returns a Promise resolving to `true` if user confirmed or `false` otherwise.
|
||||
*
|
||||
* @param message {@link MountPoint} - string or mountpoint to be used a the confirm message body
|
||||
* @param options {@link OverlayModalConfirmOptions} - options for the confirm modal
|
||||
*/
|
||||
openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,7 +119,7 @@ export class ModalService {
|
|||
|
||||
return {
|
||||
open: (mount: MountPoint, options: OverlayModalOpenOptions = {}): OverlayRef => {
|
||||
// If there is an active flyout session close it before opening a new one.
|
||||
// If there is an active modal, close it before opening a new one.
|
||||
if (this.activeModal) {
|
||||
this.activeModal.close();
|
||||
this.cleanupDom();
|
||||
|
@ -128,6 +149,65 @@ export class ModalService {
|
|||
|
||||
return modal;
|
||||
},
|
||||
openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => {
|
||||
// If there is an active modal, close it before opening a new one.
|
||||
if (this.activeModal) {
|
||||
this.activeModal.close();
|
||||
this.cleanupDom();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
const closeModal = (confirmed: boolean) => {
|
||||
resolved = true;
|
||||
modal.close();
|
||||
resolve(confirmed);
|
||||
};
|
||||
|
||||
const modal = new ModalRef();
|
||||
modal.onClose.then(() => {
|
||||
if (this.activeModal === modal) {
|
||||
this.cleanupDom();
|
||||
}
|
||||
// modal.close can be called when opening a new modal/confirm, so we need to resolve the promise in that case.
|
||||
if (!resolved) {
|
||||
closeModal(false);
|
||||
}
|
||||
});
|
||||
this.activeModal = modal;
|
||||
|
||||
const props = {
|
||||
...options,
|
||||
children:
|
||||
typeof message === 'string' ? (
|
||||
message
|
||||
) : (
|
||||
<MountWrapper mount={message} className="kbnOverlayMountWrapper" />
|
||||
),
|
||||
onCancel: () => closeModal(false),
|
||||
onConfirm: () => closeModal(true),
|
||||
cancelButtonText:
|
||||
options?.cancelButtonText ||
|
||||
t.translate('core.overlays.confirm.cancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
confirmButtonText:
|
||||
options?.confirmButtonText ||
|
||||
t.translate('core.overlays.confirm.okButton', {
|
||||
defaultMessage: 'Confirm',
|
||||
}),
|
||||
};
|
||||
|
||||
render(
|
||||
<EuiOverlayMask>
|
||||
<i18n.Context>
|
||||
<EuiConfirmModal {...props} />
|
||||
</i18n.Context>
|
||||
</EuiOverlayMask>,
|
||||
targetDomElement
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -22,9 +22,11 @@ import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock';
|
|||
import { overlayModalServiceMock } from './modal/modal_service.mock';
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const overlayStart = overlayModalServiceMock.createStartContract();
|
||||
const startContract: DeeplyMockedKeys<OverlayStart> = {
|
||||
openFlyout: overlayFlyoutServiceMock.createStartContract().open,
|
||||
openModal: overlayModalServiceMock.createStartContract().open,
|
||||
openModal: overlayStart.open,
|
||||
openConfirm: overlayStart.openConfirm,
|
||||
banners: overlayBannersServiceMock.createStartContract(),
|
||||
};
|
||||
return startContract;
|
||||
|
|
|
@ -50,6 +50,7 @@ export class OverlayService {
|
|||
banners,
|
||||
openFlyout: flyouts.open.bind(flyouts),
|
||||
openModal: modals.open.bind(modals),
|
||||
openConfirm: modals.openConfirm.bind(modals),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -62,4 +63,6 @@ export interface OverlayStart {
|
|||
openFlyout: OverlayFlyoutStart['open'];
|
||||
/** {@link OverlayModalStart#open} */
|
||||
openModal: OverlayModalStart['open'];
|
||||
/** {@link OverlayModalStart#openConfirm} */
|
||||
openConfirm: OverlayModalStart['openConfirm'];
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -68,7 +68,7 @@ export class LocalApplicationService {
|
|||
isUnmounted = true;
|
||||
});
|
||||
(async () => {
|
||||
const params = { element, appBasePath: '' };
|
||||
const params = { element, appBasePath: '', onAppLeave: () => undefined };
|
||||
unmountHandler = isAppMountDeprecated(app.mount)
|
||||
? await app.mount({ core: npStart.core }, params)
|
||||
: await app.mount(params);
|
||||
|
|
|
@ -62,6 +62,7 @@ describe('ui/new_platform', () => {
|
|||
expect(mountMock).toHaveBeenCalledWith({
|
||||
element: elementMock[0],
|
||||
appBasePath: '/test/base/path/app/test',
|
||||
onAppLeave: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -82,6 +83,7 @@ describe('ui/new_platform', () => {
|
|||
expect(mountMock).toHaveBeenCalledWith(expect.any(Object), {
|
||||
element: elementMock[0],
|
||||
appBasePath: '/test/base/path/app/test',
|
||||
onAppLeave: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -124,7 +124,11 @@ export const legacyAppRegister = (app: App) => {
|
|||
|
||||
// Root controller cannot return a Promise so use an internal async function and call it immediately
|
||||
(async () => {
|
||||
const params = { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) };
|
||||
const params = {
|
||||
element,
|
||||
appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`),
|
||||
onAppLeave: () => undefined,
|
||||
};
|
||||
const unmount = isAppMountDeprecated(app.mount)
|
||||
? await app.mount({ core: npStart.core }, params)
|
||||
: await app.mount(params);
|
||||
|
|
|
@ -346,6 +346,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
|
|||
"remove": [MockFunction],
|
||||
"replace": [MockFunction],
|
||||
},
|
||||
"openConfirm": [MockFunction],
|
||||
"openFlyout": [MockFunction],
|
||||
"openModal": [MockFunction],
|
||||
},
|
||||
|
@ -965,6 +966,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
|
|||
"remove": [MockFunction],
|
||||
"replace": [MockFunction],
|
||||
},
|
||||
"openConfirm": [MockFunction],
|
||||
"openFlyout": [MockFunction],
|
||||
"openModal": [MockFunction],
|
||||
},
|
||||
|
@ -1572,6 +1574,7 @@ exports[`QueryStringInput Should pass the query language to the language switche
|
|||
"remove": [MockFunction],
|
||||
"replace": [MockFunction],
|
||||
},
|
||||
"openConfirm": [MockFunction],
|
||||
"openFlyout": [MockFunction],
|
||||
"openModal": [MockFunction],
|
||||
},
|
||||
|
@ -2188,6 +2191,7 @@ exports[`QueryStringInput Should pass the query language to the language switche
|
|||
"remove": [MockFunction],
|
||||
"replace": [MockFunction],
|
||||
},
|
||||
"openConfirm": [MockFunction],
|
||||
"openFlyout": [MockFunction],
|
||||
"openModal": [MockFunction],
|
||||
},
|
||||
|
@ -2795,6 +2799,7 @@ exports[`QueryStringInput Should render the given query 1`] = `
|
|||
"remove": [MockFunction],
|
||||
"replace": [MockFunction],
|
||||
},
|
||||
"openConfirm": [MockFunction],
|
||||
"openFlyout": [MockFunction],
|
||||
"openModal": [MockFunction],
|
||||
},
|
||||
|
@ -3411,6 +3416,7 @@ exports[`QueryStringInput Should render the given query 1`] = `
|
|||
"remove": [MockFunction],
|
||||
"replace": [MockFunction],
|
||||
},
|
||||
"openConfirm": [MockFunction],
|
||||
"openFlyout": [MockFunction],
|
||||
"openModal": [MockFunction],
|
||||
},
|
||||
|
|
|
@ -91,7 +91,7 @@ function DevToolsWrapper({
|
|||
if (mountedTool.current) {
|
||||
mountedTool.current.unmountHandler();
|
||||
}
|
||||
const params = { element, appBasePath: '' };
|
||||
const params = { element, appBasePath: '', onAppLeave: () => undefined };
|
||||
const unmountHandler = isAppMountDeprecated(activeDevTool.mount)
|
||||
? await activeDevTool.mount(appMountContext, params)
|
||||
: await activeDevTool.mount(params);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "core_plugin_appleave",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["core_plugin_appleave"],
|
||||
"server": false,
|
||||
"ui": true
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "core_plugin_appleave",
|
||||
"version": "1.0.0",
|
||||
"main": "target/test/plugin_functional/plugins/core_plugin_appleave",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "3.5.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { AppMountParameters } from 'kibana/public';
|
||||
|
||||
const App = ({ appName }: { appName: string }) => (
|
||||
<EuiPage>
|
||||
<EuiPageBody data-test-subj="chromelessAppHome">
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>Welcome to {appName}!</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h2>{appName} home page section title</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>{appName} page content</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
|
||||
export const renderApp = (appName: string, { element }: AppMountParameters) => {
|
||||
render(<App appName={appName} />, element);
|
||||
return () => unmountComponentAtNode(element);
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginInitializer } from 'kibana/public';
|
||||
import { CoreAppLeavePlugin, CoreAppLeavePluginSetup, CoreAppLeavePluginStart } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<CoreAppLeavePluginSetup, CoreAppLeavePluginStart> = () =>
|
||||
new CoreAppLeavePlugin();
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from 'kibana/public';
|
||||
|
||||
export class CoreAppLeavePlugin
|
||||
implements Plugin<CoreAppLeavePluginSetup, CoreAppLeavePluginStart> {
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
core.application.register({
|
||||
id: 'appleave1',
|
||||
title: 'AppLeave 1',
|
||||
async mount(context, params) {
|
||||
const { renderApp } = await import('./application');
|
||||
params.onAppLeave(actions => actions.confirm('confirm-message', 'confirm-title'));
|
||||
return renderApp('AppLeave 1', params);
|
||||
},
|
||||
});
|
||||
core.application.register({
|
||||
id: 'appleave2',
|
||||
title: 'AppLeave 2',
|
||||
async mount(context, params) {
|
||||
const { renderApp } = await import('./application');
|
||||
params.onAppLeave(actions => actions.default());
|
||||
return renderApp('AppLeave 2', params);
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
export type CoreAppLeavePluginSetup = ReturnType<CoreAppLeavePlugin['setup']>;
|
||||
export type CoreAppLeavePluginStart = ReturnType<CoreAppLeavePlugin['start']>;
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": []
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import url from 'url';
|
||||
import expect from '@kbn/expect';
|
||||
import { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
const getKibanaUrl = (pathname?: string, search?: string) =>
|
||||
url.format({
|
||||
protocol: 'http:',
|
||||
hostname: process.env.TEST_KIBANA_HOST || 'localhost',
|
||||
port: process.env.TEST_KIBANA_PORT || '5620',
|
||||
pathname,
|
||||
search,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) {
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
const browser = getService('browser');
|
||||
const appsMenu = getService('appsMenu');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('application using leave confirmation', () => {
|
||||
describe('when navigating to another app', () => {
|
||||
it('prevents navigation if user click cancel on the confirmation dialog', async () => {
|
||||
await PageObjects.common.navigateToApp('appleave1');
|
||||
await appsMenu.clickLink('AppLeave 2');
|
||||
|
||||
await testSubjects.existOrFail('appLeaveConfirmModal');
|
||||
await PageObjects.common.clickCancelOnModal(false);
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/appleave1'));
|
||||
});
|
||||
it('allows navigation if user click confirm on the confirmation dialog', async () => {
|
||||
await PageObjects.common.navigateToApp('appleave1');
|
||||
await appsMenu.clickLink('AppLeave 2');
|
||||
|
||||
await testSubjects.existOrFail('appLeaveConfirmModal');
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/appleave2'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating to a legacy app', () => {
|
||||
it('prevents navigation if user click cancel on the alert dialog', async () => {
|
||||
await PageObjects.common.navigateToApp('appleave1');
|
||||
await appsMenu.clickLink('Core Legacy Compat');
|
||||
|
||||
const alert = await browser.getAlert();
|
||||
expect(alert).not.to.eql(undefined);
|
||||
alert!.dismiss();
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/appleave1'));
|
||||
});
|
||||
it('allows navigation if user click leave on the alert dialog', async () => {
|
||||
await PageObjects.common.navigateToApp('appleave1');
|
||||
await appsMenu.clickLink('Core Legacy Compat');
|
||||
|
||||
const alert = await browser.getAlert();
|
||||
expect(alert).not.to.eql(undefined);
|
||||
alert!.accept();
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/core_plugin_legacy'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -27,5 +27,6 @@ export default function({ loadTestFile }: PluginFunctionalProviderContext) {
|
|||
loadTestFile(require.resolve('./ui_plugins'));
|
||||
loadTestFile(require.resolve('./ui_settings'));
|
||||
loadTestFile(require.resolve('./top_nav'));
|
||||
loadTestFile(require.resolve('./application_leave_confirm'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ if (licenseManagementUiEnabled) {
|
|||
const unmountApp = await app.mount({ ...npStart } as any, {
|
||||
element,
|
||||
appBasePath: '',
|
||||
onAppLeave: () => undefined,
|
||||
});
|
||||
manageAngularLifecycle($scope, $route, unmountApp as any);
|
||||
},
|
||||
|
|
|
@ -43,6 +43,7 @@ routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param
|
|||
app.mount(npStart as any, {
|
||||
element: elem,
|
||||
appBasePath: '/management/elasticsearch/watcher/',
|
||||
onAppLeave: () => undefined,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue