mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
Implement ScopedHistory.block (#91099)
* implements ScopedHistory.block * add FTR tests * fix test plugin id * update generated doc * deprecates AppMountParameters.onAppLeave * typo fix * add new FTR test * fix added test
This commit is contained in:
parent
e1d0cd5270
commit
eed5f72b1a
19 changed files with 598 additions and 19 deletions
|
@ -4,6 +4,11 @@
|
|||
|
||||
## AppLeaveHandler type
|
||||
|
||||
> Warning: This API is now obsolete.
|
||||
>
|
||||
> [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) has been deprecated in favor of [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.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 `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-core-public.appmountparameters.md) for detailed usage examples.
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
|
||||
## AppMountParameters.onAppLeave property
|
||||
|
||||
> Warning: This API is now obsolete.
|
||||
>
|
||||
> [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.md) should be used instead.
|
||||
>
|
||||
|
||||
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.
|
||||
|
|
|
@ -4,15 +4,10 @@
|
|||
|
||||
## ScopedHistory.block property
|
||||
|
||||
Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md)<!-- -->.
|
||||
Add a block prompt requesting user confirmation when navigating away from the current page.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
block: (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers a modal when possible, falling back to a confirm dialog box in the beforeunload case.
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export declare class ScopedHistory<HistoryLocationState = unknown> implements Hi
|
|||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [action](./kibana-plugin-core-public.scopedhistory.action.md) | | <code>Action</code> | The last action dispatched on the history stack. |
|
||||
| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | <code>(prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback</code> | Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md)<!-- -->. |
|
||||
| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | <code>(prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback</code> | Add a block prompt requesting user confirmation when navigating away from the current page. |
|
||||
| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | <code>(location: LocationDescriptorObject<HistoryLocationState>, { prependBasePath }?: {</code><br/><code> prependBasePath?: boolean | undefined;</code><br/><code> }) => Href</code> | Creates an href (string) to the location. If <code>prependBasePath</code> is true (default), it will prepend the location's path with the scoped history basePath. |
|
||||
| [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <code><SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState></code> | Creates a <code>ScopedHistory</code> for a subpath of this <code>ScopedHistory</code>. Useful for applications that may have sub-apps that do not need access to the containing application's history. |
|
||||
| [go](./kibana-plugin-core-public.scopedhistory.go.md) | | <code>(n: number) => void</code> | Send the user forward or backwards in the history stack. |
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { map, shareReplay, takeUntil, distinctUntilChanged, filter } from 'rxjs/operators';
|
||||
import { map, shareReplay, takeUntil, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
||||
import { createBrowserHistory, History } from 'history';
|
||||
|
||||
import { MountPoint } from '../types';
|
||||
|
@ -31,6 +31,7 @@ import {
|
|||
NavigateToAppOptions,
|
||||
} from './types';
|
||||
import { getLeaveAction, isConfirmAction } from './application_leave';
|
||||
import { getUserConfirmationHandler } from './navigation_confirm';
|
||||
import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils';
|
||||
|
||||
interface SetupDeps {
|
||||
|
@ -92,6 +93,7 @@ export class ApplicationService {
|
|||
private history?: History<any>;
|
||||
private navigate?: (url: string, state: unknown, replace: boolean) => void;
|
||||
private redirectTo?: (url: string) => void;
|
||||
private overlayStart$ = new Subject<OverlayStart>();
|
||||
|
||||
public setup({
|
||||
http: { basePath },
|
||||
|
@ -101,7 +103,14 @@ export class ApplicationService {
|
|||
history,
|
||||
}: SetupDeps): InternalApplicationSetup {
|
||||
const basename = basePath.get();
|
||||
this.history = history || createBrowserHistory({ basename });
|
||||
this.history =
|
||||
history ||
|
||||
createBrowserHistory({
|
||||
basename,
|
||||
getUserConfirmation: getUserConfirmationHandler({
|
||||
overlayPromise: this.overlayStart$.pipe(take(1)).toPromise(),
|
||||
}),
|
||||
});
|
||||
|
||||
this.navigate = (url, state, replace) => {
|
||||
// basePath not needed here because `history` is configured with basename
|
||||
|
@ -173,6 +182,8 @@ export class ApplicationService {
|
|||
throw new Error('ApplicationService#setup() must be invoked before start.');
|
||||
}
|
||||
|
||||
this.overlayStart$.next(overlays);
|
||||
|
||||
const httpLoadingCount$ = new BehaviorSubject(0);
|
||||
http.addLoadingCountSource(httpLoadingCount$);
|
||||
|
||||
|
|
96
src/core/public/application/navigation_confirm.test.ts
Normal file
96
src/core/public/application/navigation_confirm.test.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { OverlayStart } from '../overlays';
|
||||
import { overlayServiceMock } from '../overlays/overlay_service.mock';
|
||||
import { getUserConfirmationHandler, ConfirmHandler } from './navigation_confirm';
|
||||
|
||||
const nextTick = () => new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
describe('getUserConfirmationHandler', () => {
|
||||
let overlayStart: ReturnType<typeof overlayServiceMock.createStartContract>;
|
||||
let overlayPromise: Promise<OverlayStart>;
|
||||
let resolvePromise: Function;
|
||||
let rejectPromise: Function;
|
||||
let fallbackHandler: jest.MockedFunction<ConfirmHandler>;
|
||||
let handler: ConfirmHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
overlayStart = overlayServiceMock.createStartContract();
|
||||
overlayPromise = new Promise((resolve, reject) => {
|
||||
resolvePromise = () => resolve(overlayStart);
|
||||
rejectPromise = () => reject('some error');
|
||||
});
|
||||
fallbackHandler = jest.fn().mockImplementation((message, callback) => {
|
||||
callback(true);
|
||||
});
|
||||
|
||||
handler = getUserConfirmationHandler({
|
||||
overlayPromise,
|
||||
fallbackHandler,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the fallback handler if the promise is not resolved yet', () => {
|
||||
const callback = jest.fn();
|
||||
handler('foo', callback);
|
||||
|
||||
expect(fallbackHandler).toHaveBeenCalledTimes(1);
|
||||
expect(fallbackHandler).toHaveBeenCalledWith('foo', callback);
|
||||
});
|
||||
|
||||
it('calls the callback with the value returned by the fallback handler', async () => {
|
||||
const callback = jest.fn();
|
||||
handler('foo', callback);
|
||||
|
||||
expect(fallbackHandler).toHaveBeenCalledTimes(1);
|
||||
expect(fallbackHandler).toHaveBeenCalledWith('foo', callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('uses the overlay handler once the promise is resolved', async () => {
|
||||
resolvePromise();
|
||||
await nextTick();
|
||||
|
||||
const callback = jest.fn();
|
||||
handler('foo', callback);
|
||||
|
||||
expect(fallbackHandler).not.toHaveBeenCalled();
|
||||
|
||||
expect(overlayStart.openConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(overlayStart.openConfirm).toHaveBeenCalledWith('foo', expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls the callback with the value returned by `openConfirm`', async () => {
|
||||
overlayStart.openConfirm.mockResolvedValue(true);
|
||||
|
||||
resolvePromise();
|
||||
await nextTick();
|
||||
|
||||
const callback = jest.fn();
|
||||
handler('foo', callback);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('uses the fallback handler if the promise rejects', async () => {
|
||||
rejectPromise();
|
||||
await nextTick();
|
||||
|
||||
const callback = jest.fn();
|
||||
handler('foo', callback);
|
||||
|
||||
expect(fallbackHandler).toHaveBeenCalledTimes(1);
|
||||
expect(overlayStart.openConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
62
src/core/public/application/navigation_confirm.ts
Normal file
62
src/core/public/application/navigation_confirm.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { OverlayStart } from 'kibana/public';
|
||||
|
||||
export type ConfirmHandlerCallback = (result: boolean) => void;
|
||||
export type ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => void;
|
||||
|
||||
interface GetUserConfirmationHandlerParams {
|
||||
overlayPromise: Promise<OverlayStart>;
|
||||
fallbackHandler?: ConfirmHandler;
|
||||
}
|
||||
|
||||
export const getUserConfirmationHandler = ({
|
||||
overlayPromise,
|
||||
fallbackHandler = windowConfirm,
|
||||
}: GetUserConfirmationHandlerParams): ConfirmHandler => {
|
||||
let overlayConfirm: ConfirmHandler;
|
||||
|
||||
overlayPromise.then(
|
||||
(overlay) => {
|
||||
overlayConfirm = getOverlayConfirmHandler(overlay);
|
||||
},
|
||||
() => {
|
||||
// should never append, but even if it does, we don't need to do anything,
|
||||
// and will just use the default window confirm instead
|
||||
}
|
||||
);
|
||||
|
||||
return (message: string, callback: ConfirmHandlerCallback) => {
|
||||
if (overlayConfirm) {
|
||||
overlayConfirm(message, callback);
|
||||
} else {
|
||||
fallbackHandler(message, callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const windowConfirm: ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => {
|
||||
const confirmed = window.confirm(message);
|
||||
callback(confirmed);
|
||||
};
|
||||
|
||||
const getOverlayConfirmHandler = (overlay: OverlayStart): ConfirmHandler => {
|
||||
return (message: string, callback: ConfirmHandlerCallback) => {
|
||||
overlay
|
||||
.openConfirm(message, { title: ' ', 'data-test-subj': 'navigationBlockConfirmModal' })
|
||||
.then(
|
||||
(confirmed) => {
|
||||
callback(confirmed);
|
||||
},
|
||||
() => {
|
||||
callback(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
|
@ -7,7 +7,8 @@
|
|||
*/
|
||||
|
||||
import { ScopedHistory } from './scoped_history';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import type { ConfirmHandler } from './navigation_confirm';
|
||||
|
||||
describe('ScopedHistory', () => {
|
||||
describe('construction', () => {
|
||||
|
@ -336,4 +337,153 @@ describe('ScopedHistory', () => {
|
|||
expect(gh.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('block', () => {
|
||||
let gh: History;
|
||||
let h: ScopedHistory;
|
||||
|
||||
const initHistory = ({
|
||||
initialPath = '/app/wow',
|
||||
scopedHistoryPath = '/app/wow',
|
||||
confirmHandler,
|
||||
}: {
|
||||
initialPath?: string;
|
||||
scopedHistoryPath?: string;
|
||||
confirmHandler?: ConfirmHandler;
|
||||
} = {}) => {
|
||||
gh = createMemoryHistory({
|
||||
getUserConfirmation: confirmHandler,
|
||||
});
|
||||
gh.push(initialPath);
|
||||
h = new ScopedHistory(gh, scopedHistoryPath);
|
||||
};
|
||||
|
||||
it('calls block on the global history', () => {
|
||||
initHistory();
|
||||
|
||||
const blockSpy = jest.spyOn(gh, 'block');
|
||||
h.block('confirm');
|
||||
|
||||
expect(blockSpy).toHaveBeenCalledTimes(1);
|
||||
expect(blockSpy).toHaveBeenCalledWith('confirm');
|
||||
});
|
||||
|
||||
it('returns a wrapped unregister function', () => {
|
||||
initHistory();
|
||||
|
||||
const blockSpy = jest.spyOn(gh, 'block');
|
||||
const unregister = jest.fn();
|
||||
blockSpy.mockReturnValue(unregister);
|
||||
|
||||
const wrapperUnregister = h.block('confirm');
|
||||
|
||||
expect(unregister).not.toHaveBeenCalled();
|
||||
|
||||
wrapperUnregister();
|
||||
|
||||
expect(unregister).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls the block handler when navigating to another app', () => {
|
||||
initHistory();
|
||||
|
||||
const blockHandler = jest.fn().mockReturnValue(true);
|
||||
|
||||
h.block(blockHandler);
|
||||
|
||||
gh.push('/app/other');
|
||||
|
||||
expect(blockHandler).toHaveBeenCalledTimes(1);
|
||||
expect(gh.location.pathname).toEqual('/app/other');
|
||||
});
|
||||
|
||||
it('calls the block handler when navigating inside the current app', () => {
|
||||
initHistory();
|
||||
|
||||
const blockHandler = jest.fn().mockReturnValue(true);
|
||||
|
||||
h.block(blockHandler);
|
||||
|
||||
gh.push('/app/wow/another-page');
|
||||
|
||||
expect(blockHandler).toHaveBeenCalledTimes(1);
|
||||
expect(gh.location.pathname).toEqual('/app/wow/another-page');
|
||||
});
|
||||
|
||||
it('can block the navigation', () => {
|
||||
initHistory();
|
||||
|
||||
const blockHandler = jest.fn().mockReturnValue(false);
|
||||
|
||||
h.block(blockHandler);
|
||||
|
||||
gh.push('/app/other');
|
||||
|
||||
expect(blockHandler).toHaveBeenCalledTimes(1);
|
||||
expect(gh.location.pathname).toEqual('/app/wow');
|
||||
});
|
||||
|
||||
it('no longer blocks the navigation when unregistered', () => {
|
||||
initHistory();
|
||||
|
||||
const blockHandler = jest.fn().mockReturnValue(false);
|
||||
|
||||
const unregister = h.block(blockHandler);
|
||||
|
||||
gh.push('/app/other');
|
||||
|
||||
expect(gh.location.pathname).toEqual('/app/wow');
|
||||
|
||||
unregister();
|
||||
|
||||
gh.push('/app/other');
|
||||
|
||||
expect(gh.location.pathname).toEqual('/app/other');
|
||||
});
|
||||
|
||||
it('throws if the history is no longer active', () => {
|
||||
initHistory();
|
||||
|
||||
gh.push('/app/other');
|
||||
|
||||
expect(() => h.block()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"ScopedHistory instance has fell out of navigation scope for basePath: /app/wow"`
|
||||
);
|
||||
});
|
||||
|
||||
it('unregisters the block handler when the history is no longer active', () => {
|
||||
initHistory();
|
||||
|
||||
const blockSpy = jest.spyOn(gh, 'block');
|
||||
const unregister = jest.fn();
|
||||
blockSpy.mockReturnValue(unregister);
|
||||
|
||||
h.block('confirm');
|
||||
|
||||
expect(unregister).not.toHaveBeenCalled();
|
||||
|
||||
gh.push('/app/other');
|
||||
|
||||
expect(unregister).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls the defined global history confirm handler', () => {
|
||||
const confirmHandler: jest.MockedFunction<ConfirmHandler> = jest
|
||||
.fn()
|
||||
.mockImplementation((message, callback) => {
|
||||
callback(true);
|
||||
});
|
||||
|
||||
initHistory({
|
||||
confirmHandler,
|
||||
});
|
||||
|
||||
h.block('are you sure');
|
||||
|
||||
gh.push('/app/other');
|
||||
|
||||
expect(confirmHandler).toHaveBeenCalledTimes(1);
|
||||
expect(gh.location.pathname).toEqual('/app/other');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -51,6 +51,10 @@ export class ScopedHistory<HistoryLocationState = unknown>
|
|||
* The key of the current position of the window in the history stack.
|
||||
*/
|
||||
private currentLocationKeyIndex: number = 0;
|
||||
/**
|
||||
* Array of the current {@link block} unregister callbacks
|
||||
*/
|
||||
private blockUnregisterCallbacks: Set<UnregisterCallback> = new Set();
|
||||
|
||||
constructor(private readonly parentHistory: History, private readonly basePath: string) {
|
||||
const parentPath = this.parentHistory.location.pathname;
|
||||
|
@ -176,18 +180,20 @@ export class ScopedHistory<HistoryLocationState = unknown>
|
|||
};
|
||||
|
||||
/**
|
||||
* Not supported. Use {@link AppMountParameters.onAppLeave}.
|
||||
*
|
||||
* @remarks
|
||||
* We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers
|
||||
* a modal when possible, falling back to a confirm dialog box in the beforeunload case.
|
||||
* Add a block prompt requesting user confirmation when navigating away from the current page.
|
||||
*/
|
||||
public block = (
|
||||
prompt?: boolean | string | TransitionPromptHook<HistoryLocationState>
|
||||
): UnregisterCallback => {
|
||||
throw new Error(
|
||||
`history.block is not supported. Please use the AppMountParameters.onAppLeave API.`
|
||||
);
|
||||
this.verifyActive();
|
||||
|
||||
const unregisterCallback = this.parentHistory.block(prompt);
|
||||
this.blockUnregisterCallbacks.add(unregisterCallback);
|
||||
|
||||
return () => {
|
||||
this.blockUnregisterCallbacks.delete(unregisterCallback);
|
||||
unregisterCallback();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -290,6 +296,12 @@ export class ScopedHistory<HistoryLocationState = unknown>
|
|||
if (!location.pathname.startsWith(this.basePath)) {
|
||||
unlisten();
|
||||
this.isActive = false;
|
||||
|
||||
for (const unregisterBlock of this.blockUnregisterCallbacks) {
|
||||
unregisterBlock();
|
||||
}
|
||||
this.blockUnregisterCallbacks.clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -478,6 +478,8 @@ export interface AppMountParameters<HistoryLocationState = unknown> {
|
|||
* return renderApp({ element, history });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @deprecated {@link ScopedHistory.block} should be used instead.
|
||||
*/
|
||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||
|
||||
|
@ -523,6 +525,7 @@ export interface AppMountParameters<HistoryLocationState = unknown> {
|
|||
* See {@link AppMountParameters} for detailed usage examples.
|
||||
*
|
||||
* @public
|
||||
* @deprecated {@link AppMountParameters.onAppLeave} has been deprecated in favor of {@link ScopedHistory.block}
|
||||
*/
|
||||
export type AppLeaveHandler = (
|
||||
factory: AppLeaveActionFactory,
|
||||
|
@ -590,6 +593,7 @@ export interface AppLeaveActionFactory {
|
|||
* so we can show to the user the right UX for him to saved his/her/their changes
|
||||
*/
|
||||
confirm(text: string, title?: string, callback?: () => void): AppLeaveConfirmAction;
|
||||
|
||||
/**
|
||||
* Returns a default action, resulting on executing the default behavior when
|
||||
* the user tries to leave an application
|
||||
|
|
|
@ -116,7 +116,7 @@ export interface AppLeaveDefaultAction {
|
|||
|
||||
// Warning: (ae-forgotten-export) The symbol "AppLeaveActionFactory" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type AppLeaveHandler = (factory: AppLeaveActionFactory, nextAppId?: string) => AppLeaveAction;
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -153,6 +153,7 @@ export interface AppMountParameters<HistoryLocationState = unknown> {
|
|||
appBasePath: string;
|
||||
element: HTMLElement;
|
||||
history: ScopedHistory<HistoryLocationState>;
|
||||
// @deprecated
|
||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||
setHeaderActionMenu: (menuMount: MountPoint | undefined) => void;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "coreHistoryBlock",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredBundles": ["kibanaReact"]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "core_history_block",
|
||||
"version": "1.0.0",
|
||||
"main": "target/test/plugin_functional/plugins/core_history_block",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Switch, Route, Prompt } from 'react-router-dom';
|
||||
import type { AppMountParameters, IBasePath, ApplicationStart } from 'kibana/public';
|
||||
import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
const HomePage = ({
|
||||
basePath,
|
||||
application,
|
||||
}: {
|
||||
basePath: IBasePath;
|
||||
application: ApplicationStart;
|
||||
}) => (
|
||||
<div data-test-subj="page-home">
|
||||
<Prompt message="Unsaved changes, are you sure you wanna leave?" />
|
||||
<RedirectAppLinks application={application}>
|
||||
<h1>HOME PAGE</h1>
|
||||
<br /> <br />
|
||||
<a data-test-subj="applink-intra-test" href={basePath.prepend(`/app/core_history_block/foo`)}>
|
||||
Link to foo on the same app
|
||||
</a>
|
||||
<br /> <br />
|
||||
<a data-test-subj="applink-external-test" href={basePath.prepend(`/app/home`)}>
|
||||
Link to the home app
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
const FooPage = ({
|
||||
basePath,
|
||||
application,
|
||||
}: {
|
||||
basePath: IBasePath;
|
||||
application: ApplicationStart;
|
||||
}) => (
|
||||
<div data-test-subj="page-home">
|
||||
<RedirectAppLinks application={application}>
|
||||
<h1>FOO PAGE</h1>
|
||||
<br /> <br />
|
||||
<a data-test-subj="applink-intra-test" href={basePath.prepend(`/app/core_history_block`)}>
|
||||
Link to home on the same app
|
||||
</a>
|
||||
<br /> <br />
|
||||
<a data-test-subj="applink-nested-test" href={basePath.prepend(`/app/home`)}>
|
||||
Link to the home app
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface AppOptions {
|
||||
basePath: IBasePath;
|
||||
application: ApplicationStart;
|
||||
}
|
||||
|
||||
export const renderApp = (
|
||||
{ basePath, application }: AppOptions,
|
||||
{ element, history }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path="/" exact={true}>
|
||||
<HomePage basePath={basePath} application={application} />
|
||||
</Route>
|
||||
<Route path="/foo" exact={true}>
|
||||
<FooPage basePath={basePath} application={application} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>,
|
||||
element
|
||||
);
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginInitializer } from 'kibana/public';
|
||||
import { CoreAppLinkPlugin, CoreAppLinkPluginSetup, CoreAppLinkPluginStart } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<CoreAppLinkPluginSetup, CoreAppLinkPluginStart> = () =>
|
||||
new CoreAppLinkPlugin();
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public';
|
||||
import { renderApp } from './app';
|
||||
|
||||
export class CoreAppLinkPlugin implements Plugin<CoreAppLinkPluginSetup, CoreAppLinkPluginStart> {
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
core.application.register({
|
||||
id: 'core_history_block',
|
||||
title: 'History block test',
|
||||
mount: async (params: AppMountParameters) => {
|
||||
const [{ application }] = await core.getStartServices();
|
||||
return renderApp(
|
||||
{
|
||||
basePath: core.http.basePath,
|
||||
application,
|
||||
},
|
||||
params
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public start() {
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
export type CoreAppLinkPluginSetup = ReturnType<CoreAppLinkPlugin['setup']>;
|
||||
export type CoreAppLinkPluginStart = ReturnType<CoreAppLinkPlugin['start']>;
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../../../typings/**/*"],
|
||||
"exclude": [],
|
||||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../../src/plugins/kibana_react/tsconfig.json" }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
const browser = getService('browser');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('application using `ScopedHistory.block`', () => {
|
||||
beforeEach(async () => {
|
||||
await PageObjects.common.navigateToApp('core_history_block');
|
||||
});
|
||||
|
||||
describe('when navigating to another app', () => {
|
||||
it('prevents navigation if user click cancel on the confirmation dialog', async () => {
|
||||
await testSubjects.click('applink-external-test');
|
||||
|
||||
await testSubjects.existOrFail('navigationBlockConfirmModal');
|
||||
await PageObjects.common.clickCancelOnModal(false);
|
||||
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block');
|
||||
});
|
||||
it('allows navigation if user click confirm on the confirmation dialog', async () => {
|
||||
await testSubjects.click('applink-external-test');
|
||||
|
||||
await testSubjects.existOrFail('navigationBlockConfirmModal');
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
expect(await browser.getCurrentUrl()).to.contain('/app/home');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating to the same app', () => {
|
||||
it('prevents navigation if user click cancel on the confirmation dialog', async () => {
|
||||
await testSubjects.click('applink-intra-test');
|
||||
|
||||
await testSubjects.existOrFail('navigationBlockConfirmModal');
|
||||
await PageObjects.common.clickCancelOnModal(false);
|
||||
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block');
|
||||
expect(await browser.getCurrentUrl()).not.to.contain('/foo');
|
||||
});
|
||||
it('allows navigation if user click confirm on the confirmation dialog', async () => {
|
||||
await testSubjects.click('applink-intra-test');
|
||||
|
||||
await testSubjects.existOrFail('navigationBlockConfirmModal');
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo');
|
||||
});
|
||||
it('allows navigating back without prompt once the block handler has been disposed', async () => {
|
||||
await testSubjects.click('applink-intra-test');
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo');
|
||||
|
||||
await testSubjects.click('applink-intra-test');
|
||||
expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block');
|
||||
expect(await browser.getCurrentUrl()).not.to.contain('/foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -20,5 +20,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
|
|||
loadTestFile(require.resolve('./application_status'));
|
||||
loadTestFile(require.resolve('./rendering'));
|
||||
loadTestFile(require.resolve('./chrome_help_menu_links'));
|
||||
loadTestFile(require.resolve('./history_block'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue