Add ApplicationService Mounting (#41007)

* Add core-only bundle

* Add ApplicationService mounting

* Add LegacyCore{Setup,Start}

* Fix PR comments

* Add functional tests

* Fix PR comments

* Fix PR comments

* Remove other usages of rootRoute

* Use state field notation

* Add support for open in new tab

* Fix PR comments

* Fix pesky await from the dead

* Update docs

* Bump @types/history
This commit is contained in:
Josh Dover 2019-09-03 13:03:05 -05:00 committed by GitHub
parent 5854d9e7e9
commit b352f67bdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 2587 additions and 453 deletions

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [App](./kibana-plugin-public.app.md)
## App interface
Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function.
<b>Signature:</b>
```typescript
export interface App extends AppBase
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [mount](./kibana-plugin-public.app.mount.md) | <code>(context: AppMountContext, params: AppMountParameters) =&gt; AppUnmount &#124; Promise&lt;AppUnmount&gt;</code> | A mount function called when the user navigates to this app's route. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [App](./kibana-plugin-public.app.md) &gt; [mount](./kibana-plugin-public.app.mount.md)
## App.mount property
A mount function called when the user navigates to this app's route.
<b>Signature:</b>
```typescript
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [capabilities](./kibana-plugin-public.appbase.capabilities.md)
## AppBase.capabilities property
Custom capabilities defined by the app.
<b>Signature:</b>
```typescript
capabilities?: Partial<Capabilities>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [euiIconType](./kibana-plugin-public.appbase.euiicontype.md)
## AppBase.euiIconType property
A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property.
<b>Signature:</b>
```typescript
euiIconType?: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [icon](./kibana-plugin-public.appbase.icon.md)
## AppBase.icon property
A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided.
<b>Signature:</b>
```typescript
icon?: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [id](./kibana-plugin-public.appbase.id.md)
## AppBase.id property
<b>Signature:</b>
```typescript
id: string;
```

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md)
## AppBase interface
<b>Signature:</b>
```typescript
export interface AppBase
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-public.appbase.capabilities.md) | <code>Partial&lt;Capabilities&gt;</code> | Custom capabilities defined by the app. |
| [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | <code>string</code> | A EUI iconType that will be used for the app's icon. This icon takes precendence over the <code>icon</code> property. |
| [icon](./kibana-plugin-public.appbase.icon.md) | <code>string</code> | A URL to an image file used as an icon. Used as a fallback if <code>euiIconType</code> is not provided. |
| [id](./kibana-plugin-public.appbase.id.md) | <code>string</code> | |
| [order](./kibana-plugin-public.appbase.order.md) | <code>number</code> | An ordinal used to sort nav links relative to one another for display. |
| [title](./kibana-plugin-public.appbase.title.md) | <code>string</code> | The title of the application. |
| [tooltip$](./kibana-plugin-public.appbase.tooltip$.md) | <code>Observable&lt;string&gt;</code> | An observable for a tooltip shown when hovering over app link. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [order](./kibana-plugin-public.appbase.order.md)
## AppBase.order property
An ordinal used to sort nav links relative to one another for display.
<b>Signature:</b>
```typescript
order?: number;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [title](./kibana-plugin-public.appbase.title.md)
## AppBase.title property
The title of the application.
<b>Signature:</b>
```typescript
title: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [tooltip$](./kibana-plugin-public.appbase.tooltip$.md)
## AppBase.tooltip$ property
An observable for a tooltip shown when hovering over app link.
<b>Signature:</b>
```typescript
tooltip$?: Observable<string>;
```

View file

@ -15,5 +15,6 @@ export interface ApplicationSetup
| Method | Description |
| --- | --- |
| [registerApp(app)](./kibana-plugin-public.applicationsetup.registerapp.md) | Register an mountable application to the system. Apps will be mounted based on their <code>rootRoute</code>. |
| [register(app)](./kibana-plugin-public.applicationsetup.register.md) | Register an mountable application to the system. |
| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. |

View file

@ -1,22 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) &gt; [registerApp](./kibana-plugin-public.applicationsetup.registerapp.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) &gt; [register](./kibana-plugin-public.applicationsetup.register.md)
## ApplicationSetup.registerApp() method
## ApplicationSetup.register() method
Register an mountable application to the system. Apps will be mounted based on their `rootRoute`<!-- -->.
Register an mountable application to the system.
<b>Signature:</b>
```typescript
registerApp(app: App): void;
register(app: App): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| app | <code>App</code> | |
| app | <code>App</code> | an [App](./kibana-plugin-public.app.md) |
<b>Returns:</b>

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) &gt; [registerMountContext](./kibana-plugin-public.applicationsetup.registermountcontext.md)
## ApplicationSetup.registerMountContext() method
Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context.
<b>Signature:</b>
```typescript
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountContext, T>): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| contextName | <code>T</code> | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. |
| provider | <code>IContextProvider&lt;AppMountContext, T&gt;</code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
<b>Returns:</b>
`void`

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [availableApps](./kibana-plugin-public.applicationstart.availableapps.md)
## ApplicationStart.availableApps property
Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions.
<b>Signature:</b>
```typescript
availableApps: readonly App[];
```

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [getUrlForApp](./kibana-plugin-public.applicationstart.geturlforapp.md)
## ApplicationStart.getUrlForApp() method
Returns a relative URL to a given app, including the global base path.
<b>Signature:</b>
```typescript
getUrlForApp(appId: string, options?: {
path?: string;
}): string;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| appId | <code>string</code> | |
| options | <code>{</code><br/><code> path?: string;</code><br/><code> }</code> | |
<b>Returns:</b>
`string`

View file

@ -15,6 +15,13 @@ export interface ApplicationStart
| Property | Type | Description |
| --- | --- | --- |
| [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) | <code>readonly App[]</code> | Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions. |
| [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | <code>RecursiveReadonly&lt;Capabilities&gt;</code> | Gets the read-only capabilities. |
## Methods
| Method | Description |
| --- | --- |
| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. |
| [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigiate to a given app |
| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. |

View file

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [navigateToApp](./kibana-plugin-public.applicationstart.navigatetoapp.md)
## ApplicationStart.navigateToApp() method
Navigiate to a given app
<b>Signature:</b>
```typescript
navigateToApp(appId: string, options?: {
path?: string;
state?: any;
}): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| appId | <code>string</code> | |
| options | <code>{</code><br/><code> path?: string;</code><br/><code> state?: any;</code><br/><code> }</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [registerMountContext](./kibana-plugin-public.applicationstart.registermountcontext.md)
## ApplicationStart.registerMountContext() method
Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context.
<b>Signature:</b>
```typescript
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountContext, T>): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| contextName | <code>T</code> | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. |
| provider | <code>IContextProvider&lt;AppMountContext, T&gt;</code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppMountContext](./kibana-plugin-public.appmountcontext.md) &gt; [core](./kibana-plugin-public.appmountcontext.core.md)
## AppMountContext.core property
Core service APIs available to mounted applications.
<b>Signature:</b>
```typescript
core: {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
uiSettings: UiSettingsClientContract;
};
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppMountContext](./kibana-plugin-public.appmountcontext.md)
## AppMountContext interface
The context object received when applications are mounted to the DOM.
<b>Signature:</b>
```typescript
export interface AppMountContext
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-public.appmountcontext.core.md) | <code>{</code><br/><code> application: Pick&lt;ApplicationStart, 'capabilities' &#124; 'navigateToApp'&gt;;</code><br/><code> chrome: ChromeStart;</code><br/><code> docLinks: DocLinksStart;</code><br/><code> http: HttpStart;</code><br/><code> i18n: I18nStart;</code><br/><code> notifications: NotificationsStart;</code><br/><code> overlays: OverlayStart;</code><br/><code> uiSettings: UiSettingsClientContract;</code><br/><code> }</code> | Core service APIs available to mounted applications. |

View file

@ -0,0 +1,53 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppMountParameters](./kibana-plugin-public.appmountparameters.md) &gt; [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md)
## AppMountParameters.appBasePath property
The base path for configuring the application's router.
<b>Signature:</b>
```typescript
appBasePath: string;
```
## 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',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
},
});
}
```
```ts
// application.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
export renderApp = (context, { appBasePath, element }) => {
ReactDOM.render(
// pass `appBasePath` to `basename`
<BrowserRouter basename={appBasePath}>
<Route path="/" exact component={HomePage} />
</BrowserRouter>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
}
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppMountParameters](./kibana-plugin-public.appmountparameters.md) &gt; [element](./kibana-plugin-public.appmountparameters.element.md)
## AppMountParameters.element property
The container element to render the application into.
<b>Signature:</b>
```typescript
element: HTMLElement;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppMountParameters](./kibana-plugin-public.appmountparameters.md)
## AppMountParameters interface
<b>Signature:</b>
```typescript
export interface AppMountParameters
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md) | <code>string</code> | The base path for configuring the application's router. |
| [element](./kibana-plugin-public.appmountparameters.element.md) | <code>HTMLElement</code> | The container element to render the application into. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppUnmount](./kibana-plugin-public.appunmount.md)
## AppUnmount type
A function called when an application should be unmounted from the page. This function should be synchronous.
<b>Signature:</b>
```typescript
export declare type AppUnmount = () => void;
```

View file

@ -9,5 +9,5 @@ An ordinal used to sort nav links relative to one another for display.
<b>Signature:</b>
```typescript
readonly order: number;
readonly order?: number;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CoreSetup](./kibana-plugin-public.coresetup.md) &gt; [application](./kibana-plugin-public.coresetup.application.md)
## CoreSetup.application property
[ApplicationSetup](./kibana-plugin-public.applicationsetup.md)
<b>Signature:</b>
```typescript
application: ApplicationSetup;
```

View file

@ -16,6 +16,7 @@ export interface CoreSetup
| Property | Type | Description |
| --- | --- | --- |
| [application](./kibana-plugin-public.coresetup.application.md) | <code>ApplicationSetup</code> | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) |
| [context](./kibana-plugin-public.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-public.contextsetup.md) |
| [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | <code>FatalErrorsSetup</code> | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) |
| [http](./kibana-plugin-public.coresetup.http.md) | <code>HttpSetup</code> | [HttpSetup](./kibana-plugin-public.httpsetup.md) |

View file

@ -9,5 +9,5 @@
<b>Signature:</b>
```typescript
application: Pick<ApplicationStart, 'capabilities'>;
application: ApplicationStart;
```

View file

@ -16,7 +16,7 @@ export interface CoreStart
| Property | Type | Description |
| --- | --- | --- |
| [application](./kibana-plugin-public.corestart.application.md) | <code>Pick&lt;ApplicationStart, 'capabilities'&gt;</code> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) |
| [application](./kibana-plugin-public.corestart.application.md) | <code>ApplicationStart</code> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) |
| [chrome](./kibana-plugin-public.corestart.chrome.md) | <code>ChromeStart</code> | [ChromeStart](./kibana-plugin-public.chromestart.md) |
| [docLinks](./kibana-plugin-public.corestart.doclinks.md) | <code>DocLinksStart</code> | [DocLinksStart](./kibana-plugin-public.doclinksstart.md) |
| [http](./kibana-plugin-public.corestart.http.md) | <code>HttpStart</code> | [HttpStart](./kibana-plugin-public.httpstart.md) |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) &gt; [injectedMetadata](./kibana-plugin-public.legacycoresetup.injectedmetadata.md)
## LegacyCoreSetup.injectedMetadata property
> Warning: This API is now obsolete.
>
>
<b>Signature:</b>
```typescript
injectedMetadata: InjectedMetadataSetup;
```

View file

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md)
## LegacyCoreSetup interface
> Warning: This API is now obsolete.
>
>
Setup interface exposed to the legacy platform via the `ui/new_platform` module.
<b>Signature:</b>
```typescript
export interface LegacyCoreSetup extends CoreSetup
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [injectedMetadata](./kibana-plugin-public.legacycoresetup.injectedmetadata.md) | <code>InjectedMetadataSetup</code> | |
## Remarks
Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreSetup](./kibana-plugin-public.coresetup.md)<!-- -->, unsupported methods will throw exceptions when called.

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) &gt; [injectedMetadata](./kibana-plugin-public.legacycorestart.injectedmetadata.md)
## LegacyCoreStart.injectedMetadata property
> Warning: This API is now obsolete.
>
>
<b>Signature:</b>
```typescript
injectedMetadata: InjectedMetadataStart;
```

View file

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md)
## LegacyCoreStart interface
> Warning: This API is now obsolete.
>
>
Start interface exposed to the legacy platform via the `ui/new_platform` module.
<b>Signature:</b>
```typescript
export interface LegacyCoreStart extends CoreStart
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [injectedMetadata](./kibana-plugin-public.legacycorestart.injectedmetadata.md) | <code>InjectedMetadataStart</code> | |
## Remarks
Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreStart](./kibana-plugin-public.corestart.md)<!-- -->, unsupported methods will throw exceptions when called.

View file

@ -23,8 +23,12 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| Interface | Description |
| --- | --- |
| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. |
| [AppBase](./kibana-plugin-public.appbase.md) | |
| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |
| [ApplicationStart](./kibana-plugin-public.applicationstart.md) | |
| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. |
| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | |
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
@ -54,6 +58,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | |
| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
@ -80,6 +86,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| Type Alias | Description |
| --- | --- |
| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. |
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | |
| [HttpBody](./kibana-plugin-public.httpbody.md) | |

View file

@ -166,6 +166,7 @@
"handlebars": "4.1.2",
"hapi": "^17.5.3",
"hapi-auth-cookie": "^9.0.0",
"history": "^4.9.0",
"hjson": "3.1.2",
"hoek": "^5.0.4",
"http-proxy-agent": "^2.1.0",
@ -304,6 +305,7 @@
"@types/hapi": "^17.0.18",
"@types/hapi-auth-cookie": "^9.1.0",
"@types/has-ansi": "^3.0.0",
"@types/history": "^4.7.3",
"@types/hoek": "^4.1.3",
"@types/humps": "^1.1.2",
"@types/jest": "^24.0.9",

View file

@ -369,6 +369,14 @@
'@types/has-ansi',
],
},
{
groupSlug: 'history',
groupName: 'history related packages',
packageNames: [
'history',
'@types/history',
],
},
{
groupSlug: 'humps',
groupName: 'humps related packages',
@ -617,14 +625,6 @@
'@types/git-url-parse',
],
},
{
groupSlug: 'history',
groupName: 'history related packages',
packageNames: [
'history',
'@types/history',
],
},
{
groupSlug: 'jsdom',
groupName: 'jsdom related packages',

View file

@ -18,14 +18,14 @@ import ReactDOM from 'react-dom';
import { MyApp } from './componnets';
export function renderApp(context, targetDomElement) {
export function renderApp(context, { element }) {
ReactDOM.render(
<MyApp mountContext={context} deps={pluginStart} />,
targetDomElement
element
);
return () => {
ReactDOM.unmountComponentAtNode(targetDomElement);
ReactDOM.unmountComponentAtNode(element);
};
}
```
@ -38,9 +38,9 @@ class MyPlugin {
application.register({
id: 'my-app',
title: 'My Application',
async mount(context, targetDomElement) {
async mount(context, params) {
const { renderApp } = await import('./applcation');
return renderApp(context, targetDomElement);
return renderApp(context, params);
}
});
}
@ -63,9 +63,7 @@ lock-in.
```ts
/** A context type that implements the Handler Context pattern from RFC-0003 */
export interface MountContext {
/** This is the base path for setting up your router. */
basename: string;
export interface AppMountContext {
/** These services serve as an example, but are subject to change. */
core: {
http: {
@ -93,6 +91,13 @@ export interface MountContext {
[contextName: string]: unknown;
}
export interface AppMountParams {
/** The base path the application is mounted on. Used to configure routers. */
appBasePath: string;
/** The element the application should render into */
element: HTMLElement;
}
export type Unmount = () => Promise<void> | void;
export interface AppSpec {
@ -109,11 +114,11 @@ export interface AppSpec {
/**
* A mount function called when the user navigates to this app's route.
* @param context the `MountContext generated for this app
* @param targetDomElement An HTMLElement to mount the application onto.
* @param context the `AppMountContext` generated for this app
* @param params the `AppMountParams`
* @returns An unmounting function that will be called to unmount the application.
*/
mount(context: MountContext, targetDomElement: HTMLElement): Unmount | Promise<Unmount>;
mount(context: MountContext, params: AppMountParams): Unmount | Promise<Unmount>;
/**
* A EUI iconType that will be used for the app's icon. This icon
@ -158,19 +163,21 @@ When an app is registered via `register`, it must provide a `mount` function
that will be invoked whenever the window's location has changed from another app
to this app.
This function is called with a `MountContext` and an `HTMLElement` for the
application to render itself to. The mount function must also return a function
that can be called by the ApplicationService to unmount the application at the
given DOM node. The mount function may return a Promise of an unmount function
in order to import UI code dynamically.
This function is called with a `AppMountContext` and an
`AppMountParams` which contains a `HTMLElement` for the application to
render itself to. The mount function must also return a function that can be
called by the ApplicationService to unmount the application at the given DOM
Element. The mount function may return a Promise of an unmount function in order
to import UI code dynamically.
The ApplicationService's `register` method will only be available during the
*setup* lifecycle event. This allows the system to know when all applications
have been registered.
The `mount` function will also get access to the `MountContext` that has many of
the same core services available during the `start` lifecycle. Plugins can also
register additional context attributes via the `registerMountContext` function.
The `mount` function will also get access to the `AppMountContext` that
has many of the same core services available during the `start` lifecycle.
Plugins can also register additional context attributes via the
`registerMountContext` function.
## Routing
@ -190,7 +197,7 @@ An example:
"overview" page: mykibana.com/app/my-app/overview
When setting up a router, your application should only handle the part of the
URL following the `context.basename` provided when you application is mounted.
URL following the `params.appBasePath` provided when you application is mounted.
### Legacy Applications
@ -211,7 +218,7 @@ a full-featured router and code-splitting. Note that using React or any other
3rd party tools featured here is not required to build a Kibana Application.
```tsx
// my_plugin/public/application.ts
// my_plugin/public/application.tsx
import React from 'react';
import ReactDOM from 'react-dom';
@ -239,16 +246,16 @@ const MyApp = ({ basename }) => (
</BrowserRouter>,
);
export function renderApp(context, targetDomElement) {
export function renderApp(context, params) {
ReactDOM.render(
// `context.basename` would be `/app/my-app` in this example.
// This exact string is not guaranteed to be stable, always reference
// `context.basename`.
<MyApp basename={context.basename} />,
targetDomElem
// `params.appBasePath` would be `/app/my-app` in this example.
// This exact string is not guaranteed to be stable, always reference the
// provided value at `params.appBasePath`.
<MyApp basename={params.appBasePath} />,
params.element
);
return () => ReactDOM.unmountComponentAtNode(targetDomElem);
return () => ReactDOM.unmountComponentAtNode(params.element);
}
```
@ -259,9 +266,9 @@ export class MyPlugin {
setup({ application }) {
application.register({
id: 'my-app',
async mount(context, targetDomElem) {
async mount(context, params) {
const { renderApp } = await import('./applcation');
return renderApp(context, targetDomElement);
return renderApp(context, params);
}
});
}

View file

@ -17,23 +17,51 @@
* under the License.
*/
import { Subject } from 'rxjs';
import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
import { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service';
import { ApplicationService } from './application_service';
import {
ApplicationSetup,
InternalApplicationStart,
ApplicationStart,
InternalApplicationSetup,
} from './types';
type ApplicationServiceContract = PublicMethodsOf<ApplicationService>;
const createSetupContractMock = (): jest.Mocked<ApplicationSetup> => ({
registerApp: jest.fn(),
registerLegacyApp: jest.fn(),
register: jest.fn(),
registerMountContext: jest.fn(),
});
const createStartContractMock = (): jest.Mocked<ApplicationStart> => ({
...capabilitiesServiceMock.createStartContract(),
const createInternalSetupContractMock = (): jest.Mocked<InternalApplicationSetup> => ({
register: jest.fn(),
registerLegacyApp: jest.fn(),
registerMountContext: jest.fn(),
});
const createStartContractMock = (legacyMode = false): jest.Mocked<ApplicationStart> => ({
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
registerMountContext: jest.fn(),
});
const createInternalStartContractMock = (): jest.Mocked<InternalApplicationStart> => ({
availableApps: new Map(),
availableLegacyApps: new Map(),
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
registerMountContext: jest.fn(),
currentAppId$: new Subject<string | undefined>(),
getComponent: jest.fn(),
});
const createMock = (): jest.Mocked<ApplicationServiceContract> => ({
setup: jest.fn().mockReturnValue(createSetupContractMock()),
start: jest.fn().mockReturnValue(createStartContractMock()),
setup: jest.fn().mockReturnValue(createInternalSetupContractMock()),
start: jest.fn().mockReturnValue(createInternalStartContractMock()),
stop: jest.fn(),
});
@ -41,4 +69,7 @@ export const applicationServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
createInternalSetupContract: createInternalSetupContractMock,
createInternalStartContract: createInternalStartContractMock,
};

View file

@ -26,3 +26,11 @@ export const CapabilitiesServiceConstructor = jest
jest.doMock('./capabilities', () => ({
CapabilitiesService: CapabilitiesServiceConstructor,
}));
export const MockHistory = {
push: jest.fn(),
};
export const createBrowserHistoryMock = jest.fn().mockReturnValue(MockHistory);
jest.doMock('history', () => ({
createBrowserHistory: createBrowserHistoryMock,
}));

View file

@ -17,57 +17,219 @@
* under the License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { MockCapabilitiesService } from './application_service.test.mocks';
import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks';
import { ApplicationService } from './application_service';
import { contextServiceMock } from '../context/context_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
describe('#setup()', () => {
describe('register', () => {
it('throws an error if two apps with the same id are registered', () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.register(Symbol(), { id: 'app1' } as any);
expect(() =>
setup.register(Symbol(), { id: 'app1' } as any)
).toThrowErrorMatchingInlineSnapshot(
`"An application is already registered with the id \\"app1\\""`
);
});
it('throws error if additional apps are registered after setup', async () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
await service.start({ http, injectedMetadata });
expect(() =>
setup.register(Symbol(), { id: 'app1' } as any)
).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
});
});
describe('registerLegacyApp', () => {
it('throws an error if two apps with the same id are registered', () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.registerLegacyApp({ id: 'app2' } as any);
expect(() =>
setup.registerLegacyApp({ id: 'app2' } as any)
).toThrowErrorMatchingInlineSnapshot(
`"A legacy application is already registered with the id \\"app2\\""`
);
});
it('throws error if additional apps are registered after setup', async () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
await service.start({ http, injectedMetadata });
expect(() =>
setup.registerLegacyApp({ id: 'app2' } as any)
).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
});
});
it("`registerMountContext` calls context container's registerContext", () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
const container = context.createContextContainer.mock.results[0].value;
const pluginId = Symbol();
const noop = () => {};
setup.registerMountContext(pluginId, 'test' as any, noop as any);
expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', noop);
});
});
describe('#start()', () => {
beforeEach(() => {
MockHistory.push.mockReset();
});
it('exposes available apps from capabilities', async () => {
const service = new ApplicationService();
const setup = service.setup();
setup.registerApp({ id: 'app1' } as any);
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.register(Symbol(), { id: 'app1' } as any);
setup.registerLegacyApp({ id: 'app2' } as any);
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
const startContract = await service.start({ injectedMetadata });
const startContract = await service.start({ http, injectedMetadata });
expect(startContract.availableApps).toMatchInlineSnapshot(`
Array [
Object {
"id": "app1",
},
]
`);
Map {
"app1" => Object {
"id": "app1",
},
}
`);
expect(startContract.availableLegacyApps).toMatchInlineSnapshot(`
Array [
Object {
"id": "app2",
},
]
`);
Map {
"app2" => Object {
"id": "app2",
},
}
`);
});
it('passes registered applications to capabilities', async () => {
const service = new ApplicationService();
const setup = service.setup();
setup.registerApp({ id: 'app1' } as any);
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.register(Symbol(), { id: 'app1' } as any);
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
await service.start({ injectedMetadata });
await service.start({ http, injectedMetadata });
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
apps: [{ id: 'app1' }],
legacyApps: [],
apps: new Map([['app1', { id: 'app1' }]]),
legacyApps: new Map(),
injectedMetadata,
});
});
it('passes registered legacy applications to capabilities', async () => {
const service = new ApplicationService();
const setup = service.setup();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.registerLegacyApp({ id: 'legacyApp1' } as any);
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
await service.start({ injectedMetadata });
await service.start({ http, injectedMetadata });
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
apps: [],
legacyApps: [{ id: 'legacyApp1' }],
apps: new Map(),
legacyApps: new Map([['legacyApp1', { id: 'legacyApp1' }]]),
injectedMetadata,
});
});
it('returns renderable JSX tree', async () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
service.setup({ context });
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getLegacyMode.mockReturnValue(false);
const start = await service.start({ http, injectedMetadata });
expect(() => shallow(React.createElement(() => start.getComponent()))).not.toThrow();
});
describe('navigateToApp', () => {
it('changes the browser history to /app/:appId', async () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
service.setup({ context });
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getLegacyMode.mockReturnValue(false);
const start = await service.start({ http, injectedMetadata });
start.navigateToApp('myTestApp');
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined);
start.navigateToApp('myOtherApp');
expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined);
});
it('appends a path if specified', async () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
service.setup({ context });
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getLegacyMode.mockReturnValue(false);
const start = await service.start({ http, injectedMetadata });
start.navigateToApp('myTestApp', { path: 'deep/link/to/location/2' });
expect(MockHistory.push).toHaveBeenCalledWith(
'/app/myTestApp/deep/link/to/location/2',
undefined
);
});
it('includes state if specified', async () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
service.setup({ context });
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getLegacyMode.mockReturnValue(false);
const start = await service.start({ http, injectedMetadata });
start.navigateToApp('myTestApp', { state: 'my-state' });
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', 'my-state');
});
it('redirects when in legacyMode', async () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
service.setup({ context });
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getLegacyMode.mockReturnValue(true);
const redirectTo = jest.fn();
const start = await service.start({ http, injectedMetadata, redirectTo });
start.navigateToApp('myTestApp');
expect(redirectTo).toHaveBeenCalledWith('/app/myTestApp');
});
});
});

View file

@ -17,108 +17,43 @@
* under the License.
*/
import { Observable, BehaviorSubject } from 'rxjs';
import { CapabilitiesService, Capabilities } from './capabilities';
import { createBrowserHistory } from 'history';
import { BehaviorSubject } from 'rxjs';
import React from 'react';
import { InjectedMetadataStart } from '../injected_metadata';
import { RecursiveReadonly } from '../../utils';
import { CapabilitiesService } from './capabilities';
import { AppRouter } from './ui';
import { HttpStart } from '../http';
import { ContextSetup, IContextContainer } from '../context';
import {
AppMountContext,
App,
LegacyApp,
AppMounter,
AppUnmount,
AppMountParameters,
InternalApplicationSetup,
InternalApplicationStart,
} from './types';
interface BaseApp {
id: string;
/**
* An ordinal used to sort nav links relative to one another for display.
*/
order: number;
/**
* The title of the application.
*/
title: string;
/**
* An observable for a tooltip shown when hovering over app link.
*/
tooltip$?: Observable<string>;
/**
* A EUI iconType that will be used for the app's icon. This icon
* takes precendence over the `icon` property.
*/
euiIconType?: string;
/**
* A URL to an image file used as an icon. Used as a fallback
* if `euiIconType` is not provided.
*/
icon?: string;
/**
* Custom capabilities defined by the app.
*/
capabilities?: Partial<Capabilities>;
}
/** @public */
export interface App extends BaseApp {
/**
* A mount function called when the user navigates to this app's `rootRoute`.
* @param targetDomElement An HTMLElement to mount the application onto.
* @returns An unmounting function that will be called to unmount the application.
*/
mount(targetDomElement: HTMLElement): () => void;
}
/** @internal */
export interface LegacyApp extends BaseApp {
appUrl: string;
subUrlBase?: string;
linkToLastSubUrl?: boolean;
}
/** @internal */
export type MixedApp = Partial<App> & Partial<LegacyApp> & BaseApp;
/** @public */
export interface ApplicationSetup {
/**
* Register an mountable application to the system. Apps will be mounted based on their `rootRoute`.
* @param app
*/
registerApp(app: App): void;
/**
* Register metadata about legacy applications. Legacy apps will not be mounted when navigated to.
* @param app
* @internal
*/
registerLegacyApp(app: LegacyApp): void;
}
/**
* @public
*/
export interface ApplicationStart {
/**
* Gets the read-only capabilities.
*/
capabilities: RecursiveReadonly<Capabilities>;
/**
* Apps available based on the current capabilities. Should be used
* to show navigation links and make routing decisions.
*/
availableApps: readonly App[];
/**
* Apps available based on the current capabilities. Should be used
* to show navigation links and make routing decisions.
* @internal
*/
availableLegacyApps: readonly LegacyApp[];
interface SetupDeps {
context: ContextSetup;
}
interface StartDeps {
http: HttpStart;
injectedMetadata: InjectedMetadataStart;
/**
* Only necessary for redirecting to legacy apps
* @deprecated
*/
redirectTo?: (path: string) => void;
}
interface AppBox {
app: App;
mount: AppMounter;
}
/**
@ -126,31 +61,122 @@ interface StartDeps {
* @internal
*/
export class ApplicationService {
private readonly apps$ = new BehaviorSubject<App[]>([]);
private readonly legacyApps$ = new BehaviorSubject<LegacyApp[]>([]);
private readonly apps$ = new BehaviorSubject<ReadonlyMap<string, AppBox>>(new Map());
private readonly legacyApps$ = new BehaviorSubject<ReadonlyMap<string, LegacyApp>>(new Map());
private readonly capabilities = new CapabilitiesService();
private mountContext?: IContextContainer<
AppMountContext,
AppUnmount | Promise<AppUnmount>,
[AppMountParameters]
>;
public setup({ context }: SetupDeps): InternalApplicationSetup {
this.mountContext = context.createContextContainer();
public setup(): ApplicationSetup {
return {
registerApp: (app: App) => {
this.apps$.next([...this.apps$.value, app]);
register: (plugin: symbol, app: App) => {
if (this.apps$.value.has(app.id)) {
throw new Error(`An application is already registered with the id "${app.id}"`);
}
if (this.apps$.isStopped) {
throw new Error(`Applications cannot be registered after "setup"`);
}
const appBox: AppBox = {
app,
mount: this.mountContext!.createHandler(plugin, app.mount),
};
this.apps$.next(new Map([...this.apps$.value.entries(), [app.id, appBox]]));
},
registerLegacyApp: (app: LegacyApp) => {
this.legacyApps$.next([...this.legacyApps$.value, app]);
if (this.legacyApps$.value.has(app.id)) {
throw new Error(`A legacy application is already registered with the id "${app.id}"`);
}
if (this.legacyApps$.isStopped) {
throw new Error(`Applications cannot be registered after "setup"`);
}
this.legacyApps$.next(new Map([...this.legacyApps$.value.entries(), [app.id, app]]));
},
registerMountContext: this.mountContext.registerContext,
};
}
public async start({
http,
injectedMetadata,
redirectTo = (path: string) => (window.location.href = path),
}: StartDeps): Promise<InternalApplicationStart> {
if (!this.mountContext) {
throw new Error(`ApplicationService#setup() must be invoked before start.`);
}
// Disable registration of new applications
this.apps$.complete();
this.legacyApps$.complete();
const legacyMode = injectedMetadata.getLegacyMode();
const currentAppId$ = new BehaviorSubject<string | undefined>(undefined);
const { availableApps, availableLegacyApps, capabilities } = await this.capabilities.start({
apps: new Map([...this.apps$.value].map(([id, { app }]) => [id, app])),
legacyApps: this.legacyApps$.value,
injectedMetadata,
});
// Only setup history if we're not in legacy mode
const history = legacyMode ? null : createBrowserHistory({ basename: http.basePath.get() });
return {
availableApps,
availableLegacyApps,
capabilities,
registerMountContext: this.mountContext.registerContext,
currentAppId$,
getUrlForApp: (appId, options: { path?: string } = {}) => {
return http.basePath.prepend(appPath(appId, options));
},
navigateToApp: (appId, { path, state }: { path?: string; state?: any } = {}) => {
if (legacyMode) {
// If we're in legacy mode, do a full page refresh to load the NP app.
redirectTo(http.basePath.prepend(appPath(appId, { path })));
} else {
// basePath not needed here because `history` is configured with basename
history!.push(appPath(appId, { path }), state);
}
},
getComponent: () => {
if (legacyMode) {
return null;
}
// Filter only available apps and map to just the mount function.
const appMounters = new Map<string, AppMounter>(
[...this.apps$.value]
.filter(([id]) => availableApps.has(id))
.map(([id, { mount }]) => [id, mount])
);
return (
<AppRouter
apps={appMounters}
legacyApps={availableLegacyApps}
basePath={http.basePath}
currentAppId$={currentAppId$}
history={history!}
redirectTo={redirectTo}
/>
);
},
};
}
public async start({ injectedMetadata }: StartDeps): Promise<ApplicationStart> {
this.apps$.complete();
this.legacyApps$.complete();
return this.capabilities.start({
apps: this.apps$.value,
legacyApps: this.legacyApps$.value,
injectedMetadata,
});
}
public stop() {}
}
const appPath = (appId: string, { path }: { path?: string } = {}): string =>
path
? `/app/${appId}/${path.replace(/^\//, '')}` // Remove preceding slash from path if present
: `/app/${appId}`;

View file

@ -18,11 +18,11 @@
*/
import { CapabilitiesService, CapabilitiesStart } from './capabilities_service';
import { deepFreeze } from '../../../utils/';
import { App, LegacyApp } from '../application_service';
import { App, LegacyApp } from '../types';
const createStartContractMock = (
apps: readonly App[] = [],
legacyApps: readonly LegacyApp[] = []
apps: ReadonlyMap<string, App> = new Map(),
legacyApps: ReadonlyMap<string, LegacyApp> = new Map()
): jest.Mocked<CapabilitiesStart> => ({
availableApps: apps,
availableLegacyApps: legacyApps,

View file

@ -19,6 +19,7 @@
import { InjectedMetadataService } from '../../injected_metadata';
import { CapabilitiesService } from './capabilities_service';
import { LegacyApp, App } from '../types';
describe('#start', () => {
const injectedMetadata = new InjectedMetadataService({
@ -39,17 +40,22 @@ describe('#start', () => {
} as any,
}).start();
const apps = [{ id: 'app1' }, { id: 'app2', capabilities: { app2: { feature: true } } }] as any;
const legacyApps = [
{ id: 'legacyApp1' },
{ id: 'legacyApp2', capabilities: { app2: { feature: true } } },
] as any;
const apps = new Map([
['app1', { id: 'app1' }],
['app2', { id: 'app2', capabilities: { app2: { feature: true } } }],
] as Array<[string, App]>);
const legacyApps = new Map([
['legacyApp1', { id: 'legacyApp1' }],
['legacyApp2', { id: 'legacyApp2', capabilities: { app2: { feature: true } } }],
] as Array<[string, LegacyApp]>);
it('filters available apps based on returned navLinks', async () => {
const service = new CapabilitiesService();
const startContract = await service.start({ apps, legacyApps, injectedMetadata });
expect(startContract.availableApps).toEqual([{ id: 'app1' }]);
expect(startContract.availableLegacyApps).toEqual([{ id: 'legacyApp1' }]);
expect(startContract.availableApps).toEqual(new Map([['app1', { id: 'app1' }]]));
expect(startContract.availableLegacyApps).toEqual(
new Map([['legacyApp1', { id: 'legacyApp1' }]])
);
});
it('does not allow Capabilities to be modified', async () => {

View file

@ -18,12 +18,12 @@
*/
import { deepFreeze, RecursiveReadonly } from '../../../utils';
import { LegacyApp, App } from '../application_service';
import { LegacyApp, App } from '../types';
import { InjectedMetadataStart } from '../../injected_metadata';
interface StartDeps {
apps: readonly App[];
legacyApps: readonly LegacyApp[];
apps: ReadonlyMap<string, App>;
legacyApps: ReadonlyMap<string, LegacyApp>;
injectedMetadata: InjectedMetadataStart;
}
@ -53,8 +53,8 @@ export interface Capabilities {
/** @internal */
export interface CapabilitiesStart {
capabilities: RecursiveReadonly<Capabilities>;
availableApps: readonly App[];
availableLegacyApps: readonly LegacyApp[];
availableApps: ReadonlyMap<string, App>;
availableLegacyApps: ReadonlyMap<string, LegacyApp>;
}
/**
@ -68,10 +68,23 @@ export class CapabilitiesService {
injectedMetadata,
}: StartDeps): Promise<CapabilitiesStart> {
const capabilities = deepFreeze(injectedMetadata.getCapabilities());
const availableApps = new Map(
[...apps].filter(
([appId]) =>
capabilities.navLinks[appId] === undefined || capabilities.navLinks[appId] === true
)
);
const availableLegacyApps = new Map(
[...legacyApps].filter(
([appId]) =>
capabilities.navLinks[appId] === undefined || capabilities.navLinks[appId] === true
)
);
return {
availableApps: apps.filter(app => capabilities.navLinks[app.id]),
availableLegacyApps: legacyApps.filter(app => capabilities.navLinks[app.id]),
availableApps,
availableLegacyApps,
capabilities,
};
}

View file

@ -17,5 +17,17 @@
* under the License.
*/
export { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service';
export { ApplicationService } from './application_service';
export { Capabilities } from './capabilities';
export {
App,
AppBase,
AppUnmount,
AppMountContext,
AppMountParameters,
ApplicationSetup,
ApplicationStart,
// Internal types
InternalApplicationStart,
LegacyApp,
} from './types';

View file

@ -0,0 +1,130 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { mount, ReactWrapper } from 'enzyme';
import { createMemoryHistory, History } from 'history';
import { BehaviorSubject } from 'rxjs';
import { I18nProvider } from '@kbn/i18n/react';
import { AppMounter, LegacyApp, AppMountParameters } from '../types';
import { httpServiceMock } from '../../http/http_service.mock';
import { AppRouter, AppNotFound } from '../ui';
const createMountHandler = (htmlString: string) =>
jest.fn(async ({ appBasePath: basename, element: el }: AppMountParameters) => {
ReactDOM.render(
<div
dangerouslySetInnerHTML={{ __html: `\nbasename: ${basename}\nhtml: ${htmlString}\n` }}
/>,
el
);
return jest.fn(() => ReactDOM.unmountComponentAtNode(el));
});
describe('AppContainer', () => {
let apps: Map<string, jest.Mock<ReturnType<AppMounter>, Parameters<AppMounter>>>;
let legacyApps: Map<string, LegacyApp>;
let history: History;
let router: ReactWrapper;
let redirectTo: jest.Mock<void, [string]>;
let currentAppId$: BehaviorSubject<string | undefined>;
const navigate = async (path: string) => {
history.push(path);
router.update();
// flushes any pending promises
return new Promise(resolve => setImmediate(resolve));
};
beforeEach(() => {
redirectTo = jest.fn();
apps = new Map([
['app1', createMountHandler('<span>App 1</span>')],
['app2', createMountHandler('<div>App 2</div>')],
]);
legacyApps = new Map([
['legacyApp1', { id: 'legacyApp1' }],
['baseApp:legacyApp2', { id: 'baseApp:legacyApp2' }],
]) as Map<string, LegacyApp>;
history = createMemoryHistory();
currentAppId$ = new BehaviorSubject<string | undefined>(undefined);
// Use 'asdf' as the basepath
const http = httpServiceMock.createStartContract({ basePath: '/asdf' });
router = mount(
<I18nProvider>
<AppRouter
redirectTo={redirectTo}
history={history}
apps={apps}
legacyApps={legacyApps}
basePath={http.basePath}
currentAppId$={currentAppId$}
/>
</I18nProvider>
);
});
it('calls mountHandler and returned unmount function when navigating between apps', async () => {
await navigate('/app/app1');
expect(apps.get('app1')!).toHaveBeenCalled();
expect(router.html()).toMatchInlineSnapshot(`
"<div><div>
basename: /asdf/app/app1
html: <span>App 1</span>
</div></div>"
`);
const app1Unmount = await apps.get('app1')!.mock.results[0].value;
await navigate('/app/app2');
expect(app1Unmount).toHaveBeenCalled();
expect(apps.get('app2')!).toHaveBeenCalled();
expect(router.html()).toMatchInlineSnapshot(`
"<div><div>
basename: /asdf/app/app2
html: <div>App 2</div>
</div></div>"
`);
});
it('updates currentApp$ after mounting', async () => {
await navigate('/app/app1');
expect(currentAppId$.value).toEqual('app1');
await navigate('/app/app2');
expect(currentAppId$.value).toEqual('app2');
});
it('sets window.location.href when navigating to legacy apps', async () => {
await navigate('/app/legacyApp1');
expect(redirectTo).toHaveBeenCalledWith('/asdf/app/legacyApp1');
});
it('handles legacy apps with subapps', async () => {
await navigate('/app/baseApp');
expect(redirectTo).toHaveBeenCalledWith('/asdf/app/baseApp');
});
it('displays error page if no app is found', async () => {
await navigate('/app/unknown');
expect(router.exists(AppNotFound)).toBe(true);
});
});

View file

@ -0,0 +1,300 @@
/*
* 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 { Observable, Subject } from 'rxjs';
import { Capabilities } from './capabilities';
import { ChromeStart } from '../chrome';
import { IContextProvider } from '../context';
import { DocLinksStart } from '../doc_links';
import { HttpStart } from '../http';
import { I18nStart } from '../i18n';
import { NotificationsStart } from '../notifications';
import { OverlayStart } from '../overlays';
import { PluginOpaqueId } from '../plugins';
import { UiSettingsClientContract } from '../ui_settings';
import { RecursiveReadonly } from '../../utils';
/** @public */
export interface AppBase {
id: string;
/**
* The title of the application.
*/
title: string;
/**
* An ordinal used to sort nav links relative to one another for display.
*/
order?: number;
/**
* An observable for a tooltip shown when hovering over app link.
*/
tooltip$?: Observable<string>;
/**
* A EUI iconType that will be used for the app's icon. This icon
* takes precendence over the `icon` property.
*/
euiIconType?: string;
/**
* A URL to an image file used as an icon. Used as a fallback
* if `euiIconType` is not provided.
*/
icon?: string;
/**
* Custom capabilities defined by the app.
*/
capabilities?: Partial<Capabilities>;
}
/**
* Extension of {@link AppBase | common app properties} with the mount function.
* @public
*/
export interface App extends AppBase {
/**
* A mount function called when the user navigates to this app's route.
* @param context The mount context for this app.
* @param targetDomElement An HTMLElement to mount the application onto.
* @returns An unmounting function that will be called to unmount the application.
*/
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
}
/** @internal */
export interface LegacyApp extends AppBase {
appUrl: string;
subUrlBase?: string;
linkToLastSubUrl?: boolean;
}
/**
* The context object received when applications are mounted to the DOM.
* @public
*/
export interface AppMountContext {
/**
* Core service APIs available to mounted applications.
*/
core: {
/** {@link ApplicationStart} */
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
/** {@link ChromeStart} */
chrome: ChromeStart;
/** {@link DocLinksStart} */
docLinks: DocLinksStart;
/** {@link HttpStart} */
http: HttpStart;
/** {@link I18nStart} */
i18n: I18nStart;
/** {@link NotificationsStart} */
notifications: NotificationsStart;
/** {@link OverlayStart} */
overlays: OverlayStart;
/** {@link UiSettingsClient} */
uiSettings: UiSettingsClientContract;
};
}
/** @public */
export interface AppMountParameters {
/**
* The container element to render the application into.
*/
element: HTMLElement;
/**
* The base path for configuring the application's 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',
* async mount(context, params) {
* const { renderApp } = await import('./application');
* return renderApp(context, params);
* },
* });
* }
* ```
*
* ```ts
* // application.tsx
* import React from 'react';
* import ReactDOM from 'react-dom';
* import { BrowserRouter, Route } from 'react-router-dom';
*
* export renderApp = (context, { appBasePath, element }) => {
* ReactDOM.render(
* // pass `appBasePath` to `basename`
* <BrowserRouter basename={appBasePath}>
* <Route path="/" exact component={HomePage} />
* </BrowserRouter>,
* element
* );
*
* return () => ReactDOM.unmountComponentAtNode(element);
* }
* ```
*/
appBasePath: string;
}
/**
* A function called when an application should be unmounted from the page. This function should be synchronous.
* @public
*/
export type AppUnmount = () => void;
/** @internal */
export type AppMounter = (params: AppMountParameters) => Promise<AppUnmount>;
/** @public */
export interface ApplicationSetup {
/**
* Register an mountable application to the system.
* @param app - an {@link App}
*/
register(app: App): void;
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
*
* @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to.
* @param provider - A {@link IContextProvider} function
*/
registerMountContext<T extends keyof AppMountContext>(
contextName: T,
provider: IContextProvider<AppMountContext, T>
): void;
}
/** @internal */
export interface InternalApplicationSetup {
/**
* Register an mountable application to the system.
* @param plugin - opaque ID of the plugin that registers this application
* @param app
*/
register(plugin: PluginOpaqueId, app: App): void;
/**
* Register metadata about legacy applications. Legacy apps will not be mounted when navigated to.
* @param app
* @internal
*/
registerLegacyApp(app: LegacyApp): void;
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
*
* @param pluginOpaqueId - The opaque ID of the plugin that is registering the context.
* @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to.
* @param provider - A {@link IContextProvider} function
*/
registerMountContext<T extends keyof AppMountContext>(
pluginOpaqueId: PluginOpaqueId,
contextName: T,
provider: IContextProvider<AppMountContext, T>
): void;
}
/** @public */
export interface ApplicationStart {
/**
* Gets the read-only capabilities.
*/
capabilities: RecursiveReadonly<Capabilities>;
/**
* Navigiate to a given app
*
* @param appId
* @param options.path - optional path inside application to deep link to
* @param options.state - optional state to forward to the application
*/
navigateToApp(appId: string, options?: { path?: string; state?: any }): void;
/**
* Returns a relative URL to a given app, including the global base path.
* @param appId
* @param options.path - optional path inside application to deep link to
*/
getUrlForApp(appId: string, options?: { path?: string }): string;
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
*
* @param pluginOpaqueId - The opaque ID of the plugin that is registering the context.
* @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to.
* @param provider - A {@link IContextProvider} function
*/
registerMountContext<T extends keyof AppMountContext>(
contextName: T,
provider: IContextProvider<AppMountContext, T>
): void;
}
/** @internal */
export interface InternalApplicationStart
extends Pick<ApplicationStart, 'capabilities' | 'navigateToApp' | 'getUrlForApp'> {
/**
* Apps available based on the current capabilities. Should be used
* to show navigation links and make routing decisions.
*/
availableApps: ReadonlyMap<string, App>;
/**
* Apps available based on the current capabilities. Should be used
* to show navigation links and make routing decisions.
* @internal
*/
availableLegacyApps: ReadonlyMap<string, LegacyApp>;
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
*
* @param pluginOpaqueId - The opaque ID of the plugin that is registering the context.
* @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to.
* @param provider - A {@link IContextProvider} function
*/
registerMountContext<T extends keyof AppMountContext>(
pluginOpaqueId: PluginOpaqueId,
contextName: T,
provider: IContextProvider<AppMountContext, T>
): void;
// Internal APIs
currentAppId$: Subject<string | undefined>;
getComponent(): JSX.Element | null;
}

View file

@ -0,0 +1,111 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Subject } from 'rxjs';
import { LegacyApp, AppMounter, AppUnmount } from '../types';
import { HttpStart } from '../../http';
import { AppNotFound } from './app_not_found_screen';
interface Props extends RouteComponentProps<{ appId: string }> {
apps: ReadonlyMap<string, AppMounter>;
legacyApps: ReadonlyMap<string, LegacyApp>;
basePath: HttpStart['basePath'];
currentAppId$: Subject<string | undefined>;
/**
* Only necessary for redirecting to legacy apps
* @deprecated
*/
redirectTo: (path: string) => void;
}
interface State {
appNotFound: boolean;
}
export class AppContainer extends React.Component<Props, State> {
private readonly containerDiv = React.createRef<HTMLDivElement>();
private unmountFunc?: AppUnmount;
state: State = { appNotFound: false };
componentDidMount() {
this.mountApp();
}
componentWillUnmount() {
this.unmountApp();
}
componentDidUpdate(prevProps: Props) {
if (prevProps.match.params.appId !== this.props.match.params.appId) {
this.unmountApp();
this.mountApp();
}
}
async mountApp() {
const { apps, legacyApps, match, basePath, currentAppId$, redirectTo } = this.props;
const { appId } = match.params;
const mount = apps.get(appId);
if (mount) {
this.unmountFunc = await mount({
appBasePath: basePath.prepend(`/app/${appId}`),
element: this.containerDiv.current!,
});
currentAppId$.next(appId);
this.setState({ appNotFound: false });
return;
}
const legacyApp = findLegacyApp(appId, legacyApps);
if (legacyApp) {
this.unmountApp();
redirectTo(basePath.prepend(`/app/${appId}`));
this.setState({ appNotFound: false });
return;
}
this.setState({ appNotFound: true });
}
async unmountApp() {
if (this.unmountFunc) {
this.unmountFunc();
this.unmountFunc = undefined;
}
}
render() {
return (
<React.Fragment>
{this.state.appNotFound && <AppNotFound />}
<div key={this.props.match.params.appId} ref={this.containerDiv} />
</React.Fragment>
);
}
}
function findLegacyApp(appId: string, apps: ReadonlyMap<string, LegacyApp>) {
const matchingApps = [...apps.entries()].filter(([id]) => id.split(':')[0] === appId);
return matchingApps.length ? matchingApps[0][1] : null;
}

View file

@ -0,0 +1,51 @@
/*
* 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 { EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
export const AppNotFound = () => (
<EuiPage style={{ minHeight: '100%' }}>
<EuiPageBody>
<EuiPageContent verticalPosition="center" horizontalPosition="center">
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={
<h2>
<FormattedMessage
id="core.application.appNotFound.title"
defaultMessage="Application Not Found"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="core.application.appNotFound.pageDescription"
defaultMessage="No application was found at this URL. Try going back or choosing an app from the menu."
/>
</p>
}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);

View file

@ -0,0 +1,53 @@
/*
* 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 } from 'history';
import React from 'react';
import { Router, Route } from 'react-router-dom';
import { Subject } from 'rxjs';
import { LegacyApp, AppMounter } from '../types';
import { AppContainer } from './app_container';
import { HttpStart } from '../../http';
interface Props {
apps: ReadonlyMap<string, AppMounter>;
legacyApps: ReadonlyMap<string, LegacyApp>;
basePath: HttpStart['basePath'];
currentAppId$: Subject<string | undefined>;
history: History;
/**
* Only necessary for redirecting to legacy apps
* @deprecated
*/
redirectTo?: (path: string) => void;
}
export const AppRouter: React.StatelessComponent<Props> = ({
history,
redirectTo = (path: string) => (window.location.href = path),
...otherProps
}) => (
<Router history={history}>
<Route
path="/app/:appId"
render={props => <AppContainer redirectTo={redirectTo} {...otherProps} {...props} />}
/>
</Router>
);

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
export { AppRouter } from './app_router';
export { AppNotFound } from './app_not_found_screen';

View file

@ -27,7 +27,7 @@ import {
const createStartContractMock = () => {
const startContract: DeeplyMockedKeys<InternalChromeStart> = {
getComponent: jest.fn(),
getHeaderComponent: jest.fn(),
navLinks: {
getNavLinks$: jest.fn(),
has: jest.fn(),

View file

@ -38,7 +38,7 @@ const store = new Map();
function defaultStartDeps() {
return {
application: applicationServiceMock.createStartContract(),
application: applicationServiceMock.createInternalStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
http: httpServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
@ -87,7 +87,7 @@ Array [
const start = await service.start(defaultStartDeps());
// Have to do some fanagling to get the type system and enzyme to accept this.
// Don't capture the snapshot because it's 600+ lines long.
expect(shallow(React.createElement(() => start.getComponent()))).toBeDefined();
expect(shallow(React.createElement(() => start.getHeaderComponent()))).toBeDefined();
});
});

View file

@ -27,7 +27,7 @@ import { IconType } from '@elastic/eui';
import { InjectedMetadataStart } from '../injected_metadata';
import { NotificationsStart } from '../notifications';
import { ApplicationStart } from '../application';
import { InternalApplicationStart } from '../application';
import { HttpStart } from '../http';
import { ChromeNavLinks, NavLinksService } from './nav_links';
@ -74,7 +74,7 @@ interface ConstructorParams {
}
interface StartDeps {
application: ApplicationStart;
application: InternalApplicationStart;
docLinks: DocLinksStart;
http: HttpStart;
injectedMetadata: InjectedMetadataStart;
@ -84,14 +84,11 @@ interface StartDeps {
/** @internal */
export class ChromeService {
private readonly stop$ = new ReplaySubject(1);
private readonly browserSupportsCsp: boolean;
private readonly navControls = new NavControlsService();
private readonly navLinks = new NavLinksService();
private readonly recentlyAccessed = new RecentlyAccessedService();
constructor({ browserSupportsCsp }: ConstructorParams) {
this.browserSupportsCsp = browserSupportsCsp;
}
constructor(private readonly params: ConstructorParams) {}
public async start({
application,
@ -115,7 +112,7 @@ export class ChromeService {
const navLinks = this.navLinks.start({ application, http });
const recentlyAccessed = await this.recentlyAccessed.start({ http });
if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
notifications.toasts.addWarning(
i18n.translate('core.chrome.legacyBrowserWarning', {
defaultMessage: 'Your browser does not meet the security requirements for Kibana.',
@ -128,11 +125,12 @@ export class ChromeService {
navLinks,
recentlyAccessed,
getComponent: () => (
getHeaderComponent: () => (
<React.Fragment>
<LoadingIndicator loadingCount$={http.getLoadingCount$()} />
<Header
application={application}
appTitle$={appTitle$.pipe(takeUntil(this.stop$))}
badge$={badge$.pipe(takeUntil(this.stop$))}
basePath={http.basePath}
@ -146,6 +144,7 @@ export class ChromeService {
takeUntil(this.stop$)
)}
kibanaVersion={injectedMetadata.getKibanaVersion()}
legacyMode={injectedMetadata.getLegacyMode()}
navLinks$={navLinks.getNavLinks$()}
recentlyAccessed$={recentlyAccessed.get$()}
navControlsLeft$={navControls.getLeft$()}
@ -374,5 +373,5 @@ export interface InternalChromeStart extends ChromeStart {
* Used only by MountingService to render the header UI
* @internal
*/
getComponent(): JSX.Element;
getHeaderComponent(): JSX.Element;
}

View file

@ -28,11 +28,6 @@ export interface ChromeNavLink {
*/
readonly id: string;
/**
* An ordinal used to sort nav links relative to one another for display.
*/
readonly order: number;
/**
* The title of the application.
*/
@ -43,6 +38,11 @@ export interface ChromeNavLink {
*/
readonly baseUrl: string;
/**
* An ordinal used to sort nav links relative to one another for display.
*/
readonly order?: number;
/**
* A tooltip shown when hovering over an app link.
*/

View file

@ -19,20 +19,27 @@
import { NavLinksService } from './nav_links_service';
import { take, map, takeLast } from 'rxjs/operators';
import { LegacyApp } from '../../application';
const mockAppService = {
availableApps: [],
availableLegacyApps: [
{ id: 'legacyApp1', order: 0, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' },
{
id: 'legacyApp2',
order: -10,
title: 'Legacy App 2',
euiIconType: 'canvasApp',
appUrl: '/app2',
},
{ id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3' },
],
availableApps: new Map(),
availableLegacyApps: new Map<string, LegacyApp>([
[
'legacyApp1',
{ id: 'legacyApp1', order: 0, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' },
],
[
'legacyApp2',
{
id: 'legacyApp2',
order: -10,
title: 'Legacy App 2',
euiIconType: 'canvasApp',
appUrl: '/app2',
},
],
['legacyApp3', { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3' }],
]),
} as any;
const mockHttp = {

View file

@ -21,11 +21,11 @@ import { sortBy } from 'lodash';
import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { NavLinkWrapper, ChromeNavLinkUpdateableFields, ChromeNavLink } from './nav_link';
import { ApplicationStart } from '../../application';
import { InternalApplicationStart } from '../../application';
import { HttpStart } from '../../http';
interface StartDeps {
application: ApplicationStart;
application: InternalApplicationStart;
http: HttpStart;
}
@ -99,10 +99,22 @@ export class NavLinksService {
private readonly stop$ = new ReplaySubject(1);
public start({ application, http }: StartDeps): ChromeNavLinks {
const legacyAppLinks = application.availableLegacyApps.map(
app =>
const appLinks = [...application.availableApps].map(
([appId, app]) =>
[
app.id,
appId,
new NavLinkWrapper({
...app,
legacy: false,
baseUrl: relativeToAbsolute(http.basePath.prepend(`/app/${appId}`)),
}),
] as [string, NavLinkWrapper]
);
const legacyAppLinks = [...application.availableLegacyApps].map(
([appId, app]) =>
[
appId,
new NavLinkWrapper({
...app,
legacy: true,
@ -112,7 +124,7 @@ export class NavLinksService {
);
const navLinks$ = new BehaviorSubject<ReadonlyMap<string, NavLinkWrapper>>(
new Map(legacyAppLinks)
new Map([...legacyAppLinks, ...appLinks])
);
const forceAppSwitcherNavigation$ = new BehaviorSubject(false);

View file

@ -65,6 +65,7 @@ import {
} from '../..';
import { HttpStart } from '../../../http';
import { ChromeHelpExtension } from '../../chrome_service';
import { ApplicationStart, InternalApplicationStart } from '../../../application/types';
// Providing a buffer between the limit and the cut off index
// protects from truncating just the last couple (6) characters
@ -115,13 +116,24 @@ function extendRecentlyAccessedHistoryItem(
};
}
function extendNavLink(navLink: ChromeNavLink) {
function extendNavLink(navLink: ChromeNavLink, urlForApp: ApplicationStart['getUrlForApp']) {
if (navLink.legacy) {
return {
...navLink,
href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl,
};
}
return {
...navLink,
href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl,
href: urlForApp(navLink.id),
};
}
function isModifiedEvent(event: MouseEvent) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void {
let current = element;
while (current) {
@ -149,6 +161,7 @@ export type HeaderProps = Pick<Props, Exclude<keyof Props, 'intl'>>;
interface Props {
kibanaVersion: string;
application: InternalApplicationStart;
appTitle$: Rx.Observable<string>;
badge$: Rx.Observable<ChromeBadge | undefined>;
breadcrumbs$: Rx.Observable<ChromeBreadcrumb[]>;
@ -159,6 +172,7 @@ interface Props {
recentlyAccessed$: Rx.Observable<ChromeRecentlyAccessedHistoryItem[]>;
forceAppSwitcherNavigation$: Rx.Observable<boolean>;
helpExtension$: Rx.Observable<ChromeHelpExtension>;
legacyMode: boolean;
navControlsLeft$: Rx.Observable<readonly ChromeNavControl[]>;
navControlsRight$: Rx.Observable<readonly ChromeNavControl[]>;
intl: InjectedIntl;
@ -169,6 +183,7 @@ interface Props {
interface State {
appTitle: string;
currentAppId?: string;
isVisible: boolean;
navLinks: ReadonlyArray<ReturnType<typeof extendNavLink>>;
recentlyAccessed: ReadonlyArray<ReturnType<typeof extendRecentlyAccessedHistoryItem>>;
@ -203,7 +218,11 @@ class HeaderUI extends Component<Props, State> {
this.props.navLinks$,
this.props.recentlyAccessed$,
// Types for combineLatest only handle up to 6 inferred types so we combine these two separately.
Rx.combineLatest(this.props.navControlsLeft$, this.props.navControlsRight$)
Rx.combineLatest(
this.props.navControlsLeft$,
this.props.navControlsRight$,
this.props.application.currentAppId$
)
).subscribe({
next: ([
appTitle,
@ -211,18 +230,21 @@ class HeaderUI extends Component<Props, State> {
forceNavigation,
navLinks,
recentlyAccessed,
[navControlsLeft, navControlsRight],
[navControlsLeft, navControlsRight, currentAppId],
]) => {
this.setState({
appTitle,
isVisible,
forceNavigation,
navLinks: navLinks.map(navLink => extendNavLink(navLink)),
navLinks: navLinks.map(navLink =>
extendNavLink(navLink, this.props.application.getUrlForApp)
),
recentlyAccessed: recentlyAccessed.map(ra =>
extendRecentlyAccessedHistoryItem(navLinks, ra, this.props.basePath)
),
navControlsLeft,
navControlsRight,
currentAppId,
});
},
});
@ -263,6 +285,7 @@ class HeaderUI extends Component<Props, State> {
public render() {
const {
application,
badge$,
basePath,
breadcrumbs$,
@ -272,9 +295,11 @@ class HeaderUI extends Component<Props, State> {
kibanaDocLink,
kibanaVersion,
onIsLockedUpdate,
legacyMode,
} = this.props;
const {
appTitle,
currentAppId,
isVisible,
navControlsLeft,
navControlsRight,
@ -291,9 +316,26 @@ class HeaderUI extends Component<Props, State> {
.map(navLink => ({
key: navLink.id,
label: navLink.title,
// Use href and onClick to support "open in new tab" and SPA navigation in the same link
href: navLink.href,
onClick: (event: MouseEvent) => {
if (
!legacyMode && // ignore when in legacy mode
!navLink.legacy && // ignore links to legacy apps
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
application.navigateToApp(navLink.id);
}
},
// Legacy apps use `active` property, NP apps should match the current app
isActive: navLink.active || currentAppId === navLink.id,
isDisabled: navLink.disabled,
isActive: navLink.active,
iconType: navLink.euiIconType,
icon:
!navLink.euiIconType && navLink.icon ? (

View file

@ -272,7 +272,9 @@ describe('#start()', () => {
await startCore();
expect(MockRenderingService.start).toHaveBeenCalledTimes(1);
expect(MockRenderingService.start).toHaveBeenCalledWith({
application: expect.any(Object),
chrome: expect.any(Object),
injectedMetadata: expect.any(Object),
targetDomElement: expect.any(HTMLElement),
});
});
@ -364,7 +366,7 @@ describe('LegacyPlatformService targetDomElement', () => {
it('only mounts the element when start, after setting up the legacyPlatformService', async () => {
const core = createCoreSystem();
let targetDomElementInStart: HTMLElement | null;
let targetDomElementInStart: HTMLElement | undefined;
MockLegacyPlatformService.start.mockImplementation(({ targetDomElement }) => {
targetDomElementInStart = targetDomElement;
});

View file

@ -20,23 +20,29 @@
import './core.css';
import { CoreId } from '../server';
import { InternalCoreSetup, InternalCoreStart } from '.';
import { CoreSetup, CoreStart } from '.';
import { ChromeService } from './chrome';
import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors';
import { HttpService } from './http';
import { I18nService } from './i18n';
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
import {
InjectedMetadataParams,
InjectedMetadataService,
InjectedMetadataSetup,
InjectedMetadataStart,
} from './injected_metadata';
import { LegacyPlatformParams, LegacyPlatformService } from './legacy';
import { NotificationsService } from './notifications';
import { OverlayService } from './overlays';
import { PluginsService } from './plugins';
import { UiSettingsService } from './ui_settings';
import { ApplicationService } from './application';
import { mapToObject } from '../utils/';
import { mapToObject, pick } from '../utils/';
import { DocLinksService } from './doc_links';
import { RenderingService } from './rendering';
import { SavedObjectsService } from './saved_objects/saved_objects_service';
import { ContextService } from './context';
import { InternalApplicationSetup, InternalApplicationStart } from './application/types';
interface Params {
rootDomElement: HTMLElement;
@ -51,6 +57,18 @@ export interface CoreContext {
coreId: CoreId;
}
/** @internal */
export interface InternalCoreSetup extends Omit<CoreSetup, 'application'> {
application: InternalApplicationSetup;
injectedMetadata: InjectedMetadataSetup;
}
/** @internal */
export interface InternalCoreStart extends Omit<CoreStart, 'application'> {
application: InternalApplicationStart;
injectedMetadata: InjectedMetadataStart;
}
/**
* The CoreSystem is the root of the new platform, and setups all parts
* of Kibana in the UI, including the LegacyPlatform which is managed
@ -77,6 +95,7 @@ export class CoreSystem {
private readonly context: ContextService;
private readonly rootDomElement: HTMLElement;
private readonly coreContext: CoreContext;
private fatalErrorsSetup: FatalErrorsSetup | null = null;
constructor(params: Params) {
@ -106,14 +125,14 @@ export class CoreSystem {
this.savedObjects = new SavedObjectsService();
this.uiSettings = new UiSettingsService();
this.overlay = new OverlayService();
this.application = new ApplicationService();
this.chrome = new ChromeService({ browserSupportsCsp });
this.docLinks = new DocLinksService();
this.rendering = new RenderingService();
this.application = new ApplicationService();
const core: CoreContext = { coreId: Symbol('core') };
this.context = new ContextService(core);
this.plugins = new PluginsService(core, injectedMetadata.uiPlugins);
this.coreContext = { coreId: Symbol('core') };
this.context = new ContextService(this.coreContext);
this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins);
this.legacyPlatform = new LegacyPlatformService({
requireLegacyFiles,
@ -133,10 +152,10 @@ export class CoreSystem {
const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
const notifications = this.notifications.setup({ uiSettings });
const application = this.application.setup();
const pluginDependencies = this.plugins.getOpaqueIds();
const context = this.context.setup({ pluginDependencies });
const application = this.application.setup({ context });
const core: InternalCoreSetup = {
application,
@ -150,7 +169,11 @@ export class CoreSystem {
// Services that do not expose contracts at setup
const plugins = await this.plugins.setup(core);
await this.legacyPlatform.setup({ core, plugins: mapToObject(plugins.contracts) });
await this.legacyPlatform.setup({
core,
plugins: mapToObject(plugins.contracts),
});
return { fatalErrors: this.fatalErrorsSetup };
} catch (error) {
@ -171,7 +194,7 @@ export class CoreSystem {
const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const savedObjects = await this.savedObjects.start({ http });
const i18n = await this.i18n.start();
const application = await this.application.start({ injectedMetadata });
const application = await this.application.start({ http, injectedMetadata });
const coreUiTargetDomElement = document.createElement('div');
coreUiTargetDomElement.id = 'kibana-body';
@ -200,6 +223,17 @@ export class CoreSystem {
});
const uiSettings = await this.uiSettings.start();
application.registerMountContext(this.coreContext.coreId, 'core', () => ({
application: pick(application, ['capabilities', 'navigateToApp']),
chrome,
docLinks,
http,
i18n,
notifications,
overlays,
uiSettings,
}));
const core: InternalCoreStart = {
application,
chrome,
@ -215,9 +249,12 @@ export class CoreSystem {
const plugins = await this.plugins.start(core);
const rendering = this.rendering.start({
application,
chrome,
injectedMetadata,
targetDomElement: coreUiTargetDomElement,
});
await this.legacyPlatform.start({
core,
plugins: mapToObject(plugins.contracts),

View file

@ -25,7 +25,7 @@ type ServiceSetupMockType = jest.Mocked<HttpSetup> & {
basePath: jest.Mocked<HttpSetup['basePath']>;
};
const createServiceMock = (): ServiceSetupMockType => ({
const createServiceMock = ({ basePath = '' } = {}): ServiceSetupMockType => ({
fetch: jest.fn(),
get: jest.fn(),
head: jest.fn(),
@ -35,8 +35,8 @@ const createServiceMock = (): ServiceSetupMockType => ({
delete: jest.fn(),
options: jest.fn(),
basePath: {
get: jest.fn(),
prepend: jest.fn(),
get: jest.fn(() => basePath),
prepend: jest.fn(path => `${basePath}${path}`),
remove: jest.fn(),
},
addLoadingCount: jest.fn(),
@ -46,22 +46,19 @@ const createServiceMock = (): ServiceSetupMockType => ({
removeAllInterceptors: jest.fn(),
});
const createSetupContractMock = createServiceMock;
const createStartContractMock = createServiceMock;
const createMock = () => {
const createMock = ({ basePath = '' } = {}) => {
const mocked: jest.Mocked<Required<HttpService>> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createSetupContractMock());
mocked.setup.mockReturnValue(createServiceMock({ basePath }));
mocked.start.mockReturnValue(createServiceMock({ basePath }));
return mocked;
};
export const httpServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
createSetupContract: createServiceMock,
createStartContract: createServiceMock,
};

View file

@ -71,6 +71,9 @@ import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } fr
export { CoreContext, CoreSystem } from './core_system';
export { RecursiveReadonly } from '../utils';
export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application';
export {
SavedObjectsBatchResponse,
SavedObjectsBulkCreateObject,
@ -114,6 +117,8 @@ export {
* https://github.com/Microsoft/web-build-tools/issues/1237
*/
export interface CoreSetup {
/** {@link ApplicationSetup} */
application: ApplicationSetup;
/** {@link ContextSetup} */
context: ContextSetup;
/** {@link FatalErrorsSetup} */
@ -137,7 +142,7 @@ export interface CoreSetup {
*/
export interface CoreStart {
/** {@link ApplicationStart} */
application: Pick<ApplicationStart, 'capabilities'>;
application: ApplicationStart;
/** {@link ChromeStart} */
chrome: ChromeStart;
/** {@link DocLinksStart} */
@ -156,15 +161,33 @@ export interface CoreStart {
uiSettings: UiSettingsClientContract;
}
/** @internal */
export interface InternalCoreSetup extends CoreSetup {
application: ApplicationSetup;
/**
* Setup interface exposed to the legacy platform via the `ui/new_platform` module.
*
* @remarks
* Some methods are not supported in the legacy platform and while present to make this type compatibile with
* {@link CoreSetup}, unsupported methods will throw exceptions when called.
*
* @public
* @deprecated
*/
export interface LegacyCoreSetup extends CoreSetup {
/** @deprecated */
injectedMetadata: InjectedMetadataSetup;
}
/** @internal */
export interface InternalCoreStart extends CoreStart {
application: ApplicationStart;
/**
* Start interface exposed to the legacy platform via the `ui/new_platform` module.
*
* @remarks
* Some methods are not supported in the legacy platform and while present to make this type compatibile with
* {@link CoreStart}, unsupported methods will throw exceptions when called.
*
* @public
* @deprecated
*/
export interface LegacyCoreStart extends CoreStart {
/** @deprecated */
injectedMetadata: InjectedMetadataStart;
}

View file

@ -25,6 +25,7 @@ const createSetupContractMock = () => {
getKibanaBranch: jest.fn(),
getCapabilities: jest.fn(),
getCspConfig: jest.fn(),
getLegacyMode: jest.fn(),
getLegacyMetadata: jest.fn(),
getPlugins: jest.fn(),
getInjectedVar: jest.fn(),
@ -34,6 +35,7 @@ const createSetupContractMock = () => {
setupContract.getCapabilities.mockReturnValue({} as any);
setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
setupContract.getKibanaVersion.mockReturnValue('kibanaVersion');
setupContract.getLegacyMode.mockReturnValue(true);
setupContract.getLegacyMetadata.mockReturnValue({
nav: [],
uiSettings: {

View file

@ -51,6 +51,7 @@ export interface InjectedMetadataParams {
plugin: DiscoveredPlugin;
}>;
capabilities: Capabilities;
legacyMode: boolean;
legacyMetadata: {
app: unknown;
translations: unknown;
@ -112,6 +113,10 @@ export class InjectedMetadataService {
return this.state.uiPlugins;
},
getLegacyMode: () => {
return this.state.legacyMode;
},
getLegacyMetadata: () => {
return this.state.legacyMetadata;
},
@ -156,6 +161,8 @@ export interface InjectedMetadataSetup {
id: string;
plugin: DiscoveredPlugin;
}>;
/** Indicates whether or not we are rendering a known legacy app. */
getLegacyMode: () => boolean;
getLegacyMetadata: () => {
app: unknown;
translations: unknown;

View file

@ -61,7 +61,7 @@ import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock';
import { contextServiceMock } from '../context/context_service.mock';
const applicationSetup = applicationServiceMock.createSetupContract();
const applicationSetup = applicationServiceMock.createInternalSetupContract();
const contextSetup = contextServiceMock.createSetupContract();
const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract();
const httpSetup = httpServiceMock.createSetupContract();
@ -88,7 +88,7 @@ const defaultSetupDeps = {
plugins: {},
};
const applicationStart = applicationServiceMock.createStartContract();
const applicationStart = applicationServiceMock.createInternalStartContract();
const docLinksStart = docLinksServiceMock.createStartContract();
const httpStart = httpServiceMock.createStartContract();
const chromeStart = chromeServiceMock.createStartContract();
@ -98,6 +98,7 @@ const notificationsStart = notificationServiceMock.createStartContract();
const overlayStart = overlayServiceMock.createStartContract();
const uiSettingsStart = uiSettingsServiceMock.createStartContract();
const savedObjectsStart = savedObjectsMock.createStartContract();
const mockStorage = { getItem: jest.fn() } as any;
const defaultStartDeps = {
core: {
@ -112,6 +113,7 @@ const defaultStartDeps = {
uiSettings: uiSettingsStart,
savedObjects: savedObjectsStart,
},
lastSubUrlStorage: mockStorage,
targetDomElement: document.createElement('div'),
plugins: {},
};
@ -132,12 +134,29 @@ describe('#setup()', () => {
legacyPlatform.setup(defaultSetupDeps);
expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1);
expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(defaultSetupDeps.core, {});
expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(expect.any(Object), {});
});
});
});
describe('#start()', () => {
it('fetches and sets legacy lastSubUrls', () => {
chromeStart.navLinks.getAll.mockReturnValue([
{ id: 'link1', baseUrl: 'http://wowza.com/app1', legacy: true } as any,
]);
mockStorage.getItem.mockReturnValue('http://wowza.com/app1/subUrl');
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start({ ...defaultStartDeps, lastSubUrlStorage: mockStorage });
expect(chromeStart.navLinks.update).toHaveBeenCalledWith('link1', {
url: 'http://wowza.com/app1/subUrl',
});
});
it('initializes ui/new_platform with core APIs', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
@ -147,7 +166,7 @@ describe('#start()', () => {
legacyPlatform.start(defaultStartDeps);
expect(mockUiNewPlatformStart).toHaveBeenCalledTimes(1);
expect(mockUiNewPlatformStart).toHaveBeenCalledWith(defaultStartDeps.core, {});
expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {});
});
describe('useLegacyTestHarness = false', () => {

View file

@ -18,7 +18,8 @@
*/
import angular from 'angular';
import { InternalCoreSetup, InternalCoreStart } from '../';
import { InternalCoreSetup, InternalCoreStart } from '../core_system';
import { LegacyCoreSetup, LegacyCoreStart } from '../';
/** @internal */
export interface LegacyPlatformParams {
@ -34,7 +35,8 @@ interface SetupDeps {
interface StartDeps {
core: InternalCoreStart;
plugins: Record<string, unknown>;
targetDomElement: HTMLElement;
lastSubUrlStorage?: Storage;
targetDomElement?: HTMLElement;
}
interface BootstrapModule {
@ -55,10 +57,7 @@ export class LegacyPlatformService {
constructor(private readonly params: LegacyPlatformParams) {}
public setup({ core, plugins }: SetupDeps) {
// Inject parts of the new platform into parts of the legacy platform
// so that legacy APIs/modules can mimic their new platform counterparts
require('ui/new_platform').__setup__(core, plugins);
// Always register legacy apps, even if not in legacy mode.
core.injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) =>
core.application.registerLegacyApp({
id: navLink.id,
@ -71,12 +70,57 @@ export class LegacyPlatformService {
linkToLastSubUrl: navLink.linkToLastSubUrl,
})
);
}
public start({ core, targetDomElement, plugins }: StartDeps) {
const legacyCore: LegacyCoreSetup = {
...core,
application: {
register: notSupported(`core.application.register()`),
registerMountContext: notSupported(`core.application.registerMountContext()`),
},
};
// Inject parts of the new platform into parts of the legacy platform
// so that legacy APIs/modules can mimic their new platform counterparts
require('ui/new_platform').__start__(core, plugins);
if (core.injectedMetadata.getLegacyMode()) {
require('ui/new_platform').__setup__(legacyCore, plugins);
}
}
public start({
core,
targetDomElement,
plugins,
lastSubUrlStorage = window.sessionStorage,
}: StartDeps) {
// Initialize legacy sub urls
core.chrome.navLinks
.getAll()
.filter(link => link.legacy)
.forEach(navLink => {
const lastSubUrl = lastSubUrlStorage.getItem(`lastSubUrl:${navLink.baseUrl}`);
core.chrome.navLinks.update(navLink.id, {
url: lastSubUrl || navLink.url || navLink.baseUrl,
});
});
// Only import and bootstrap legacy platform if we're in legacy mode.
if (!core.injectedMetadata.getLegacyMode()) {
return;
}
const legacyCore: LegacyCoreStart = {
...core,
application: {
capabilities: core.application.capabilities,
getUrlForApp: core.application.getUrlForApp,
navigateToApp: core.application.navigateToApp,
registerMountContext: notSupported(`core.application.registerMountContext()`),
},
};
// Inject parts of the new platform into parts of the legacy platform
// so that legacy APIs/modules can mimic their new platform counterparts
require('ui/new_platform').__start__(legacyCore, plugins);
// Load the bootstrap module before loading the legacy platform files so that
// the bootstrap module can modify the environment a bit first
@ -91,7 +135,8 @@ export class LegacyPlatformService {
this.targetDomElement = targetDomElement;
this.bootstrapModule.bootstrap(this.targetDomElement);
// `targetDomElement` is always defined when in legacy mode
this.bootstrapModule.bootstrap(this.targetDomElement!);
}
public stop() {
@ -129,3 +174,7 @@ export class LegacyPlatformService {
return require('ui/chrome');
}
}
const notSupported = (methodName: string) => (...args: any[]) => {
throw new Error(`${methodName} is not supported in the legacy platform.`);
};

View file

@ -42,6 +42,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
function createCoreSetupMock() {
const mock: MockedKeys<CoreSetup> = {
application: applicationServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),

View file

@ -76,7 +76,12 @@ export function createPluginSetupContext<
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
): CoreSetup {
return {
context: omit(deps.context, 'setCurrentPlugin'),
application: {
register: app => deps.application.register(plugin.opaqueId, app),
registerMountContext: (contextName, provider) =>
deps.application.registerMountContext(plugin.opaqueId, contextName, provider),
},
context: deps.context,
fatalErrors: deps.fatalErrors,
http: deps.http,
notifications: deps.notifications,
@ -107,6 +112,10 @@ export function createPluginStartContext<
return {
application: {
capabilities: deps.application.capabilities,
navigateToApp: deps.application.navigateToApp,
getUrlForApp: deps.application.getUrlForApp,
registerMountContext: (contextName, provider) =>
deps.application.registerMountContext(plugin.opaqueId, contextName, provider),
},
docLinks: deps.docLinks,
http: deps.http,

View file

@ -72,7 +72,7 @@ beforeEach(() => {
},
];
mockSetupDeps = {
application: applicationServiceMock.createSetupContract(),
application: applicationServiceMock.createInternalSetupContract(),
context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
@ -81,10 +81,11 @@ beforeEach(() => {
uiSettings: uiSettingsServiceMock.createSetupContract(),
};
mockSetupContext = {
...omit(mockSetupDeps, 'application', 'injectedMetadata'),
...omit(mockSetupDeps, 'injectedMetadata'),
application: expect.any(Object),
};
mockStartDeps = {
application: applicationServiceMock.createStartContract(),
application: applicationServiceMock.createInternalStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
http: httpServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
@ -97,9 +98,7 @@ beforeEach(() => {
};
mockStartContext = {
...omit(mockStartDeps, 'injectedMetadata'),
application: {
capabilities: mockStartDeps.application.capabilities,
},
application: expect.any(Object),
chrome: omit(mockStartDeps.chrome, 'getComponent'),
};

View file

@ -26,7 +26,7 @@ import {
createPluginSetupContext,
createPluginStartContext,
} from './plugin_context';
import { InternalCoreSetup, InternalCoreStart } from '..';
import { InternalCoreSetup, InternalCoreStart } from '../core_system';
/** @internal */
export type PluginsServiceSetupDeps = InternalCoreSetup;

View file

@ -11,24 +11,65 @@ import React from 'react';
import * as Rx from 'rxjs';
import { EuiGlobalToastListToast as Toast } from '@elastic/eui';
// @public
export interface App extends AppBase {
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
}
// @public (undocumented)
export interface AppBase {
capabilities?: Partial<Capabilities>;
euiIconType?: string;
icon?: string;
// (undocumented)
id: string;
order?: number;
title: string;
tooltip$?: Observable<string>;
}
// @public (undocumented)
export interface ApplicationSetup {
// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts
registerApp(app: App): void;
// Warning: (ae-forgotten-export) The symbol "LegacyApp" needs to be exported by the entry point index.d.ts
//
// @internal
registerLegacyApp(app: LegacyApp): void;
register(app: App): void;
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountContext, T>): void;
}
// @public (undocumented)
export interface ApplicationStart {
availableApps: readonly App[];
// @internal
availableLegacyApps: readonly LegacyApp[];
capabilities: RecursiveReadonly<Capabilities>;
getUrlForApp(appId: string, options?: {
path?: string;
}): string;
navigateToApp(appId: string, options?: {
path?: string;
state?: any;
}): void;
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountContext, T>): void;
}
// @public
export interface AppMountContext {
core: {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
uiSettings: UiSettingsClientContract;
};
}
// @public (undocumented)
export interface AppMountParameters {
appBasePath: string;
element: HTMLElement;
}
// @public
export type AppUnmount = () => void;
// @public
export interface Capabilities {
[key: string]: Record<string, boolean | Record<string, boolean>>;
@ -105,7 +146,7 @@ export interface ChromeNavLink {
readonly legacy: boolean;
// @deprecated
readonly linkToLastSubUrl?: boolean;
readonly order: number;
readonly order?: number;
// @deprecated
readonly subUrlBase?: string;
readonly title: string;
@ -185,6 +226,8 @@ export interface CoreContext {
// @public
export interface CoreSetup {
// (undocumented)
application: ApplicationSetup;
// (undocumented)
context: ContextSetup;
// (undocumented)
@ -200,7 +243,7 @@ export interface CoreSetup {
// @public
export interface CoreStart {
// (undocumented)
application: Pick<ApplicationStart, 'capabilities'>;
application: ApplicationStart;
// (undocumented)
chrome: ChromeStart;
// (undocumented)
@ -505,23 +548,19 @@ export type IContextHandler<TContext extends {}, TReturn, THandlerParameters ext
// @public
export type IContextProvider<TContext extends Record<string, any>, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial<TContext>, ...rest: TProviderParameters) => Promise<TContext[TContextName]> | TContext[TContextName];
// @internal (undocumented)
export interface InternalCoreSetup extends CoreSetup {
// (undocumented)
application: ApplicationSetup;
// @public @deprecated
export interface LegacyCoreSetup extends CoreSetup {
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts
//
// (undocumented)
// @deprecated (undocumented)
injectedMetadata: InjectedMetadataSetup;
}
// @internal (undocumented)
export interface InternalCoreStart extends CoreStart {
// (undocumented)
application: ApplicationStart;
// @public @deprecated
export interface LegacyCoreStart extends CoreStart {
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
// @deprecated (undocumented)
injectedMetadata: InjectedMetadataStart;
}

View file

@ -21,46 +21,67 @@ import React from 'react';
import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { RenderingService } from './rendering_service';
import { InternalApplicationStart } from '../application';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
describe('RenderingService#start', () => {
const getService = () => {
const getService = ({ legacyMode = false }: { legacyMode?: boolean } = {}) => {
const rendering = new RenderingService();
const application = {
getComponent: () => <div>Hello application!</div>,
} as InternalApplicationStart;
const chrome = chromeServiceMock.createStartContract();
chrome.getComponent.mockReturnValue(<div>Hello chrome!</div>);
chrome.getHeaderComponent.mockReturnValue(<div>Hello chrome!</div>);
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getLegacyMode.mockReturnValue(legacyMode);
const targetDomElement = document.createElement('div');
const start = rendering.start({ chrome, targetDomElement });
const start = rendering.start({ application, chrome, injectedMetadata, targetDomElement });
return { start, targetDomElement };
};
it('renders into provided DOM element', () => {
it('renders application service into provided DOM element', () => {
const { targetDomElement } = getService();
expect(targetDomElement).toMatchInlineSnapshot(`
<div>
<div
class="content"
data-test-subj="kibanaChrome"
>
<div>
Hello chrome!
</div>
<div />
</div>
</div>
`);
expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(`
<div
class="application"
>
<div>
Hello application!
</div>
</div>
`);
});
it('returns a div for the legacy service to render into', () => {
const {
start: { legacyTargetDomElement },
targetDomElement,
} = getService();
legacyTargetDomElement.innerHTML = '<span id="legacy">Hello legacy!</span>';
expect(targetDomElement.querySelector('#legacy')).toMatchInlineSnapshot(`
<span
id="legacy"
>
Hello legacy!
</span>
`);
it('contains wrapper divs', () => {
const { targetDomElement } = getService();
expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined();
expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined();
});
describe('legacyMode', () => {
it('renders into provided DOM element', () => {
const { targetDomElement } = getService({ legacyMode: true });
expect(targetDomElement).toMatchInlineSnapshot(`
<div>
<div
class="content"
data-test-subj="kibanaChrome"
>
<div>
Hello chrome!
</div>
<div />
</div>
</div>
`);
});
it('returns a div for the legacy service to render into', () => {
const {
start: { legacyTargetDomElement },
targetDomElement,
} = getService({ legacyMode: true });
expect(targetDomElement.contains(legacyTargetDomElement!)).toBe(true);
});
});
});

View file

@ -22,9 +22,13 @@ import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { InternalChromeStart } from '../chrome';
import { InternalApplicationStart } from '../application';
import { InjectedMetadataStart } from '../injected_metadata';
interface StartDeps {
application: InternalApplicationStart;
chrome: InternalChromeStart;
injectedMetadata: InjectedMetadataStart;
targetDomElement: HTMLDivElement;
}
@ -39,28 +43,40 @@ interface StartDeps {
* @internal
*/
export class RenderingService {
start({ chrome, targetDomElement }: StartDeps) {
const chromeUi = chrome.getComponent();
const legacyRef = React.createRef<HTMLDivElement>();
start({ application, chrome, injectedMetadata, targetDomElement }: StartDeps): RenderingStart {
const chromeUi = chrome.getHeaderComponent();
const appUi = application.getComponent();
const legacyMode = injectedMetadata.getLegacyMode();
const legacyRef = legacyMode ? React.createRef<HTMLDivElement>() : null;
ReactDOM.render(
<I18nProvider>
<div className="content" data-test-subj="kibanaChrome">
{chromeUi}
<div ref={legacyRef} />
{!legacyMode && (
<div className="app-wrapper">
<div className="app-wrapper-panel">
<div className="application">{appUi}</div>
</div>
</div>
)}
{legacyMode && <div ref={legacyRef} />}
</div>
</I18nProvider>,
targetDomElement
);
return {
legacyTargetDomElement: legacyRef.current!,
// When in legacy mode, return legacy div, otherwise undefined.
legacyTargetDomElement: legacyRef ? legacyRef.current! : undefined,
};
}
}
/** @internal */
export interface RenderingStart {
legacyTargetDomElement: HTMLDivElement;
legacyTargetDomElement?: HTMLDivElement;
}

View file

@ -24,7 +24,9 @@ export type ContextContainerMock = jest.Mocked<IContextContainer<any, any, any>>
const createContextMock = () => {
const contextMock: ContextContainerMock = {
registerContext: jest.fn(),
createHandler: jest.fn(),
createHandler: jest.fn((id, handler) => (...args: any[]) =>
Promise.resolve(handler({}, ...args))
),
};
contextMock.createHandler.mockImplementation((pluginId, handler) => (...args) =>
handler({}, ...args)

View file

@ -17,10 +17,7 @@
* under the License.
*/
export function pick<T extends Record<string, unknown>, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce(
(acc, key) => {
if (obj.hasOwnProperty(key)) {

View file

@ -24,6 +24,7 @@ export default {
testMatch: [
'**/integration_tests/**/*.test.js',
'**/integration_tests/**/*.test.ts',
'**/integration_tests/**/*.test.tsx',
],
testPathIgnorePatterns: config.testPathIgnorePatterns.filter(
(pattern) => !pattern.includes('integration_tests')

View file

@ -85,6 +85,7 @@ const coreSystem = new CoreSystem({
injectedMetadata: {
version: '1.2.3',
buildNumber: 1234,
legacyMode: true,
legacyMetadata: {
nav: [],
version: '1.2.3',

View file

@ -95,7 +95,6 @@ const waitForBootstrap = new Promise(resolve => {
document.body.setAttribute('id', `${internals.app.id}-app`);
chrome.setupAngular();
// targetDomElement.setAttribute('id', 'kibana-body');
targetDomElement.setAttribute('kbn-chrome', 'true');
targetDomElement.setAttribute('ng-class', '{ \'hidden-chrome\': !chrome.getVisible() }');
targetDomElement.className = 'app-wrapper';

View file

@ -77,15 +77,21 @@ export function kbnChromeProvider(chrome, internals) {
// Non-scope based code (e.g., React)
// Banners
ReactDOM.render(
<I18nContext>
<GlobalBannerList
banners={banners.list}
subscribe={banners.onChange}
/>
</I18nContext>,
document.getElementById('globalBannerList')
);
const bannerListContainer = document.getElementById('globalBannerList');
// Banners not supported in New Platform yet
// https://github.com/elastic/kibana/issues/41986
if (bannerListContainer) {
ReactDOM.render(
<I18nContext>
<GlobalBannerList
banners={banners.list}
subscribe={banners.onChange}
/>
</I18nContext>,
bannerListContainer
);
}
return chrome;
}

View file

@ -33,7 +33,7 @@ import * as Rx from 'rxjs';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { InternalCoreStart } from 'kibana/public';
import { CoreStart, LegacyCoreStart } from 'kibana/public';
import { fatalError } from 'ui/notify';
import { capabilities } from 'ui/capabilities';
@ -77,7 +77,7 @@ export const configureAppAngularModule = (angularModule: IModule) => {
.run($setupUrlOverflowHandling(newPlatform));
};
const getEsUrl = (newPlatform: InternalCoreStart) => {
const getEsUrl = (newPlatform: CoreStart) => {
const a = document.createElement('a');
a.href = newPlatform.http.basePath.prepend('/elasticsearch');
const protocolPort = /https/.test(a.protocol) ? 443 : 80;
@ -90,7 +90,7 @@ const getEsUrl = (newPlatform: InternalCoreStart) => {
};
};
const setupCompileProvider = (newPlatform: InternalCoreStart) => (
const setupCompileProvider = (newPlatform: LegacyCoreStart) => (
$compileProvider: ICompileProvider
) => {
if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) {
@ -98,7 +98,7 @@ const setupCompileProvider = (newPlatform: InternalCoreStart) => (
}
};
const setupLocationProvider = (newPlatform: InternalCoreStart) => (
const setupLocationProvider = (newPlatform: CoreStart) => (
$locationProvider: ILocationProvider
) => {
$locationProvider.html5Mode({
@ -110,7 +110,7 @@ const setupLocationProvider = (newPlatform: InternalCoreStart) => (
$locationProvider.hashPrefix('');
};
export const $setupXsrfRequestInterceptor = (newPlatform: InternalCoreStart) => {
export const $setupXsrfRequestInterceptor = (newPlatform: LegacyCoreStart) => {
const version = newPlatform.injectedMetadata.getLegacyMetadata().version;
// Configure jQuery prefilter
@ -145,7 +145,7 @@ export const $setupXsrfRequestInterceptor = (newPlatform: InternalCoreStart) =>
* @param {HttpService} $http
* @return {undefined}
*/
const capture$httpLoadingCount = (newPlatform: InternalCoreStart) => (
const capture$httpLoadingCount = (newPlatform: CoreStart) => (
$rootScope: IRootScopeService,
$http: IHttpService
) => {
@ -166,7 +166,7 @@ const capture$httpLoadingCount = (newPlatform: InternalCoreStart) => (
* lets us integrate with the angular router so that we can automatically clear
* the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly
*/
const $setupBreadcrumbsAutoClear = (newPlatform: InternalCoreStart) => (
const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => (
$rootScope: IRootScopeService,
$injector: any
) => {
@ -213,7 +213,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: InternalCoreStart) => (
* lets us integrate with the angular router so that we can automatically clear
* the badge if we switch to a Kibana app that does not use the badge correctly
*/
const $setupBadgeAutoClear = (newPlatform: InternalCoreStart) => (
const $setupBadgeAutoClear = (newPlatform: CoreStart) => (
$rootScope: IRootScopeService,
$injector: any
) => {
@ -253,7 +253,7 @@ const $setupBadgeAutoClear = (newPlatform: InternalCoreStart) => (
* the helpExtension if we switch to a Kibana app that does not set its own
* helpExtension
*/
const $setupHelpExtensionAutoClear = (newPlatform: InternalCoreStart) => (
const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => (
$rootScope: IRootScopeService,
$injector: any
) => {
@ -285,7 +285,7 @@ const $setupHelpExtensionAutoClear = (newPlatform: InternalCoreStart) => (
});
};
const $setupUrlOverflowHandling = (newPlatform: InternalCoreStart) => (
const $setupUrlOverflowHandling = (newPlatform: CoreStart) => (
$location: ILocationService,
$rootScope: IRootScopeService,
Private: any,

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { InternalCoreSetup, InternalCoreStart } from '../../../../core/public';
import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public';
import { Plugin as DataPlugin } from '../../../../plugins/data/public';
import {
Setup as InspectorSetup,
@ -34,12 +34,12 @@ export interface PluginsStart {
}
export const npSetup = {
core: (null as unknown) as InternalCoreSetup,
core: (null as unknown) as LegacyCoreSetup,
plugins: {} as PluginsSetup,
};
export const npStart = {
core: (null as unknown) as InternalCoreStart,
core: (null as unknown) as LegacyCoreStart,
plugins: {} as PluginsStart,
};
@ -48,18 +48,18 @@ export const npStart = {
* @internal
*/
export function __reset__() {
npSetup.core = (null as unknown) as InternalCoreSetup;
npSetup.core = (null as unknown) as LegacyCoreSetup;
npSetup.plugins = {} as any;
npStart.core = (null as unknown) as InternalCoreStart;
npStart.core = (null as unknown) as LegacyCoreStart;
npStart.plugins = {} as any;
}
export function __setup__(coreSetup: InternalCoreSetup, plugins: PluginsSetup) {
export function __setup__(coreSetup: LegacyCoreSetup, plugins: PluginsSetup) {
npSetup.core = coreSetup;
npSetup.plugins = plugins;
}
export function __start__(coreStart: InternalCoreStart, plugins: PluginsStart) {
export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) {
npStart.core = coreStart;
npStart.plugins = plugins;
}

View file

@ -81,6 +81,13 @@ export class UiBundlesController {
this._postLoaders = [];
this._bundles = [];
// create a bundle for core-only with no modules
this.add({
id: 'core',
modules: [],
template: appEntryTemplate
});
// create a bundle for each uiApp
for (const uiApp of uiApps) {
this.add({

View file

@ -102,9 +102,7 @@ export function uiRenderMixin(kbnServer, server, config) {
async handler(request, h) {
const { id } = request.params;
const app = server.getUiAppById(id) || server.getHiddenUiAppById(id);
if (!app) {
throw Boom.notFound(`Unknown app: ${id}`);
}
const isCore = !app;
const uiSettings = request.getUiSettingsService();
const darkMode = !authEnabled || request.auth.isAuthenticated
@ -130,7 +128,9 @@ export function uiRenderMixin(kbnServer, server, config) {
),
`${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`,
`${regularBundlePath}/commons.style.css`,
`${regularBundlePath}/${app.getId()}.style.css`,
...(
!isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : []
),
...kbnServer.uiExports.styleSheetPaths
.filter(path => (
path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light')
@ -145,7 +145,7 @@ export function uiRenderMixin(kbnServer, server, config) {
const bootstrap = new AppBootstrap({
templateData: {
appId: app.getId(),
appId: isCore ? 'core' : app.getId(),
regularBundlePath,
dllBundlePath,
styleSheetPaths,
@ -164,12 +164,11 @@ export function uiRenderMixin(kbnServer, server, config) {
});
server.route({
path: '/app/{id}',
path: '/app/{id}/{any*}',
method: 'GET',
async handler(req, h) {
const id = req.params.id;
const app = server.getUiAppById(id);
if (!app) throw Boom.notFound('Unknown app ' + id);
try {
if (kbnServer.status.isGreen()) {
@ -183,9 +182,15 @@ export function uiRenderMixin(kbnServer, server, config) {
}
});
async function getLegacyKibanaPayload({ app, translations, request, includeUserProvidedConfig }) {
async function getUiSettings({ request, includeUserProvidedConfig }) {
const uiSettings = request.getUiSettingsService();
return props({
defaults: uiSettings.getDefaults(),
user: includeUserProvidedConfig && uiSettings.getUserProvided()
});
}
async function getLegacyKibanaPayload({ app, translations, request, includeUserProvidedConfig }) {
return {
app,
translations,
@ -198,16 +203,15 @@ export function uiRenderMixin(kbnServer, server, config) {
basePath: request.getBasePath(),
serverName: config.get('server.name'),
devMode: config.get('env.dev'),
uiSettings: await props({
defaults: uiSettings.getDefaults(),
user: includeUserProvidedConfig && uiSettings.getUserProvided()
})
uiSettings: await getUiSettings({ request, includeUserProvidedConfig }),
};
}
async function renderApp({ app, h, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) {
const request = h.request;
const basePath = request.getBasePath();
const uiSettings = await getUiSettings({ request, includeUserProvidedConfig });
app = app || { getId: () => 'core' };
const legacyMetadata = await getLegacyKibanaPayload({
app,
@ -228,13 +232,14 @@ export function uiRenderMixin(kbnServer, server, config) {
bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`,
i18n: (id, options) => i18n.translate(id, options),
locale: i18n.getLocale(),
darkMode: get(legacyMetadata.uiSettings.user, ['theme:darkMode', 'userValue'], false),
darkMode: get(uiSettings.user, ['theme:darkMode', 'userValue'], false),
injectedMetadata: {
version: kbnServer.version,
buildNumber: config.get('pkg.buildNum'),
branch: config.get('pkg.branch'),
basePath,
legacyMode: app.getId() !== 'core',
i18n: {
translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`,
},
@ -245,7 +250,7 @@ export function uiRenderMixin(kbnServer, server, config) {
request,
mergeVariables(
injectedVarsOverrides,
await server.getInjectedUiAppVars(app.getId()),
app ? await server.getInjectedUiAppVars(app.getId()) : {},
defaultInjectedVars,
),
),

View file

@ -114,7 +114,7 @@ block content
}
}
.kibanaWelcomeView(id="kbn_loading_message", style="display: none;")
.kibanaWelcomeView(id="kbn_loading_message", style="display: none;", data-test-subj="kbnLoadingMessage")
.kibanaLoaderWrap
.kibanaLoader
.kibanaWelcomeLogoCircle

View file

@ -151,11 +151,22 @@ export function CommonPageProvider({ getService, getPageObjects }) {
navigateToApp(appName, { basePath = '', shouldLoginIfPrompted = true, shouldAcceptAlert = true, hash = '' } = {}) {
const self = this;
const appConfig = config.get(['apps', appName]);
const appUrl = getUrl.noAuth(config.get('servers.kibana'), {
pathname: `${basePath}${appConfig.pathname}`,
hash: hash || appConfig.hash,
});
let appUrl;
if (config.has(['apps', appName])) {
// Legacy applications
const appConfig = config.get(['apps', appName]);
appUrl = getUrl.noAuth(config.get('servers.kibana'), {
pathname: `${basePath}${appConfig.pathname}`,
hash: hash || appConfig.hash,
});
} else {
appUrl = getUrl.noAuth(config.get('servers.kibana'), {
pathname: `${basePath}/app/${appName}`,
hash
});
}
log.debug('navigating to ' + appName + ' url: ' + appUrl);
function navigateTo(url) {

View file

@ -0,0 +1,137 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageHeader,
EuiPageHeaderSection,
EuiPageSideBar,
EuiTitle,
EuiSideNav,
} from '@elastic/eui';
import { AppMountContext, AppMountParameters } from 'kibana/public';
const Home = () => (
<EuiPageBody data-test-subj="fooAppHome">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Welcome to Foo!</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>Bar home page section title</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>Wow what a home page this is!</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
const PageA = () => (
<EuiPageBody data-test-subj="fooAppPageA">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Page A</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>Page A section title</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>Page A's content goes here</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
type NavProps = RouteComponentProps & {
navigateToApp: AppMountContext['core']['application']['navigateToApp'];
};
const Nav = withRouter(({ history, navigateToApp }: NavProps) => (
<EuiSideNav
items={[
{
name: 'Foo',
id: 'foo',
items: [
{
id: 'home',
name: 'Home',
onClick: () => history.push('/'),
'data-test-subj': 'fooNavHome',
},
{
id: 'page-a',
name: 'Page A',
onClick: () => history.push('/page-a'),
'data-test-subj': 'fooNavPageA',
},
{
id: 'linktobar',
name: 'Open Bar / Page B',
onClick: () => navigateToApp('bar', { path: 'page-b?query=here', state: 'foo!!' }),
'data-test-subj': 'fooNavBarPageB',
},
],
},
]}
/>
));
const FooApp = ({ basename, context }: { basename: string; context: AppMountContext }) => (
<Router basename={basename}>
<EuiPage>
<EuiPageSideBar>
<Nav navigateToApp={context.core.application.navigateToApp} />
</EuiPageSideBar>
<Route path="/" exact component={Home} />
<Route path="/page-a" component={PageA} />
</EuiPage>
</Router>
);
export const renderApp = (
context: AppMountContext,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(<FooApp basename={appBasePath} context={context} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -21,6 +21,15 @@ import { Plugin, CoreSetup } from 'kibana/public';
export class CorePluginAPlugin implements Plugin<CorePluginAPluginSetup, CorePluginAPluginStart> {
public setup(core: CoreSetup, deps: {}) {
core.application.register({
id: 'foo',
title: 'Foo',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
},
});
return {
getGreeting() {
return 'Hello from Plugin A!';

View file

@ -0,0 +1,144 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageHeader,
EuiPageHeaderSection,
EuiPageSideBar,
EuiTitle,
EuiSideNav,
} from '@elastic/eui';
import { AppMountContext, AppMountParameters } from 'kibana/public';
const Home = () => (
<EuiPageBody data-test-subj="barAppHome">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Welcome to Bar!</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>Bar home page sction</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>It feels so homey!</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
const PageB = ({ location }: RouteComponentProps) => {
const searchParams: any[] = [];
new URLSearchParams(location.search).forEach((value, key) => searchParams.push([key, value]));
return (
<EuiPageBody data-test-subj="barAppPageB">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Page B</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>
Search params:{' '}
<span data-test-subj="barAppPageBQuery">{JSON.stringify(searchParams)}</span>
</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
</EuiPageContent>
</EuiPageBody>
);
};
type NavProps = RouteComponentProps & {
navigateToApp: AppMountContext['core']['application']['navigateToApp'];
};
const Nav = withRouter(({ history, navigateToApp }: NavProps) => (
<EuiSideNav
items={[
{
name: 'Bar',
id: 'bar',
items: [
{
id: 'home',
name: 'Home',
onClick: () => navigateToApp('bar', { path: '/' }),
'data-test-subj': 'barNavHome',
},
{
id: 'page-b',
name: 'Page B',
onClick: () => history.push('/page-b', { bar: 'page-b' }),
'data-test-subj': 'barNavPageB',
},
{
id: 'linktofoo',
name: 'Open Foo',
onClick: () => navigateToApp('foo'),
'data-test-subj': 'barNavFooHome',
},
],
},
]}
/>
));
const BarApp = ({ basename, context }: { basename: string; context: AppMountContext }) => (
<Router basename={basename}>
<EuiPage>
<EuiPageSideBar>
<Nav navigateToApp={context.core.application.navigateToApp} />
</EuiPageSideBar>
<Route path="/" exact component={Home} />
<Route path="/page-b" component={PageB} />
</EuiPage>
</Router>
);
export const renderApp = (
context: AppMountContext,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(<BarApp basename={appBasePath} context={context} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -34,6 +34,15 @@ export class CorePluginBPlugin
implements Plugin<CorePluginBPluginSetup, CorePluginBPluginStart, CorePluginBDeps> {
public setup(core: CoreSetup, deps: CorePluginBDeps) {
window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`;
core.application.register({
id: 'bar',
title: 'Bar',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
},
});
}
public start() {}

View file

@ -0,0 +1,96 @@
/*
* 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 expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['common']);
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
const loadingScreenNotShown = async () =>
expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false);
const loadingScreenShown = () =>
testSubjects.existOrFail('kbnLoadingMessage');
describe('ui applications', function describeIndexTests() {
before(async () => {
await PageObjects.common.navigateToApp('foo');
});
it('starts on home page', async () => {
await testSubjects.existOrFail('fooAppHome');
});
it('navigates to its own pages', async () => {
// Go to page A
await testSubjects.click('fooNavPageA');
expect(await browser.getCurrentUrl()).to.eql(`http://localhost:5620/app/foo/page-a`);
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppPageA');
// Go to home page
await testSubjects.click('fooNavHome');
expect(await browser.getCurrentUrl()).to.eql(`http://localhost:5620/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(`http://localhost:5620/app/foo/page-a`);
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppPageA');
});
it('navigates to other apps', async () => {
await testSubjects.click('fooNavBarPageB');
await loadingScreenNotShown();
await testSubjects.existOrFail('barAppPageB');
expect(await browser.getCurrentUrl()).to.eql(`http://localhost:5620/app/bar/page-b?query=here`);
});
it('preserves query parameters across apps', async () => {
const querySpan = await testSubjects.find('barAppPageBQuery');
expect(await querySpan.getVisibleText()).to.eql(`[["query","here"]]`);
});
it('can use the back button to navigate back to previous app', async () => {
await browser.goBack();
expect(await browser.getCurrentUrl()).to.eql(`http://localhost:5620/app/foo/page-a`);
await loadingScreenNotShown();
await testSubjects.existOrFail('fooAppPageA');
});
it('can navigate from NP apps to legacy apps', async () => {
await appsMenu.clickLink('Management');
await loadingScreenShown();
await testSubjects.existOrFail('managementNav');
});
it('can navigate from legacy apps to NP apps', async () => {
await appsMenu.clickLink('Foo');
await loadingScreenShown();
await testSubjects.existOrFail('fooAppHome');
});
});
}

View file

@ -19,6 +19,7 @@
export default function ({ loadTestFile }) {
describe('core plugins', () => {
loadTestFile(require.resolve('./applications'));
loadTestFile(require.resolve('./ui_plugins'));
loadTestFile(require.resolve('./server_plugins.js'));
});

View file

@ -6,7 +6,7 @@
import { Location } from 'history';
import React from 'react';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
import { useKibanaCore } from '../../../../../observability/public';
import { getAPMHref } from '../../shared/Links/apm/APMLink';
import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs';
@ -15,7 +15,7 @@ import { routes } from './route_config';
interface Props {
location: Location;
breadcrumbs: Breadcrumb[];
core: InternalCoreStart;
core: LegacyCoreStart;
}
function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) {

View file

@ -31,7 +31,7 @@ import moment from 'moment-timezone';
import React, { Component } from 'react';
import styled from 'styled-components';
import { toastNotifications } from 'ui/notify';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
import { KibanaCoreContext } from '../../../../../../observability/public';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { KibanaLink } from '../../../shared/Links/KibanaLink';
@ -40,7 +40,7 @@ import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';
type ScheduleKey = keyof Schedule;
const getUserTimezone = memoize((core: InternalCoreStart): string => {
const getUserTimezone = memoize((core: LegacyCoreStart): string => {
return core.uiSettings.get('dateFormat:tz') === 'Browser'
? moment.tz.guess()
: core.uiSettings.get('dateFormat:tz');

View file

@ -12,7 +12,7 @@ import * as callApmApi from '../../../../services/rest/callApmApi';
import { ServiceOverview } from '..';
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
@ -30,7 +30,7 @@ describe('Service Overview -> View', () => {
prepend: (path: string) => `/basepath${path}`
}
}
} as unknown) as InternalCoreStart;
} as unknown) as LegacyCoreStart;
// mock urlParams
spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({

View file

@ -15,7 +15,7 @@ import { DiscoverErrorLink } from '../DiscoverErrorLink';
import { DiscoverSpanLink } from '../DiscoverSpanLink';
import { DiscoverTransactionLink } from '../DiscoverTransactionLink';
import * as kibanaCore from '../../../../../../../observability/public/context/kibana_core';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
jest.mock('ui/kfetch');
@ -32,7 +32,7 @@ beforeAll(() => {
prepend: (path: string) => `/basepath${path}`
}
}
} as unknown) as InternalCoreStart;
} as unknown) as LegacyCoreStart;
jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock);
});

View file

@ -9,7 +9,7 @@ import React from 'react';
import { getRenderedHref } from '../../../utils/testHelpers';
import { InfraLink } from './InfraLink';
import * as kibanaCore from '../../../../../observability/public/context/kibana_core';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
const coreMock = ({
http: {
@ -17,7 +17,7 @@ const coreMock = ({
prepend: (path: string) => `/basepath${path}`
}
}
} as unknown) as InternalCoreStart;
} as unknown) as LegacyCoreStart;
jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock);

View file

@ -9,7 +9,7 @@ import React from 'react';
import { getRenderedHref } from '../../../utils/testHelpers';
import { KibanaLink } from './KibanaLink';
import * as kibanaCore from '../../../../../observability/public/context/kibana_core';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
describe('KibanaLink', () => {
beforeEach(() => {
@ -19,7 +19,7 @@ describe('KibanaLink', () => {
prepend: (path: string) => `/basepath${path}`
}
}
} as unknown) as InternalCoreStart;
} as unknown) as LegacyCoreStart;
jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock);
});

View file

@ -9,7 +9,7 @@ import React from 'react';
import { getRenderedHref } from '../../../../utils/testHelpers';
import { MLJobLink } from './MLJobLink';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
describe('MLJobLink', () => {
beforeEach(() => {
@ -19,7 +19,7 @@ describe('MLJobLink', () => {
prepend: (path: string) => `/basepath${path}`
}
}
} as unknown) as InternalCoreStart;
} as unknown) as LegacyCoreStart;
spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock);
});

View file

@ -10,7 +10,7 @@ import { getRenderedHref } from '../../../../utils/testHelpers';
import { MLLink } from './MLLink';
import * as savedObjects from '../../../../services/rest/savedObjects';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
jest.mock('ui/kfetch');
@ -20,7 +20,7 @@ const coreMock = ({
prepend: (path: string) => `/basepath${path}`
}
}
} as unknown) as InternalCoreStart;
} as unknown) as LegacyCoreStart;
jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock);

View file

@ -13,7 +13,7 @@ import * as Transactions from './mockData';
import * as apmIndexPatternHooks from '../../../../hooks/useAPMIndexPattern';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
import { ISavedObject } from '../../../../services/rest/savedObjects';
import { InternalCoreStart } from 'src/core/public';
import { LegacyCoreStart } from 'src/core/public';
jest.mock('ui/kfetch');
@ -35,7 +35,7 @@ describe('TransactionActionMenu component', () => {
prepend: (path: string) => `/basepath${path}`
}
}
} as unknown) as InternalCoreStart;
} as unknown) as LegacyCoreStart;
jest
.spyOn(apmIndexPatternHooks, 'useAPMIndexPattern')

Some files were not shown because too many files have changed in this diff Show more