Add ApplicationService Mounting (#41007) (#44692)

This commit is contained in:
Josh Dover 2019-09-03 15:35:22 -05:00 committed by GitHub
parent 65cca6bc64
commit 069843e406
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 2550 additions and 675 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

@ -165,6 +165,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",
@ -302,6 +303,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

@ -1,252 +0,0 @@
- Start Date: 2019-03-22
- RFC PR: [#33740](https://github.com/elastic/kibana/pull/33740)
- Kibana Issue: (leave this empty)
# Summary
In order to support the action service we need a way to encrypt/decrypt
attributes on saved objects that works with security and spaces filtering as
well as performing audit logging. Sufficiently hides the private key used and
removes encrypted attributes from being exposed through regular means.
# Basic example
Register saved object type with the `encrypted_saved_objects` plugin:
```typescript
server.plugins.encrypted_saved_objects.registerType({
type: 'server-action',
attributesToEncrypt: new Set(['credentials', 'apiKey']),
});
```
Use the same API to create saved objects with encrypted attributes as for any other saved object type:
```typescript
const savedObject = await server.savedObjects
.getScopedSavedObjectsClient(request)
.create('server-action', {
name: 'my-server-action',
data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
credentials: { username: 'some-user', password: 'some-password' },
apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb'
});
// savedObject = {
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
// type: 'server-action',
// name: 'my-server-action',
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
// };
```
Use dedicated method to retrieve saved object with decrypted attributes on behalf of Kibana internal user:
```typescript
const savedObject = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser(
'server-action',
'dd9750b9-ef0a-444c-8405-4dfcc2e9d670'
);
// savedObject = {
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
// type: 'server-action',
// name: 'my-server-action',
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
// credentials: { username: 'some-user', password: 'some-password' },
// apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb',
// };
```
# Motivation
Main motivation is the storage and usage of third-party credentials for use with
the action service to do notifications. Also perform other types integrations,
call webhooks using tokens.
# Detailed design
In order for this to be in basic it needs to be done as a wrapper around the
saved object client. This can be added from the `x-pack` plugin.
## General
To be able to manage saved objects with encrypted attributes from any plugin one should
do the following:
1. Define `encrypted_saved_objects` plugin as a dependency.
2. Add attributes to be encrypted in `mappings.json` file for the respective saved object type. These attributes should
always have a `binary` type since they'll contain encrypted content as a `Base64` encoded string and should never be
searchable or analyzed. This makes defining of attributes that require encryption explicit and auditable, and significantly
simplifies implementation:
```json
{
"server-action": {
"properties": {
"name": { "type": "keyword" },
"data": {
"properties": {
"location": { "type": "geo_shape" },
"email": { "type": "text" }
}
},
"credentials": { "type": "binary" },
"apiKey": { "type": "binary" }
}
}
}
```
3. Register saved object type and attributes that should be encrypted with `encrypted_saved_objects` plugin:
```typescript
server.plugins.encrypted_saved_objects.registerType({
type: 'server-action',
attributesToEncrypt: new Set(['credentials', 'apiKey']),
attributesToExcludeFromAAD: new Set(['data']),
});
```
Notice the optional `attributesToExcludeFromAAD` property, it allows one to exclude some of the saved object attributes
from Additional authenticated data (AAD), read more about that below in `Encryption and decryption` section.
Since `encrypted_saved_objects` adds its own wrapper (`EncryptedSavedObjectsClientWrapper`) into `SavedObjectsClient`
wrapper chain consumers will be able to create, update, delete and retrieve saved objects using standard Saved Objects API.
Two main responsibilities of the wrapper are:
* It encrypts attributes that are supposed to be encrypted during `create`, `bulkCreate` and `update` operations
* It strips encrypted attributes from **any** saved object returned from the Saved Objects API
As noted above the wrapper is stripping encrypted attributes from saved objects returned from the API methods, that means
that there is no way at all to retrieve encrypted attributes using standard Saved Objects API unless `encrypted_saved_objects`
plugin is disabled. This potentially can lead to the situation when consumer retrieves saved object, updates its non-encrypted
properties and passes that same object to the `update` Saved Objects API method without re-defining encrypted attributes. In
this case only specified attributes will be updated and encrypted attributes will stay untouched. And if these updated
attributes are included into AAD, that is true by default for all attributes unless they are specifically excluded via
`attributesToExcludeFromAAD`, then it will be no longer possible to decrypt encrypted attributes. At this stage we consider
this as a developer mistake and don't prevent it from happening in any way apart from logging this type of event. Partial
update of only attributes that are not the part of AAD will not cause this issue.
Saved object ID is an essential part of AAD used during encryption process and hence should be as hard to guess as possible.
To fulfil this requirement wrapper generates highly random IDs (UUIDv4) for the saved objects that contain encrypted
attributes and hence consumers are not allowed to specify ID when calling `create` or `bulkCreate` method and if they try
to do so the error will be thrown.
To reduce the risk of unintentional decryption and consequent leaking of the sensitive information there is only one way
to retrieve saved object and decrypt its encrypted attributes and it's exposed only through `encrypted_saved_objects` plugin:
```typescript
const savedObject = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser(
'server-action',
'dd9750b9-ef0a-444c-8405-4dfcc2e9d670'
);
// savedObject = {
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
// type: 'server-action',
// name: 'my-server-action',
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
// credentials: { username: 'some-user', password: 'some-password' },
// apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb',
// };
```
As can be seen from the method name, the request to retrieve saved object and decrypt its attributes is performed on
behalf of the internal Kibana user and hence isn't supposed to be called within user request context.
**Note:** the fact that saved object with encrypted attributes is created using standard Saved Objects API within a
particular user and space context, but retrieved out of any context makes it unclear how consumers are supposed to
provide that context and retrieve saved object from a particular space. Current plan for `getDecryptedAsInternalUser`
method is to accept a third `BaseOptions` argument that allows consumers to specify `namespace` that they can retrieve
from the request using public `spaces` plugin API.
## Encryption and decryption
Saved object attributes are encrypted using [@elastic/node-crypto](https://github.com/elastic/node-crypto) library. Please
take a look at the source code of this library to know how encryption is performed exactly, what algorithm and encryption
parameters are used, but in short it's AES Encryption with AES-256-GCM that uses random initialization vector and salt.
As with encryption key for Kibana's session cookie, master encryption key used by `encrypted_saved_objects` plugin can be
defined as a configuration value (`xpack.encrypted_saved_objects.encryptionKey`) via `kibana.yml`, but it's **highly
recommended** to define this key in the [Kibana Keystore](https://www.elastic.co/guide/en/kibana/current/secure-settings.html)
instead. The master key should be cryptographically safe and be equal or greater than 32 bytes.
To prevent certain vectors of attacks where raw content of encrypted attributes of one saved object is copied to another
saved object which would unintentionally allow it to decrypt content that was not supposed to be decrypted we rely on Additional
authenticated data (AAD) during encryption and decryption. AAD consists of the following components:
* Saved object ID
* Saved object type
* Saved object attributes
AAD does not include encrypted attributes themselves and attributes defined in optional `attributesToExcludeFromAAD`
parameter provided during saved object type registration with `encrypted_saved_objects` plugin. There are a number of
reasons why one would want to exclude certain attributes from AAD:
* if attribute contains large amount of data that can significantly slow down encryption and decryption, especially during
bulk operations (e.g. large geo shape or arbitrary HTML document)
* if attribute contains data that is supposed to be updated separately from encrypted attributes or attributes included
into AAD (e.g some user defined content associated with the email action or alert)
## Audit
Encrypted attributes will most likely contain sensitive information and any attempt to access these should be properly
logged to allow any further audit procedures. The following events will be logged with Kibana audit log functionality:
* Successful attempt to encrypt attributes (incl. saved object ID, type and attributes names)
* Failed attempt to encrypt attribute (incl. saved object ID, type and attribute name)
* Successful attempt to decrypt attributes (incl. saved object ID, type and attributes names)
* Failed attempt to decrypt attribute (incl. saved object ID, type and attribute name)
In addition to audit log events we'll issue ordinary log events for any attempts to save, update or decrypt saved objects
with missing attributes that were supposed to be encrypted/decrypted based on the registration parameters.
# Benefits
* None of the registered types will expose their encrypted details. The saved
objects with their unencrypted attributes could still be obtained and searched
on. The wrapper will follow all the security and spaces filtering of saved
objects so that only users with appropriate permissions will be able to obtain
the scrubbed objects or _save_ objects with encrypted attributes.
* No explicit access to a method that takes in an encrypted string exists. If the
type was not registered no decryption is possible. No need to handle the saved object
with the encrypted attributes reducing the risk of accidentally returning it in a
handler.
# Drawbacks
* It isn't possible to decrypt existing encrypted attributes once encryption key changes
* Possibly have a performance impact on Saved Objects API operations that require encryption/decryption
* Will require non trivial tests to test functionality along with spaces and security
* The attributes that are encrypted have to be defined and if they change they need to be migrated
# Out of scope
* Encryption key rotation mechanism, either regular or emergency
* Mechanism that would detect and warn when Kibana does not use keystore to store encryption key
# Alternatives
Only allow this to be used within the Actions service itself where the details
of the saved object are handled there directly. And the saved objects are
`hidden` but still use the security and spaces wrappers.
# Adoption strategy
Integration should be pretty easy which would include depending on the plugin, registering the desired saved object type
with it and defining encrypted attributes in the `mappings.json`.
# How we teach this
The `encrypted_saved_objects` as the name of the `thing` where it's seen as a separate
extension on top of the saved object service.
Provide a README.md in the plugin directory with the usage examples.
# Unresolved questions
* Is it acceptable to have this plugin in Basic?
* Are there any other use-cases that are not served with that interface?
* How would this work with Saved Objects Export\Import API?
* How would this work with migrations, if the attribute names wanted to be
changed, a decrypt context would need to be created for migration?

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';
@ -73,7 +73,7 @@ interface ConstructorParams {
}
interface StartDeps {
application: ApplicationStart;
application: InternalApplicationStart;
docLinks: DocLinksStart;
http: HttpStart;
injectedMetadata: InjectedMetadataStart;
@ -83,14 +83,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,
@ -114,7 +111,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.',
@ -127,11 +124,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}
@ -145,6 +143,7 @@ export class ChromeService {
takeUntil(this.stop$)
)}
kibanaVersion={injectedMetadata.getKibanaVersion()}
legacyMode={injectedMetadata.getLegacyMode()}
navLinks$={navLinks.getNavLinks$()}
recentlyAccessed$={recentlyAccessed.get$()}
navControlsLeft$={navControls.getLeft$()}
@ -373,5 +372,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

@ -72,6 +72,9 @@ import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } fr
/** @interal */
export { CoreContext, CoreSystem } from './core_system';
export { RecursiveReadonly } from '../utils';
export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application';
export {
SavedObjectsBatchResponse,
SavedObjectsBulkCreateObject,
@ -115,6 +118,8 @@ export {
* https://github.com/Microsoft/web-build-tools/issues/1237
*/
export interface CoreSetup {
/** {@link ApplicationSetup} */
application: ApplicationSetup;
/** {@link ContextSetup} */
context: ContextSetup;
/** {@link FatalErrorsSetup} */
@ -138,7 +143,7 @@ export interface CoreSetup {
*/
export interface CoreStart {
/** {@link ApplicationStart} */
application: Pick<ApplicationStart, 'capabilities'>;
application: ApplicationStart;
/** {@link ChromeStart} */
chrome: ChromeStart;
/** {@link DocLinksStart} */
@ -157,15 +162,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

@ -10,24 +10,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>>;
@ -102,7 +143,7 @@ export interface ChromeNavLink {
readonly legacy: boolean;
// @deprecated
readonly linkToLastSubUrl?: boolean;
readonly order: number;
readonly order?: number;
// @deprecated
readonly subUrlBase?: string;
readonly title: string;
@ -182,6 +223,8 @@ export interface CoreContext {
// @public
export interface CoreSetup {
// (undocumented)
application: ApplicationSetup;
// (undocumented)
context: ContextSetup;
// (undocumented)
@ -197,7 +240,7 @@ export interface CoreSetup {
// @public
export interface CoreStart {
// (undocumented)
application: Pick<ApplicationStart, 'capabilities'>;
application: ApplicationStart;
// (undocumented)
chrome: ChromeStart;
// (undocumented)
@ -502,23 +545,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

@ -7,7 +7,7 @@
import { Location } from 'history';
import { last } from 'lodash';
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';
@ -16,7 +16,7 @@ import { routes } from './route_config';
interface Props {
location: Location;
breadcrumbs: Breadcrumb[];
core: InternalCoreStart;
core: LegacyCoreStart;
}
class UpdateBreadcrumbsComponent extends React.Component<Props> {

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