mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* add `replace` option to `navigateToApp` * use `unknown` type for state * add test when `replace` is false
This commit is contained in:
parent
fb64e71a76
commit
0d78bfcf22
15 changed files with 219 additions and 23 deletions
|
@ -9,10 +9,7 @@ Navigate to a given app
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
navigateToApp(appId: string, options?: {
|
||||
path?: string;
|
||||
state?: any;
|
||||
}): Promise<void>;
|
||||
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise<void>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -20,7 +17,7 @@ navigateToApp(appId: string, options?: {
|
|||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| appId | <code>string</code> | |
|
||||
| options | <code>{</code><br/><code> path?: string;</code><br/><code> state?: any;</code><br/><code> }</code> | |
|
||||
| options | <code>NavigateToAppOptions</code> | navigation options |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
|
||||
| [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) | Start interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
|
||||
| [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) | |
|
||||
| [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) | Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) |
|
||||
| [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | |
|
||||
| [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | |
|
||||
| [OverlayBannersStart](./kibana-plugin-core-public.overlaybannersstart.md) | |
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md)
|
||||
|
||||
## NavigateToAppOptions interface
|
||||
|
||||
Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface NavigateToAppOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | <code>string</code> | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.appbase.defaultpath.md)<!-- -->\` as default. |
|
||||
| [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | <code>boolean</code> | if true, will not create a new history entry when navigating (using <code>replace</code> instead of <code>push</code>) |
|
||||
| [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | <code>unknown</code> | optional state to forward to the application |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) > [path](./kibana-plugin-core-public.navigatetoappoptions.path.md)
|
||||
|
||||
## NavigateToAppOptions.path property
|
||||
|
||||
optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.appbase.defaultpath.md)<!-- -->\` as default.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
path?: string;
|
||||
```
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) > [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md)
|
||||
|
||||
## NavigateToAppOptions.replace property
|
||||
|
||||
if true, will not create a new history entry when navigating (using `replace` instead of `push`<!-- -->)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
replace?: boolean;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
This option not be used when navigating from and/or to legacy applications.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) > [state](./kibana-plugin-core-public.navigatetoappoptions.state.md)
|
||||
|
||||
## NavigateToAppOptions.state property
|
||||
|
||||
optional state to forward to the application
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
state?: unknown;
|
||||
```
|
|
@ -76,6 +76,7 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = `
|
|||
history={
|
||||
Object {
|
||||
"push": [MockFunction],
|
||||
"replace": [MockFunction],
|
||||
}
|
||||
}
|
||||
mounters={Map {}}
|
||||
|
|
|
@ -29,6 +29,7 @@ jest.doMock('./capabilities', () => ({
|
|||
|
||||
export const MockHistory = {
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
};
|
||||
export const createBrowserHistoryMock = jest.fn().mockReturnValue(MockHistory);
|
||||
jest.doMock('history', () => ({
|
||||
|
|
|
@ -482,9 +482,6 @@ describe('#setup()', () => {
|
|||
|
||||
describe('#start()', () => {
|
||||
beforeEach(() => {
|
||||
MockHistory.push.mockReset();
|
||||
parseAppUrlMock.mockReset();
|
||||
|
||||
const http = httpServiceMock.createSetupContract({ basePath: '/base-path' });
|
||||
setupDeps = {
|
||||
http,
|
||||
|
@ -497,6 +494,12 @@ describe('#start()', () => {
|
|||
service = new ApplicationService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
MockHistory.push.mockReset();
|
||||
MockHistory.replace.mockReset();
|
||||
parseAppUrlMock.mockReset();
|
||||
});
|
||||
|
||||
it('rejects if called prior to #setup()', async () => {
|
||||
await expect(service.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"ApplicationService#setup() must be invoked before start."`
|
||||
|
@ -924,6 +927,79 @@ describe('#start()', () => {
|
|||
await navigateToApp('baseApp:legacyApp1');
|
||||
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/baseApp');
|
||||
});
|
||||
|
||||
describe('when `replace` option is true', () => {
|
||||
it('use `history.replace` instead of `history.push`', async () => {
|
||||
service.setup(setupDeps);
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('myTestApp', { replace: true });
|
||||
expect(MockHistory.replace).toHaveBeenCalledWith('/app/myTestApp', undefined);
|
||||
|
||||
await navigateToApp('myOtherApp', { replace: true });
|
||||
expect(MockHistory.replace).toHaveBeenCalledWith('/app/myOtherApp', undefined);
|
||||
});
|
||||
|
||||
it('includes state if specified', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('myTestApp', { state: 'my-state', replace: true });
|
||||
expect(MockHistory.replace).toHaveBeenCalledWith('/app/myTestApp', 'my-state');
|
||||
|
||||
await navigateToApp('app2', { state: 'my-state', replace: true });
|
||||
expect(MockHistory.replace).toHaveBeenCalledWith('/custom/path', 'my-state');
|
||||
});
|
||||
it('appends a path if specified', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('myTestApp', { path: 'deep/link/to/location/2', replace: true });
|
||||
expect(MockHistory.replace).toHaveBeenCalledWith(
|
||||
'/app/myTestApp/deep/link/to/location/2',
|
||||
undefined
|
||||
);
|
||||
|
||||
await navigateToApp('app2', { path: 'deep/link/to/location/2', replace: true });
|
||||
expect(MockHistory.replace).toHaveBeenCalledWith(
|
||||
'/custom/path/deep/link/to/location/2',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
it('do not change the behavior when in legacy mode', async () => {
|
||||
setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
|
||||
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
|
||||
service.setup(setupDeps);
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('alpha', { replace: true });
|
||||
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/alpha');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `replace` option is false', () => {
|
||||
it('behave as when the option is unspecified', async () => {
|
||||
service.setup(setupDeps);
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('myTestApp', { replace: false });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined);
|
||||
|
||||
await navigateToApp('myOtherApp', { replace: false });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined);
|
||||
|
||||
expect(MockHistory.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToUrl', () => {
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
LegacyApp,
|
||||
LegacyAppMounter,
|
||||
Mounter,
|
||||
NavigateToAppOptions,
|
||||
} from './types';
|
||||
import { getLeaveAction, isConfirmAction } from './application_leave';
|
||||
import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils';
|
||||
|
@ -105,7 +106,7 @@ export class ApplicationService {
|
|||
private registrationClosed = false;
|
||||
private history?: History<any>;
|
||||
private mountContext?: IContextContainer<AppMountDeprecated>;
|
||||
private navigate?: (url: string, state: any) => void;
|
||||
private navigate?: (url: string, state: unknown, replace: boolean) => void;
|
||||
private redirectTo?: (url: string) => void;
|
||||
|
||||
public setup({
|
||||
|
@ -125,10 +126,16 @@ export class ApplicationService {
|
|||
this.history = history || createBrowserHistory({ basename });
|
||||
}
|
||||
|
||||
// If we do not have history available, use redirectTo to do a full page refresh.
|
||||
this.navigate = (url, state) =>
|
||||
// basePath not needed here because `history` is configured with basename
|
||||
this.history ? this.history.push(url, state) : redirectTo(basePath.prepend(url));
|
||||
this.navigate = (url, state, replace) => {
|
||||
if (this.history) {
|
||||
// basePath not needed here because `history` is configured with basename
|
||||
return replace ? this.history.replace(url, state) : this.history.push(url, state);
|
||||
} else {
|
||||
// If we do not have history available (legacy mode), use redirectTo to do a full page refresh.
|
||||
return redirectTo(basePath.prepend(url));
|
||||
}
|
||||
};
|
||||
|
||||
this.redirectTo = redirectTo;
|
||||
this.mountContext = context.createContextContainer();
|
||||
|
||||
|
@ -278,14 +285,14 @@ export class ApplicationService {
|
|||
|
||||
const navigateToApp: InternalApplicationStart['navigateToApp'] = async (
|
||||
appId,
|
||||
{ path, state }: { path?: string; state?: any } = {}
|
||||
{ path, state, replace = false }: NavigateToAppOptions = {}
|
||||
) => {
|
||||
if (await this.shouldNavigate(overlays)) {
|
||||
if (path === undefined) {
|
||||
path = applications$.value.get(appId)?.defaultPath;
|
||||
}
|
||||
this.appLeaveHandlers.delete(this.currentAppId$.value!);
|
||||
this.navigate!(getAppUrl(availableMounters, appId, path), state);
|
||||
this.navigate!(getAppUrl(availableMounters, appId, path), state, replace);
|
||||
this.currentAppId$.next(appId);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -40,6 +40,7 @@ export {
|
|||
AppLeaveDefaultAction,
|
||||
AppLeaveConfirmAction,
|
||||
LegacyApp,
|
||||
NavigateToAppOptions,
|
||||
PublicAppInfo,
|
||||
PublicLegacyAppInfo,
|
||||
// Internal types
|
||||
|
|
|
@ -125,6 +125,25 @@ describe('ApplicationService', () => {
|
|||
|
||||
expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1');
|
||||
});
|
||||
|
||||
it('replaces the current history entry when the `replace` option is true', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(Symbol(), {
|
||||
id: 'app1',
|
||||
title: 'App1',
|
||||
mount: async ({}: AppMountParameters) => {
|
||||
return () => undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('app1', { path: '/foo' });
|
||||
await navigateToApp('app1', { path: '/bar', replace: true });
|
||||
|
||||
expect(history.entries.map((entry) => entry.pathname)).toEqual(['/', '/app/app1/bar']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -661,6 +661,28 @@ export interface InternalApplicationSetup extends Pick<ApplicationSetup, 'regist
|
|||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the {@link ApplicationStart.navigateToApp | navigateToApp API}
|
||||
*/
|
||||
export interface NavigateToAppOptions {
|
||||
/**
|
||||
* optional path inside application to deep link to.
|
||||
* If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default.
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* optional state to forward to the application
|
||||
*/
|
||||
state?: unknown;
|
||||
/**
|
||||
* if true, will not create a new history entry when navigating (using `replace` instead of `push`)
|
||||
*
|
||||
* @remarks
|
||||
* This option not be used when navigating from and/or to legacy applications.
|
||||
*/
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ApplicationStart {
|
||||
/**
|
||||
|
@ -681,11 +703,9 @@ export interface ApplicationStart {
|
|||
* Navigate to a given app
|
||||
*
|
||||
* @param appId
|
||||
* @param options.path - optional path inside application to deep link to.
|
||||
* If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default.
|
||||
* @param options.state - optional state to forward to the application
|
||||
* @param options - navigation options
|
||||
*/
|
||||
navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise<void>;
|
||||
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.
|
||||
|
|
|
@ -126,6 +126,7 @@ export {
|
|||
ScopedHistory,
|
||||
LegacyApp,
|
||||
PublicLegacyAppInfo,
|
||||
NavigateToAppOptions,
|
||||
} from './application';
|
||||
|
||||
export {
|
||||
|
|
|
@ -238,10 +238,7 @@ export interface ApplicationStart {
|
|||
path?: string;
|
||||
absolute?: boolean;
|
||||
}): string;
|
||||
navigateToApp(appId: string, options?: {
|
||||
path?: string;
|
||||
state?: any;
|
||||
}): Promise<void>;
|
||||
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise<void>;
|
||||
navigateToUrl(url: string): Promise<void>;
|
||||
// @deprecated
|
||||
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
|
||||
|
@ -1035,6 +1032,15 @@ export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulPart
|
|||
// @public
|
||||
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "NavigateToAppOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export interface NavigateToAppOptions {
|
||||
path?: string;
|
||||
replace?: boolean;
|
||||
state?: unknown;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "NavType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue