[7.x] Add getStartServices API (#50231) (#52943)

This commit is contained in:
Josh Dover 2019-12-12 16:00:14 -06:00 committed by GitHub
parent b184978f1f
commit 12153e5733
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 388 additions and 94 deletions

View file

@ -17,5 +17,5 @@ export interface App extends AppBase
| Property | Type | Description |
| --- | --- | --- |
| [chromeless](./kibana-plugin-public.app.chromeless.md) | <code>boolean</code> | Hide the UI chrome when the application is mounted. Defaults to <code>false</code>. Takes precedence over chrome service visibility settings. |
| [mount](./kibana-plugin-public.app.mount.md) | <code>(context: AppMountContext, params: AppMountParameters) =&gt; AppUnmount &#124; Promise&lt;AppUnmount&gt;</code> | A mount function called when the user navigates to this app's route. |
| [mount](./kibana-plugin-public.app.mount.md) | <code>AppMount &#124; AppMountDeprecated</code> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md)<!-- -->. |

View file

@ -4,10 +4,15 @@
## App.mount property
A mount function called when the user navigates to this app's route.
A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md)<!-- -->.
<b>Signature:</b>
```typescript
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
mount: AppMount | AppMountDeprecated;
```
## Remarks
When function has two arguments, it will be called with a [context](./kibana-plugin-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->.

View file

@ -16,5 +16,5 @@ export interface ApplicationSetup
| Method | Description |
| --- | --- |
| [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. |
| [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. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->. |

View file

@ -4,12 +4,16 @@
## 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.
> Warning: This API is now obsolete.
>
>
Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->.
<b>Signature:</b>
```typescript
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<App['mount'], T>): void;
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
```
## Parameters
@ -17,7 +21,7 @@ registerMountContext<T extends keyof AppMountContext>(contextName: T, provider:
| Parameter | Type | Description |
| --- | --- | --- |
| contextName | <code>T</code> | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. |
| provider | <code>IContextProvider&lt;App['mount'], T&gt;</code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
| provider | <code>IContextProvider&lt;AppMountDeprecated, T&gt;</code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
<b>Returns:</b>

View file

@ -23,5 +23,5 @@ export interface ApplicationStart
| --- | --- |
| [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. |
| [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. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->. |

View file

@ -4,12 +4,16 @@
## 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.
> Warning: This API is now obsolete.
>
>
Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->.
<b>Signature:</b>
```typescript
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<App['mount'], T>): void;
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
```
## Parameters
@ -17,7 +21,7 @@ registerMountContext<T extends keyof AppMountContext>(contextName: T, provider:
| Parameter | Type | Description |
| --- | --- | --- |
| contextName | <code>T</code> | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. |
| provider | <code>IContextProvider&lt;App['mount'], T&gt;</code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
| provider | <code>IContextProvider&lt;AppMountDeprecated, T&gt;</code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function |
<b>Returns:</b>

View file

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

View file

@ -4,7 +4,11 @@
## AppMountContext interface
The context object received when applications are mounted to the DOM.
> Warning: This API is now obsolete.
>
>
The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->.
<b>Signature:</b>

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md)
## AppMountDeprecated type
> Warning: This API is now obsolete.
>
>
A mount function called when the user navigates to this app's route.
<b>Signature:</b>
```typescript
export declare type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
```
## Remarks
When function has two arguments, it will be called with a [context](./kibana-plugin-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->.

View file

@ -4,6 +4,10 @@
## CoreSetup.context property
> Warning: This API is now obsolete.
>
>
[ContextSetup](./kibana-plugin-public.contextsetup.md)
<b>Signature:</b>

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [CoreSetup](./kibana-plugin-public.coresetup.md) &gt; [getStartServices](./kibana-plugin-public.coresetup.getstartservices.md)
## CoreSetup.getStartServices() method
Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-public.app.mount.md)<!-- -->. Promise will not resolve until Core and plugin dependencies have completed `start`<!-- -->.
<b>Signature:</b>
```typescript
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
```
<b>Returns:</b>
`Promise<[CoreStart, TPluginsStart]>`

View file

@ -9,7 +9,7 @@ Core services exposed to the `Plugin` setup lifecycle
<b>Signature:</b>
```typescript
export interface CoreSetup
export interface CoreSetup<TPluginsStart extends object = object>
```
## Properties
@ -24,3 +24,9 @@ export interface CoreSetup
| [notifications](./kibana-plugin-public.coresetup.notifications.md) | <code>NotificationsSetup</code> | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) |
| [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) | <code>IUiSettingsClient</code> | [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) |
## Methods
| Method | Description |
| --- | --- |
| [getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-public.app.mount.md)<!-- -->. Promise will not resolve until Core and plugin dependencies have completed <code>start</code>. |

View file

@ -13,7 +13,7 @@ Setup interface exposed to the legacy platform via the `ui/new_platform` module.
<b>Signature:</b>
```typescript
export interface LegacyCoreSetup extends CoreSetup
export interface LegacyCoreSetup extends CoreSetup<any>
```
## Properties

View file

@ -26,7 +26,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [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. |
| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md)<!-- -->. |
| [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) | |
@ -98,6 +98,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| Type Alias | Description |
| --- | --- |
| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. |
| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. |
| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. |
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | |

View file

@ -7,14 +7,14 @@
<b>Signature:</b>
```typescript
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
setup(core: CoreSetup<TPluginsStart>, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| core | <code>CoreSetup</code> | |
| core | <code>CoreSetup&lt;TPluginsStart&gt;</code> | |
| plugins | <code>TPluginsSetup</code> | |
<b>Returns:</b>

View file

@ -32,9 +32,9 @@ describe('#setup()', () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.register(Symbol(), { id: 'app1' } as any);
setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any);
expect(() =>
setup.register(Symbol(), { id: 'app1' } as any)
setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any)
).toThrowErrorMatchingInlineSnapshot(
`"An application is already registered with the id \\"app1\\""`
);
@ -51,6 +51,18 @@ describe('#setup()', () => {
setup.register(Symbol(), { id: 'app1' } as any)
).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
});
it('logs a warning when registering a deprecated app mount', async () => {
const consoleWarnSpy = jest.spyOn(console, 'warn');
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.register(Symbol(), { id: 'app1', mount: (ctx: any, params: any) => {} } as any);
expect(consoleWarnSpy).toHaveBeenCalledWith(
`App [app1] is using deprecated mount context. Use core.getStartServices() instead.`
);
consoleWarnSpy.mockRestore();
});
});
describe('registerLegacyApp', () => {
@ -100,7 +112,7 @@ describe('#start()', () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.register(Symbol(), { id: 'app1' } as any);
setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any);
setup.registerLegacyApp({ id: 'app2' } as any);
const http = httpServiceMock.createStartContract();
@ -108,12 +120,13 @@ describe('#start()', () => {
const startContract = await service.start({ http, injectedMetadata });
expect(startContract.availableApps).toMatchInlineSnapshot(`
Map {
"app1" => Object {
"id": "app1",
},
}
`);
Map {
"app1" => Object {
"id": "app1",
"mount": [MockFunction],
},
}
`);
expect(startContract.availableLegacyApps).toMatchInlineSnapshot(`
Map {
"app2" => Object {
@ -127,14 +140,15 @@ describe('#start()', () => {
const service = new ApplicationService();
const context = contextServiceMock.createSetupContract();
const setup = service.setup({ context });
setup.register(Symbol(), { id: 'app1' } as any);
const app1 = { id: 'app1', mount: jest.fn() };
setup.register(Symbol(), app1 as any);
const http = httpServiceMock.createStartContract();
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
await service.start({ http, injectedMetadata });
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
apps: new Map([['app1', { id: 'app1' }]]),
apps: new Map([['app1', app1]]),
legacyApps: new Map(),
http,
});

View file

@ -29,7 +29,8 @@ import { ContextSetup, IContextContainer } from '../context';
import {
App,
LegacyApp,
AppMounter,
AppMount,
AppMountDeprecated,
InternalApplicationSetup,
InternalApplicationStart,
} from './types';
@ -50,7 +51,7 @@ interface StartDeps {
interface AppBox {
app: App;
mount: AppMounter;
mount: AppMount;
}
/**
@ -61,7 +62,7 @@ export class ApplicationService {
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<App['mount']>;
private mountContext?: IContextContainer<AppMountDeprecated>;
public setup({ context }: SetupDeps): InternalApplicationSetup {
this.mountContext = context.createContextContainer();
@ -75,10 +76,21 @@ export class ApplicationService {
throw new Error(`Applications cannot be registered after "setup"`);
}
const appBox: AppBox = {
app,
mount: this.mountContext!.createHandler(plugin, app.mount),
};
let appBox: AppBox;
if (isAppMountDeprecated(app.mount)) {
// eslint-disable-next-line no-console
console.warn(
`App [${app.id}] is using deprecated mount context. Use core.getStartServices() instead.`
);
appBox = {
app,
mount: this.mountContext!.createHandler(plugin, app.mount),
};
} else {
appBox = { app, mount: app.mount };
}
this.apps$.next(new Map([...this.apps$.value.entries(), [app.id, appBox]]));
},
registerLegacyApp: (app: LegacyApp) => {
@ -146,7 +158,7 @@ export class ApplicationService {
}
// Filter only available apps and map to just the mount function.
const appMounters = new Map<string, AppMounter>(
const appMounts = new Map<string, AppMount>(
[...this.apps$.value]
.filter(([id]) => availableApps.has(id))
.map(([id, { mount }]) => [id, mount])
@ -154,7 +166,7 @@ export class ApplicationService {
return (
<AppRouter
apps={appMounters}
apps={appMounts}
legacyApps={availableLegacyApps}
basePath={http.basePath}
currentAppId$={currentAppId$}
@ -173,3 +185,8 @@ const appPath = (appId: string, { path }: { path?: string } = {}): string =>
path
? `/app/${appId}/${path.replace(/^\//, '')}` // Remove preceding slash from path if present
: `/app/${appId}`;
function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated {
// Mount functions with two arguments are assumed to expect deprecated `context` object.
return mount.length === 2;
}

View file

@ -22,6 +22,8 @@ export { Capabilities } from './capabilities';
export {
App,
AppBase,
AppMount,
AppMountDeprecated,
AppUnmount,
AppMountContext,
AppMountParameters,

View file

@ -24,7 +24,7 @@ import { BehaviorSubject } from 'rxjs';
import { I18nProvider } from '@kbn/i18n/react';
import { AppMounter, LegacyApp, AppMountParameters } from '../types';
import { AppMount, LegacyApp, AppMountParameters } from '../types';
import { httpServiceMock } from '../../http/http_service.mock';
import { AppRouter, AppNotFound } from '../ui';
@ -35,7 +35,7 @@ const createMountHandler = (htmlString: string) =>
});
describe('AppContainer', () => {
let apps: Map<string, jest.Mock<ReturnType<AppMounter>, Parameters<AppMounter>>>;
let apps: Map<string, jest.Mock<ReturnType<AppMount>, Parameters<AppMount>>>;
let legacyApps: Map<string, LegacyApp>;
let history: History;
let router: ReactWrapper;

View file

@ -75,12 +75,14 @@ export interface AppBase {
*/
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.
* A mount function called when the user navigates to this app's route. May have signature of {@link AppMount} or
* {@link AppMountDeprecated}.
*
* @remarks
* When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument.
* This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}.
*/
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
mount: AppMount | AppMountDeprecated;
/**
* Hide the UI chrome when the application is mounted. Defaults to `false`.
@ -97,7 +99,39 @@ export interface LegacyApp extends AppBase {
}
/**
* The context object received when applications are mounted to the DOM.
* A mount function called when the user navigates to this app's route.
*
* @param params {@link AppMountParameters}
* @returns An unmounting function that will be called to unmount the application. See {@link AppUnmount}.
*
* @public
*/
export type AppMount = (params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
/**
* A mount function called when the user navigates to this app's route.
*
* @remarks
* When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument.
* This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}.
*
* @param context The mount context for this app. Deprecated, use {@link CoreSetup.getStartServices}.
* @param params {@link AppMountParameters}
* @returns An unmounting function that will be called to unmount the application. See {@link AppUnmount}.
*
* @deprecated
* @public
*/
export type AppMountDeprecated = (
context: AppMountContext,
params: AppMountParameters
) => AppUnmount | Promise<AppUnmount>;
/**
* The context object received when applications are mounted to the DOM. Deprecated, use
* {@link CoreSetup.getStartServices}.
*
* @deprecated
* @public
*/
export interface AppMountContext {
@ -192,9 +226,6 @@ export interface AppMountParameters {
*/
export type AppUnmount = () => void;
/** @internal */
export type AppMounter = (params: AppMountParameters) => Promise<AppUnmount>;
/** @public */
export interface ApplicationSetup {
/**
@ -205,14 +236,15 @@ export interface ApplicationSetup {
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.
*
* @deprecated
* @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<App['mount'], T>
provider: IContextProvider<AppMountDeprecated, T>
): void;
}
@ -234,8 +266,9 @@ export interface InternalApplicationSetup {
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.
*
* @deprecated
* @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
@ -243,7 +276,7 @@ export interface InternalApplicationSetup {
registerMountContext<T extends keyof AppMountContext>(
pluginOpaqueId: PluginOpaqueId,
contextName: T,
provider: IContextProvider<App['mount'], T>
provider: IContextProvider<AppMountDeprecated, T>
): void;
}
@ -272,15 +305,16 @@ export interface ApplicationStart {
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.
*
* @deprecated
* @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<App['mount'], T>
provider: IContextProvider<AppMountDeprecated, T>
): void;
}
@ -301,8 +335,9 @@ export interface InternalApplicationStart
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context.
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.
*
* @deprecated
* @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
@ -310,7 +345,7 @@ export interface InternalApplicationStart
registerMountContext<T extends keyof AppMountContext>(
pluginOpaqueId: PluginOpaqueId,
contextName: T,
provider: IContextProvider<App['mount'], T>
provider: IContextProvider<AppMountDeprecated, T>
): void;
// Internal APIs

View file

@ -21,12 +21,12 @@ import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Subject } from 'rxjs';
import { LegacyApp, AppMounter, AppUnmount } from '../types';
import { LegacyApp, AppMount, AppUnmount } from '../types';
import { HttpStart } from '../../http';
import { AppNotFound } from './app_not_found_screen';
interface Props extends RouteComponentProps<{ appId: string }> {
apps: ReadonlyMap<string, AppMounter>;
apps: ReadonlyMap<string, AppMount>;
legacyApps: ReadonlyMap<string, LegacyApp>;
basePath: HttpStart['basePath'];
currentAppId$: Subject<string | undefined>;

View file

@ -22,12 +22,12 @@ import React from 'react';
import { Router, Route } from 'react-router-dom';
import { Subject } from 'rxjs';
import { LegacyApp, AppMounter } from '../types';
import { LegacyApp, AppMount } from '../types';
import { AppContainer } from './app_container';
import { HttpStart } from '../../http';
interface Props {
apps: ReadonlyMap<string, AppMounter>;
apps: ReadonlyMap<string, AppMount>;
legacyApps: ReadonlyMap<string, LegacyApp>;
basePath: HttpStart['basePath'];
currentAppId$: Subject<string | undefined>;

View file

@ -64,7 +64,7 @@ export interface CoreContext {
}
/** @internal */
export interface InternalCoreSetup extends Omit<CoreSetup, 'application'> {
export interface InternalCoreSetup extends Omit<CoreSetup, 'application' | 'getStartServices'> {
application: InternalApplicationSetup;
injectedMetadata: InjectedMetadataSetup;
}
@ -253,11 +253,11 @@ export class CoreSystem {
docLinks,
http,
i18n,
injectedMetadata: pick(injectedMetadata, ['getInjectedVar']),
notifications,
overlays,
savedObjects,
uiSettings,
injectedMetadata: pick(injectedMetadata, ['getInjectedVar']),
}));
const core: InternalCoreStart = {

View file

@ -80,7 +80,17 @@ import {
export { CoreContext, CoreSystem } from './core_system';
export { RecursiveReadonly } from '../utils';
export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application';
export {
ApplicationSetup,
ApplicationStart,
App,
AppBase,
AppMount,
AppMountDeprecated,
AppUnmount,
AppMountContext,
AppMountParameters,
} from './application';
export {
SavedObjectsBatchResponse,
@ -146,10 +156,13 @@ export { MountPoint, UnmountCallback } from './types';
* navigation in the generated docs until there's a fix for
* https://github.com/Microsoft/web-build-tools/issues/1237
*/
export interface CoreSetup {
export interface CoreSetup<TPluginsStart extends object = object> {
/** {@link ApplicationSetup} */
application: ApplicationSetup;
/** {@link ContextSetup} */
/**
* {@link ContextSetup}
* @deprecated
*/
context: ContextSetup;
/** {@link FatalErrorsSetup} */
fatalErrors: FatalErrorsSetup;
@ -168,6 +181,13 @@ export interface CoreSetup {
injectedMetadata: {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
};
/**
* Allows plugins to get access to APIs available in start inside async
* handlers, such as {@link App.mount}. Promise will not resolve until Core
* and plugin dependencies have completed `start`.
*/
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
}
/**
@ -219,7 +239,7 @@ export interface CoreStart {
* @public
* @deprecated
*/
export interface LegacyCoreSetup extends CoreSetup {
export interface LegacyCoreSetup extends CoreSetup<any> {
/** @deprecated */
injectedMetadata: InjectedMetadataSetup;
}
@ -240,8 +260,6 @@ export interface LegacyCoreStart extends CoreStart {
}
export {
ApplicationSetup,
ApplicationStart,
Capabilities,
ChromeBadge,
ChromeBrand,

View file

@ -169,6 +169,20 @@ describe('#start()', () => {
expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {});
});
it('resolves getStartServices with core and plugin APIs', async () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.setup(defaultSetupDeps);
legacyPlatform.start(defaultStartDeps);
const { getStartServices } = mockUiNewPlatformSetup.mock.calls[0][0];
const [coreStart, pluginsStart] = await getStartServices();
expect(coreStart).toEqual(expect.any(Object));
expect(pluginsStart).toBe(defaultStartDeps.plugins);
});
describe('useLegacyTestHarness = false', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({

View file

@ -18,6 +18,8 @@
*/
import angular from 'angular';
import { first } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { InternalCoreSetup, InternalCoreStart } from '../core_system';
import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../';
@ -55,6 +57,8 @@ export class LegacyPlatformService {
public readonly legacyId = Symbol();
private bootstrapModule?: BootstrapModule;
private targetDomElement?: HTMLElement;
private readonly startDependencies$ = new Subject<[LegacyCoreStart, object]>();
private readonly startDependencies = this.startDependencies$.pipe(first()).toPromise();
constructor(private readonly params: LegacyPlatformParams) {}
@ -75,6 +79,7 @@ export class LegacyPlatformService {
const legacyCore: LegacyCoreSetup = {
...core,
getStartServices: () => this.startDependencies,
application: {
register: notSupported(`core.application.register()`),
registerMountContext: notSupported(`core.application.registerMountContext()`),
@ -120,6 +125,8 @@ export class LegacyPlatformService {
},
};
this.startDependencies$.next([legacyCore, plugins]);
// 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);

View file

@ -46,6 +46,9 @@ function createCoreSetupMock({ basePath = '' } = {}) {
application: applicationServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
getStartServices: jest.fn<Promise<[ReturnType<typeof createCoreStartMock>, object]>, []>(() =>
Promise.resolve([createCoreStartMock({ basePath }), {}])
),
http: httpServiceMock.createSetupContract({ basePath }),
notifications: notificationServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
@ -75,6 +78,7 @@ function createCoreStartMock({ basePath = '' } = {}) {
return mock;
}
function pluginInitializerContextMock() {
const mock: PluginInitializerContext = {
opaqueId: Symbol(),

View file

@ -106,6 +106,33 @@ describe('PluginWrapper', () => {
expect(mockPlugin.start).toHaveBeenCalledWith(context, deps);
});
test("`start` resolves `startDependencies` Promise after plugin's start", async () => {
expect.assertions(2);
let startDependenciesResolved = false;
mockPluginLoader.mockResolvedValueOnce(() => ({
setup: jest.fn(),
start: async () => {
// Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves.
await new Promise(resolve => setTimeout(resolve, 10));
expect(startDependenciesResolved).toBe(false);
},
}));
await plugin.load(addBasePath);
await plugin.setup({} as any, {} as any);
const context = { any: 'thing' } as any;
const deps = { otherDep: 'value' };
// Add promise callback prior to calling `start` to ensure calls in `setup` will not resolve before `start` is
// called.
const startDependenciesCheck = plugin.startDependencies.then(res => {
startDependenciesResolved = true;
expect(res).toEqual([context, deps]);
});
await plugin.start(context, deps);
await startDependenciesCheck;
});
test('`stop` fails if plugin is not setup up', async () => {
expect(() => plugin.stop()).toThrowErrorMatchingInlineSnapshot(
`"Plugin \\"plugin-a\\" can't be stopped since it isn't set up."`

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { Subject } from 'rxjs';
import { first } from 'rxjs/operators';
import { DiscoveredPlugin, PluginOpaqueId } from '../../server';
import { PluginInitializerContext } from './plugin_context';
import { loadPluginBundle } from './plugin_loader';
@ -33,7 +35,7 @@ export interface Plugin<
TPluginsSetup extends object = object,
TPluginsStart extends object = object
> {
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
setup(core: CoreSetup<TPluginsStart>, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
stop?(): void;
}
@ -70,6 +72,9 @@ export class PluginWrapper<
private initializer?: PluginInitializer<TSetup, TStart, TPluginsSetup, TPluginsStart>;
private instance?: Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart]>();
public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise();
constructor(
public readonly discoveredPlugin: DiscoveredPlugin,
public readonly opaqueId: PluginOpaqueId,
@ -100,7 +105,7 @@ export class PluginWrapper<
* @param plugins The dictionary where the key is the dependency name and the value
* is the contract returned by the dependency's `setup` function.
*/
public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) {
public async setup(setupContext: CoreSetup<TPluginsStart>, plugins: TPluginsSetup) {
this.instance = await this.createPluginInstance();
return await this.instance.setup(setupContext, plugins);
@ -118,7 +123,11 @@ export class PluginWrapper<
throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`);
}
return await this.instance.start(startContext, plugins);
const startContract = await this.instance.start(startContext, plugins);
this.startDependencies$.next([startContext, plugins]);
return startContract;
}
/**

View file

@ -107,6 +107,7 @@ export function createPluginSetupContext<
injectedMetadata: {
getInjectedVar: deps.injectedMetadata.getInjectedVar,
},
getStartServices: () => plugin.startDependencies,
};
}

View file

@ -98,6 +98,7 @@ describe('PluginsService', () => {
mockSetupContext = {
...mockSetupDeps,
application: expect.any(Object),
getStartServices: expect.any(Function),
};
mockStartDeps = {
application: applicationServiceMock.createInternalStartContract(),

View file

@ -19,7 +19,7 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type
// @public
export interface App extends AppBase {
chromeless?: boolean;
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
mount: AppMount | AppMountDeprecated;
}
// @public (undocumented)
@ -37,7 +37,8 @@ export interface AppBase {
// @public (undocumented)
export interface ApplicationSetup {
register(app: App): void;
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<App['mount'], T>): void;
// @deprecated
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
}
// @public (undocumented)
@ -50,10 +51,14 @@ export interface ApplicationStart {
path?: string;
state?: any;
}): void;
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<App['mount'], T>): void;
// @deprecated
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
}
// @public
export type AppMount = (params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
// @public @deprecated
export interface AppMountContext {
core: {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
@ -71,6 +76,9 @@ export interface AppMountContext {
};
}
// @public @deprecated
export type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
// @public (undocumented)
export interface AppMountParameters {
appBasePath: string;
@ -275,13 +283,14 @@ export interface CoreContext {
}
// @public
export interface CoreSetup {
export interface CoreSetup<TPluginsStart extends object = object> {
// (undocumented)
application: ApplicationSetup;
// (undocumented)
// @deprecated (undocumented)
context: ContextSetup;
// (undocumented)
fatalErrors: FatalErrorsSetup;
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
// (undocumented)
http: HttpSetup;
// @deprecated
@ -653,7 +662,7 @@ export interface IUiSettingsClient {
}
// @public @deprecated
export interface LegacyCoreSetup extends CoreSetup {
export interface LegacyCoreSetup extends CoreSetup<any> {
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts
//
// @deprecated (undocumented)
@ -749,7 +758,7 @@ export interface PackageInfo {
// @public
export interface Plugin<TSetup = void, TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object> {
// (undocumented)
setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
setup(core: CoreSetup<TPluginsStart>, plugins: TPluginsSetup): TSetup | Promise<TSetup>;
// (undocumented)
start(core: CoreStart, plugins: TPluginsStart): TStart | Promise<TStart>;
// (undocumented)

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { App, AppUnmount } from 'kibana/public';
import { App, AppUnmount, AppMountDeprecated } from 'kibana/public';
import { UIRoutes } from 'ui/routes';
import { ILocationService, IScope } from 'angular';
import { npStart } from 'ui/new_platform';
@ -68,7 +68,10 @@ export class LocalApplicationService {
isUnmounted = true;
});
(async () => {
unmountHandler = await app.mount({ core: npStart.core }, { element, appBasePath: '' });
const params = { element, appBasePath: '' };
unmountHandler = isAppMountDeprecated(app.mount)
? await app.mount({ core: npStart.core }, params)
: await app.mount(params);
// immediately unmount app if scope got destroyed in the meantime
if (isUnmounted) {
unmountHandler();
@ -90,3 +93,8 @@ export class LocalApplicationService {
}
export const localApplicationService = new LocalApplicationService();
function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated {
// Mount functions with two arguments are assumed to expect deprecated `context` object.
return mount.length === 2;
}

View file

@ -58,6 +58,26 @@ describe('ui/new_platform', () => {
const scopeMock = { $on: jest.fn() };
const elementMock = [document.createElement('div')];
controller(scopeMock, elementMock);
expect(mountMock).toHaveBeenCalledWith({
element: elementMock[0],
appBasePath: '/test/base/path/app/test',
});
});
test('controller calls deprecated context app.mount when invoked', () => {
const unmountMock = jest.fn();
// Two arguments changes how this is called.
const mountMock = jest.fn((context, params) => unmountMock);
legacyAppRegister({
id: 'test',
title: 'Test',
mount: mountMock,
});
const controller = setRootControllerMock.mock.calls[0][1];
const scopeMock = { $on: jest.fn() };
const elementMock = [document.createElement('div')];
controller(scopeMock, elementMock);
expect(mountMock).toHaveBeenCalledWith(expect.any(Object), {
element: elementMock[0],

View file

@ -20,7 +20,7 @@ import { IScope } from 'angular';
import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public';
import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public';
import { LegacyCoreSetup, LegacyCoreStart, App } from '../../../../core/public';
import { LegacyCoreSetup, LegacyCoreStart, App, AppMountDeprecated } from '../../../../core/public';
import { Plugin as DataPlugin } from '../../../../plugins/data/public';
import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public';
import {
@ -111,13 +111,18 @@ export const legacyAppRegister = (app: App) => {
// Root controller cannot return a Promise so use an internal async function and call it immediately
(async () => {
const unmount = await app.mount(
{ core: npStart.core },
{ element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) }
);
const params = { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) };
const unmount = isAppMountDeprecated(app.mount)
? await app.mount({ core: npStart.core }, params)
: await app.mount(params);
$scope.$on('$destroy', () => {
unmount();
});
})();
});
};
function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated {
// Mount functions with two arguments are assumed to expect deprecated `context` object.
return mount.length === 2;
}

View file

@ -25,7 +25,7 @@ import * as React from 'react';
import ReactDOM from 'react-dom';
import { useEffect, useRef } from 'react';
import { AppMountContext } from 'kibana/public';
import { AppMountContext, AppMountDeprecated } from 'kibana/public';
import { DevTool } from './plugin';
interface DevToolsWrapperProps {
@ -91,10 +91,10 @@ function DevToolsWrapper({
if (mountedTool.current) {
mountedTool.current.unmountHandler();
}
const unmountHandler = await activeDevTool.mount(appMountContext, {
element,
appBasePath: '',
});
const params = { element, appBasePath: '' };
const unmountHandler = isAppMountDeprecated(activeDevTool.mount)
? await activeDevTool.mount(appMountContext, params)
: await activeDevTool.mount(params);
mountedTool.current = {
devTool: activeDevTool,
mountpoint: element,
@ -182,3 +182,8 @@ export function renderApp(
return () => ReactDOM.unmountComponentAtNode(element);
}
function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated {
// Mount functions with two arguments are assumed to expect deprecated `context` object.
return mount.length === 2;
}

View file

@ -24,6 +24,7 @@ declare global {
interface Window {
corePluginB?: string;
hasAccessToInjectedMetadata?: boolean;
receivedStartServices?: boolean;
env?: PluginInitializerContext['env'];
}
}
@ -40,6 +41,9 @@ export class CorePluginBPlugin
public setup(core: CoreSetup, deps: CorePluginBDeps) {
window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`;
window.hasAccessToInjectedMetadata = 'getInjectedVar' in core.injectedMetadata;
core.getStartServices().then(([coreStart, plugins]) => {
window.receivedStartServices = 'overlays' in coreStart;
});
core.application.register({
id: 'bar',

View file

@ -22,8 +22,8 @@ import { npSetup } from 'ui/new_platform';
npSetup.core.application.register({
id: 'core_legacy_compat',
title: 'Core Legacy Compat',
async mount(...args) {
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(...args);
return renderApp(context, params);
},
});

View file

@ -36,28 +36,41 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
expect(corePluginB).to.equal(`Plugin A said: Hello from Plugin A!`);
});
});
describe('have injectedMetadata service provided', function describeIndexTests() {
before(async () => {
await PageObjects.common.navigateToApp('bar');
});
it('should attach string to window.corePluginB', async () => {
it('should attach boolean to window.hasAccessToInjectedMetadata', async () => {
const hasAccessToInjectedMetadata = await browser.execute(
'return window.hasAccessToInjectedMetadata'
);
expect(hasAccessToInjectedMetadata).to.equal(true);
});
});
describe('have env data provided', function describeIndexTests() {
before(async () => {
await PageObjects.common.navigateToApp('bar');
});
it('should attach pluginContext to window.corePluginB', async () => {
it('should attach pluginContext to window.env', async () => {
const envData: any = await browser.execute('return window.env');
expect(envData.mode.dev).to.be(true);
expect(envData.packageInfo.version).to.be.a('string');
});
});
describe('have access to start services via coreSetup.getStartServices', function describeIndexTests() {
before(async () => {
await PageObjects.common.navigateToApp('bar');
});
it('should attach boolean to window.receivedStartServices', async () => {
const receivedStartServices = await browser.execute('return window.receivedStartServices');
expect(receivedStartServices).to.equal(true);
});
});
});
}