mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
parent
7579bff946
commit
cbb96a7213
58 changed files with 1479 additions and 119 deletions
|
@ -9,7 +9,7 @@ Extension of [common app properties](./kibana-plugin-public.appbase.md) with the
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface App extends AppBase
|
||||
export interface App<HistoryLocationState = unknown> extends AppBase
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
@ -18,5 +18,5 @@ export interface App extends AppBase
|
|||
| --- | --- | --- |
|
||||
| [appRoute](./kibana-plugin-public.app.approute.md) | <code>string</code> | Override the application's routing path from <code>/app/${id}</code>. Must be unique across registered applications. Should not include the base path from HTTP. |
|
||||
| [chromeless](./kibana-plugin-public.app.chromeless.md) | <code>boolean</code> | Hide the UI chrome when the application is mounted. Defaults to <code>false</code>. Takes precedence over chrome service visibility settings. |
|
||||
| [mount](./kibana-plugin-public.app.mount.md) | <code>AppMount | AppMountDeprecated</code> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md)<!-- -->. |
|
||||
| [mount](./kibana-plugin-public.app.mount.md) | <code>AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState></code> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md)<!-- -->. |
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ A mount function called when the user navigates to this app's route. May have si
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
mount: AppMount | AppMountDeprecated;
|
||||
mount: AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState>;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
|
|
@ -9,14 +9,14 @@ Register an mountable application to the system.
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
register(app: App): void;
|
||||
register<HistoryLocationState = unknown>(app: App<HistoryLocationState>): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| app | <code>App</code> | an [App](./kibana-plugin-public.app.md) |
|
||||
| app | <code>App<HistoryLocationState></code> | an [App](./kibana-plugin-public.app.md) |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@ A mount function called when the user navigates to this app's route.
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AppMount = (params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
|
||||
export declare type AppMount<HistoryLocationState = unknown> = (params: AppMountParameters<HistoryLocationState>) => AppUnmount | Promise<AppUnmount>;
|
||||
```
|
||||
|
|
|
@ -13,7 +13,7 @@ A mount function called when the user navigates to this app's route.
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
|
||||
export declare type AppMountDeprecated<HistoryLocationState = unknown> = (context: AppMountContext, params: AppMountParameters<HistoryLocationState>) => AppUnmount | Promise<AppUnmount>;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
|
||||
## AppMountParameters.appBasePath property
|
||||
|
||||
> Warning: This API is now obsolete.
|
||||
>
|
||||
> Use [AppMountParameters.history](./kibana-plugin-public.appmountparameters.history.md) instead.
|
||||
>
|
||||
|
||||
The route path for configuring navigation to the application. This string should not include the base path from HTTP.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
@ -39,10 +44,10 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
|
||||
import { CoreStart, AppMountParams } from 'src/core/public';
|
||||
import { CoreStart, AppMountParameters } from 'src/core/public';
|
||||
import { MyPluginDepsStart } from './plugin';
|
||||
|
||||
export renderApp = ({ appBasePath, element }: AppMountParams) => {
|
||||
export renderApp = ({ appBasePath, element }: AppMountParameters) => {
|
||||
ReactDOM.render(
|
||||
// pass `appBasePath` to `basename`
|
||||
<BrowserRouter basename={appBasePath}>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<!-- 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) > [history](./kibana-plugin-public.appmountparameters.history.md)
|
||||
|
||||
## AppMountParameters.history property
|
||||
|
||||
A scoped history instance for your application. Should be used to wire up your applications Router.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
history: ScopedHistory<HistoryLocationState>;
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
How to configure react-router with a base path:
|
||||
|
||||
```ts
|
||||
// inside your plugin's setup function
|
||||
export class MyPlugin implements Plugin {
|
||||
setup({ application }) {
|
||||
application.register({
|
||||
id: 'my-app',
|
||||
appRoute: '/my-app',
|
||||
async mount(params) {
|
||||
const { renderApp } = await import('./application');
|
||||
return renderApp(params);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```ts
|
||||
// application.tsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route } from 'react-router-dom';
|
||||
|
||||
import { CoreStart, AppMountParameters } from 'src/core/public';
|
||||
import { MyPluginDepsStart } from './plugin';
|
||||
|
||||
export renderApp = ({ element, history }: AppMountParameters) => {
|
||||
ReactDOM.render(
|
||||
// pass `appBasePath` to `basename`
|
||||
<Router history={history}>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
</Router>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface AppMountParameters
|
||||
export interface AppMountParameters<HistoryLocationState = unknown>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
@ -17,5 +17,6 @@ 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. |
|
||||
| [history](./kibana-plugin-public.appmountparameters.history.md) | <code>ScopedHistory<HistoryLocationState></code> | A scoped history instance for your application. Should be used to wire up your applications Router. |
|
||||
| [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. |
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| Class | Description |
|
||||
| --- | --- |
|
||||
| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. |
|
||||
| [ScopedHistory](./kibana-plugin-public.scopedhistory.md) | A wrapper around a <code>History</code> instance that is scoped to a particular base path of the history stack. Behaves similarly to the <code>basename</code> option except that this wrapper hides any history stack entries from outside the scope of this base path.<!-- -->This wrapper also allows Core and Plugins to share a single underlying global <code>History</code> instance without exposing the history of other applications.<!-- -->The [createSubHistory](./kibana-plugin-public.scopedhistory.createsubhistory.md) method is particularly useful for applications that contain any number of "sub-apps" which should not have access to the main application's history or basePath. |
|
||||
| [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. |
|
||||
|
||||
|
|
|
@ -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) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [(constructor)](./kibana-plugin-public.scopedhistory._constructor_.md)
|
||||
|
||||
## ScopedHistory.(constructor)
|
||||
|
||||
Constructs a new instance of the `ScopedHistory` class
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(parentHistory: History, basePath: string);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| parentHistory | <code>History</code> | |
|
||||
| basePath | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [action](./kibana-plugin-public.scopedhistory.action.md)
|
||||
|
||||
## ScopedHistory.action property
|
||||
|
||||
The last action dispatched on the history stack.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
get action(): Action;
|
||||
```
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [block](./kibana-plugin-public.scopedhistory.block.md)
|
||||
|
||||
## ScopedHistory.block property
|
||||
|
||||
Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-public.appmountparameters.onappleave.md)<!-- -->.
|
||||
|
||||
<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.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [createHref](./kibana-plugin-public.scopedhistory.createhref.md)
|
||||
|
||||
## ScopedHistory.createHref property
|
||||
|
||||
Creates an href (string) to the location.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
createHref: (location: LocationDescriptorObject<HistoryLocationState>) => string;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [createSubHistory](./kibana-plugin-public.scopedhistory.createsubhistory.md)
|
||||
|
||||
## ScopedHistory.createSubHistory property
|
||||
|
||||
Creates a `ScopedHistory` for a subpath of this `ScopedHistory`<!-- -->. Useful for applications that may have sub-apps that do not need access to the containing application's history.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
createSubHistory: <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [go](./kibana-plugin-public.scopedhistory.go.md)
|
||||
|
||||
## ScopedHistory.go property
|
||||
|
||||
Send the user forward or backwards in the history stack.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
go: (n: number) => void;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [goBack](./kibana-plugin-public.scopedhistory.goback.md)
|
||||
|
||||
## ScopedHistory.goBack property
|
||||
|
||||
Send the user one location back in the history stack. Equivalent to calling [ScopedHistory.go(-1)](./kibana-plugin-public.scopedhistory.go.md)<!-- -->. If no more entries are available backwards, this is a no-op.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
goBack: () => void;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [goForward](./kibana-plugin-public.scopedhistory.goforward.md)
|
||||
|
||||
## ScopedHistory.goForward property
|
||||
|
||||
Send the user one location forward in the history stack. Equivalent to calling [ScopedHistory.go(1)](./kibana-plugin-public.scopedhistory.go.md)<!-- -->. If no more entries are available forwards, this is a no-op.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
goForward: () => void;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [length](./kibana-plugin-public.scopedhistory.length.md)
|
||||
|
||||
## ScopedHistory.length property
|
||||
|
||||
The number of entries in the history stack, including all entries forwards and backwards from the current location.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
get length(): number;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [listen](./kibana-plugin-public.scopedhistory.listen.md)
|
||||
|
||||
## ScopedHistory.listen property
|
||||
|
||||
Adds a listener for location updates.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
listen: (listener: (location: Location<HistoryLocationState>, action: Action) => void) => UnregisterCallback;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [location](./kibana-plugin-public.scopedhistory.location.md)
|
||||
|
||||
## ScopedHistory.location property
|
||||
|
||||
The current location of the history stack.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
get location(): Location<HistoryLocationState>;
|
||||
```
|
|
@ -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) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md)
|
||||
|
||||
## ScopedHistory class
|
||||
|
||||
A wrapper around a `History` instance that is scoped to a particular base path of the history stack. Behaves similarly to the `basename` option except that this wrapper hides any history stack entries from outside the scope of this base path.
|
||||
|
||||
This wrapper also allows Core and Plugins to share a single underlying global `History` instance without exposing the history of other applications.
|
||||
|
||||
The [createSubHistory](./kibana-plugin-public.scopedhistory.createsubhistory.md) method is particularly useful for applications that contain any number of "sub-apps" which should not have access to the main application's history or basePath.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare class ScopedHistory<HistoryLocationState = unknown> implements History<HistoryLocationState>
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(parentHistory, basePath)](./kibana-plugin-public.scopedhistory._constructor_.md) | | Constructs a new instance of the <code>ScopedHistory</code> class |
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [action](./kibana-plugin-public.scopedhistory.action.md) | | <code>Action</code> | The last action dispatched on the history stack. |
|
||||
| [block](./kibana-plugin-public.scopedhistory.block.md) | | <code>(prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback</code> | Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-public.appmountparameters.onappleave.md)<!-- -->. |
|
||||
| [createHref](./kibana-plugin-public.scopedhistory.createhref.md) | | <code>(location: LocationDescriptorObject<HistoryLocationState>) => string</code> | Creates an href (string) to the location. |
|
||||
| [createSubHistory](./kibana-plugin-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-public.scopedhistory.go.md) | | <code>(n: number) => void</code> | Send the user forward or backwards in the history stack. |
|
||||
| [goBack](./kibana-plugin-public.scopedhistory.goback.md) | | <code>() => void</code> | Send the user one location back in the history stack. Equivalent to calling [ScopedHistory.go(-1)](./kibana-plugin-public.scopedhistory.go.md)<!-- -->. If no more entries are available backwards, this is a no-op. |
|
||||
| [goForward](./kibana-plugin-public.scopedhistory.goforward.md) | | <code>() => void</code> | Send the user one location forward in the history stack. Equivalent to calling [ScopedHistory.go(1)](./kibana-plugin-public.scopedhistory.go.md)<!-- -->. If no more entries are available forwards, this is a no-op. |
|
||||
| [length](./kibana-plugin-public.scopedhistory.length.md) | | <code>number</code> | The number of entries in the history stack, including all entries forwards and backwards from the current location. |
|
||||
| [listen](./kibana-plugin-public.scopedhistory.listen.md) | | <code>(listener: (location: Location<HistoryLocationState>, action: Action) => void) => UnregisterCallback</code> | Adds a listener for location updates. |
|
||||
| [location](./kibana-plugin-public.scopedhistory.location.md) | | <code>Location<HistoryLocationState></code> | The current location of the history stack. |
|
||||
| [push](./kibana-plugin-public.scopedhistory.push.md) | | <code>(pathOrLocation: string | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void</code> | Pushes a new location onto the history stack. If there are forward entries in the stack, they will be removed. |
|
||||
| [replace](./kibana-plugin-public.scopedhistory.replace.md) | | <code>(pathOrLocation: string | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void</code> | Replaces the current location in the history stack. Does not remove forward or backward entries. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [push](./kibana-plugin-public.scopedhistory.push.md)
|
||||
|
||||
## ScopedHistory.push property
|
||||
|
||||
Pushes a new location onto the history stack. If there are forward entries in the stack, they will be removed.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
push: (pathOrLocation: string | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ScopedHistory](./kibana-plugin-public.scopedhistory.md) > [replace](./kibana-plugin-public.scopedhistory.replace.md)
|
||||
|
||||
## ScopedHistory.replace property
|
||||
|
||||
Replaces the current location in the history stack. Does not remove forward or backward entries.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
replace: (pathOrLocation: string | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void;
|
||||
```
|
|
@ -588,6 +588,16 @@ describe('#start()', () => {
|
|||
expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link');
|
||||
});
|
||||
|
||||
it('does not append trailing slash if hash is provided in path parameter', async () => {
|
||||
service.setup(setupDeps);
|
||||
const { getUrlForApp } = await service.start(startDeps);
|
||||
|
||||
expect(getUrlForApp('app1', { path: '#basic-hash' })).toBe('/base-path/app/app1#basic-hash');
|
||||
expect(getUrlForApp('app1', { path: '#/hash/router/path' })).toBe(
|
||||
'/base-path/app/app1#/hash/router/path'
|
||||
);
|
||||
});
|
||||
|
||||
it('creates absolute URLs when `absolute` parameter is true', async () => {
|
||||
service.setup(setupDeps);
|
||||
const { getUrlForApp } = await service.start(startDeps);
|
||||
|
@ -646,6 +656,26 @@ describe('#start()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('appends a path if specified with hash', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('myTestApp', { path: '#basic-hash' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp#basic-hash', undefined);
|
||||
|
||||
await navigateToApp('myTestApp', { path: '#/hash/router/path' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp#/hash/router/path', undefined);
|
||||
|
||||
await navigateToApp('app2', { path: '#basic-hash' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/custom/path#basic-hash', undefined);
|
||||
|
||||
await navigateToApp('app2', { path: '#/hash/router/path' });
|
||||
expect(MockHistory.push).toHaveBeenCalledWith('/custom/path#/hash/router/path', undefined);
|
||||
});
|
||||
|
||||
it('includes state if specified', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
|
|
|
@ -76,10 +76,19 @@ function filterAvailable<T>(m: Map<string, T>, capabilities: Capabilities) {
|
|||
}
|
||||
const findMounter = (mounters: Map<string, Mounter>, appRoute?: string) =>
|
||||
[...mounters].find(([, mounter]) => mounter.appRoute === appRoute);
|
||||
const getAppUrl = (mounters: Map<string, Mounter>, appId: string, path: string = '') =>
|
||||
`/${mounters.get(appId)?.appRoute ?? `/app/${appId}`}/${path}`
|
||||
|
||||
const getAppUrl = (mounters: Map<string, Mounter>, appId: string, path: string = '') => {
|
||||
const appBasePath = mounters.get(appId)?.appRoute
|
||||
? `/${mounters.get(appId)!.appRoute}`
|
||||
: `/app/${appId}`;
|
||||
|
||||
// Only preppend slash if not a hash or query path
|
||||
path = path.startsWith('#') || path.startsWith('?') ? path : `/${path}`;
|
||||
|
||||
return `${appBasePath}${path}`
|
||||
.replace(/\/{2,}/g, '/') // Remove duplicate slashes
|
||||
.replace(/\/$/, ''); // Remove trailing slash
|
||||
};
|
||||
|
||||
const allApplicationsFilter = '__ALL__';
|
||||
|
||||
|
@ -93,7 +102,7 @@ interface AppUpdaterWrapper {
|
|||
* @internal
|
||||
*/
|
||||
export class ApplicationService {
|
||||
private readonly apps = new Map<string, App | LegacyApp>();
|
||||
private readonly apps = new Map<string, App<any> | LegacyApp>();
|
||||
private readonly mounters = new Map<string, Mounter>();
|
||||
private readonly capabilities = new CapabilitiesService();
|
||||
private readonly appLeaveHandlers = new Map<string, AppLeaveHandler>();
|
||||
|
@ -143,7 +152,7 @@ export class ApplicationService {
|
|||
|
||||
return {
|
||||
registerMountContext: this.mountContext!.registerContext,
|
||||
register: (plugin, app) => {
|
||||
register: (plugin, app: App<any>) => {
|
||||
app = { appRoute: `/app/${app.id}`, ...app };
|
||||
|
||||
if (this.registrationClosed) {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
export { ApplicationService } from './application_service';
|
||||
export { Capabilities } from './capabilities';
|
||||
export { ScopedHistory } from './scoped_history';
|
||||
export {
|
||||
App,
|
||||
AppBase,
|
||||
|
|
|
@ -25,15 +25,17 @@ import { AppRouter, AppNotFound } from '../ui';
|
|||
import { EitherApp, MockedMounterMap, MockedMounterTuple } from '../test_types';
|
||||
import { createRenderer, createAppMounter, createLegacyAppMounter, getUnmounter } from './utils';
|
||||
import { AppStatus } from '../types';
|
||||
import { ScopedHistory } from '../scoped_history';
|
||||
|
||||
describe('AppContainer', () => {
|
||||
let mounters: MockedMounterMap<EitherApp>;
|
||||
let history: History;
|
||||
let globalHistory: History;
|
||||
let appStatuses$: BehaviorSubject<Map<string, AppStatus>>;
|
||||
let update: ReturnType<typeof createRenderer>;
|
||||
let scopedAppHistory: History;
|
||||
|
||||
const navigate = (path: string) => {
|
||||
history.push(path);
|
||||
globalHistory.push(path);
|
||||
return update();
|
||||
};
|
||||
const mockMountersToMounters = () =>
|
||||
|
@ -53,20 +55,35 @@ describe('AppContainer', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mounters = new Map([
|
||||
createAppMounter('app1', '<span>App 1</span>'),
|
||||
createAppMounter({ appId: 'app1', html: '<span>App 1</span>' }),
|
||||
createLegacyAppMounter('legacyApp1', jest.fn()),
|
||||
createAppMounter('app2', '<div>App 2</div>'),
|
||||
createAppMounter({ appId: 'app2', html: '<div>App 2</div>' }),
|
||||
createLegacyAppMounter('baseApp:legacyApp2', jest.fn()),
|
||||
createAppMounter('app3', '<div>Chromeless A</div>', '/chromeless-a/path'),
|
||||
createAppMounter('app4', '<div>Chromeless B</div>', '/chromeless-b/path'),
|
||||
createAppMounter('disabledApp', '<div>Disabled app</div>'),
|
||||
createAppMounter({
|
||||
appId: 'app3',
|
||||
html: '<div>Chromeless A</div>',
|
||||
appRoute: '/chromeless-a/path',
|
||||
}),
|
||||
createAppMounter({
|
||||
appId: 'app4',
|
||||
html: '<div>Chromeless B</div>',
|
||||
appRoute: '/chromeless-b/path',
|
||||
}),
|
||||
createAppMounter({ appId: 'disabledApp', html: '<div>Disabled app</div>' }),
|
||||
createLegacyAppMounter('disabledLegacyApp', jest.fn()),
|
||||
createAppMounter({
|
||||
appId: 'scopedApp',
|
||||
extraMountHook: ({ history }) => {
|
||||
scopedAppHistory = history;
|
||||
history.push('/subpath');
|
||||
},
|
||||
}),
|
||||
] as Array<MockedMounterTuple<EitherApp>>);
|
||||
history = createMemoryHistory();
|
||||
globalHistory = createMemoryHistory();
|
||||
appStatuses$ = mountersToAppStatus$();
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={appStatuses$}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
|
@ -177,12 +194,24 @@ describe('AppContainer', () => {
|
|||
});
|
||||
|
||||
it('should not mount when partial route path matches', async () => {
|
||||
mounters.set(...createAppMounter('spaces', '<div>Custom Space</div>', '/spaces/fake-login'));
|
||||
mounters.set(...createAppMounter('login', '<div>Login Page</div>', '/fake-login'));
|
||||
history = createMemoryHistory();
|
||||
mounters.set(
|
||||
...createAppMounter({
|
||||
appId: 'spaces',
|
||||
html: '<div>Custom Space</div>',
|
||||
appRoute: '/spaces/fake-login',
|
||||
})
|
||||
);
|
||||
mounters.set(
|
||||
...createAppMounter({
|
||||
appId: 'login',
|
||||
html: '<div>Login Page</div>',
|
||||
appRoute: '/fake-login',
|
||||
})
|
||||
);
|
||||
globalHistory = createMemoryHistory();
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={mountersToAppStatus$()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
|
@ -196,12 +225,24 @@ describe('AppContainer', () => {
|
|||
});
|
||||
|
||||
it('should not mount when partial route path has higher specificity', async () => {
|
||||
mounters.set(...createAppMounter('login', '<div>Login Page</div>', '/fake-login'));
|
||||
mounters.set(...createAppMounter('spaces', '<div>Custom Space</div>', '/spaces/fake-login'));
|
||||
history = createMemoryHistory();
|
||||
mounters.set(
|
||||
...createAppMounter({
|
||||
appId: 'login',
|
||||
html: '<div>Login Page</div>',
|
||||
appRoute: '/fake-login',
|
||||
})
|
||||
);
|
||||
mounters.set(
|
||||
...createAppMounter({
|
||||
appId: 'spaces',
|
||||
html: '<div>Custom Space</div>',
|
||||
appRoute: '/spaces/fake-login',
|
||||
})
|
||||
);
|
||||
globalHistory = createMemoryHistory();
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={mountersToAppStatus$()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
|
@ -232,17 +273,17 @@ describe('AppContainer', () => {
|
|||
|
||||
// Hitting back button within app does not trigger re-render
|
||||
await navigate('/app/app1/page2');
|
||||
history.goBack();
|
||||
globalHistory.goBack();
|
||||
await update();
|
||||
expect(mounter.mount).toHaveBeenCalledTimes(1);
|
||||
expect(unmount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not remount when when changing pages within app using hash history', async () => {
|
||||
history = createHashHistory();
|
||||
globalHistory = createHashHistory();
|
||||
update = createRenderer(
|
||||
<AppRouter
|
||||
history={history}
|
||||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={mountersToAppStatus$()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
|
@ -269,30 +310,49 @@ describe('AppContainer', () => {
|
|||
expect(unmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('pushes global history changes to inner scoped history', async () => {
|
||||
const scopedApp = mounters.get('scopedApp');
|
||||
await navigate('/app/scopedApp');
|
||||
|
||||
// Verify that internal app's redirect propagated
|
||||
expect(scopedApp?.mounter.mount).toHaveBeenCalledTimes(1);
|
||||
expect(scopedAppHistory.location.pathname).toEqual('/subpath');
|
||||
expect(globalHistory.location.pathname).toEqual('/app/scopedApp/subpath');
|
||||
|
||||
// Simulate user clicking on navlink again to return to app root
|
||||
globalHistory.push('/app/scopedApp');
|
||||
// Should not call mount again
|
||||
expect(scopedApp?.mounter.mount).toHaveBeenCalledTimes(1);
|
||||
expect(scopedApp?.unmount).not.toHaveBeenCalled();
|
||||
// Inner scoped history should be synced
|
||||
expect(scopedAppHistory.location.pathname).toEqual('');
|
||||
|
||||
// Make sure going back to subpath works
|
||||
globalHistory.goBack();
|
||||
expect(scopedApp?.mounter.mount).toHaveBeenCalledTimes(1);
|
||||
expect(scopedApp?.unmount).not.toHaveBeenCalled();
|
||||
expect(scopedAppHistory.location.pathname).toEqual('/subpath');
|
||||
expect(globalHistory.location.pathname).toEqual('/app/scopedApp/subpath');
|
||||
});
|
||||
|
||||
it('calls legacy mount handler', async () => {
|
||||
await navigate('/app/legacyApp1');
|
||||
expect(mounters.get('legacyApp1')!.mounter.mount.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"appBasePath": "/app/legacyApp1",
|
||||
"element": <div />,
|
||||
"onAppLeave": [Function],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(mounters.get('legacyApp1')!.mounter.mount.mock.calls[0][0]).toMatchObject({
|
||||
appBasePath: '/app/legacyApp1',
|
||||
element: expect.any(HTMLDivElement),
|
||||
onAppLeave: expect.any(Function),
|
||||
history: expect.any(ScopedHistory),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles legacy apps with subapps', async () => {
|
||||
await navigate('/app/baseApp');
|
||||
expect(mounters.get('baseApp:legacyApp2')!.mounter.mount.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"appBasePath": "/app/baseApp",
|
||||
"element": <div />,
|
||||
"onAppLeave": [Function],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(mounters.get('baseApp:legacyApp2')!.mounter.mount.mock.calls[0][0]).toMatchObject({
|
||||
appBasePath: '/app/baseApp',
|
||||
element: expect.any(HTMLDivElement),
|
||||
onAppLeave: expect.any(Function),
|
||||
history: expect.any(ScopedHistory),
|
||||
});
|
||||
});
|
||||
|
||||
it('displays error page if no app is found', async () => {
|
||||
|
|
|
@ -40,11 +40,17 @@ export const createRenderer = (element: ReactElement | null): Renderer => {
|
|||
});
|
||||
};
|
||||
|
||||
export const createAppMounter = (
|
||||
appId: string,
|
||||
html: string,
|
||||
appRoute = `/app/${appId}`
|
||||
): MockedMounterTuple<App> => {
|
||||
export const createAppMounter = ({
|
||||
appId,
|
||||
html = `<div>App ${appId}</div>`,
|
||||
appRoute = `/app/${appId}`,
|
||||
extraMountHook,
|
||||
}: {
|
||||
appId: string;
|
||||
html?: string;
|
||||
appRoute?: string;
|
||||
extraMountHook?: (params: AppMountParameters) => void;
|
||||
}): MockedMounterTuple<App> => {
|
||||
const unmount = jest.fn();
|
||||
return [
|
||||
appId,
|
||||
|
@ -52,11 +58,15 @@ export const createAppMounter = (
|
|||
mounter: {
|
||||
appRoute,
|
||||
appBasePath: appRoute,
|
||||
mount: jest.fn(async ({ appBasePath: basename, element }: AppMountParameters) => {
|
||||
mount: jest.fn(async (params: AppMountParameters) => {
|
||||
const { appBasePath: basename, element } = params;
|
||||
Object.assign(element, {
|
||||
innerHTML: `<div>\nbasename: ${basename}\nhtml: ${html}\n</div>`,
|
||||
});
|
||||
unmount.mockImplementation(() => Object.assign(element, { innerHTML: '' }));
|
||||
if (extraMountHook) {
|
||||
extraMountHook(params);
|
||||
}
|
||||
return unmount;
|
||||
}),
|
||||
},
|
||||
|
|
57
src/core/public/application/scoped_history.mock.ts
Normal file
57
src/core/public/application/scoped_history.mock.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { Location } from 'history';
|
||||
import { ScopedHistory } from './scoped_history';
|
||||
|
||||
type ScopedHistoryMock = jest.Mocked<Pick<ScopedHistory, keyof ScopedHistory>>;
|
||||
const createMock = ({
|
||||
pathname = '/',
|
||||
search = '',
|
||||
hash = '',
|
||||
key,
|
||||
state,
|
||||
}: Partial<Location> = {}) => {
|
||||
const mock: ScopedHistoryMock = {
|
||||
block: jest.fn(),
|
||||
createHref: jest.fn(),
|
||||
createSubHistory: jest.fn(),
|
||||
go: jest.fn(),
|
||||
goBack: jest.fn(),
|
||||
goForward: jest.fn(),
|
||||
listen: jest.fn(),
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
action: 'PUSH',
|
||||
length: 1,
|
||||
location: {
|
||||
pathname,
|
||||
search,
|
||||
state,
|
||||
hash,
|
||||
key,
|
||||
},
|
||||
};
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
export const scopedHistoryMock = {
|
||||
create: createMock,
|
||||
};
|
329
src/core/public/application/scoped_history.test.ts
Normal file
329
src/core/public/application/scoped_history.test.ts
Normal file
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* 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 { ScopedHistory } from './scoped_history';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
describe('ScopedHistory', () => {
|
||||
describe('construction', () => {
|
||||
it('succeeds if current location matches basePath', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
expect(() => new ScopedHistory(gh, '/app/wow')).not.toThrow();
|
||||
gh.push('/app/wow/');
|
||||
expect(() => new ScopedHistory(gh, '/app/wow')).not.toThrow();
|
||||
gh.push('/app/wow/sub-page');
|
||||
expect(() => new ScopedHistory(gh, '/app/wow')).not.toThrow();
|
||||
});
|
||||
|
||||
it('fails if current location does not match basePath', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/other');
|
||||
expect(() => new ScopedHistory(gh, '/app/wow')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Browser location [/app/other] is not currently in expected basePath [/app/wow]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigation', () => {
|
||||
it('supports push', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const pushSpy = jest.spyOn(gh, 'push');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
h.push('/new-page', { some: 'state' });
|
||||
expect(pushSpy).toHaveBeenCalledWith('/app/wow/new-page', { some: 'state' });
|
||||
expect(gh.length).toBe(3); // ['', '/app/wow', '/app/wow/new-page']
|
||||
expect(h.length).toBe(2);
|
||||
});
|
||||
|
||||
it('supports unbound push', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const pushSpy = jest.spyOn(gh, 'push');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
const { push } = h;
|
||||
push('/new-page', { some: 'state' });
|
||||
expect(pushSpy).toHaveBeenCalledWith('/app/wow/new-page', { some: 'state' });
|
||||
expect(gh.length).toBe(3); // ['', '/app/wow', '/app/wow/new-page']
|
||||
expect(h.length).toBe(2);
|
||||
});
|
||||
|
||||
it('supports replace', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const replaceSpy = jest.spyOn(gh, 'replace');
|
||||
const h = new ScopedHistory(gh, '/app/wow'); // ['']
|
||||
h.push('/first-page'); // ['', '/first-page']
|
||||
h.push('/second-page'); // ['', '/first-page', '/second-page']
|
||||
h.goBack(); // ['', '/first-page', '/second-page']
|
||||
h.replace('/first-page-replacement', { some: 'state' }); // ['', '/first-page-replacement', '/second-page']
|
||||
expect(replaceSpy).toHaveBeenCalledWith('/app/wow/first-page-replacement', { some: 'state' });
|
||||
expect(h.length).toBe(3);
|
||||
expect(gh.length).toBe(4); // ['', '/app/wow', '/app/wow/first-page-replacement', '/app/wow/second-page']
|
||||
});
|
||||
|
||||
it('hides previous stack', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/alpha');
|
||||
gh.push('/app/beta');
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
expect(h.length).toBe(1);
|
||||
expect(h.location.pathname).toEqual('');
|
||||
});
|
||||
|
||||
it('cannot go back further than local stack', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/alpha');
|
||||
gh.push('/app/beta');
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
h.push('/new-page');
|
||||
expect(h.length).toBe(2);
|
||||
expect(h.location.pathname).toEqual('/new-page');
|
||||
|
||||
// Test first back
|
||||
h.goBack();
|
||||
expect(h.length).toBe(2);
|
||||
expect(h.location.pathname).toEqual('');
|
||||
expect(gh.location.pathname).toEqual('/app/wow');
|
||||
|
||||
// Second back should be no-op
|
||||
h.goBack();
|
||||
expect(h.length).toBe(2);
|
||||
expect(h.location.pathname).toEqual('');
|
||||
expect(gh.location.pathname).toEqual('/app/wow');
|
||||
});
|
||||
|
||||
it('cannot go forward further than local stack', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/alpha');
|
||||
gh.push('/app/beta');
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
h.push('/new-page');
|
||||
expect(h.length).toBe(2);
|
||||
|
||||
// Go back so we can go forward
|
||||
h.goBack();
|
||||
expect(h.length).toBe(2);
|
||||
expect(h.location.pathname).toEqual('');
|
||||
expect(gh.location.pathname).toEqual('/app/wow');
|
||||
|
||||
// Going forward should increase length and return to /new-page
|
||||
h.goForward();
|
||||
expect(h.length).toBe(2);
|
||||
expect(h.location.pathname).toEqual('/new-page');
|
||||
expect(gh.location.pathname).toEqual('/app/wow/new-page');
|
||||
|
||||
// Second forward should be no-op
|
||||
h.goForward();
|
||||
expect(h.length).toBe(2);
|
||||
expect(h.location.pathname).toEqual('/new-page');
|
||||
expect(gh.location.pathname).toEqual('/app/wow/new-page');
|
||||
});
|
||||
|
||||
it('reacts to navigations from parent history', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
h.push('/page-1');
|
||||
h.push('/page-2');
|
||||
|
||||
gh.goBack();
|
||||
expect(h.location.pathname).toEqual('/page-1');
|
||||
expect(h.length).toBe(3);
|
||||
|
||||
gh.goForward();
|
||||
expect(h.location.pathname).toEqual('/page-2');
|
||||
expect(h.length).toBe(3);
|
||||
|
||||
// Go back to /app/wow and push a new location
|
||||
gh.goBack();
|
||||
gh.goBack();
|
||||
gh.push('/app/wow/page-3');
|
||||
expect(h.location.pathname).toEqual('/page-3');
|
||||
expect(h.length).toBe(2); // ['', '/page-3']
|
||||
});
|
||||
|
||||
it('increments length on push and removes length when going back and then pushing', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
expect(gh.length).toBe(2);
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
expect(h.length).toBe(1);
|
||||
h.push('/page1');
|
||||
expect(h.length).toBe(2);
|
||||
h.push('/page2');
|
||||
expect(h.length).toBe(3);
|
||||
h.push('/page3');
|
||||
expect(h.length).toBe(4);
|
||||
h.push('/page4');
|
||||
expect(h.length).toBe(5);
|
||||
h.push('/page5');
|
||||
expect(h.length).toBe(6);
|
||||
h.goBack(); // back/forward should not reduce the length
|
||||
expect(h.length).toBe(6);
|
||||
h.goBack();
|
||||
expect(h.length).toBe(6);
|
||||
h.push('/page6'); // length should only change if a new location is pushed from a point further back in the history
|
||||
expect(h.length).toBe(5);
|
||||
h.goBack();
|
||||
expect(h.length).toBe(5);
|
||||
h.goBack();
|
||||
expect(h.length).toBe(5);
|
||||
h.push('/page7');
|
||||
expect(h.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('teardown behavior', () => {
|
||||
it('throws exceptions after falling out of scope', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
expect(gh.length).toBe(2);
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
gh.push('/app/other');
|
||||
expect(() => h.location).toThrowErrorMatchingInlineSnapshot(
|
||||
`"ScopedHistory instance has fell out of navigation scope for basePath: /app/wow"`
|
||||
);
|
||||
expect(() => h.push('/new-page')).toThrow();
|
||||
expect(() => h.replace('/new-page')).toThrow();
|
||||
expect(() => h.goBack()).toThrow();
|
||||
expect(() => h.goForward()).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listen', () => {
|
||||
it('calls callback with scoped location', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
const listenPaths: string[] = [];
|
||||
h.listen(l => listenPaths.push(l.pathname));
|
||||
h.push('/first-page');
|
||||
h.push('/second-page');
|
||||
h.push('/third-page');
|
||||
h.go(-2);
|
||||
h.goForward();
|
||||
expect(listenPaths).toEqual([
|
||||
'/first-page',
|
||||
'/second-page',
|
||||
'/third-page',
|
||||
'/first-page',
|
||||
'/second-page',
|
||||
]);
|
||||
});
|
||||
|
||||
it('stops calling callback after unlisten is called', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
const listenPaths: string[] = [];
|
||||
const unlisten = h.listen(l => listenPaths.push(l.pathname));
|
||||
h.push('/first-page');
|
||||
unlisten();
|
||||
h.push('/second-page');
|
||||
h.push('/third-page');
|
||||
h.go(-2);
|
||||
h.goForward();
|
||||
expect(listenPaths).toEqual(['/first-page']);
|
||||
});
|
||||
|
||||
it('stops calling callback after browser leaves scope', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
const listenPaths: string[] = [];
|
||||
h.listen(l => listenPaths.push(l.pathname));
|
||||
h.push('/first-page');
|
||||
gh.push('/app/other');
|
||||
gh.push('/second-page');
|
||||
gh.push('/third-page');
|
||||
gh.go(-2);
|
||||
gh.goForward();
|
||||
expect(listenPaths).toEqual(['/first-page']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createHref', () => {
|
||||
it('creates scoped hrefs', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
expect(h.createHref({ pathname: '' })).toEqual(`/`);
|
||||
expect(h.createHref({ pathname: '/new-page', search: '?alpha=true' })).toEqual(
|
||||
`/new-page?alpha=true`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action', () => {
|
||||
it('provides last history action', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
gh.push('/alpha');
|
||||
gh.goBack();
|
||||
const h = new ScopedHistory(gh, '/app/wow');
|
||||
expect(h.action).toBe('POP');
|
||||
gh.push('/app/wow/page-1');
|
||||
expect(h.action).toBe('PUSH');
|
||||
h.replace('/page-2');
|
||||
expect(h.action).toBe('REPLACE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSubHistory', () => {
|
||||
it('supports push', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const ghPushSpy = jest.spyOn(gh, 'push');
|
||||
const h1 = new ScopedHistory(gh, '/app/wow');
|
||||
h1.push('/new-page');
|
||||
const h1PushSpy = jest.spyOn(h1, 'push');
|
||||
const h2 = h1.createSubHistory('/new-page');
|
||||
h2.push('/sub-page', { some: 'state' });
|
||||
expect(h1PushSpy).toHaveBeenCalledWith('/new-page/sub-page', { some: 'state' });
|
||||
expect(ghPushSpy).toHaveBeenCalledWith('/app/wow/new-page/sub-page', { some: 'state' });
|
||||
expect(h2.length).toBe(2);
|
||||
expect(h1.length).toBe(3);
|
||||
expect(gh.length).toBe(4);
|
||||
});
|
||||
|
||||
it('supports replace', () => {
|
||||
const gh = createMemoryHistory();
|
||||
gh.push('/app/wow');
|
||||
const ghReplaceSpy = jest.spyOn(gh, 'replace');
|
||||
const h1 = new ScopedHistory(gh, '/app/wow');
|
||||
h1.push('/new-page');
|
||||
const h1ReplaceSpy = jest.spyOn(h1, 'replace');
|
||||
const h2 = h1.createSubHistory('/new-page');
|
||||
h2.push('/sub-page');
|
||||
h2.replace('/other-sub-page', { some: 'state' });
|
||||
expect(h1ReplaceSpy).toHaveBeenCalledWith('/new-page/other-sub-page', { some: 'state' });
|
||||
expect(ghReplaceSpy).toHaveBeenCalledWith('/app/wow/new-page/other-sub-page', {
|
||||
some: 'state',
|
||||
});
|
||||
expect(h2.length).toBe(2);
|
||||
expect(h1.length).toBe(3);
|
||||
expect(gh.length).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
318
src/core/public/application/scoped_history.ts
Normal file
318
src/core/public/application/scoped_history.ts
Normal file
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
* 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 {
|
||||
History,
|
||||
Path,
|
||||
LocationDescriptorObject,
|
||||
TransitionPromptHook,
|
||||
UnregisterCallback,
|
||||
LocationListener,
|
||||
Location,
|
||||
Href,
|
||||
Action,
|
||||
} from 'history';
|
||||
|
||||
/**
|
||||
* A wrapper around a `History` instance that is scoped to a particular base path of the history stack. Behaves
|
||||
* similarly to the `basename` option except that this wrapper hides any history stack entries from outside the scope
|
||||
* of this base path.
|
||||
*
|
||||
* This wrapper also allows Core and Plugins to share a single underlying global `History` instance without exposing
|
||||
* the history of other applications.
|
||||
*
|
||||
* The {@link ScopedHistory.createSubHistory | createSubHistory} method is particularly useful for applications that
|
||||
* contain any number of "sub-apps" which should not have access to the main application's history or basePath.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ScopedHistory<HistoryLocationState = unknown>
|
||||
implements History<HistoryLocationState> {
|
||||
/**
|
||||
* Tracks whether or not the user has left this history's scope. All methods throw errors if called after scope has
|
||||
* been left.
|
||||
*/
|
||||
private isActive = true;
|
||||
/**
|
||||
* All active listeners on this history instance.
|
||||
*/
|
||||
private listeners = new Set<LocationListener<HistoryLocationState>>();
|
||||
/**
|
||||
* Array of the local history stack. Only stores {@link Location.key} to use tracking an index of the current
|
||||
* position of the window in the history stack.
|
||||
*/
|
||||
private locationKeys: Array<string | undefined> = [];
|
||||
/**
|
||||
* The key of the current position of the window in the history stack.
|
||||
*/
|
||||
private currentLocationKeyIndex: number = 0;
|
||||
|
||||
constructor(private readonly parentHistory: History, private readonly basePath: string) {
|
||||
const parentPath = this.parentHistory.location.pathname;
|
||||
if (!parentPath.startsWith(basePath)) {
|
||||
throw new Error(
|
||||
`Browser location [${parentPath}] is not currently in expected basePath [${basePath}]`
|
||||
);
|
||||
}
|
||||
|
||||
this.locationKeys.push(this.parentHistory.location.key);
|
||||
this.setupHistoryListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `ScopedHistory` for a subpath of this `ScopedHistory`. Useful for applications that may have sub-apps
|
||||
* that do not need access to the containing application's history.
|
||||
*
|
||||
* @param basePath the URL path scope for the sub history
|
||||
*/
|
||||
public createSubHistory = <SubHistoryLocationState = unknown>(
|
||||
basePath: string
|
||||
): ScopedHistory<SubHistoryLocationState> => {
|
||||
return new ScopedHistory<SubHistoryLocationState>(this, basePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* The number of entries in the history stack, including all entries forwards and backwards from the current location.
|
||||
*/
|
||||
public get length() {
|
||||
this.verifyActive();
|
||||
return this.locationKeys.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current location of the history stack.
|
||||
*/
|
||||
public get location() {
|
||||
this.verifyActive();
|
||||
return this.stripBasePath(this.parentHistory.location);
|
||||
}
|
||||
|
||||
/**
|
||||
* The last action dispatched on the history stack.
|
||||
*/
|
||||
public get action() {
|
||||
this.verifyActive();
|
||||
return this.parentHistory.action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a new location onto the history stack. If there are forward entries in the stack, they will be removed.
|
||||
*
|
||||
* @param pathOrLocation a string or location descriptor
|
||||
* @param state
|
||||
*/
|
||||
public push = (
|
||||
pathOrLocation: Path | LocationDescriptorObject<HistoryLocationState>,
|
||||
state?: HistoryLocationState
|
||||
): void => {
|
||||
this.verifyActive();
|
||||
if (typeof pathOrLocation === 'string') {
|
||||
this.parentHistory.push(this.prependBasePath(pathOrLocation), state);
|
||||
} else {
|
||||
this.parentHistory.push(this.prependBasePath(pathOrLocation));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces the current location in the history stack. Does not remove forward or backward entries.
|
||||
*
|
||||
* @param pathOrLocation a string or location descriptor
|
||||
* @param state
|
||||
*/
|
||||
public replace = (
|
||||
pathOrLocation: Path | LocationDescriptorObject<HistoryLocationState>,
|
||||
state?: HistoryLocationState
|
||||
): void => {
|
||||
this.verifyActive();
|
||||
if (typeof pathOrLocation === 'string') {
|
||||
this.parentHistory.replace(this.prependBasePath(pathOrLocation), state);
|
||||
} else {
|
||||
this.parentHistory.replace(this.prependBasePath(pathOrLocation));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send the user forward or backwards in the history stack.
|
||||
*
|
||||
* @param n number of positions in the stack to go. Negative numbers indicate number of entries backward, positive
|
||||
* numbers for forwards. If passed 0, the current location will be reloaded. If `n` exceeds the number of
|
||||
* entries available, this is a no-op.
|
||||
*/
|
||||
public go = (n: number): void => {
|
||||
this.verifyActive();
|
||||
if (n === 0) {
|
||||
this.parentHistory.go(n);
|
||||
} else if (n < 0) {
|
||||
if (this.currentLocationKeyIndex + 1 + n >= 1) {
|
||||
this.parentHistory.go(n);
|
||||
}
|
||||
} else if (n <= this.currentLocationKeyIndex + this.locationKeys.length - 1) {
|
||||
this.parentHistory.go(n);
|
||||
}
|
||||
// no-op if no conditions above are met
|
||||
};
|
||||
|
||||
/**
|
||||
* Send the user one location back in the history stack. Equivalent to calling
|
||||
* {@link ScopedHistory.go | ScopedHistory.go(-1)}. If no more entries are available backwards, this is a no-op.
|
||||
*/
|
||||
public goBack = () => {
|
||||
this.verifyActive();
|
||||
this.go(-1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send the user one location forward in the history stack. Equivalent to calling
|
||||
* {@link ScopedHistory.go | ScopedHistory.go(1)}. If no more entries are available forwards, this is a no-op.
|
||||
*/
|
||||
public goForward = () => {
|
||||
this.verifyActive();
|
||||
this.go(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public block = (
|
||||
prompt?: boolean | string | TransitionPromptHook<HistoryLocationState>
|
||||
): UnregisterCallback => {
|
||||
throw new Error(
|
||||
`history.block is not supported. Please use the AppMountParams.onAppLeave API.`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a listener for location updates.
|
||||
*
|
||||
* @param listener a function that receives location updates.
|
||||
* @returns an function to unsubscribe the listener.
|
||||
*/
|
||||
public listen = (
|
||||
listener: (location: Location<HistoryLocationState>, action: Action) => void
|
||||
): UnregisterCallback => {
|
||||
this.verifyActive();
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an href (string) to the location.
|
||||
*
|
||||
* @param location
|
||||
*/
|
||||
public createHref = (location: LocationDescriptorObject<HistoryLocationState>): Href => {
|
||||
this.verifyActive();
|
||||
return this.parentHistory.createHref(location);
|
||||
};
|
||||
|
||||
private prependBasePath(path: Path): Path;
|
||||
private prependBasePath(
|
||||
location: LocationDescriptorObject<HistoryLocationState>
|
||||
): LocationDescriptorObject<HistoryLocationState>;
|
||||
/**
|
||||
* Prepends the scoped base path to the Path or Location
|
||||
*/
|
||||
private prependBasePath(
|
||||
pathOrLocation: Path | LocationDescriptorObject<HistoryLocationState>
|
||||
): Path | LocationDescriptorObject<HistoryLocationState> {
|
||||
if (typeof pathOrLocation === 'string') {
|
||||
return this.prependBasePathToString(pathOrLocation);
|
||||
} else {
|
||||
return {
|
||||
...pathOrLocation,
|
||||
pathname:
|
||||
pathOrLocation.pathname !== undefined
|
||||
? this.prependBasePathToString(pathOrLocation.pathname)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends the base path to string.
|
||||
*/
|
||||
private prependBasePathToString(path: string): string {
|
||||
path = path.startsWith('/') ? path.slice(1) : path;
|
||||
return path.length ? `${this.basePath}/${path}` : this.basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the base path from a location.
|
||||
*/
|
||||
private stripBasePath(location: Location<HistoryLocationState>): Location<HistoryLocationState> {
|
||||
return {
|
||||
...location,
|
||||
pathname: location.pathname.replace(new RegExp(`^${this.basePath}`), ''),
|
||||
};
|
||||
}
|
||||
|
||||
/** Called on each public method to ensure that we have not fallen out of scope yet. */
|
||||
private verifyActive() {
|
||||
if (!this.isActive) {
|
||||
throw new Error(
|
||||
`ScopedHistory instance has fell out of navigation scope for basePath: ${this.basePath}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the listener on the parent history instance used to follow navigation updates and track our internal
|
||||
* state. Also forwards events to child listeners with the base path stripped from the location.
|
||||
*/
|
||||
private setupHistoryListener() {
|
||||
const unlisten = this.parentHistory.listen((location, action) => {
|
||||
// If the user navigates outside the scope of this basePath, tear it down.
|
||||
if (!location.pathname.startsWith(this.basePath)) {
|
||||
unlisten();
|
||||
this.isActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track location keys using the same algorithm the browser uses internally.
|
||||
* - On PUSH, remove all items that came after the current location and append the new location.
|
||||
* - On POP, set the current location, but do not change the entries.
|
||||
* - On REPLACE, override the location for the current index with the new location.
|
||||
*/
|
||||
if (action === 'PUSH') {
|
||||
this.locationKeys = [
|
||||
...this.locationKeys.slice(0, this.currentLocationKeyIndex + 1),
|
||||
location.key,
|
||||
];
|
||||
this.currentLocationKeyIndex = this.locationKeys.indexOf(location.key); // should always be the last index
|
||||
} else if (action === 'POP') {
|
||||
this.currentLocationKeyIndex = this.locationKeys.indexOf(location.key);
|
||||
} else if (action === 'REPLACE') {
|
||||
this.locationKeys[this.currentLocationKeyIndex] = location.key;
|
||||
} else {
|
||||
throw new Error(`Unrecognized history action: ${action}`);
|
||||
}
|
||||
|
||||
[...this.listeners].forEach(listener => {
|
||||
listener(this.stripBasePath(location), action);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ import { IUiSettingsClient } from '../ui_settings';
|
|||
import { RecursiveReadonly } from '../../utils';
|
||||
import { SavedObjectsStart } from '../saved_objects';
|
||||
import { AppCategory } from '../../types';
|
||||
import { ScopedHistory } from './scoped_history';
|
||||
|
||||
/** @public */
|
||||
export interface AppBase {
|
||||
|
@ -199,7 +200,7 @@ export type AppUpdater = (app: AppBase) => Partial<AppUpdatableFields> | undefin
|
|||
* Extension of {@link AppBase | common app properties} with the mount function.
|
||||
* @public
|
||||
*/
|
||||
export interface App extends AppBase {
|
||||
export interface App<HistoryLocationState = unknown> extends AppBase {
|
||||
/**
|
||||
* A mount function called when the user navigates to this app's route. May have signature of {@link AppMount} or
|
||||
* {@link AppMountDeprecated}.
|
||||
|
@ -208,7 +209,7 @@ export interface App extends AppBase {
|
|||
* When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument.
|
||||
* This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}.
|
||||
*/
|
||||
mount: AppMount | AppMountDeprecated;
|
||||
mount: AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState>;
|
||||
|
||||
/**
|
||||
* Hide the UI chrome when the application is mounted. Defaults to `false`.
|
||||
|
@ -240,7 +241,9 @@ export interface LegacyApp extends AppBase {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppMount = (params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
|
||||
export type AppMount<HistoryLocationState = unknown> = (
|
||||
params: AppMountParameters<HistoryLocationState>
|
||||
) => AppUnmount | Promise<AppUnmount>;
|
||||
|
||||
/**
|
||||
* A mount function called when the user navigates to this app's route.
|
||||
|
@ -256,9 +259,9 @@ export type AppMount = (params: AppMountParameters) => AppUnmount | Promise<AppU
|
|||
* @deprecated
|
||||
* @public
|
||||
*/
|
||||
export type AppMountDeprecated = (
|
||||
export type AppMountDeprecated<HistoryLocationState = unknown> = (
|
||||
context: AppMountContext,
|
||||
params: AppMountParameters
|
||||
params: AppMountParameters<HistoryLocationState>
|
||||
) => AppUnmount | Promise<AppUnmount>;
|
||||
|
||||
/**
|
||||
|
@ -304,16 +307,65 @@ export interface AppMountContext {
|
|||
}
|
||||
|
||||
/** @public */
|
||||
export interface AppMountParameters {
|
||||
export interface AppMountParameters<HistoryLocationState = unknown> {
|
||||
/**
|
||||
* The container element to render the application into.
|
||||
*/
|
||||
element: HTMLElement;
|
||||
|
||||
/**
|
||||
* A scoped history instance for your application. Should be used to wire up
|
||||
* your applications Router.
|
||||
*
|
||||
* @example
|
||||
* How to configure react-router with a base path:
|
||||
*
|
||||
* ```ts
|
||||
* // inside your plugin's setup function
|
||||
* export class MyPlugin implements Plugin {
|
||||
* setup({ application }) {
|
||||
* application.register({
|
||||
* id: 'my-app',
|
||||
* appRoute: '/my-app',
|
||||
* async mount(params) {
|
||||
* const { renderApp } = await import('./application');
|
||||
* return renderApp(params);
|
||||
* },
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* // application.tsx
|
||||
* import React from 'react';
|
||||
* import ReactDOM from 'react-dom';
|
||||
* import { Router, Route } from 'react-router-dom';
|
||||
*
|
||||
* import { CoreStart, AppMountParameters } from 'src/core/public';
|
||||
* import { MyPluginDepsStart } from './plugin';
|
||||
*
|
||||
* export renderApp = ({ element, history }: AppMountParameters) => {
|
||||
* ReactDOM.render(
|
||||
* // pass `appBasePath` to `basename`
|
||||
* <Router history={history}>
|
||||
* <Route path="/" exact component={HomePage} />
|
||||
* </Router>,
|
||||
* element
|
||||
* );
|
||||
*
|
||||
* return () => ReactDOM.unmountComponentAtNode(element);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
history: ScopedHistory<HistoryLocationState>;
|
||||
|
||||
/**
|
||||
* The route path for configuring navigation to the application.
|
||||
* This string should not include the base path from HTTP.
|
||||
*
|
||||
* @deprecated Use {@link AppMountParameters.history} instead.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* How to configure react-router with a base path:
|
||||
|
@ -340,10 +392,10 @@ export interface AppMountParameters {
|
|||
* import ReactDOM from 'react-dom';
|
||||
* import { BrowserRouter, Route } from 'react-router-dom';
|
||||
*
|
||||
* import { CoreStart, AppMountParams } from 'src/core/public';
|
||||
* import { CoreStart, AppMountParameters } from 'src/core/public';
|
||||
* import { MyPluginDepsStart } from './plugin';
|
||||
*
|
||||
* export renderApp = ({ appBasePath, element }: AppMountParams) => {
|
||||
* export renderApp = ({ appBasePath, element }: AppMountParameters) => {
|
||||
* ReactDOM.render(
|
||||
* // pass `appBasePath` to `basename`
|
||||
* <BrowserRouter basename={appBasePath}>
|
||||
|
@ -498,8 +550,9 @@ export interface ApplicationSetup {
|
|||
/**
|
||||
* Register an mountable application to the system.
|
||||
* @param app - an {@link App}
|
||||
* @typeParam HistoryLocationState - shape of the `History` state on {@link AppMountParameters.history}, defaults to `unknown`.
|
||||
*/
|
||||
register(app: App): void;
|
||||
register<HistoryLocationState = unknown>(app: App<HistoryLocationState>): void;
|
||||
|
||||
/**
|
||||
* Register an application updater that can be used to change the {@link AppUpdatableFields} fields
|
||||
|
@ -551,7 +604,10 @@ export interface InternalApplicationSetup extends Pick<ApplicationSetup, 'regist
|
|||
* @param plugin - opaque ID of the plugin that registers this application
|
||||
* @param app
|
||||
*/
|
||||
register(plugin: PluginOpaqueId, app: App): void;
|
||||
register<HistoryLocationState = unknown>(
|
||||
plugin: PluginOpaqueId,
|
||||
app: App<HistoryLocationState>
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Register metadata about legacy applications. Legacy apps will not be mounted when navigated to.
|
||||
|
|
|
@ -22,6 +22,8 @@ import { mount } from 'enzyme';
|
|||
|
||||
import { AppContainer } from './app_container';
|
||||
import { Mounter, AppMountParameters, AppStatus } from '../types';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { ScopedHistory } from '../scoped_history';
|
||||
|
||||
describe('AppContainer', () => {
|
||||
const appId = 'someApp';
|
||||
|
@ -60,10 +62,15 @@ describe('AppContainer', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<AppContainer
|
||||
appPath={`/app/${appId}`}
|
||||
appId={appId}
|
||||
appStatus={AppStatus.inaccessible}
|
||||
mounter={mounter}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
createScopedHistory={(appPath: string) =>
|
||||
// Create a history using the appPath as the current location
|
||||
new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -28,18 +28,24 @@ import React, {
|
|||
|
||||
import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types';
|
||||
import { AppNotFound } from './app_not_found_screen';
|
||||
import { ScopedHistory } from '../scoped_history';
|
||||
|
||||
interface Props {
|
||||
/** Path application is mounted on without the global basePath */
|
||||
appPath: string;
|
||||
appId: string;
|
||||
mounter?: Mounter;
|
||||
appStatus: AppStatus;
|
||||
setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void;
|
||||
createScopedHistory: (appUrl: string) => ScopedHistory;
|
||||
}
|
||||
|
||||
export const AppContainer: FunctionComponent<Props> = ({
|
||||
mounter,
|
||||
appId,
|
||||
appPath,
|
||||
setAppLeaveHandler,
|
||||
createScopedHistory,
|
||||
appStatus,
|
||||
}: Props) => {
|
||||
const [appNotFound, setAppNotFound] = useState(false);
|
||||
|
@ -67,6 +73,7 @@ export const AppContainer: FunctionComponent<Props> = ({
|
|||
unmountRef.current =
|
||||
(await mounter.mount({
|
||||
appBasePath: mounter.appBasePath,
|
||||
history: createScopedHistory(appPath),
|
||||
element: elementRef.current!,
|
||||
onAppLeave: handler => setAppLeaveHandler(appId, handler),
|
||||
})) || null;
|
||||
|
@ -75,7 +82,7 @@ export const AppContainer: FunctionComponent<Props> = ({
|
|||
mount();
|
||||
|
||||
return unmount;
|
||||
}, [appId, appStatus, mounter, setAppLeaveHandler]);
|
||||
}, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom';
|
||||
import { History } from 'history';
|
||||
import { Observable } from 'rxjs';
|
||||
|
@ -25,6 +25,7 @@ import { useObservable } from 'react-use';
|
|||
|
||||
import { AppLeaveHandler, AppStatus, Mounter } from '../types';
|
||||
import { AppContainer } from './app_container';
|
||||
import { ScopedHistory } from '../scoped_history';
|
||||
|
||||
interface Props {
|
||||
mounters: Map<string, Mounter>;
|
||||
|
@ -44,6 +45,11 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
appStatuses$,
|
||||
}) => {
|
||||
const appStatuses = useObservable(appStatuses$, new Map());
|
||||
const createScopedHistory = useMemo(
|
||||
() => (appPath: string) => new ScopedHistory(history, appPath),
|
||||
[history]
|
||||
);
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
|
@ -56,12 +62,12 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
<Route
|
||||
key={mounter.appRoute}
|
||||
path={mounter.appRoute}
|
||||
render={() => (
|
||||
render={({ match: { url } }) => (
|
||||
<AppContainer
|
||||
mounter={mounter}
|
||||
appId={appId}
|
||||
appPath={url}
|
||||
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
createScopedHistory={createScopedHistory}
|
||||
{...{ appId, mounter, setAppLeaveHandler }}
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
|
@ -72,6 +78,7 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
render={({
|
||||
match: {
|
||||
params: { appId },
|
||||
url,
|
||||
},
|
||||
}: RouteComponentProps<Params>) => {
|
||||
// Find the mounter including legacy mounters with subapps:
|
||||
|
@ -81,10 +88,11 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
|
||||
return (
|
||||
<AppContainer
|
||||
mounter={mounter}
|
||||
appPath={url}
|
||||
appId={id}
|
||||
appStatus={appStatuses.get(id) ?? AppStatus.inaccessible}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
createScopedHistory={createScopedHistory}
|
||||
{...{ mounter, setAppLeaveHandler }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -109,6 +109,7 @@ export {
|
|||
AppNavLinkStatus,
|
||||
AppUpdatableFields,
|
||||
AppUpdater,
|
||||
ScopedHistory,
|
||||
} from './application';
|
||||
|
||||
export {
|
||||
|
|
|
@ -41,6 +41,7 @@ export { notificationServiceMock } from './notifications/notifications_service.m
|
|||
export { overlayServiceMock } from './overlays/overlay_service.mock';
|
||||
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
|
||||
export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
|
||||
export { scopedHistoryMock } from './application/scoped_history.mock';
|
||||
|
||||
function createCoreSetupMock({ basePath = '' } = {}) {
|
||||
const mock = {
|
||||
|
|
|
@ -4,25 +4,30 @@
|
|||
|
||||
```ts
|
||||
|
||||
import { Action } from 'history';
|
||||
import { Breadcrumb } from '@elastic/eui';
|
||||
import { EuiButtonEmptyProps } from '@elastic/eui';
|
||||
import { EuiConfirmModalProps } from '@elastic/eui';
|
||||
import { EuiGlobalToastListToast } from '@elastic/eui';
|
||||
import { ExclusiveUnion } from '@elastic/eui';
|
||||
import { History } from 'history';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import { LocationDescriptorObject } from 'history';
|
||||
import { MaybePromise } from '@kbn/utility-types';
|
||||
import { Observable } from 'rxjs';
|
||||
import React from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
import { ShallowPromise } from '@kbn/utility-types';
|
||||
import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types';
|
||||
import { UnregisterCallback } from 'history';
|
||||
import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types';
|
||||
|
||||
// @public
|
||||
export interface App extends AppBase {
|
||||
export interface App<HistoryLocationState = unknown> extends AppBase {
|
||||
appRoute?: string;
|
||||
chromeless?: boolean;
|
||||
mount: AppMount | AppMountDeprecated;
|
||||
mount: AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -89,7 +94,7 @@ export type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction
|
|||
|
||||
// @public (undocumented)
|
||||
export interface ApplicationSetup {
|
||||
register(app: App): void;
|
||||
register<HistoryLocationState = unknown>(app: App<HistoryLocationState>): void;
|
||||
registerAppUpdater(appUpdater$: Observable<AppUpdater>): void;
|
||||
// @deprecated
|
||||
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
|
||||
|
@ -112,7 +117,7 @@ export interface ApplicationStart {
|
|||
}
|
||||
|
||||
// @public
|
||||
export type AppMount = (params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
|
||||
export type AppMount<HistoryLocationState = unknown> = (params: AppMountParameters<HistoryLocationState>) => AppUnmount | Promise<AppUnmount>;
|
||||
|
||||
// @public @deprecated
|
||||
export interface AppMountContext {
|
||||
|
@ -133,12 +138,14 @@ export interface AppMountContext {
|
|||
}
|
||||
|
||||
// @public @deprecated
|
||||
export type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
|
||||
export type AppMountDeprecated<HistoryLocationState = unknown> = (context: AppMountContext, params: AppMountParameters<HistoryLocationState>) => AppUnmount | Promise<AppUnmount>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface AppMountParameters {
|
||||
export interface AppMountParameters<HistoryLocationState = unknown> {
|
||||
// @deprecated
|
||||
appBasePath: string;
|
||||
element: HTMLElement;
|
||||
history: ScopedHistory<HistoryLocationState>;
|
||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||
}
|
||||
|
||||
|
@ -1175,6 +1182,23 @@ export interface SavedObjectsUpdateOptions {
|
|||
version?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class ScopedHistory<HistoryLocationState = unknown> implements History<HistoryLocationState> {
|
||||
constructor(parentHistory: History, basePath: string);
|
||||
get action(): Action;
|
||||
block: (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback;
|
||||
createHref: (location: LocationDescriptorObject<HistoryLocationState>) => string;
|
||||
createSubHistory: <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState>;
|
||||
go: (n: number) => void;
|
||||
goBack: () => void;
|
||||
goForward: () => void;
|
||||
get length(): number;
|
||||
listen: (listener: (location: Location<HistoryLocationState>, action: Action) => void) => UnregisterCallback;
|
||||
get location(): Location<HistoryLocationState>;
|
||||
push: (pathOrLocation: string | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void;
|
||||
replace: (pathOrLocation: string | LocationDescriptorObject<HistoryLocationState>, state?: HistoryLocationState | undefined) => void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class SimpleSavedObject<T = unknown> {
|
||||
constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObject<T>);
|
||||
|
|
|
@ -68,7 +68,13 @@ export class LocalApplicationService {
|
|||
isUnmounted = true;
|
||||
});
|
||||
(async () => {
|
||||
const params = { element, appBasePath: '', onAppLeave: () => undefined };
|
||||
const params = {
|
||||
element,
|
||||
appBasePath: '',
|
||||
onAppLeave: () => undefined,
|
||||
// TODO: adapt to use Core's ScopedHistory
|
||||
history: {} as any,
|
||||
};
|
||||
unmountHandler = isAppMountDeprecated(app.mount)
|
||||
? await app.mount({ core: npStart.core }, params)
|
||||
: await app.mount(params);
|
||||
|
|
|
@ -17,8 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { scopedHistoryMock } from '../../../../core/public/mocks';
|
||||
|
||||
export const setRootControllerMock = jest.fn();
|
||||
|
||||
jest.doMock('ui/chrome', () => ({
|
||||
setRootController: setRootControllerMock,
|
||||
}));
|
||||
|
||||
export const historyMock = scopedHistoryMock.create();
|
||||
jest.doMock('../../../../core/public', () => ({
|
||||
ScopedHistory: jest.fn(() => historyMock),
|
||||
}));
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { setRootControllerMock } from './new_platform.test.mocks';
|
||||
jest.mock('history');
|
||||
|
||||
import { setRootControllerMock, historyMock } from './new_platform.test.mocks';
|
||||
import { legacyAppRegister, __reset__, __setup__ } from './new_platform';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
|
||||
|
@ -63,6 +65,7 @@ describe('ui/new_platform', () => {
|
|||
element: elementMock[0],
|
||||
appBasePath: '/test/base/path/app/test',
|
||||
onAppLeave: expect.any(Function),
|
||||
history: historyMock,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -84,6 +87,7 @@ describe('ui/new_platform', () => {
|
|||
element: elementMock[0],
|
||||
appBasePath: '/test/base/path/app/test',
|
||||
onAppLeave: expect.any(Function),
|
||||
history: historyMock,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,14 @@ import { IScope } from 'angular';
|
|||
|
||||
import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public';
|
||||
import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public';
|
||||
import { LegacyCoreSetup, LegacyCoreStart, App, AppMountDeprecated } from '../../../../core/public';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import {
|
||||
LegacyCoreSetup,
|
||||
LegacyCoreStart,
|
||||
App,
|
||||
AppMountDeprecated,
|
||||
ScopedHistory,
|
||||
} from '../../../../core/public';
|
||||
import { Plugin as DataPlugin } from '../../../../plugins/data/public';
|
||||
import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public';
|
||||
import {
|
||||
|
@ -126,7 +133,7 @@ let legacyAppRegistered = false;
|
|||
* Exported for testing only. Use `npSetup.core.application.register` in legacy apps.
|
||||
* @internal
|
||||
*/
|
||||
export const legacyAppRegister = (app: App) => {
|
||||
export const legacyAppRegister = (app: App<any>) => {
|
||||
if (legacyAppRegistered) {
|
||||
throw new Error(`core.application.register may only be called once for legacy plugins.`);
|
||||
}
|
||||
|
@ -137,9 +144,15 @@ export const legacyAppRegister = (app: App) => {
|
|||
|
||||
// Root controller cannot return a Promise so use an internal async function and call it immediately
|
||||
(async () => {
|
||||
const appRoute = app.appRoute || `/app/${app.id}`;
|
||||
const appBasePath = npSetup.core.http.basePath.prepend(appRoute);
|
||||
const params = {
|
||||
element,
|
||||
appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`),
|
||||
appBasePath,
|
||||
history: new ScopedHistory(
|
||||
createBrowserHistory({ basename: npSetup.core.http.basePath.get() }),
|
||||
appRoute
|
||||
),
|
||||
onAppLeave: () => undefined,
|
||||
};
|
||||
const unmount = isAppMountDeprecated(app.mount)
|
||||
|
|
|
@ -91,7 +91,13 @@ function DevToolsWrapper({
|
|||
if (mountedTool.current) {
|
||||
mountedTool.current.unmountHandler();
|
||||
}
|
||||
const params = { element, appBasePath: '', onAppLeave: () => undefined };
|
||||
const params = {
|
||||
element,
|
||||
appBasePath: '',
|
||||
onAppLeave: () => undefined,
|
||||
// TODO: adapt to use Core's ScopedHistory
|
||||
history: {} as any,
|
||||
};
|
||||
const unmountHandler = isAppMountDeprecated(activeDevTool.mount)
|
||||
? await activeDevTool.mount(appMountContext, params)
|
||||
: await activeDevTool.mount(params);
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { History } from 'history';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
|
@ -115,8 +116,8 @@ const Nav = withRouter(({ history, navigateToApp }: NavProps) => (
|
|||
/>
|
||||
));
|
||||
|
||||
const FooApp = ({ basename, context }: { basename: string; context: AppMountContext }) => (
|
||||
<Router basename={basename}>
|
||||
const FooApp = ({ history, context }: { history: History; context: AppMountContext }) => (
|
||||
<Router history={history}>
|
||||
<EuiPage>
|
||||
<EuiPageSideBar>
|
||||
<Nav navigateToApp={context.core.application.navigateToApp} />
|
||||
|
@ -127,11 +128,8 @@ const FooApp = ({ basename, context }: { basename: string; context: AppMountCont
|
|||
</Router>
|
||||
);
|
||||
|
||||
export const renderApp = (
|
||||
context: AppMountContext,
|
||||
{ appBasePath, element }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(<FooApp basename={appBasePath} context={context} />, element);
|
||||
export const renderApp = (context: AppMountContext, { history, element }: AppMountParameters) => {
|
||||
ReactDOM.render(<FooApp history={history} context={context} />, element);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { History } from 'history';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
|
@ -122,8 +123,8 @@ const Nav = withRouter(({ history, navigateToApp }: NavProps) => (
|
|||
/>
|
||||
));
|
||||
|
||||
const BarApp = ({ basename, context }: { basename: string; context: AppMountContext }) => (
|
||||
<Router basename={basename}>
|
||||
const BarApp = ({ history, context }: { history: History; context: AppMountContext }) => (
|
||||
<Router history={history}>
|
||||
<EuiPage>
|
||||
<EuiPageSideBar>
|
||||
<Nav navigateToApp={context.core.application.navigateToApp} />
|
||||
|
@ -134,11 +135,8 @@ const BarApp = ({ basename, context }: { basename: string; context: AppMountCont
|
|||
</Router>
|
||||
);
|
||||
|
||||
export const renderApp = (
|
||||
context: AppMountContext,
|
||||
{ appBasePath, element }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(<BarApp basename={appBasePath} context={context} />, element);
|
||||
export const renderApp = (context: AppMountContext, { history, element }: AppMountParameters) => {
|
||||
ReactDOM.render(<BarApp history={history} context={context} />, element);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ export default function(kibana: any) {
|
|||
require: ['kibana'],
|
||||
uiExports: {
|
||||
app: {
|
||||
id: 'core_legacy_compat',
|
||||
title: 'Core Legacy Compat',
|
||||
description: 'This is a sample plugin to test core to legacy compatibility',
|
||||
main: 'plugins/core_plugin_legacy/index',
|
||||
|
|
34
test/plugin_functional/plugins/legacy_plugin/index.ts
Normal file
34
test/plugin_functional/plugins/legacy_plugin/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: 'legacy_plugin',
|
||||
require: ['kibana'],
|
||||
uiExports: {
|
||||
app: {
|
||||
id: 'legacy_app',
|
||||
title: 'Legacy App',
|
||||
description: 'This is a sample plugin to test legacy apps',
|
||||
main: 'plugins/legacy_plugin/index',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
17
test/plugin_functional/plugins/legacy_plugin/package.json
Normal file
17
test/plugin_functional/plugins/legacy_plugin/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "legacy_plugin",
|
||||
"version": "1.0.0",
|
||||
"main": "index.tsx",
|
||||
"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.7.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { IScope } from 'angular';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
const App = () => <h1 data-test-subj="legacyAppH1">legacy_app</h1>;
|
||||
|
||||
chrome.setRootController('legacy_app', ($scope: IScope, $element: JQLite) => {
|
||||
const element = $element[0];
|
||||
|
||||
ReactDOM.render(<App />, element);
|
||||
$scope.$on('$destroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(element);
|
||||
});
|
||||
});
|
15
test/plugin_functional/plugins/legacy_plugin/tsconfig.json
Normal file
15
test/plugin_functional/plugins/legacy_plugin/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": []
|
||||
}
|
|
@ -73,7 +73,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
const alert = await browser.getAlert();
|
||||
expect(alert).not.to.eql(undefined);
|
||||
alert!.accept();
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/core_plugin_legacy'));
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/core_legacy_compat'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
const appsMenu = getService('appsMenu');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const find = getService('find');
|
||||
const retry = getService('retry');
|
||||
|
||||
const loadingScreenNotShown = async () =>
|
||||
expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false);
|
||||
|
@ -48,6 +49,14 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
search,
|
||||
});
|
||||
|
||||
/** Use retry logic to make URL assertions less flaky */
|
||||
const waitForUrlToBe = (pathname?: string, search?: string) => {
|
||||
const expectedUrl = getKibanaUrl(pathname, search);
|
||||
return retry.waitFor(`Url to be ${expectedUrl}`, async () => {
|
||||
return (await browser.getCurrentUrl()) === expectedUrl;
|
||||
});
|
||||
};
|
||||
|
||||
describe('ui applications', function describeIndexTests() {
|
||||
before(async () => {
|
||||
await PageObjects.common.navigateToApp('foo');
|
||||
|
@ -60,29 +69,36 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
it('navigates to its own pages', async () => {
|
||||
// Go to page A
|
||||
await testSubjects.click('fooNavPageA');
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/foo/page-a'));
|
||||
await waitForUrlToBe('/app/foo/page-a');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('fooAppPageA');
|
||||
|
||||
// Go to home page
|
||||
await testSubjects.click('fooNavHome');
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/foo/'));
|
||||
await waitForUrlToBe('/app/foo');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('fooAppHome');
|
||||
});
|
||||
|
||||
it('can use the back button to navigate within an app', async () => {
|
||||
await browser.goBack();
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/foo/page-a'));
|
||||
await waitForUrlToBe('/app/foo/page-a');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('fooAppPageA');
|
||||
});
|
||||
|
||||
it('navigates to app root when navlink is clicked', async () => {
|
||||
await appsMenu.clickLink('Foo');
|
||||
await waitForUrlToBe('/app/foo');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('fooAppHome');
|
||||
});
|
||||
|
||||
it('navigates to other apps', async () => {
|
||||
await testSubjects.click('fooNavBarPageB');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('barAppPageB');
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/bar/page-b', 'query=here'));
|
||||
await waitForUrlToBe('/app/bar/page-b', 'query=here');
|
||||
});
|
||||
|
||||
it('preserves query parameters across apps', async () => {
|
||||
|
@ -92,9 +108,9 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
|
||||
it('can use the back button to navigate back to previous app', async () => {
|
||||
await browser.goBack();
|
||||
expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/foo/page-a'));
|
||||
await waitForUrlToBe('/app/foo');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('fooAppPageA');
|
||||
await testSubjects.existOrFail('fooAppHome');
|
||||
});
|
||||
|
||||
it('chromeless applications are not visible in apps list', async () => {
|
||||
|
|
|
@ -45,7 +45,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
|
||||
describe('application service compatibility layer', () => {
|
||||
it('can render legacy apps', async () => {
|
||||
await PageObjects.common.navigateToApp('core_plugin_legacy');
|
||||
await PageObjects.common.navigateToApp('core_legacy_compat');
|
||||
expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -106,7 +106,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
});
|
||||
|
||||
it('renders "legacy" application', async () => {
|
||||
await navigateTo('/render/core_plugin_legacy');
|
||||
await navigateTo('/render/legacy_app');
|
||||
|
||||
const [loadingMessage, legacyMode, userSettings] = await Promise.all([
|
||||
findLoadingMessage(),
|
||||
|
@ -119,12 +119,12 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
|
||||
await find.waitForElementStale(loadingMessage);
|
||||
|
||||
expect(await exists('coreLegacyCompatH1')).to.be(true);
|
||||
expect(await exists('legacyAppH1')).to.be(true);
|
||||
expect(await exists('renderingHeader')).to.be(false);
|
||||
});
|
||||
|
||||
it('renders "legacy" application without user settings', async () => {
|
||||
await navigateTo('/render/core_plugin_legacy?includeUserSettings=false');
|
||||
await navigateTo('/render/legacy_app?includeUserSettings=false');
|
||||
|
||||
const [loadingMessage, legacyMode, userSettings] = await Promise.all([
|
||||
findLoadingMessage(),
|
||||
|
@ -137,7 +137,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
|
|||
|
||||
await find.waitForElementStale(loadingMessage);
|
||||
|
||||
expect(await exists('coreLegacyCompatH1')).to.be(true);
|
||||
expect(await exists('legacyAppH1')).to.be(true);
|
||||
expect(await exists('renderingHeader')).to.be(false);
|
||||
});
|
||||
|
||||
|
|
|
@ -74,11 +74,13 @@ if (chrome.getInjected('ilmUiEnabled')) {
|
|||
...core,
|
||||
application: {
|
||||
...core.application,
|
||||
async register(app: App) {
|
||||
async register(app: App<any>) {
|
||||
const unmountApp = await app.mount({ ...npStart } as any, {
|
||||
element,
|
||||
appBasePath: '',
|
||||
onAppLeave: () => undefined,
|
||||
// TODO: adapt to use Core's ScopedHistory
|
||||
history: {} as any,
|
||||
});
|
||||
manageAngularLifecycle($scope, $route, unmountApp as any);
|
||||
},
|
||||
|
|
|
@ -63,11 +63,13 @@ if (licenseManagementUiEnabled) {
|
|||
...npSetup.core,
|
||||
application: {
|
||||
...npSetup.core.application,
|
||||
async register(app: App) {
|
||||
async register(app: App<any>) {
|
||||
const unmountApp = await app.mount({ ...npStart } as any, {
|
||||
element,
|
||||
appBasePath: '',
|
||||
onAppLeave: () => undefined,
|
||||
// TODO: adapt to use Core's ScopedHistory
|
||||
history: {} as any,
|
||||
});
|
||||
manageAngularLifecycle($scope, $route, unmountApp as any);
|
||||
},
|
||||
|
|
|
@ -19,6 +19,7 @@ export class MlPlugin implements Plugin<Setup, Start> {
|
|||
element: params.element,
|
||||
appBasePath: params.appBasePath,
|
||||
onAppLeave: params.onAppLeave,
|
||||
history: params.history,
|
||||
data,
|
||||
__LEGACY,
|
||||
security,
|
||||
|
|
|
@ -46,7 +46,6 @@ export class Plugin {
|
|||
title: PLUGIN.TITLE,
|
||||
});
|
||||
core.application.register({
|
||||
appRoute: '/app/uptime#/',
|
||||
id: PLUGIN.ID,
|
||||
euiIconType: 'uptimeApp',
|
||||
order: 8900,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue