mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Add ApplicationService Mounting (#41007)
* Add core-only bundle * Add ApplicationService mounting * Add LegacyCore{Setup,Start} * Fix PR comments * Add functional tests * Fix PR comments * Fix PR comments * Remove other usages of rootRoute * Use state field notation * Add support for open in new tab * Fix PR comments * Fix pesky await from the dead * Update docs * Bump @types/history
This commit is contained in:
parent
5854d9e7e9
commit
b352f67bdb
104 changed files with 2587 additions and 453 deletions
20
docs/development/core/public/kibana-plugin-public.app.md
Normal file
20
docs/development/core/public/kibana-plugin-public.app.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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) => AppUnmount | Promise<AppUnmount></code> | A mount function called when the user navigates to this app's route. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [App](./kibana-plugin-public.app.md) > [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>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [capabilities](./kibana-plugin-public.appbase.capabilities.md)
|
||||
|
||||
## AppBase.capabilities property
|
||||
|
||||
Custom capabilities defined by the app.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
capabilities?: Partial<Capabilities>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [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;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [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;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [id](./kibana-plugin-public.appbase.id.md)
|
||||
|
||||
## AppBase.id property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
id: string;
|
||||
```
|
25
docs/development/core/public/kibana-plugin-public.appbase.md
Normal file
25
docs/development/core/public/kibana-plugin-public.appbase.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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<Capabilities></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<string></code> | An observable for a tooltip shown when hovering over app link. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [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;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [title](./kibana-plugin-public.appbase.title.md)
|
||||
|
||||
## AppBase.title property
|
||||
|
||||
The title of the application.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
title: string;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [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>;
|
||||
```
|
|
@ -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. |
|
||||
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerApp](./kibana-plugin-public.applicationsetup.registerapp.md)
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [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>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [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<AppMountContext, T></code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [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[];
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [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`
|
||||
|
|
@ -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<Capabilities></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. |
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [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`
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [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<AppMountContext, T></code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountContext](./kibana-plugin-public.appmountcontext.md) > [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;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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<ApplicationStart, 'capabilities' | 'navigateToApp'>;</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. |
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [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);
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [element](./kibana-plugin-public.appmountparameters.element.md)
|
||||
|
||||
## AppMountParameters.element property
|
||||
|
||||
The container element to render the application into.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
element: HTMLElement;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md)
|
||||
|
||||
## 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. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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;
|
||||
```
|
|
@ -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;
|
||||
```
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [application](./kibana-plugin-public.coresetup.application.md)
|
||||
|
||||
## CoreSetup.application property
|
||||
|
||||
[ApplicationSetup](./kibana-plugin-public.applicationsetup.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
application: ApplicationSetup;
|
||||
```
|
|
@ -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) |
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
application: Pick<ApplicationStart, 'capabilities'>;
|
||||
application: ApplicationStart;
|
||||
```
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface CoreStart
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [application](./kibana-plugin-public.corestart.application.md) | <code>Pick<ApplicationStart, 'capabilities'></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) |
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) > [injectedMetadata](./kibana-plugin-public.legacycoresetup.injectedmetadata.md)
|
||||
|
||||
## LegacyCoreSetup.injectedMetadata property
|
||||
|
||||
> Warning: This API is now obsolete.
|
||||
>
|
||||
>
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
injectedMetadata: InjectedMetadataSetup;
|
||||
```
|
|
@ -0,0 +1,28 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) > [injectedMetadata](./kibana-plugin-public.legacycorestart.injectedmetadata.md)
|
||||
|
||||
## LegacyCoreStart.injectedMetadata property
|
||||
|
||||
> Warning: This API is now obsolete.
|
||||
>
|
||||
>
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
injectedMetadata: InjectedMetadataStart;
|
||||
```
|
|
@ -0,0 +1,28 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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.
|
||||
|
|
@ -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) | |
|
||||
|
|
|
@ -166,6 +166,7 @@
|
|||
"handlebars": "4.1.2",
|
||||
"hapi": "^17.5.3",
|
||||
"hapi-auth-cookie": "^9.0.0",
|
||||
"history": "^4.9.0",
|
||||
"hjson": "3.1.2",
|
||||
"hoek": "^5.0.4",
|
||||
"http-proxy-agent": "^2.1.0",
|
||||
|
@ -304,6 +305,7 @@
|
|||
"@types/hapi": "^17.0.18",
|
||||
"@types/hapi-auth-cookie": "^9.1.0",
|
||||
"@types/has-ansi": "^3.0.0",
|
||||
"@types/history": "^4.7.3",
|
||||
"@types/hoek": "^4.1.3",
|
||||
"@types/humps": "^1.1.2",
|
||||
"@types/jest": "^24.0.9",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -18,14 +18,14 @@ import ReactDOM from 'react-dom';
|
|||
|
||||
import { MyApp } from './componnets';
|
||||
|
||||
export function renderApp(context, targetDomElement) {
|
||||
export function renderApp(context, { element }) {
|
||||
ReactDOM.render(
|
||||
<MyApp mountContext={context} deps={pluginStart} />,
|
||||
targetDomElement
|
||||
element
|
||||
);
|
||||
|
||||
return () => {
|
||||
ReactDOM.unmountComponentAtNode(targetDomElement);
|
||||
ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
@ -38,9 +38,9 @@ class MyPlugin {
|
|||
application.register({
|
||||
id: 'my-app',
|
||||
title: 'My Application',
|
||||
async mount(context, targetDomElement) {
|
||||
async mount(context, params) {
|
||||
const { renderApp } = await import('./applcation');
|
||||
return renderApp(context, targetDomElement);
|
||||
return renderApp(context, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -63,9 +63,7 @@ lock-in.
|
|||
|
||||
```ts
|
||||
/** A context type that implements the Handler Context pattern from RFC-0003 */
|
||||
export interface MountContext {
|
||||
/** This is the base path for setting up your router. */
|
||||
basename: string;
|
||||
export interface AppMountContext {
|
||||
/** These services serve as an example, but are subject to change. */
|
||||
core: {
|
||||
http: {
|
||||
|
@ -93,6 +91,13 @@ export interface MountContext {
|
|||
[contextName: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AppMountParams {
|
||||
/** The base path the application is mounted on. Used to configure routers. */
|
||||
appBasePath: string;
|
||||
/** The element the application should render into */
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
export type Unmount = () => Promise<void> | void;
|
||||
|
||||
export interface AppSpec {
|
||||
|
@ -109,11 +114,11 @@ export interface AppSpec {
|
|||
|
||||
/**
|
||||
* A mount function called when the user navigates to this app's route.
|
||||
* @param context the `MountContext generated for this app
|
||||
* @param targetDomElement An HTMLElement to mount the application onto.
|
||||
* @param context the `AppMountContext` generated for this app
|
||||
* @param params the `AppMountParams`
|
||||
* @returns An unmounting function that will be called to unmount the application.
|
||||
*/
|
||||
mount(context: MountContext, targetDomElement: HTMLElement): Unmount | Promise<Unmount>;
|
||||
mount(context: MountContext, params: AppMountParams): Unmount | Promise<Unmount>;
|
||||
|
||||
/**
|
||||
* A EUI iconType that will be used for the app's icon. This icon
|
||||
|
@ -158,19 +163,21 @@ When an app is registered via `register`, it must provide a `mount` function
|
|||
that will be invoked whenever the window's location has changed from another app
|
||||
to this app.
|
||||
|
||||
This function is called with a `MountContext` and an `HTMLElement` for the
|
||||
application to render itself to. The mount function must also return a function
|
||||
that can be called by the ApplicationService to unmount the application at the
|
||||
given DOM node. The mount function may return a Promise of an unmount function
|
||||
in order to import UI code dynamically.
|
||||
This function is called with a `AppMountContext` and an
|
||||
`AppMountParams` which contains a `HTMLElement` for the application to
|
||||
render itself to. The mount function must also return a function that can be
|
||||
called by the ApplicationService to unmount the application at the given DOM
|
||||
Element. The mount function may return a Promise of an unmount function in order
|
||||
to import UI code dynamically.
|
||||
|
||||
The ApplicationService's `register` method will only be available during the
|
||||
*setup* lifecycle event. This allows the system to know when all applications
|
||||
have been registered.
|
||||
|
||||
The `mount` function will also get access to the `MountContext` that has many of
|
||||
the same core services available during the `start` lifecycle. Plugins can also
|
||||
register additional context attributes via the `registerMountContext` function.
|
||||
The `mount` function will also get access to the `AppMountContext` that
|
||||
has many of the same core services available during the `start` lifecycle.
|
||||
Plugins can also register additional context attributes via the
|
||||
`registerMountContext` function.
|
||||
|
||||
## Routing
|
||||
|
||||
|
@ -190,7 +197,7 @@ An example:
|
|||
"overview" page: mykibana.com/app/my-app/overview
|
||||
|
||||
When setting up a router, your application should only handle the part of the
|
||||
URL following the `context.basename` provided when you application is mounted.
|
||||
URL following the `params.appBasePath` provided when you application is mounted.
|
||||
|
||||
### Legacy Applications
|
||||
|
||||
|
@ -211,7 +218,7 @@ a full-featured router and code-splitting. Note that using React or any other
|
|||
3rd party tools featured here is not required to build a Kibana Application.
|
||||
|
||||
```tsx
|
||||
// my_plugin/public/application.ts
|
||||
// my_plugin/public/application.tsx
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -239,16 +246,16 @@ const MyApp = ({ basename }) => (
|
|||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
export function renderApp(context, targetDomElement) {
|
||||
export function renderApp(context, params) {
|
||||
ReactDOM.render(
|
||||
// `context.basename` would be `/app/my-app` in this example.
|
||||
// This exact string is not guaranteed to be stable, always reference
|
||||
// `context.basename`.
|
||||
<MyApp basename={context.basename} />,
|
||||
targetDomElem
|
||||
// `params.appBasePath` would be `/app/my-app` in this example.
|
||||
// This exact string is not guaranteed to be stable, always reference the
|
||||
// provided value at `params.appBasePath`.
|
||||
<MyApp basename={params.appBasePath} />,
|
||||
params.element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(targetDomElem);
|
||||
return () => ReactDOM.unmountComponentAtNode(params.element);
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -259,9 +266,9 @@ export class MyPlugin {
|
|||
setup({ application }) {
|
||||
application.register({
|
||||
id: 'my-app',
|
||||
async mount(context, targetDomElem) {
|
||||
async mount(context, params) {
|
||||
const { renderApp } = await import('./applcation');
|
||||
return renderApp(context, targetDomElement);
|
||||
return renderApp(context, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
130
src/core/public/application/integration_tests/router.test.tsx
Normal file
130
src/core/public/application/integration_tests/router.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
300
src/core/public/application/types.ts
Normal file
300
src/core/public/application/types.ts
Normal 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;
|
||||
}
|
111
src/core/public/application/ui/app_container.tsx
Normal file
111
src/core/public/application/ui/app_container.tsx
Normal 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;
|
||||
}
|
51
src/core/public/application/ui/app_not_found_screen.tsx
Normal file
51
src/core/public/application/ui/app_not_found_screen.tsx
Normal 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>
|
||||
);
|
53
src/core/public/application/ui/app_router.tsx
Normal file
53
src/core/public/application/ui/app_router.tsx
Normal 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>
|
||||
);
|
21
src/core/public/application/ui/index.ts
Normal file
21
src/core/public/application/ui/index.ts
Normal 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';
|
|
@ -27,7 +27,7 @@ import {
|
|||
|
||||
const createStartContractMock = () => {
|
||||
const startContract: DeeplyMockedKeys<InternalChromeStart> = {
|
||||
getComponent: jest.fn(),
|
||||
getHeaderComponent: jest.fn(),
|
||||
navLinks: {
|
||||
getNavLinks$: jest.fn(),
|
||||
has: jest.fn(),
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ import { IconType } from '@elastic/eui';
|
|||
|
||||
import { InjectedMetadataStart } from '../injected_metadata';
|
||||
import { NotificationsStart } from '../notifications';
|
||||
import { ApplicationStart } from '../application';
|
||||
import { InternalApplicationStart } from '../application';
|
||||
import { HttpStart } from '../http';
|
||||
|
||||
import { ChromeNavLinks, NavLinksService } from './nav_links';
|
||||
|
@ -74,7 +74,7 @@ interface ConstructorParams {
|
|||
}
|
||||
|
||||
interface StartDeps {
|
||||
application: ApplicationStart;
|
||||
application: InternalApplicationStart;
|
||||
docLinks: DocLinksStart;
|
||||
http: HttpStart;
|
||||
injectedMetadata: InjectedMetadataStart;
|
||||
|
@ -84,14 +84,11 @@ interface StartDeps {
|
|||
/** @internal */
|
||||
export class ChromeService {
|
||||
private readonly stop$ = new ReplaySubject(1);
|
||||
private readonly browserSupportsCsp: boolean;
|
||||
private readonly navControls = new NavControlsService();
|
||||
private readonly navLinks = new NavLinksService();
|
||||
private readonly recentlyAccessed = new RecentlyAccessedService();
|
||||
|
||||
constructor({ browserSupportsCsp }: ConstructorParams) {
|
||||
this.browserSupportsCsp = browserSupportsCsp;
|
||||
}
|
||||
constructor(private readonly params: ConstructorParams) {}
|
||||
|
||||
public async start({
|
||||
application,
|
||||
|
@ -115,7 +112,7 @@ export class ChromeService {
|
|||
const navLinks = this.navLinks.start({ application, http });
|
||||
const recentlyAccessed = await this.recentlyAccessed.start({ http });
|
||||
|
||||
if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
|
||||
if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
|
||||
notifications.toasts.addWarning(
|
||||
i18n.translate('core.chrome.legacyBrowserWarning', {
|
||||
defaultMessage: 'Your browser does not meet the security requirements for Kibana.',
|
||||
|
@ -128,11 +125,12 @@ export class ChromeService {
|
|||
navLinks,
|
||||
recentlyAccessed,
|
||||
|
||||
getComponent: () => (
|
||||
getHeaderComponent: () => (
|
||||
<React.Fragment>
|
||||
<LoadingIndicator loadingCount$={http.getLoadingCount$()} />
|
||||
|
||||
<Header
|
||||
application={application}
|
||||
appTitle$={appTitle$.pipe(takeUntil(this.stop$))}
|
||||
badge$={badge$.pipe(takeUntil(this.stop$))}
|
||||
basePath={http.basePath}
|
||||
|
@ -146,6 +144,7 @@ export class ChromeService {
|
|||
takeUntil(this.stop$)
|
||||
)}
|
||||
kibanaVersion={injectedMetadata.getKibanaVersion()}
|
||||
legacyMode={injectedMetadata.getLegacyMode()}
|
||||
navLinks$={navLinks.getNavLinks$()}
|
||||
recentlyAccessed$={recentlyAccessed.get$()}
|
||||
navControlsLeft$={navControls.getLeft$()}
|
||||
|
@ -374,5 +373,5 @@ export interface InternalChromeStart extends ChromeStart {
|
|||
* Used only by MountingService to render the header UI
|
||||
* @internal
|
||||
*/
|
||||
getComponent(): JSX.Element;
|
||||
getHeaderComponent(): JSX.Element;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -71,6 +71,9 @@ import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } fr
|
|||
|
||||
export { CoreContext, CoreSystem } from './core_system';
|
||||
export { RecursiveReadonly } from '../utils';
|
||||
|
||||
export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application';
|
||||
|
||||
export {
|
||||
SavedObjectsBatchResponse,
|
||||
SavedObjectsBulkCreateObject,
|
||||
|
@ -114,6 +117,8 @@ export {
|
|||
* https://github.com/Microsoft/web-build-tools/issues/1237
|
||||
*/
|
||||
export interface CoreSetup {
|
||||
/** {@link ApplicationSetup} */
|
||||
application: ApplicationSetup;
|
||||
/** {@link ContextSetup} */
|
||||
context: ContextSetup;
|
||||
/** {@link FatalErrorsSetup} */
|
||||
|
@ -137,7 +142,7 @@ export interface CoreSetup {
|
|||
*/
|
||||
export interface CoreStart {
|
||||
/** {@link ApplicationStart} */
|
||||
application: Pick<ApplicationStart, 'capabilities'>;
|
||||
application: ApplicationStart;
|
||||
/** {@link ChromeStart} */
|
||||
chrome: ChromeStart;
|
||||
/** {@link DocLinksStart} */
|
||||
|
@ -156,15 +161,33 @@ export interface CoreStart {
|
|||
uiSettings: UiSettingsClientContract;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalCoreSetup extends CoreSetup {
|
||||
application: ApplicationSetup;
|
||||
/**
|
||||
* Setup interface exposed to the legacy platform via the `ui/new_platform` module.
|
||||
*
|
||||
* @remarks
|
||||
* Some methods are not supported in the legacy platform and while present to make this type compatibile with
|
||||
* {@link CoreSetup}, unsupported methods will throw exceptions when called.
|
||||
*
|
||||
* @public
|
||||
* @deprecated
|
||||
*/
|
||||
export interface LegacyCoreSetup extends CoreSetup {
|
||||
/** @deprecated */
|
||||
injectedMetadata: InjectedMetadataSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalCoreStart extends CoreStart {
|
||||
application: ApplicationStart;
|
||||
/**
|
||||
* Start interface exposed to the legacy platform via the `ui/new_platform` module.
|
||||
*
|
||||
* @remarks
|
||||
* Some methods are not supported in the legacy platform and while present to make this type compatibile with
|
||||
* {@link CoreStart}, unsupported methods will throw exceptions when called.
|
||||
*
|
||||
* @public
|
||||
* @deprecated
|
||||
*/
|
||||
export interface LegacyCoreStart extends CoreStart {
|
||||
/** @deprecated */
|
||||
injectedMetadata: InjectedMetadataStart;
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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.`);
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -11,24 +11,65 @@ import React from 'react';
|
|||
import * as Rx from 'rxjs';
|
||||
import { EuiGlobalToastListToast as Toast } from '@elastic/eui';
|
||||
|
||||
// @public
|
||||
export interface App extends AppBase {
|
||||
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface AppBase {
|
||||
capabilities?: Partial<Capabilities>;
|
||||
euiIconType?: string;
|
||||
icon?: string;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
order?: number;
|
||||
title: string;
|
||||
tooltip$?: Observable<string>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ApplicationSetup {
|
||||
// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts
|
||||
registerApp(app: App): void;
|
||||
// Warning: (ae-forgotten-export) The symbol "LegacyApp" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @internal
|
||||
registerLegacyApp(app: LegacyApp): void;
|
||||
register(app: App): void;
|
||||
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountContext, T>): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ApplicationStart {
|
||||
availableApps: readonly App[];
|
||||
// @internal
|
||||
availableLegacyApps: readonly LegacyApp[];
|
||||
capabilities: RecursiveReadonly<Capabilities>;
|
||||
getUrlForApp(appId: string, options?: {
|
||||
path?: string;
|
||||
}): string;
|
||||
navigateToApp(appId: string, options?: {
|
||||
path?: string;
|
||||
state?: any;
|
||||
}): void;
|
||||
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountContext, T>): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface AppMountContext {
|
||||
core: {
|
||||
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
|
||||
chrome: ChromeStart;
|
||||
docLinks: DocLinksStart;
|
||||
http: HttpStart;
|
||||
i18n: I18nStart;
|
||||
notifications: NotificationsStart;
|
||||
overlays: OverlayStart;
|
||||
uiSettings: UiSettingsClientContract;
|
||||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface AppMountParameters {
|
||||
appBasePath: string;
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type AppUnmount = () => void;
|
||||
|
||||
// @public
|
||||
export interface Capabilities {
|
||||
[key: string]: Record<string, boolean | Record<string, boolean>>;
|
||||
|
@ -105,7 +146,7 @@ export interface ChromeNavLink {
|
|||
readonly legacy: boolean;
|
||||
// @deprecated
|
||||
readonly linkToLastSubUrl?: boolean;
|
||||
readonly order: number;
|
||||
readonly order?: number;
|
||||
// @deprecated
|
||||
readonly subUrlBase?: string;
|
||||
readonly title: string;
|
||||
|
@ -185,6 +226,8 @@ export interface CoreContext {
|
|||
|
||||
// @public
|
||||
export interface CoreSetup {
|
||||
// (undocumented)
|
||||
application: ApplicationSetup;
|
||||
// (undocumented)
|
||||
context: ContextSetup;
|
||||
// (undocumented)
|
||||
|
@ -200,7 +243,7 @@ export interface CoreSetup {
|
|||
// @public
|
||||
export interface CoreStart {
|
||||
// (undocumented)
|
||||
application: Pick<ApplicationStart, 'capabilities'>;
|
||||
application: ApplicationStart;
|
||||
// (undocumented)
|
||||
chrome: ChromeStart;
|
||||
// (undocumented)
|
||||
|
@ -505,23 +548,19 @@ export type IContextHandler<TContext extends {}, TReturn, THandlerParameters ext
|
|||
// @public
|
||||
export type IContextProvider<TContext extends Record<string, any>, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial<TContext>, ...rest: TProviderParameters) => Promise<TContext[TContextName]> | TContext[TContextName];
|
||||
|
||||
// @internal (undocumented)
|
||||
export interface InternalCoreSetup extends CoreSetup {
|
||||
// (undocumented)
|
||||
application: ApplicationSetup;
|
||||
// @public @deprecated
|
||||
export interface LegacyCoreSetup extends CoreSetup {
|
||||
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
// @deprecated (undocumented)
|
||||
injectedMetadata: InjectedMetadataSetup;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
export interface InternalCoreStart extends CoreStart {
|
||||
// (undocumented)
|
||||
application: ApplicationStart;
|
||||
// @public @deprecated
|
||||
export interface LegacyCoreStart extends CoreStart {
|
||||
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
// @deprecated (undocumented)
|
||||
injectedMetadata: InjectedMetadataStart;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -85,6 +85,7 @@ const coreSystem = new CoreSystem({
|
|||
injectedMetadata: {
|
||||
version: '1.2.3',
|
||||
buildNumber: 1234,
|
||||
legacyMode: true,
|
||||
legacyMetadata: {
|
||||
nav: [],
|
||||
version: '1.2.3',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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!';
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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() {}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { InternalCoreStart } from 'src/core/public';
|
||||
import { LegacyCoreStart } from 'src/core/public';
|
||||
import { useKibanaCore } from '../../../../../observability/public';
|
||||
import { getAPMHref } from '../../shared/Links/apm/APMLink';
|
||||
import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs';
|
||||
|
@ -15,7 +15,7 @@ import { routes } from './route_config';
|
|||
interface Props {
|
||||
location: Location;
|
||||
breadcrumbs: Breadcrumb[];
|
||||
core: InternalCoreStart;
|
||||
core: LegacyCoreStart;
|
||||
}
|
||||
|
||||
function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue