| LegacyRequest, requestSpecificBasePath: string) => void;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.irenderoptions.includeusersettings.md b/docs/development/core/server/kibana-plugin-server.irenderoptions.includeusersettings.md
new file mode 100644
index 0000000000000..cedf3d27d0887
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.irenderoptions.includeusersettings.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRenderOptions](./kibana-plugin-server.irenderoptions.md) > [includeUserSettings](./kibana-plugin-server.irenderoptions.includeusersettings.md)
+
+## IRenderOptions.includeUserSettings property
+
+Set whether to output user settings in the page metadata. `true` by default.
+
+Signature:
+
+```typescript
+includeUserSettings?: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.irenderoptions.md b/docs/development/core/server/kibana-plugin-server.irenderoptions.md
new file mode 100644
index 0000000000000..34bed8b5e078c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.irenderoptions.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRenderOptions](./kibana-plugin-server.irenderoptions.md)
+
+## IRenderOptions interface
+
+
+Signature:
+
+```typescript
+export interface IRenderOptions
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [includeUserSettings](./kibana-plugin-server.irenderoptions.includeusersettings.md) | boolean
| Set whether to output user settings in the page metadata. true
by default. |
+
diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md
index 238424b1df1d5..ff71f13466cf8 100644
--- a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md
+++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md
@@ -1,13 +1,13 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md)
-
-## IRouter.handleLegacyErrors property
-
-Wrap a router handler to catch and converts legacy boom errors to proper custom errors.
-
-Signature:
-
-```typescript
-(handler: RequestHandler
) => RequestHandler
;
-```
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md)
+
+## IRouter.handleLegacyErrors property
+
+Wrap a router handler to catch and converts legacy boom errors to proper custom errors.
+
+Signature:
+
+```typescript
+handleLegacyErrors:
(handler: RequestHandler
) => RequestHandler
;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.md b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.md
new file mode 100644
index 0000000000000..2e6daa58db25f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md)
+
+## IScopedRenderingClient interface
+
+
+Signature:
+
+```typescript
+export interface IScopedRenderingClient
+```
+
+## Methods
+
+| Method | Description |
+| --- | --- |
+| [render(options)](./kibana-plugin-server.iscopedrenderingclient.render.md) | Generate a KibanaResponse
which renders an HTML page bootstrapped with the core
bundle. Intended as a response body for HTTP route handlers. |
+
diff --git a/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.render.md b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.render.md
new file mode 100644
index 0000000000000..1bc78dd84571d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.render.md
@@ -0,0 +1,41 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) > [render](./kibana-plugin-server.iscopedrenderingclient.render.md)
+
+## IScopedRenderingClient.render() method
+
+Generate a `KibanaResponse` which renders an HTML page bootstrapped with the `core` bundle. Intended as a response body for HTTP route handlers.
+
+Signature:
+
+```typescript
+render(options?: IRenderOptions): Promise;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| options | IRenderOptions
| |
+
+Returns:
+
+`Promise`
+
+## Example
+
+
+```ts
+router.get(
+ { path: '/', validate: false },
+ (context, request, response) =>
+ response.ok({
+ body: await context.core.rendering.render(),
+ headers: {
+ 'content-security-policy': context.core.http.csp.header,
+ },
+ })
+);
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md
index 09ebf1170715b..c4c043a903d06 100644
--- a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md
+++ b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md
@@ -7,7 +7,5 @@
Signature:
```typescript
-core: InternalCoreSetup & {
- plugins: PluginsServiceSetup;
- };
+core: LegacyCoreSetup;
```
diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md
index 4475318522dfa..7961cedd2c054 100644
--- a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md
+++ b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md
@@ -18,6 +18,6 @@ export interface LegacyServiceSetupDeps
| Property | Type | Description |
| --- | --- | --- |
-| [core](./kibana-plugin-server.legacyservicesetupdeps.core.md) | InternalCoreSetup & {
plugins: PluginsServiceSetup;
}
| |
+| [core](./kibana-plugin-server.legacyservicesetupdeps.core.md) | LegacyCoreSetup
| |
| [plugins](./kibana-plugin-server.legacyservicesetupdeps.plugins.md) | Record<string, unknown>
| |
diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md
index c5cf473aaa01a..47018f4594967 100644
--- a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md
+++ b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md
@@ -7,7 +7,5 @@
Signature:
```typescript
-core: InternalCoreStart & {
- plugins: PluginsServiceStart;
- };
+core: LegacyCoreStart;
```
diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md
index 801138b64e46a..602fe5356d525 100644
--- a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md
+++ b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md
@@ -18,6 +18,6 @@ export interface LegacyServiceStartDeps
| Property | Type | Description |
| --- | --- | --- |
-| [core](./kibana-plugin-server.legacyservicestartdeps.core.md) | InternalCoreStart & {
plugins: PluginsServiceStart;
}
| |
+| [core](./kibana-plugin-server.legacyservicestartdeps.core.md) | LegacyCoreStart
| |
| [plugins](./kibana-plugin-server.legacyservicestartdeps.plugins.md) | Record<string, unknown>
| |
diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md
index 9c8aafb158bfd..5e7f84c55244d 100644
--- a/docs/development/core/server/kibana-plugin-server.md
+++ b/docs/development/core/server/kibana-plugin-server.md
@@ -70,7 +70,9 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [IKibanaResponse](./kibana-plugin-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-server.requesthandler.md) execution |
| [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. |
| [IndexSettingsDeprecationInfo](./kibana-plugin-server.indexsettingsdeprecationinfo.md) | |
+| [IRenderOptions](./kibana-plugin-server.irenderoptions.md) | |
| [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-server.routeconfig.md) and [RequestHandler](./kibana-plugin-server.requesthandler.md) for more information about arguments to route registrations. |
+| [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) | |
| [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. |
| [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. |
| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | |
@@ -91,7 +93,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. |
| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | |
| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | |
-| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request |
+| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request |
| [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. |
| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. |
| [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route |
diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md
index 2d8b27ecb6c67..d1760dafd5bb6 100644
--- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md
+++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md
@@ -8,6 +8,7 @@
```typescript
core: {
+ rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
};
diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md
index d9b781e1e550e..7c8625a5824ee 100644
--- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md
+++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md
@@ -6,7 +6,7 @@
Plugin specific context passed to a route handler.
-Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request
+Provides the following clients: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request
Signature:
@@ -18,5 +18,5 @@ export interface RequestHandlerContext
| Property | Type | Description |
| --- | --- | --- |
-| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
}
| |
+| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
}
| |
diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md
index 4fbcf0981f114..23a72fc3c68b3 100644
--- a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md
+++ b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md
@@ -1,62 +1,62 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [validate](./kibana-plugin-server.routeconfig.validate.md)
-
-## RouteConfig.validate property
-
-A schema created with `@kbn/config-schema` that every request will be validated against.
-
-Signature:
-
-```typescript
-RouteValidatorFullConfig | false;
-```
-
-## Remarks
-
-You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`;
-
-## Example
-
-
-```ts
- import { schema } from '@kbn/config-schema';
- router.get({
- path: 'path/{id}',
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- query: schema.object({...}),
- body: schema.object({...}),
- },
-},
-(context, req, res,) {
- req.params; // type Readonly<{id: string}>
- console.log(req.params.id); // value
-});
-
-router.get({
- path: 'path/{id}',
- validate: false, // handler has no access to params, query, body values.
-},
-(context, req, res,) {
- req.params; // type Readonly<{}>;
- console.log(req.params.id); // undefined
-});
-
-router.get({
- path: 'path/{id}',
- validate: {
- // handler has access to raw non-validated params in runtime
- params: schema.object({}, { allowUnknowns: true })
- },
-},
-(context, req, res,) {
- req.params; // type Readonly<{}>;
- console.log(req.params.id); // value
- myValidationLibrary.validate({ params: req.params });
-});
-
-```
-
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [validate](./kibana-plugin-server.routeconfig.validate.md)
+
+## RouteConfig.validate property
+
+A schema created with `@kbn/config-schema` that every request will be validated against.
+
+Signature:
+
+```typescript
+validate: RouteValidatorFullConfig
| false;
+```
+
+## Remarks
+
+You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`;
+
+## Example
+
+
+```ts
+ import { schema } from '@kbn/config-schema';
+ router.get({
+ path: 'path/{id}',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ query: schema.object({...}),
+ body: schema.object({...}),
+ },
+},
+(context, req, res,) {
+ req.params; // type Readonly<{id: string}>
+ console.log(req.params.id); // value
+});
+
+router.get({
+ path: 'path/{id}',
+ validate: false, // handler has no access to params, query, body values.
+},
+(context, req, res,) {
+ req.params; // type Readonly<{}>;
+ console.log(req.params.id); // undefined
+});
+
+router.get({
+ path: 'path/{id}',
+ validate: {
+ // handler has access to raw non-validated params in runtime
+ params: schema.object({}, { allowUnknowns: true })
+ },
+},
+(context, req, res,) {
+ req.params; // type Readonly<{}>;
+ console.log(req.params.id); // value
+ myValidationLibrary.validate({ params: req.params });
+});
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md b/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md
index 31dc6ceb91995..551e13faaf154 100644
--- a/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md
+++ b/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md
@@ -1,21 +1,21 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) > [(constructor)](./kibana-plugin-server.routevalidationerror._constructor_.md)
-
-## RouteValidationError.(constructor)
-
-Constructs a new instance of the `RouteValidationError` class
-
-Signature:
-
-```typescript
-constructor(error;: Error | string, path?: string[];)
-```
-
-## Parameters
-
-| Parameter | Type | Description |
-| --- | --- | --- |
-| error | Error | string
| |
-| path | string[]
| |
-
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) > [(constructor)](./kibana-plugin-server.routevalidationerror._constructor_.md)
+
+## RouteValidationError.(constructor)
+
+Constructs a new instance of the `RouteValidationError` class
+
+Signature:
+
+```typescript
+constructor(error: Error | string, path?: string[]);
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| error | Error | string
| |
+| path | string[]
| |
+
diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md
index 2462ae17943be..36ea6103fb352 100644
--- a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md
+++ b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md
@@ -1,13 +1,13 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [badRequest](./kibana-plugin-server.routevalidationresultfactory.badrequest.md)
-
-## RouteValidationResultFactory.badRequest property
-
-Signature:
-
-```typescript
-(error: Error | string, path?: string[]) => {
- RouteValidationError;
- };
-```
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [badRequest](./kibana-plugin-server.routevalidationresultfactory.badrequest.md)
+
+## RouteValidationResultFactory.badRequest property
+
+Signature:
+
+```typescript
+badRequest: (error: Error | string, path?: string[]) => {
+ error: RouteValidationError;
+ };
+```
diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md
index c86ef616de103..eca6a31bd547f 100644
--- a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md
+++ b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md
@@ -1,13 +1,13 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [ok](./kibana-plugin-server.routevalidationresultfactory.ok.md)
-
-## RouteValidationResultFactory.ok property
-
-Signature:
-
-```typescript
-(value: T) => {
- T;
- };
-```
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [ok](./kibana-plugin-server.routevalidationresultfactory.ok.md)
+
+## RouteValidationResultFactory.ok property
+
+Signature:
+
+```typescript
+ok: (value: T) => {
+ value: T;
+ };
+```
diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md
index b1c75e6dbdf67..0406a372c4e9d 100644
--- a/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md
+++ b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md
@@ -1,18 +1,17 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) > [unsafe](./kibana-plugin-server.routevalidatoroptions.unsafe.md)
-
-## RouteValidatorOptions.unsafe property
-
-Set the `unsafe` config to avoid running some additional internal \*safe\* validations on top of your custom validation
-
-Signature:
-
-```typescript
-unsafe?: {
- params?: boolean;
- query?: boolean;
- body?: boolean;
- }
-
-```
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) > [unsafe](./kibana-plugin-server.routevalidatoroptions.unsafe.md)
+
+## RouteValidatorOptions.unsafe property
+
+Set the `unsafe` config to avoid running some additional internal \*safe\* validations on top of your custom validation
+
+Signature:
+
+```typescript
+unsafe?: {
+ params?: boolean;
+ query?: boolean;
+ body?: boolean;
+ };
+```
diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts
index d97f7485fb4d2..3fa4bdcbc5fa5 100644
--- a/src/cli/cluster/cluster_manager.ts
+++ b/src/cli/cluster/cluster_manager.ts
@@ -26,7 +26,7 @@ import { first, mapTo, filter, map, take } from 'rxjs/operators';
import { REPO_ROOT } from '@kbn/dev-utils';
import { FSWatcher } from 'chokidar';
-import { LegacyConfig } from '../../core/server/legacy/config';
+import { LegacyConfig } from '../../core/server/legacy';
import { BasePathProxyServer } from '../../core/server/http';
// @ts-ignore
diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts
index a2db755224636..b2e2161c92cc8 100644
--- a/src/core/public/application/application_service.mock.ts
+++ b/src/core/public/application/application_service.mock.ts
@@ -20,15 +20,13 @@
import { Subject } from 'rxjs';
import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
-import { ApplicationService } from './application_service';
import {
ApplicationSetup,
InternalApplicationStart,
ApplicationStart,
InternalApplicationSetup,
} from './types';
-
-type ApplicationServiceContract = PublicMethodsOf;
+import { ApplicationServiceContract } from './test_types';
const createSetupContractMock = (): jest.Mocked => ({
register: jest.fn(),
@@ -41,23 +39,27 @@ const createInternalSetupContractMock = (): jest.Mocked => ({
+const createStartContractMock = (): jest.Mocked => ({
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
registerMountContext: jest.fn(),
});
-const createInternalStartContractMock = (): jest.Mocked => ({
- availableApps: new Map(),
- availableLegacyApps: new Map(),
- capabilities: capabilitiesServiceMock.createStartContract().capabilities,
- navigateToApp: jest.fn(),
- getUrlForApp: jest.fn(),
- registerMountContext: jest.fn(),
- currentAppId$: new Subject(),
- getComponent: jest.fn(),
-});
+const createInternalStartContractMock = (): jest.Mocked => {
+ const currentAppId$ = new Subject();
+
+ return {
+ availableApps: new Map(),
+ availableLegacyApps: new Map(),
+ capabilities: capabilitiesServiceMock.createStartContract().capabilities,
+ currentAppId$: currentAppId$.asObservable(),
+ getComponent: jest.fn(),
+ getUrlForApp: jest.fn(),
+ navigateToApp: jest.fn().mockImplementation(appId => currentAppId$.next(appId)),
+ registerMountContext: jest.fn(),
+ };
+};
const createMock = (): jest.Mocked => ({
setup: jest.fn().mockReturnValue(createInternalSetupContractMock()),
@@ -69,7 +71,6 @@ export const applicationServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
-
createInternalSetupContract: createInternalSetupContractMock,
createInternalStartContract: createInternalStartContractMock,
};
diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts
new file mode 100644
index 0000000000000..d064b17ace142
--- /dev/null
+++ b/src/core/public/application/application_service.test.ts
@@ -0,0 +1,441 @@
+/*
+ * 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 { createElement } from 'react';
+import { Subject } from 'rxjs';
+import { bufferCount, skip, takeUntil } from 'rxjs/operators';
+import { shallow } from 'enzyme';
+
+import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
+import { contextServiceMock } from '../context/context_service.mock';
+import { httpServiceMock } from '../http/http_service.mock';
+import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks';
+import { MockLifecycle } from './test_types';
+import { ApplicationService } from './application_service';
+
+function mount() {}
+
+describe('#setup()', () => {
+ let setupDeps: MockLifecycle<'setup'>;
+ let startDeps: MockLifecycle<'start'>;
+ let service: ApplicationService;
+
+ beforeEach(() => {
+ const http = httpServiceMock.createSetupContract({ basePath: '/test' });
+ setupDeps = {
+ http,
+ context: contextServiceMock.createSetupContract(),
+ injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
+ };
+ setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
+ startDeps = { http, injectedMetadata: setupDeps.injectedMetadata };
+ service = new ApplicationService();
+ });
+
+ describe('register', () => {
+ it('throws an error if two apps with the same id are registered', () => {
+ const { register } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app1', mount } as any);
+ expect(() =>
+ register(Symbol(), { id: 'app1', mount } as any)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"An application is already registered with the id \\"app1\\""`
+ );
+ });
+
+ it('throws error if additional apps are registered after setup', async () => {
+ const { register } = service.setup(setupDeps);
+
+ await service.start(startDeps);
+ expect(() =>
+ register(Symbol(), { id: 'app1', mount } as any)
+ ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
+ });
+
+ it('throws an error if an App with the same appRoute is registered', () => {
+ const { register, registerLegacyApp } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app1', mount } as any);
+
+ expect(() =>
+ register(Symbol(), { id: 'app2', mount, appRoute: '/app/app1' } as any)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"An application is already registered with the appRoute \\"/app/app1\\""`
+ );
+ expect(() => registerLegacyApp({ id: 'app1' } as any)).not.toThrow();
+
+ register(Symbol(), { id: 'app-next', mount, appRoute: '/app/app3' } as any);
+
+ expect(() =>
+ register(Symbol(), { id: 'app2', mount, appRoute: '/app/app3' } as any)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"An application is already registered with the appRoute \\"/app/app3\\""`
+ );
+ expect(() => registerLegacyApp({ id: 'app3' } as any)).not.toThrow();
+ });
+
+ it('throws an error if an App starts with the HTTP base path', () => {
+ const { register } = service.setup(setupDeps);
+
+ expect(() =>
+ register(Symbol(), { id: 'app2', mount, appRoute: '/test/app2' } as any)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot register an application route that includes HTTP base path"`
+ );
+ });
+ });
+
+ describe('registerLegacyApp', () => {
+ it('throws an error if two apps with the same id are registered', () => {
+ const { registerLegacyApp } = service.setup(setupDeps);
+
+ registerLegacyApp({ id: 'app2' } as any);
+ expect(() => 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 { registerLegacyApp } = service.setup(setupDeps);
+
+ await service.start(startDeps);
+ expect(() => registerLegacyApp({ id: 'app2' } as any)).toThrowErrorMatchingInlineSnapshot(
+ `"Applications cannot be registered after \\"setup\\""`
+ );
+ });
+
+ it('throws an error if a LegacyApp with the same appRoute is registered', () => {
+ const { register, registerLegacyApp } = service.setup(setupDeps);
+
+ registerLegacyApp({ id: 'app1' } as any);
+
+ expect(() =>
+ register(Symbol(), { id: 'app2', mount, appRoute: '/app/app1' } as any)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"An application is already registered with the appRoute \\"/app/app1\\""`
+ );
+ expect(() => registerLegacyApp({ id: 'app1:other' } as any)).not.toThrow();
+ });
+ });
+
+ it("`registerMountContext` calls context container's registerContext", () => {
+ const { registerMountContext } = service.setup(setupDeps);
+ const container = setupDeps.context.createContextContainer.mock.results[0].value;
+ const pluginId = Symbol();
+
+ registerMountContext(pluginId, 'test' as any, mount as any);
+ expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', mount);
+ });
+});
+
+describe('#start()', () => {
+ let setupDeps: MockLifecycle<'setup'>;
+ let startDeps: MockLifecycle<'start'>;
+ let service: ApplicationService;
+
+ beforeEach(() => {
+ MockHistory.push.mockReset();
+ const http = httpServiceMock.createSetupContract({ basePath: '/test' });
+ setupDeps = {
+ http,
+ context: contextServiceMock.createSetupContract(),
+ injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
+ };
+ setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
+ startDeps = { http, injectedMetadata: setupDeps.injectedMetadata };
+ service = new ApplicationService();
+ });
+
+ it('rejects if called prior to #setup()', async () => {
+ await expect(service.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"ApplicationService#setup() must be invoked before start."`
+ );
+ });
+
+ it('exposes available apps', async () => {
+ setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
+ const { register, registerLegacyApp } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app1', mount } as any);
+ registerLegacyApp({ id: 'app2' } as any);
+
+ const { availableApps, availableLegacyApps } = await service.start(startDeps);
+
+ expect(availableApps).toMatchInlineSnapshot(`
+ Map {
+ "app1" => Object {
+ "appRoute": "/app/app1",
+ "id": "app1",
+ "mount": [Function],
+ },
+ }
+ `);
+ expect(availableLegacyApps).toMatchInlineSnapshot(`
+ Map {
+ "app2" => Object {
+ "id": "app2",
+ },
+ }
+ `);
+ });
+
+ it('passes appIds to capabilities', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app1', mount } as any);
+ register(Symbol(), { id: 'app2', mount } as any);
+ register(Symbol(), { id: 'app3', mount } as any);
+ await service.start(startDeps);
+
+ expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
+ appIds: ['app1', 'app2', 'app3'],
+ http: setupDeps.http,
+ });
+ });
+
+ it('filters available applications based on capabilities', async () => {
+ MockCapabilitiesService.start.mockResolvedValueOnce({
+ capabilities: {
+ navLinks: {
+ app1: true,
+ app2: false,
+ legacyApp1: true,
+ legacyApp2: false,
+ },
+ },
+ } as any);
+
+ const { register, registerLegacyApp } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app1', mount } as any);
+ registerLegacyApp({ id: 'legacyApp1' } as any);
+ register(Symbol(), { id: 'app2', mount } as any);
+ registerLegacyApp({ id: 'legacyApp2' } as any);
+
+ const { availableApps, availableLegacyApps } = await service.start(startDeps);
+
+ expect(availableApps).toMatchInlineSnapshot(`
+ Map {
+ "app1" => Object {
+ "appRoute": "/app/app1",
+ "id": "app1",
+ "mount": [Function],
+ },
+ }
+ `);
+ expect(availableLegacyApps).toMatchInlineSnapshot(`
+ Map {
+ "legacyApp1" => Object {
+ "id": "legacyApp1",
+ },
+ }
+ `);
+ });
+
+ describe('getComponent', () => {
+ it('returns renderable JSX tree', async () => {
+ service.setup(setupDeps);
+
+ const { getComponent } = await service.start(startDeps);
+
+ expect(() => shallow(createElement(getComponent))).not.toThrow();
+ expect(getComponent()).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ it('renders null when in legacy mode', async () => {
+ setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
+ service.setup(setupDeps);
+
+ const { getComponent } = await service.start(startDeps);
+
+ expect(() => shallow(createElement(getComponent))).not.toThrow();
+ expect(getComponent()).toBe(null);
+ });
+ });
+
+ describe('getUrlForApp', () => {
+ it('creates URL for unregistered appId', async () => {
+ service.setup(setupDeps);
+
+ const { getUrlForApp } = await service.start(startDeps);
+
+ expect(getUrlForApp('app1')).toBe('/app/app1');
+ });
+
+ it('creates URL for registered appId', async () => {
+ const { register, registerLegacyApp } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app1', mount } as any);
+ registerLegacyApp({ id: 'legacyApp1' } as any);
+ register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+
+ const { getUrlForApp } = await service.start(startDeps);
+
+ expect(getUrlForApp('app1')).toBe('/app/app1');
+ expect(getUrlForApp('legacyApp1')).toBe('/app/legacyApp1');
+ expect(getUrlForApp('app2')).toBe('/custom/path');
+ });
+
+ it('creates URLs with path parameter', async () => {
+ service.setup(setupDeps);
+
+ const { getUrlForApp } = await service.start(startDeps);
+
+ expect(getUrlForApp('app1', { path: 'deep/link' })).toBe('/app/app1/deep/link');
+ expect(getUrlForApp('app1', { path: '/deep//link/' })).toBe('/app/app1/deep/link');
+ expect(getUrlForApp('app1', { path: '//deep/link//' })).toBe('/app/app1/deep/link');
+ expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/app/app1/deep/link');
+ });
+ });
+
+ describe('navigateToApp', () => {
+ it('changes the browser history to /app/:appId', async () => {
+ service.setup(setupDeps);
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ navigateToApp('myTestApp');
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined);
+
+ navigateToApp('myOtherApp');
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined);
+ });
+
+ it('changes the browser history for custom appRoutes', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ navigateToApp('myTestApp');
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined);
+
+ navigateToApp('app2');
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom/path', undefined);
+ });
+
+ it('appends a path if specified', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ navigateToApp('myTestApp', { path: 'deep/link/to/location/2' });
+ expect(MockHistory.push).toHaveBeenCalledWith(
+ '/app/myTestApp/deep/link/to/location/2',
+ undefined
+ );
+
+ navigateToApp('app2', { path: 'deep/link/to/location/2' });
+ expect(MockHistory.push).toHaveBeenCalledWith(
+ '/custom/path/deep/link/to/location/2',
+ undefined
+ );
+ });
+
+ it('includes state if specified', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ navigateToApp('myTestApp', { state: 'my-state' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', 'my-state');
+
+ navigateToApp('app2', { state: 'my-state' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom/path', 'my-state');
+ });
+
+ it('redirects when in legacyMode', async () => {
+ setupDeps.redirectTo = jest.fn();
+ setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
+ service.setup(setupDeps);
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ navigateToApp('myTestApp');
+ expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/myTestApp');
+ });
+
+ it('updates currentApp$ after mounting', async () => {
+ service.setup(setupDeps);
+
+ const { currentAppId$, navigateToApp } = await service.start(startDeps);
+ const stop$ = new Subject();
+ const promise = currentAppId$.pipe(skip(1), bufferCount(4), takeUntil(stop$)).toPromise();
+
+ await navigateToApp('alpha');
+ await navigateToApp('beta');
+ await navigateToApp('gamma');
+ await navigateToApp('delta');
+ stop$.next();
+
+ const appIds = await promise;
+
+ expect(appIds).toMatchInlineSnapshot(`
+ Array [
+ "alpha",
+ "beta",
+ "gamma",
+ "delta",
+ ]
+ `);
+ });
+
+ it('sets window.location.href when navigating to legacy apps', async () => {
+ setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
+ setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
+ setupDeps.redirectTo = jest.fn();
+ service.setup(setupDeps);
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ await navigateToApp('alpha');
+ expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/alpha');
+ });
+
+ it('handles legacy apps with subapps', async () => {
+ setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
+ setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
+ setupDeps.redirectTo = jest.fn();
+
+ const { registerLegacyApp } = service.setup(setupDeps);
+
+ registerLegacyApp({ id: 'baseApp:legacyApp1' } as any);
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ await navigateToApp('baseApp:legacyApp1');
+ expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/baseApp');
+ });
+ });
+});
diff --git a/src/core/public/application/application_service.test.tsx b/src/core/public/application/application_service.test.tsx
deleted file mode 100644
index 32634572466a6..0000000000000
--- a/src/core/public/application/application_service.test.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import React from 'react';
-
-import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
-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', mount: jest.fn() } as any);
- expect(() =>
- setup.register(Symbol(), { id: 'app1', mount: jest.fn() } 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\\""`);
- });
-
- 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', () => {
- 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 context = contextServiceMock.createSetupContract();
- const setup = service.setup({ context });
- setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any);
- setup.registerLegacyApp({ id: 'app2' } as any);
-
- const http = httpServiceMock.createStartContract();
- const injectedMetadata = injectedMetadataServiceMock.createStartContract();
- const startContract = await service.start({ http, injectedMetadata });
-
- expect(startContract.availableApps).toMatchInlineSnapshot(`
- Map {
- "app1" => Object {
- "id": "app1",
- "mount": [MockFunction],
- },
- }
- `);
- expect(startContract.availableLegacyApps).toMatchInlineSnapshot(`
- Map {
- "app2" => Object {
- "id": "app2",
- },
- }
- `);
- });
-
- it('passes registered applications to capabilities', async () => {
- const service = new ApplicationService();
- const context = contextServiceMock.createSetupContract();
- const setup = service.setup({ context });
- 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', app1]]),
- legacyApps: new Map(),
- http,
- });
- });
-
- it('passes registered legacy applications to capabilities', async () => {
- const service = new ApplicationService();
- 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({ http, injectedMetadata });
-
- expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
- apps: new Map(),
- legacyApps: new Map([['legacyApp1', { id: 'legacyApp1' }]]),
- http,
- });
- });
-
- 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');
- });
- });
-});
diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx
index df00c84028e6f..a96b9dea9b9c7 100644
--- a/src/core/public/application/application_service.tsx
+++ b/src/core/public/application/application_service.tsx
@@ -17,31 +17,32 @@
* under the License.
*/
-import { createBrowserHistory } from 'history';
-import { BehaviorSubject } from 'rxjs';
import React from 'react';
+import { BehaviorSubject, Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { createBrowserHistory, History } from 'history';
-import { InjectedMetadataStart } from '../injected_metadata';
-import { CapabilitiesService } from './capabilities';
-import { AppRouter } from './ui';
-import { HttpStart } from '../http';
+import { InjectedMetadataSetup, InjectedMetadataStart } from '../injected_metadata';
+import { HttpSetup, HttpStart } from '../http';
import { ContextSetup, IContextContainer } from '../context';
+import { AppRouter } from './ui';
+import { CapabilitiesService, Capabilities } from './capabilities';
import {
App,
LegacyApp,
AppMount,
AppMountDeprecated,
+ AppMounter,
+ LegacyAppMounter,
+ Mounter,
InternalApplicationSetup,
InternalApplicationStart,
} from './types';
interface SetupDeps {
context: ContextSetup;
-}
-
-interface StartDeps {
- http: HttpStart;
- injectedMetadata: InjectedMetadataStart;
+ http: HttpSetup;
+ injectedMetadata: InjectedMetadataSetup;
/**
* Only necessary for redirecting to legacy apps
* @deprecated
@@ -49,144 +50,158 @@ interface StartDeps {
redirectTo?: (path: string) => void;
}
-interface AppBox {
- app: App;
- mount: AppMount;
+interface StartDeps {
+ injectedMetadata: InjectedMetadataStart;
+ http: HttpStart;
}
+// Mount functions with two arguments are assumed to expect deprecated `context` object.
+const isAppMountDeprecated = (mount: (...args: any[]) => any): mount is AppMountDeprecated =>
+ mount.length === 2;
+const filterAvailable = (map: Map, capabilities: Capabilities) =>
+ new Map(
+ [...map].filter(
+ ([id]) => capabilities.navLinks[id] === undefined || capabilities.navLinks[id] === true
+ )
+ );
+const findMounter = (mounters: Map, appRoute?: string) =>
+ [...mounters].find(([, mounter]) => mounter.appRoute === appRoute);
+const getAppUrl = (mounters: Map, appId: string, path: string = '') =>
+ `/${mounters.get(appId)?.appRoute ?? `/app/${appId}`}/${path}`
+ .replace(/\/{2,}/g, '/') // Remove duplicate slashes
+ .replace(/\/$/, ''); // Remove trailing slash
+
/**
* Service that is responsible for registering new applications.
* @internal
*/
export class ApplicationService {
- private readonly apps$ = new BehaviorSubject>(new Map());
- private readonly legacyApps$ = new BehaviorSubject>(new Map());
+ private readonly apps = new Map();
+ private readonly legacyApps = new Map();
+ private readonly mounters = new Map();
private readonly capabilities = new CapabilitiesService();
+ private currentAppId$ = new BehaviorSubject(undefined);
+ private stop$ = new Subject();
+ private registrationClosed = false;
+ private history?: History;
private mountContext?: IContextContainer;
+ private navigate?: (url: string, state: any) => void;
- public setup({ context }: SetupDeps): InternalApplicationSetup {
+ public setup({
+ context,
+ http: { basePath },
+ injectedMetadata,
+ redirectTo = (path: string) => (window.location.href = path),
+ }: SetupDeps): InternalApplicationSetup {
+ const basename = basePath.get();
+ // Only setup history if we're not in legacy mode
+ if (!injectedMetadata.getLegacyMode()) {
+ this.history = createBrowserHistory({ basename });
+ }
+
+ // If we do not have history available, use redirectTo to do a full page refresh.
+ this.navigate = (url, state) =>
+ // basePath not needed here because `history` is configured with basename
+ this.history ? this.history.push(url, state) : redirectTo(basePath.prepend(url));
this.mountContext = context.createContextContainer();
return {
- 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) {
+ registerMountContext: this.mountContext!.registerContext,
+ register: (plugin, app) => {
+ app = { appRoute: `/app/${app.id}`, ...app };
+
+ if (this.registrationClosed) {
throw new Error(`Applications cannot be registered after "setup"`);
+ } else if (this.apps.has(app.id)) {
+ throw new Error(`An application is already registered with the id "${app.id}"`);
+ } else if (findMounter(this.mounters, app.appRoute)) {
+ throw new Error(
+ `An application is already registered with the appRoute "${app.appRoute}"`
+ );
+ } else if (basename && app.appRoute!.startsWith(basename)) {
+ throw new Error('Cannot register an application route that includes HTTP base path');
}
- let appBox: AppBox;
+ let handler: AppMount;
+
if (isAppMountDeprecated(app.mount)) {
+ handler = this.mountContext!.createHandler(plugin, 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 };
+ handler = app.mount;
}
- this.apps$.next(new Map([...this.apps$.value.entries(), [app.id, appBox]]));
+ const mount: AppMounter = async params => {
+ const unmount = await handler(params);
+ this.currentAppId$.next(app.id);
+ return unmount;
+ };
+ this.apps.set(app.id, app);
+ this.mounters.set(app.id, {
+ appRoute: app.appRoute!,
+ appBasePath: basePath.prepend(app.appRoute!),
+ mount,
+ unmountBeforeMounting: false,
+ });
},
- registerLegacyApp: (app: LegacyApp) => {
- if (this.legacyApps$.value.has(app.id)) {
+ registerLegacyApp: app => {
+ const appRoute = `/app/${app.id.split(':')[0]}`;
+
+ if (this.registrationClosed) {
+ throw new Error('Applications cannot be registered after "setup"');
+ } else if (this.legacyApps.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"`);
+ } else if (basename && appRoute!.startsWith(basename)) {
+ throw new Error('Cannot register an application route that includes HTTP base path');
}
- this.legacyApps$.next(new Map([...this.legacyApps$.value.entries(), [app.id, app]]));
+ const appBasePath = basePath.prepend(appRoute);
+ const mount: LegacyAppMounter = () => redirectTo(appBasePath);
+ this.legacyApps.set(app.id, app);
+ this.mounters.set(app.id, {
+ appRoute,
+ appBasePath,
+ mount,
+ unmountBeforeMounting: true,
+ });
},
- registerMountContext: this.mountContext!.registerContext,
};
}
- public async start({
- http,
- injectedMetadata,
- redirectTo = (path: string) => (window.location.href = path),
- }: StartDeps): Promise {
+ public async start({ injectedMetadata, http }: StartDeps): Promise {
if (!this.mountContext) {
- throw new Error(`ApplicationService#setup() must be invoked before start.`);
+ 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(undefined);
- const { availableApps, availableLegacyApps, capabilities } = await this.capabilities.start({
+ this.registrationClosed = true;
+ const { capabilities } = await this.capabilities.start({
+ appIds: [...this.mounters.keys()],
http,
- apps: new Map([...this.apps$.value].map(([id, { app }]) => [id, app])),
- legacyApps: this.legacyApps$.value,
});
-
- // Only setup history if we're not in legacy mode
- const history = legacyMode ? null : createBrowserHistory({ basename: http.basePath.get() });
+ const availableMounters = filterAvailable(this.mounters, capabilities);
return {
- availableApps,
- availableLegacyApps,
+ availableApps: filterAvailable(this.apps, capabilities),
+ availableLegacyApps: filterAvailable(this.legacyApps, capabilities),
capabilities,
+ currentAppId$: this.currentAppId$.pipe(takeUntil(this.stop$)),
registerMountContext: this.mountContext.registerContext,
- currentAppId$,
-
- getUrlForApp: (appId, options: { path?: string } = {}) => {
- return http.basePath.prepend(appPath(appId, options));
- },
-
+ getUrlForApp: (appId, { path }: { path?: string } = {}) =>
+ getAppUrl(availableMounters, appId, path),
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 appMounts = new Map(
- [...this.apps$.value]
- .filter(([id]) => availableApps.has(id))
- .map(([id, { mount }]) => [id, mount])
- );
-
- return (
-
- );
+ this.navigate!(getAppUrl(availableMounters, appId, path), state);
+ this.currentAppId$.next(appId);
},
+ getComponent: () =>
+ this.history ? : null,
};
}
- public stop() {}
-}
-
-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;
+ public stop() {
+ this.stop$.next();
+ this.currentAppId$.complete();
+ }
}
diff --git a/src/core/public/application/capabilities/capabilities_service.mock.ts b/src/core/public/application/capabilities/capabilities_service.mock.ts
index 29c3275f0e3b2..54aaa31e08859 100644
--- a/src/core/public/application/capabilities/capabilities_service.mock.ts
+++ b/src/core/public/application/capabilities/capabilities_service.mock.ts
@@ -17,15 +17,9 @@
* under the License.
*/
import { CapabilitiesService, CapabilitiesStart } from './capabilities_service';
-import { deepFreeze } from '../../../utils/';
-import { App, LegacyApp } from '../types';
+import { deepFreeze } from '../../../utils';
-const createStartContractMock = (
- apps: ReadonlyMap = new Map(),
- legacyApps: ReadonlyMap = new Map()
-): jest.Mocked => ({
- availableApps: apps,
- availableLegacyApps: legacyApps,
+const createStartContractMock = (): jest.Mocked => ({
capabilities: deepFreeze({
catalogue: {},
management: {},
@@ -33,11 +27,8 @@ const createStartContractMock = (
}),
});
-type CapabilitiesServiceContract = PublicMethodsOf;
-const createMock = (): jest.Mocked => ({
- start: jest
- .fn()
- .mockImplementation(({ apps, legacyApps }) => createStartContractMock(apps, legacyApps)),
+const createMock = (): jest.Mocked> => ({
+ start: jest.fn().mockImplementation(createStartContractMock),
});
export const capabilitiesServiceMock = {
diff --git a/src/core/public/application/capabilities/capabilities_service.test.ts b/src/core/public/application/capabilities/capabilities_service.test.ts
index 3245be8dd502d..dfbb449b4d58e 100644
--- a/src/core/public/application/capabilities/capabilities_service.test.ts
+++ b/src/core/public/application/capabilities/capabilities_service.test.ts
@@ -19,7 +19,6 @@
import { httpServiceMock, HttpSetupMock } from '../../http/http_service.mock';
import { CapabilitiesService } from './capabilities_service';
-import { LegacyApp, App } from '../types';
const mockedCapabilities = {
catalogue: {},
@@ -42,36 +41,22 @@ describe('#start', () => {
http.post.mockReturnValue(Promise.resolve(mockedCapabilities));
});
- const apps = new Map([
- ['app1', { id: 'app1' }],
- ['app2', { id: 'app2', capabilities: { app2: { feature: true } } }],
- ['appMissingInCapabilities', { id: 'appMissingInCapabilities' }],
- ] 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 () => {
+ it('only returns capabilities for given appIds', async () => {
const service = new CapabilitiesService();
- const startContract = await service.start({ apps, legacyApps, http });
- expect(startContract.availableApps).toEqual(
- new Map([
- ['app1', { id: 'app1' }],
- ['appMissingInCapabilities', { id: 'appMissingInCapabilities' }],
- ])
- );
- expect(startContract.availableLegacyApps).toEqual(
- new Map([['legacyApp1', { id: 'legacyApp1' }]])
- );
+ const { capabilities } = await service.start({
+ http,
+ appIds: ['app1', 'app2', 'legacyApp1', 'legacyApp2'],
+ });
+
+ // @ts-ignore TypeScript knows this shouldn't be possible
+ expect(() => (capabilities.foo = 'foo')).toThrowError();
});
it('does not allow Capabilities to be modified', async () => {
const service = new CapabilitiesService();
const { capabilities } = await service.start({
- apps,
- legacyApps,
http,
+ appIds: ['app1', 'app2', 'legacyApp1', 'legacyApp2'],
});
// @ts-ignore TypeScript knows this shouldn't be possible
diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx
index 24d9765953c44..05d718e1073df 100644
--- a/src/core/public/application/capabilities/capabilities_service.tsx
+++ b/src/core/public/application/capabilities/capabilities_service.tsx
@@ -19,22 +19,16 @@
import { Capabilities } from '../../../types/capabilities';
import { deepFreeze, RecursiveReadonly } from '../../../utils';
-import { LegacyApp, App } from '../types';
import { HttpStart } from '../../http';
interface StartDeps {
- apps: ReadonlyMap;
- legacyApps: ReadonlyMap;
+ appIds: string[];
http: HttpStart;
}
-export { Capabilities };
-
/** @internal */
export interface CapabilitiesStart {
capabilities: RecursiveReadonly;
- availableApps: ReadonlyMap;
- availableLegacyApps: ReadonlyMap;
}
/**
@@ -42,41 +36,14 @@ export interface CapabilitiesStart {
* @internal
*/
export class CapabilitiesService {
- public async start({ apps, legacyApps, http }: StartDeps): Promise {
- const capabilities = await this.fetchCapabilities(http, [...apps.keys(), ...legacyApps.keys()]);
-
- 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
- )
- );
+ public async start({ appIds, http }: StartDeps): Promise {
+ const route = http.anonymousPaths.isAnonymous(window.location.pathname) ? '/defaults' : '';
+ const capabilities = await http.post(`/api/core/capabilities${route}`, {
+ body: JSON.stringify({ applications: appIds }),
+ });
return {
- availableApps,
- availableLegacyApps,
- capabilities,
+ capabilities: deepFreeze(capabilities),
};
}
-
- private async fetchCapabilities(http: HttpStart, appIds: string[]): Promise {
- const payload = JSON.stringify({
- applications: appIds,
- });
-
- const url = http.anonymousPaths.isAnonymous(window.location.pathname)
- ? '/api/core/capabilities/defaults'
- : '/api/core/capabilities';
- const capabilities = await http.post(url, {
- body: payload,
- });
- return deepFreeze(capabilities);
- }
}
diff --git a/src/core/public/application/capabilities/index.ts b/src/core/public/application/capabilities/index.ts
index 9d8bec955eb97..e4112a55ef6bd 100644
--- a/src/core/public/application/capabilities/index.ts
+++ b/src/core/public/application/capabilities/index.ts
@@ -17,4 +17,5 @@
* under the License.
*/
-export { Capabilities, CapabilitiesService } from './capabilities_service';
+export { Capabilities } from '../../../types/capabilities';
+export { CapabilitiesService } from './capabilities_service';
diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx
index 81aef5204c7e2..ffc10820a9c37 100644
--- a/src/core/public/application/integration_tests/router.test.tsx
+++ b/src/core/public/application/integration_tests/router.test.tsx
@@ -18,107 +18,105 @@
*/
import React from 'react';
-import { mount, ReactWrapper } from 'enzyme';
import { createMemoryHistory, History } from 'history';
-import { BehaviorSubject } from 'rxjs';
-import { I18nProvider } from '@kbn/i18n/react';
-
-import { AppMount, 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) => {
- el.innerHTML = `\nbasename: ${basename}\nhtml: ${htmlString}\n
`;
- return jest.fn(() => (el.innerHTML = ''));
- });
+import { EitherApp, MockedMounterMap, MockedMounterTuple } from '../test_types';
+import { createRenderer, createAppMounter, createLegacyAppMounter } from './utils';
describe('AppContainer', () => {
- let apps: Map, Parameters>>;
- let legacyApps: Map;
+ let mounters: MockedMounterMap;
let history: History;
- let router: ReactWrapper;
- let redirectTo: jest.Mock;
- let currentAppId$: BehaviorSubject;
-
- const navigate = async (path: string) => {
- history.push(path);
- router.update();
- // flushes any pending promises
- return new Promise(resolve => setImmediate(resolve));
- };
+ let navigate: ReturnType;
beforeEach(() => {
- redirectTo = jest.fn();
- apps = new Map([
- ['app1', createMountHandler('App 1')],
- ['app2', createMountHandler('App 2
')],
- ]);
- legacyApps = new Map([
- ['legacyApp1', { id: 'legacyApp1' }],
- ['baseApp:legacyApp2', { id: 'baseApp:legacyApp2' }],
- ]) as Map;
+ mounters = new Map([
+ createAppMounter('app1', 'App 1'),
+ createLegacyAppMounter('legacyApp1', jest.fn()),
+ createAppMounter('app2', 'App 2
'),
+ createLegacyAppMounter('baseApp:legacyApp2', jest.fn()),
+ createAppMounter('app3', 'App 3
', '/custom/path'),
+ ] as Array>);
history = createMemoryHistory();
- currentAppId$ = new BehaviorSubject(undefined);
- // Use 'asdf' as the basepath
- const http = httpServiceMock.createStartContract({ basePath: '/asdf' });
- router = mount(
-
-
-
- );
+ navigate = createRenderer(, history.push);
});
- 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(`
+ it('calls mount handler and returned unmount function when navigating between apps', async () => {
+ const dom1 = await navigate('/app/app1');
+ const app1 = mounters.get('app1')!;
+
+ expect(app1.mount).toHaveBeenCalled();
+ expect(dom1?.html()).toMatchInlineSnapshot(`
"
- basename: /asdf/app/app1
+ basename: /app/app1
html: App 1
"
`);
- const app1Unmount = await apps.get('app1')!.mock.results[0].value;
- await navigate('/app/app2');
- expect(app1Unmount).toHaveBeenCalled();
+ const app1Unmount = await app1.mount.mock.results[0].value;
+ const dom2 = await navigate('/app/app2');
- expect(apps.get('app2')!).toHaveBeenCalled();
- expect(router.html()).toMatchInlineSnapshot(`
+ expect(app1Unmount).toHaveBeenCalled();
+ expect(mounters.get('app2')!.mount).toHaveBeenCalled();
+ expect(dom2?.html()).toMatchInlineSnapshot(`
"
- basename: /asdf/app/app2
+ basename: /app/app2
html:
App 2
"
`);
});
- 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('should not mount when partial route path matches', async () => {
+ mounters.set(...createAppMounter('spaces', 'Custom Space
', '/spaces/fake-login'));
+ mounters.set(...createAppMounter('login', 'Login Page
', '/fake-login'));
+ history = createMemoryHistory();
+ navigate = createRenderer(, history.push);
+
+ await navigate('/fake-login');
+
+ expect(mounters.get('spaces')!.mount).not.toHaveBeenCalled();
+ expect(mounters.get('login')!.mount).toHaveBeenCalled();
+ });
+
+ it('should not mount when partial route path has higher specificity', async () => {
+ mounters.set(...createAppMounter('login', 'Login Page
', '/fake-login'));
+ mounters.set(...createAppMounter('spaces', 'Custom Space
', '/spaces/fake-login'));
+ history = createMemoryHistory();
+ navigate = createRenderer(, history.push);
+
+ await navigate('/spaces/fake-login');
+
+ expect(mounters.get('spaces')!.mount).toHaveBeenCalled();
+ expect(mounters.get('login')!.mount).not.toHaveBeenCalled();
});
- it('sets window.location.href when navigating to legacy apps', async () => {
+ it('calls legacy mount handler', async () => {
await navigate('/app/legacyApp1');
- expect(redirectTo).toHaveBeenCalledWith('/asdf/app/legacyApp1');
+ expect(mounters.get('legacyApp1')!.mount.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "appBasePath": "/app/legacyApp1",
+ "element": ,
+ },
+ ]
+ `);
});
it('handles legacy apps with subapps', async () => {
await navigate('/app/baseApp');
- expect(redirectTo).toHaveBeenCalledWith('/asdf/app/baseApp');
+ expect(mounters.get('baseApp:legacyApp2')!.mount.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "appBasePath": "/app/baseApp",
+ "element": ,
+ },
+ ]
+ `);
});
it('displays error page if no app is found', async () => {
- await navigate('/app/unknown');
- expect(router.exists(AppNotFound)).toBe(true);
+ const dom = await navigate('/app/unknown');
+
+ expect(dom?.exists(AppNotFound)).toBe(true);
});
});
diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx
new file mode 100644
index 0000000000000..b8ade4d1d8787
--- /dev/null
+++ b/src/core/public/application/integration_tests/utils.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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, { ReactElement } from 'react';
+import { mount } from 'enzyme';
+
+import { I18nProvider } from '@kbn/i18n/react';
+
+import { App, LegacyApp, AppMountParameters } from '../types';
+import { MockedMounter, MockedMounterTuple } from '../test_types';
+
+type Dom = ReturnType | null;
+type Renderer = (item: string) => Dom | Promise;
+
+export const createRenderer = (
+ element: ReactElement | null,
+ callback?: (item: string) => void | Promise
+): Renderer => {
+ const dom: Dom = element && mount({element});
+
+ return item =>
+ new Promise(async resolve => {
+ if (callback) {
+ await callback(item);
+ }
+ if (dom) {
+ dom.update();
+ }
+ setImmediate(() => resolve(dom)); // flushes any pending promises
+ });
+};
+
+export const createAppMounter = (
+ appId: string,
+ html: string,
+ appRoute = `/app/${appId}`
+): MockedMounterTuple => [
+ appId,
+ {
+ appRoute,
+ appBasePath: appRoute,
+ mount: jest.fn(async ({ appBasePath: basename, element }: AppMountParameters) => {
+ Object.assign(element, {
+ innerHTML: `\nbasename: ${basename}\nhtml: ${html}\n
`,
+ });
+ return jest.fn(() => Object.assign(element, { innerHTML: '' }));
+ }),
+ },
+];
+
+export const createLegacyAppMounter = (
+ appId: string,
+ legacyMount: MockedMounter['mount']
+): MockedMounterTuple => [
+ appId,
+ {
+ appRoute: `/app/${appId.split(':')[0]}`,
+ appBasePath: `/app/${appId.split(':')[0]}`,
+ unmountBeforeMounting: true,
+ mount: legacyMount,
+ },
+];
diff --git a/src/core/public/application/test_types.ts b/src/core/public/application/test_types.ts
new file mode 100644
index 0000000000000..f5fb639eaa32c
--- /dev/null
+++ b/src/core/public/application/test_types.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { App, LegacyApp, Mounter } from './types';
+import { ApplicationService } from './application_service';
+
+/** @internal */
+export type ApplicationServiceContract = PublicMethodsOf;
+/** @internal */
+export type EitherApp = App | LegacyApp;
+/** @internal */
+export type MockedMounter = jest.Mocked>>;
+/** @internal */
+export type MockedMounterTuple = [string, MockedMounter];
+/** @internal */
+export type MockedMounterMap = Map>;
+/** @internal */
+export type MockLifecycle<
+ T extends keyof ApplicationService,
+ U = Parameters[0]
+> = { [P in keyof U]: jest.Mocked };
diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts
index fd009066fc664..c026851af7eb8 100644
--- a/src/core/public/application/types.ts
+++ b/src/core/public/application/types.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Observable, Subject } from 'rxjs';
+import { Observable } from 'rxjs';
import { Capabilities } from './capabilities';
import { ChromeStart } from '../chrome';
@@ -89,6 +89,13 @@ export interface App extends AppBase {
* Takes precedence over chrome service visibility settings.
*/
chromeless?: boolean;
+
+ /**
+ * Override the application's routing path from `/app/${id}`.
+ * Must be unique across registered applications. Should not include the
+ * base path from HTTP.
+ */
+ appRoute?: string;
}
/** @internal */
@@ -177,7 +184,8 @@ export interface AppMountParameters {
element: HTMLElement;
/**
- * The base path for configuring the application's router.
+ * The route path for configuring navigation to the application.
+ * This string should not include the base path from HTTP.
*
* @example
*
@@ -189,6 +197,7 @@ export interface AppMountParameters {
* setup({ application }) {
* application.register({
* id: 'my-app',
+ * appRoute: '/my-app',
* async mount(params) {
* const { renderApp } = await import('./application');
* return renderApp(params);
@@ -229,6 +238,23 @@ export interface AppMountParameters {
*/
export type AppUnmount = () => void;
+/** @internal */
+export type AppMounter = (params: AppMountParameters) => Promise;
+
+/** @internal */
+export type LegacyAppMounter = (params: AppMountParameters) => void;
+
+/** @internal */
+export type Mounter = SelectivePartial<
+ {
+ appRoute: string;
+ appBasePath: string;
+ mount: T extends LegacyApp ? LegacyAppMounter : AppMounter;
+ unmountBeforeMounting: T extends LegacyApp ? true : boolean;
+ },
+ T extends LegacyApp ? never : 'unmountBeforeMounting'
+>;
+
/** @public */
export interface ApplicationSetup {
/**
@@ -352,6 +378,12 @@ export interface InternalApplicationStart
): void;
// Internal APIs
- currentAppId$: Subject;
+ currentAppId$: Observable;
getComponent(): JSX.Element | null;
}
+
+/** @internal */
+type SelectivePartial = Partial> &
+ Required>> extends infer U
+ ? { [P in keyof U]: U[P] }
+ : never;
diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx
index 9c2bb30e79503..96ee91c7c21fb 100644
--- a/src/core/public/application/ui/app_container.tsx
+++ b/src/core/public/application/ui/app_container.tsx
@@ -17,95 +17,60 @@
* under the License.
*/
-import React from 'react';
-import { RouteComponentProps } from 'react-router-dom';
-import { Subject } from 'rxjs';
-
-import { LegacyApp, AppMount, AppUnmount } from '../types';
-import { HttpStart } from '../../http';
+import React, {
+ Fragment,
+ FunctionComponent,
+ useLayoutEffect,
+ useRef,
+ useState,
+ MutableRefObject,
+} from 'react';
+
+import { AppUnmount, Mounter } from '../types';
import { AppNotFound } from './app_not_found_screen';
-interface Props extends RouteComponentProps<{ appId: string }> {
- apps: ReadonlyMap;
- legacyApps: ReadonlyMap;
- basePath: HttpStart['basePath'];
- currentAppId$: Subject;
- /**
- * Only necessary for redirecting to legacy apps
- * @deprecated
- */
- redirectTo: (path: string) => void;
-}
-
-interface State {
- appNotFound: boolean;
-}
-
-export class AppContainer extends React.Component {
- private readonly containerDiv = React.createRef();
- 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 (
-
- {this.state.appNotFound && }
-
-
- );
- }
+interface Props {
+ appId: string;
+ mounter?: Mounter;
}
-function findLegacyApp(appId: string, apps: ReadonlyMap) {
- const matchingApps = [...apps.entries()].filter(([id]) => id.split(':')[0] === appId);
- return matchingApps.length ? matchingApps[0][1] : null;
-}
+export const AppContainer: FunctionComponent = ({ mounter, appId }: Props) => {
+ const [appNotFound, setAppNotFound] = useState(false);
+ const elementRef = useRef(null);
+ const unmountRef: MutableRefObject = useRef(null);
+
+ useLayoutEffect(() => {
+ const unmount = () => {
+ if (unmountRef.current) {
+ unmountRef.current();
+ unmountRef.current = null;
+ }
+ };
+ const mount = async () => {
+ if (!mounter) {
+ return setAppNotFound(true);
+ }
+
+ if (mounter.unmountBeforeMounting) {
+ unmount();
+ }
+
+ unmountRef.current =
+ (await mounter.mount({
+ appBasePath: mounter.appBasePath,
+ element: elementRef.current!,
+ })) || null;
+ setAppNotFound(false);
+ };
+
+ mount();
+ return unmount;
+ });
+
+ return (
+
+ {appNotFound && }
+
+
+ );
+};
diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx
index 67701a33dabf4..8db46f9794277 100644
--- a/src/core/public/application/ui/app_router.tsx
+++ b/src/core/public/application/ui/app_router.tsx
@@ -17,37 +17,53 @@
* under the License.
*/
+import React, { FunctionComponent } from 'react';
import { History } from 'history';
-import React from 'react';
-import { Router, Route } from 'react-router-dom';
-import { Subject } from 'rxjs';
+import { Router, Route, RouteComponentProps, Switch } from 'react-router-dom';
-import { LegacyApp, AppMount } from '../types';
+import { Mounter } from '../types';
import { AppContainer } from './app_container';
-import { HttpStart } from '../../http';
interface Props {
- apps: ReadonlyMap;
- legacyApps: ReadonlyMap;
- basePath: HttpStart['basePath'];
- currentAppId$: Subject;
+ mounters: Map;
history: History;
- /**
- * Only necessary for redirecting to legacy apps
- * @deprecated
- */
- redirectTo?: (path: string) => void;
}
-export const AppRouter: React.FunctionComponent = ({
- history,
- redirectTo = (path: string) => (window.location.href = path),
- ...otherProps
-}) => (
+interface Params {
+ appId: string;
+}
+
+export const AppRouter: FunctionComponent = ({ history, mounters }) => (
- }
- />
+
+ {[...mounters].flatMap(([appId, mounter]) =>
+ // Remove /app paths from the routes as they will be handled by the
+ // "named" route parameter `:appId` below
+ mounter.appBasePath.startsWith('/app')
+ ? []
+ : [
+ }
+ />,
+ ]
+ )}
+ ) => {
+ // Find the mounter including legacy mounters with subapps:
+ const [id, mounter] = mounters.has(appId)
+ ? [appId, mounters.get(appId)]
+ : [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? [];
+
+ return ;
+ }}
+ />
+
);
diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts
index 9656739421686..d9c35b20db03b 100644
--- a/src/core/public/chrome/chrome_service.test.ts
+++ b/src/core/public/chrome/chrome_service.test.ts
@@ -211,14 +211,14 @@ describe('start', () => {
new FakeApp('beta', true),
new FakeApp('gamma', false),
]);
- const { availableApps, currentAppId$ } = startDeps.application;
+ const { availableApps, navigateToApp } = startDeps.application;
const { chrome, service } = await start({ startDeps });
const promise = chrome
.getIsVisible$()
.pipe(toArray())
.toPromise();
- [...availableApps.keys()].forEach(appId => currentAppId$.next(appId));
+ [...availableApps.keys()].forEach(appId => navigateToApp(appId));
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
@@ -233,14 +233,14 @@ describe('start', () => {
it('changing visibility has no effect on chrome-hiding application', async () => {
const startDeps = defaultStartDeps([new FakeApp('alpha', true)]);
- const { currentAppId$ } = startDeps.application;
+ const { navigateToApp } = startDeps.application;
const { chrome, service } = await start({ startDeps });
const promise = chrome
.getIsVisible$()
.pipe(toArray())
.toPromise();
- currentAppId$.next('alpha');
+ navigateToApp('alpha');
chrome.setIsVisible(true);
service.stop();
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index 25c00836a4db7..18c0c9870d72f 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -127,7 +127,7 @@ export class ChromeService {
)
);
this.isVisible$ = combineLatest(this.appHidden$, this.toggleHidden$).pipe(
- map(([appHidden, chromeHidden]) => !(appHidden || chromeHidden)),
+ map(([appHidden, toggleHidden]) => !(appHidden || toggleHidden)),
takeUntil(this.stop$)
);
}
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index 2a9dca96062dc..485c11aae6508 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -174,7 +174,7 @@ export class CoreSystem {
[this.legacy.legacyId, [...pluginDependencies.keys()]],
]),
});
- const application = this.application.setup({ context });
+ const application = this.application.setup({ context, http, injectedMetadata });
const core: InternalCoreSetup = {
application,
@@ -307,6 +307,7 @@ export class CoreSystem {
this.uiSettings.stop();
this.chrome.stop();
this.i18n.stop();
+ this.application.stop();
this.rootDomElement.textContent = '';
}
}
diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts
index 6a44000bf617e..0bde1b68e1876 100644
--- a/src/core/public/injected_metadata/injected_metadata_service.ts
+++ b/src/core/public/injected_metadata/injected_metadata_service.ts
@@ -80,9 +80,6 @@ export interface InjectedMetadataParams {
user?: Record;
};
};
- apm: {
- [key: string]: unknown;
- };
};
}
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index dfbb6b4a6fbf5..f61741571dc1d 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -18,6 +18,7 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type
// @public
export interface App extends AppBase {
+ appRoute?: string;
chromeless?: boolean;
mount: AppMount | AppMountDeprecated;
}
diff --git a/src/legacy/ui/ui_nav_links/ui_nav_links_mixin.js b/src/core/server/config/config.mock.ts
similarity index 61%
rename from src/legacy/ui/ui_nav_links/ui_nav_links_mixin.js
rename to src/core/server/config/config.mock.ts
index e445f5e9126d4..e098fa142b9d1 100644
--- a/src/legacy/ui/ui_nav_links/ui_nav_links_mixin.js
+++ b/src/core/server/config/config.mock.ts
@@ -17,18 +17,18 @@
* under the License.
*/
-import { UiNavLink } from './ui_nav_link';
+import { Config } from './config';
-export function uiNavLinksMixin(kbnServer, server) {
- const uiApps = server.getAllUiApps();
+type ConfigMock = jest.Mocked;
- const { navLinkSpecs = [] } = kbnServer.uiExports;
+const createConfigMock = (): ConfigMock => ({
+ has: jest.fn(),
+ get: jest.fn(),
+ set: jest.fn(),
+ getFlattenedPaths: jest.fn(),
+ toRaw: jest.fn(),
+});
- const fromSpecs = navLinkSpecs.map(navLinkSpec => new UiNavLink(navLinkSpec));
-
- const fromApps = uiApps.map(app => app.getNavLink()).filter(Boolean);
-
- const uiNavLinks = fromSpecs.concat(fromApps).sort((a, b) => a.getOrder() - b.getOrder());
-
- server.decorate('server', 'getUiNavLinks', () => uiNavLinks.slice(0));
-}
+export const configMock = {
+ create: createConfigMock,
+};
diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts
index 1668b409050b7..700ae04f00d47 100644
--- a/src/core/server/http/http_service.mock.ts
+++ b/src/core/server/http/http_service.mock.ts
@@ -20,6 +20,7 @@
import { Server } from 'hapi';
import { CspConfig } from '../csp';
import { mockRouter } from './router/router.mock';
+import { configMock } from '../config/config.mock';
import { InternalHttpServiceSetup } from './types';
import { HttpService } from './http_service';
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
@@ -28,13 +29,14 @@ import { sessionStorageMock } from './cookie_session_storage.mocks';
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
import { OnPreResponseToolkit } from './lifecycle/on_pre_response';
+type BasePathMocked = jest.Mocked;
export type HttpServiceSetupMock = jest.Mocked & {
- basePath: jest.Mocked;
+ basePath: BasePathMocked;
};
-const createBasePathMock = (): jest.Mocked => ({
- serverBasePath: '/mock-server-basepath',
- get: jest.fn(),
+const createBasePathMock = (serverBasePath = '/mock-server-basepath'): BasePathMocked => ({
+ serverBasePath,
+ get: jest.fn().mockReturnValue(serverBasePath),
set: jest.fn(),
prepend: jest.fn(),
remove: jest.fn(),
@@ -44,9 +46,12 @@ const createSetupContractMock = () => {
const setupContract: HttpServiceSetupMock = {
// we can mock other hapi server methods when we need it
server: ({
+ name: 'http-server-test',
+ version: 'kibana',
route: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
+ config: jest.fn().mockReturnValue(configMock.create()),
} as unknown) as jest.MockedClass,
createCookieSessionStorageFactory: jest.fn(),
registerOnPreAuth: jest.fn(),
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 878f854f2a517..953fa0738597c 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -41,6 +41,7 @@
import { ElasticsearchServiceSetup, IScopedClusterClient } from './elasticsearch';
import { HttpServiceSetup } from './http';
+import { IScopedRenderingClient } from './rendering';
import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins';
import { ContextSetup } from './context';
import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings';
@@ -149,6 +150,7 @@ export {
SessionCookieValidationResult,
SessionStorageFactory,
} from './http';
+export { RenderingServiceSetup, IRenderOptions, LegacyRenderOptions } from './rendering';
export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
export {
@@ -229,12 +231,21 @@ export {
SavedObjectsMigrationVersion,
} from './types';
-export { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy';
+export {
+ LegacyServiceSetupDeps,
+ LegacyServiceStartDeps,
+ LegacyServiceDiscoverPlugins,
+ LegacyConfig,
+ LegacyUiExports,
+ LegacyInternals,
+} from './legacy';
/**
* Plugin specific context passed to a route handler.
*
* Provides the following clients:
+ * - {@link IScopedRenderingClient | rendering} - Rendering client
+ * which uses the data of the incoming request
* - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client
* which uses the credentials of the incoming request
* - {@link ScopedClusterClient | elasticsearch.dataClient} - Elasticsearch
@@ -248,6 +259,7 @@ export { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy';
*/
export interface RequestHandlerContext {
core: {
+ rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
};
@@ -301,6 +313,7 @@ export {
CapabilitiesSetup,
CapabilitiesStart,
ContextSetup,
+ IScopedRenderingClient,
PluginsServiceSetup,
PluginsServiceStart,
PluginOpaqueId,
diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts
index 52adaaccab4b7..be4d830c55eab 100644
--- a/src/core/server/internal_types.ts
+++ b/src/core/server/internal_types.ts
@@ -17,15 +17,15 @@
* under the License.
*/
+import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
+import { ContextSetup } from './context';
import { InternalElasticsearchServiceSetup } from './elasticsearch';
import { InternalHttpServiceSetup } from './http';
-import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings';
-import { ContextSetup } from './context';
import {
- InternalSavedObjectsServiceStart,
InternalSavedObjectsServiceSetup,
+ InternalSavedObjectsServiceStart,
} from './saved_objects';
-import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
+import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings';
import { UuidServiceSetup } from './uuid';
/** @internal */
diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/legacy/config/ensure_valid_configuration.ts
index 026683a7b7cb0..a68d3df577a89 100644
--- a/src/core/server/legacy/config/ensure_valid_configuration.ts
+++ b/src/core/server/legacy/config/ensure_valid_configuration.ts
@@ -19,7 +19,7 @@
import { getUnusedConfigKeys } from './get_unused_config_keys';
import { ConfigService } from '../../config';
-import { LegacyServiceDiscoverPlugins } from '../legacy_service';
+import { LegacyServiceDiscoverPlugins } from '../types';
import { CriticalError } from '../../errors';
export async function ensureValidConfiguration(
diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts
index bf011fa01a342..c4452fc6a1209 100644
--- a/src/core/server/legacy/config/get_unused_config_keys.test.ts
+++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts
@@ -17,8 +17,7 @@
* under the License.
*/
-import { LegacyPluginSpec } from '../plugins/find_legacy_plugin_specs';
-import { LegacyConfig } from './types';
+import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types';
import { getUnusedConfigKeys } from './get_unused_config_keys';
describe('getUnusedConfigKeys', () => {
@@ -26,7 +25,7 @@ describe('getUnusedConfigKeys', () => {
jest.resetAllMocks();
});
- const getConfig = (values: Record = {}): LegacyConfig =>
+ const getConfig = (values: LegacyVars = {}): LegacyConfig =>
({
get: () => values as any,
} as LegacyConfig);
diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts
index 73cc7d8c50474..e425082ba126d 100644
--- a/src/core/server/legacy/config/get_unused_config_keys.ts
+++ b/src/core/server/legacy/config/get_unused_config_keys.ts
@@ -22,8 +22,7 @@ import { difference, get, set } from 'lodash';
import { getTransform } from '../../../../legacy/deprecation/index';
import { unset, getFlattenedObject } from '../../../../legacy/utils';
import { hasConfigPathIntersection } from '../../config';
-import { LegacyPluginSpec } from '../plugins/find_legacy_plugin_specs';
-import { LegacyConfig } from './types';
+import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types';
const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object));
@@ -37,7 +36,7 @@ export async function getUnusedConfigKeys({
coreHandledConfigPaths: string[];
pluginSpecs: LegacyPluginSpec[];
disabledPluginSpecs: LegacyPluginSpec[];
- settings: Record;
+ settings: LegacyVars;
legacyConfig: LegacyConfig;
}) {
// transform deprecated plugin settings
diff --git a/src/core/server/legacy/config/index.ts b/src/core/server/legacy/config/index.ts
index c3f308fd6d903..f10e3f22d53c5 100644
--- a/src/core/server/legacy/config/index.ts
+++ b/src/core/server/legacy/config/index.ts
@@ -20,9 +20,3 @@
export { ensureValidConfiguration } from './ensure_valid_configuration';
export { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter';
export { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters';
-export {
- LegacyConfig,
- LegacyConfigDeprecation,
- LegacyConfigDeprecationFactory,
- LegacyConfigDeprecationProvider,
-} from './types';
diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts
index 144e057c118f7..8651d05064492 100644
--- a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts
+++ b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts
@@ -17,11 +17,11 @@
* under the License.
*/
-import { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters';
-import { LegacyConfigDeprecationProvider } from './types';
import { ConfigDeprecation } from '../../config';
import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory';
import { applyDeprecations } from '../../config/deprecation/apply_deprecations';
+import { LegacyConfigDeprecationProvider } from '../types';
+import { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters';
jest.spyOn(configDeprecationFactory, 'unusedFromRoot');
jest.spyOn(configDeprecationFactory, 'renameFromRoot');
diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.ts
index b0e3bc37e1510..1e0733969e662 100644
--- a/src/core/server/legacy/config/legacy_deprecation_adapters.ts
+++ b/src/core/server/legacy/config/legacy_deprecation_adapters.ts
@@ -18,8 +18,8 @@
*/
import { ConfigDeprecation, ConfigDeprecationProvider } from '../../config/deprecation';
-import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from './index';
import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory';
+import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from '../types';
const convertLegacyDeprecation = (
legacyDeprecation: LegacyConfigDeprecation
diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts
index ffcbfda4e024d..bdcde8262ef98 100644
--- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts
+++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts
@@ -19,6 +19,7 @@
import { ConfigPath } from '../../config';
import { ObjectToConfigAdapter } from '../../config/object_to_config_adapter';
+import { LegacyVars } from '../types';
/**
* Represents logging config supported by the legacy platform.
@@ -77,7 +78,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
};
}
- private static transformPlugins(configValue: Record) {
+ private static transformPlugins(configValue: LegacyVars) {
// These properties are the only ones we use from the existing `plugins` config node
// since `scanDirs` isn't respected by new platform plugin discovery.
return {
@@ -94,7 +95,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
case 'server':
return LegacyObjectToConfigAdapter.transformServer(configValue);
case 'plugins':
- return LegacyObjectToConfigAdapter.transformPlugins(configValue as Record);
+ return LegacyObjectToConfigAdapter.transformPlugins(configValue as LegacyVars);
default:
return configValue;
}
diff --git a/src/core/server/legacy/config/types.ts b/src/core/server/legacy/config/types.ts
deleted file mode 100644
index cac1002d6c244..0000000000000
--- a/src/core/server/legacy/config/types.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * New platform representation of the legacy configuration (KibanaConfig)
- *
- * @internal
- */
-export interface LegacyConfig {
- get(key?: string): T;
- has(key: string): boolean;
- set(key: string, value: any): void;
- set(config: Record): void;
-}
-
-/**
- * Representation of a legacy configuration deprecation factory used for
- * legacy plugin deprecations.
- *
- * @internal
- */
-export interface LegacyConfigDeprecationFactory {
- rename(oldKey: string, newKey: string): LegacyConfigDeprecation;
- unused(unusedKey: string): LegacyConfigDeprecation;
-}
-
-/**
- * Representation of a legacy configuration deprecation.
- *
- * @internal
- */
-export type LegacyConfigDeprecation = (
- settings: Record,
- log: (msg: string) => void
-) => void;
-
-/**
- * Representation of a legacy configuration deprecation provider.
- *
- * @internal
- */
-export type LegacyConfigDeprecationProvider = (
- factory: LegacyConfigDeprecationFactory
-) => LegacyConfigDeprecation[] | Promise;
diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts
index 10686fc521d35..208e9b1167253 100644
--- a/src/core/server/legacy/index.ts
+++ b/src/core/server/legacy/index.ts
@@ -18,6 +18,10 @@
*/
/** @internal */
-export { LegacyObjectToConfigAdapter, ensureValidConfiguration, LegacyConfig } from './config';
+export { LegacyObjectToConfigAdapter, ensureValidConfiguration } from './config';
/** @internal */
-export { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy_service';
+export { LegacyInternals } from './legacy_internals';
+/** @internal */
+export { LegacyService, ILegacyService } from './legacy_service';
+/** @internal */
+export * from './types';
diff --git a/src/core/server/legacy/legacy_internals.test.ts b/src/core/server/legacy/legacy_internals.test.ts
new file mode 100644
index 0000000000000..dcab62627442b
--- /dev/null
+++ b/src/core/server/legacy/legacy_internals.test.ts
@@ -0,0 +1,211 @@
+/*
+ * 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 { Server } from 'hapi';
+
+import { configMock } from '../config/config.mock';
+import { httpServiceMock } from '../http/http_service.mock';
+import { httpServerMock } from '../http/http_server.mocks';
+import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks';
+import { LegacyInternals } from './legacy_internals';
+import { ILegacyInternals, LegacyConfig, LegacyVars, LegacyUiExports } from './types';
+
+function varsProvider(vars: LegacyVars, configValue?: any) {
+ return {
+ fn: jest.fn().mockReturnValue(vars),
+ pluginSpec: {
+ readConfigValue: jest.fn().mockReturnValue(configValue),
+ },
+ };
+}
+
+describe('LegacyInternals', () => {
+ describe('getInjectedUiAppVars()', () => {
+ let uiExports: LegacyUiExports;
+ let config: LegacyConfig;
+ let server: Server;
+ let legacyInternals: ILegacyInternals;
+
+ beforeEach(async () => {
+ uiExports = findLegacyPluginSpecsMock().uiExports;
+ config = configMock.create() as any;
+ server = httpServiceMock.createSetupContract().server;
+ legacyInternals = new LegacyInternals(uiExports, config, server);
+ });
+
+ it('gets with no injectors', async () => {
+ await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(
+ `Object {}`
+ );
+ });
+
+ it('gets with no matching injectors', async () => {
+ const injector = jest.fn().mockResolvedValue({ not: 'core' });
+ legacyInternals.injectUiAppVars('not-core', injector);
+
+ await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(
+ `Object {}`
+ );
+ expect(injector).not.toHaveBeenCalled();
+ });
+
+ it('gets with single matching injector', async () => {
+ const injector = jest.fn().mockResolvedValue({ is: 'core' });
+ legacyInternals.injectUiAppVars('core', injector);
+
+ await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(`
+ Object {
+ "is": "core",
+ }
+ `);
+ expect(injector).toHaveBeenCalled();
+ });
+
+ it('gets with multiple matching injectors', async () => {
+ const injectors = [
+ jest.fn().mockResolvedValue({ is: 'core' }),
+ jest.fn().mockReturnValue({ sync: 'injector' }),
+ jest.fn().mockResolvedValue({ is: 'merged-core' }),
+ ];
+
+ injectors.forEach(injector => legacyInternals.injectUiAppVars('core', injector));
+
+ await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(`
+ Object {
+ "is": "merged-core",
+ "sync": "injector",
+ }
+ `);
+ expect(injectors[0]).toHaveBeenCalled();
+ expect(injectors[1]).toHaveBeenCalled();
+ expect(injectors[2]).toHaveBeenCalled();
+ });
+ });
+
+ describe('getVars()', () => {
+ let uiExports: LegacyUiExports;
+ let config: LegacyConfig;
+ let server: Server;
+ let legacyInternals: LegacyInternals;
+
+ beforeEach(async () => {
+ uiExports = findLegacyPluginSpecsMock().uiExports;
+ config = configMock.create() as any;
+ server = httpServiceMock.createSetupContract().server;
+ legacyInternals = new LegacyInternals(uiExports, config, server);
+ });
+
+ it('gets: no default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => {
+ const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest());
+
+ expect(vars).toMatchInlineSnapshot(`Object {}`);
+ });
+
+ it('gets: with default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => {
+ uiExports.defaultInjectedVarProviders = [
+ varsProvider({ alpha: 'alpha' }),
+ varsProvider({ gamma: 'gamma' }),
+ varsProvider({ alpha: 'beta' }),
+ ];
+
+ const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest());
+
+ expect(vars).toMatchInlineSnapshot(`
+ Object {
+ "alpha": "beta",
+ "gamma": "gamma",
+ }
+ `);
+ });
+
+ it('gets: no default injectors, with injected vars replacers, with ui app injectors, no inject arg', async () => {
+ uiExports.injectedVarsReplacers = [
+ jest.fn(async vars => ({ ...vars, added: 'key' })),
+ jest.fn(vars => vars),
+ jest.fn(vars => ({ replaced: 'all' })),
+ jest.fn(async vars => ({ ...vars, added: 'last-key' })),
+ ];
+
+ const request = httpServerMock.createRawRequest();
+ const vars = await legacyInternals.getVars('core', request);
+
+ expect(vars).toMatchInlineSnapshot(`
+ Object {
+ "added": "last-key",
+ "replaced": "all",
+ }
+ `);
+ });
+
+ it('gets: no default injectors, no injected vars replacers, with ui app injectors, no inject arg', async () => {
+ legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' }));
+ legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' }));
+ legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' }));
+
+ const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest());
+
+ expect(vars).toMatchInlineSnapshot(`
+ Object {
+ "is": "merged-core",
+ "sync": "injector",
+ }
+ `);
+ });
+
+ it('gets: no default injectors, no injected vars replacers, no ui app injectors, with inject arg', async () => {
+ const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), {
+ injected: 'arg',
+ });
+
+ expect(vars).toMatchInlineSnapshot(`
+ Object {
+ "injected": "arg",
+ }
+ `);
+ });
+
+ it('gets: with default injectors, with injected vars replacers, with ui app injectors, with inject arg', async () => {
+ uiExports.defaultInjectedVarProviders = [
+ varsProvider({ alpha: 'alpha' }),
+ varsProvider({ gamma: 'gamma' }),
+ varsProvider({ alpha: 'beta' }),
+ ];
+ uiExports.injectedVarsReplacers = [jest.fn(async vars => ({ ...vars, gamma: 'delta' }))];
+
+ legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' }));
+ legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' }));
+ legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' }));
+
+ const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), {
+ injected: 'arg',
+ sync: 'arg',
+ });
+
+ expect(vars).toMatchInlineSnapshot(`
+ Object {
+ "alpha": "beta",
+ "gamma": "delta",
+ "injected": "arg",
+ "is": "merged-core",
+ "sync": "arg",
+ }
+ `);
+ });
+ });
+});
diff --git a/src/core/server/legacy/legacy_internals.ts b/src/core/server/legacy/legacy_internals.ts
new file mode 100644
index 0000000000000..3bf54e5f75dce
--- /dev/null
+++ b/src/core/server/legacy/legacy_internals.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 { Server } from 'hapi';
+
+import { LegacyRequest } from '../http';
+import { mergeVars } from './merge_vars';
+import { ILegacyInternals, LegacyVars, VarsInjector, LegacyConfig, LegacyUiExports } from './types';
+
+/**
+ * @internal
+ * @deprecated
+ */
+export class LegacyInternals implements ILegacyInternals {
+ private readonly injectors = new Map>();
+ private cachedDefaultVars?: LegacyVars;
+
+ constructor(
+ private readonly uiExports: LegacyUiExports,
+ private readonly config: LegacyConfig,
+ private readonly server: Server
+ ) {}
+
+ private get defaultVars(): LegacyVars {
+ if (this.cachedDefaultVars) {
+ return this.cachedDefaultVars;
+ }
+
+ const { defaultInjectedVarProviders = [] } = this.uiExports;
+
+ return (this.cachedDefaultVars = defaultInjectedVarProviders.reduce(
+ (vars, { fn, pluginSpec }) =>
+ mergeVars(vars, fn(this.server, pluginSpec.readConfigValue(this.config, []))),
+ {}
+ ));
+ }
+
+ private replaceVars(vars: LegacyVars, request: LegacyRequest) {
+ const { injectedVarsReplacers = [] } = this.uiExports;
+
+ return injectedVarsReplacers.reduce(
+ async (injected, replacer) => replacer(await injected, request, this.server),
+ Promise.resolve(vars)
+ );
+ }
+
+ public injectUiAppVars(id: string, injector: VarsInjector) {
+ if (!this.injectors.has(id)) {
+ this.injectors.set(id, new Set());
+ }
+
+ this.injectors.get(id)!.add(injector);
+ }
+
+ public getInjectedUiAppVars(id: string) {
+ return [...(this.injectors.get(id) || [])].reduce(
+ async (promise, injector) => ({
+ ...(await promise),
+ ...(await injector()),
+ }),
+ Promise.resolve({})
+ );
+ }
+
+ public async getVars(id: string, request: LegacyRequest, injected: LegacyVars = {}) {
+ return this.replaceVars(
+ mergeVars(this.defaultVars, await this.getInjectedUiAppVars(id), injected),
+ request
+ );
+ }
+}
diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts
index ac0319cdf4eb5..495141cdcb58d 100644
--- a/src/core/server/legacy/legacy_service.mock.ts
+++ b/src/core/server/legacy/legacy_service.mock.ts
@@ -17,23 +17,33 @@
* under the License.
*/
-import { LegacyServiceDiscoverPlugins } from './legacy_service';
+import { LegacyService } from './legacy_service';
+import { LegacyServiceDiscoverPlugins, LegacyServiceSetupDeps } from './types';
-const createDiscoverMock = () => {
- const setupContract: DeeplyMockedKeys = {
- pluginSpecs: [],
- disabledPluginSpecs: [],
- uiExports: {} as any,
- settings: {},
- pluginExtendedConfig: {
- get: jest.fn(),
- has: jest.fn(),
- set: jest.fn(),
- } as any,
- };
- return setupContract;
-};
+type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>;
+
+const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({
+ pluginSpecs: [],
+ uiExports: {} as any,
+ navLinks: [],
+ pluginExtendedConfig: {
+ get: jest.fn(),
+ has: jest.fn(),
+ set: jest.fn(),
+ },
+ disabledPluginSpecs: [],
+ settings: {},
+});
+const createLegacyServiceMock = (): LegacyServiceMock => ({
+ legacyId: Symbol(),
+ discoverPlugins: jest.fn().mockResolvedValue(createDiscoverPluginsMock()),
+ setup: jest.fn(),
+ start: jest.fn(),
+ stop: jest.fn(),
+});
export const legacyServiceMock = {
- createDiscover: createDiscoverMock,
+ create: createLegacyServiceMock,
+ createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps),
+ createDiscoverPlugins: createDiscoverPluginsMock,
};
diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts
index e8d4a0ed0bd4d..451a75ced7ae2 100644
--- a/src/core/server/legacy/legacy_service.test.mocks.ts
+++ b/src/core/server/legacy/legacy_service.test.mocks.ts
@@ -17,18 +17,19 @@
* under the License.
*/
-export const findLegacyPluginSpecsMock = jest
- .fn()
- .mockImplementation((settings: Record) => ({
- pluginSpecs: [],
- pluginExtendedConfig: {
- has: jest.fn(),
- get: jest.fn(() => settings),
- set: jest.fn(),
- },
- disabledPluginSpecs: [],
- uiExports: [],
- }));
+import { LegacyVars } from './types';
+
+export const findLegacyPluginSpecsMock = jest.fn().mockImplementation((settings: LegacyVars) => ({
+ pluginSpecs: [],
+ pluginExtendedConfig: {
+ has: jest.fn(),
+ get: jest.fn().mockReturnValue(settings),
+ set: jest.fn(),
+ },
+ disabledPluginSpecs: [],
+ uiExports: {},
+ navLinks: [],
+}));
jest.doMock('./plugins/find_legacy_plugin_specs.ts', () => ({
findLegacyPluginSpecs: findLegacyPluginSpecsMock,
}));
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index c652bb1c94887..608392e4943f9 100644
--- a/src/core/server/legacy/legacy_service.test.ts
+++ b/src/core/server/legacy/legacy_service.test.ts
@@ -25,7 +25,7 @@ jest.mock('./config/legacy_deprecation_adapters', () => ({
import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks';
import { BehaviorSubject, throwError } from 'rxjs';
-import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.';
+
// @ts-ignore: implicit any for JS file
import { ClusterManager as MockClusterManager } from '../../../cli/cluster/cluster_manager';
import KbnServer from '../../../legacy/server/kbn_server';
@@ -33,7 +33,6 @@ import { Config, Env, ObjectToConfigAdapter } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { BasePathProxyServer } from '../http';
import { DiscoveredPlugin } from '../plugins';
-import { findLegacyPluginSpecs } from './plugins/find_legacy_plugin_specs';
import { configServiceMock } from '../config/config_service.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
@@ -42,7 +41,11 @@ import { httpServiceMock } from '../http/http_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock';
+import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service';
import { uuidServiceMock } from '../uuid/uuid_service.mock';
+import { findLegacyPluginSpecs } from './plugins';
+import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types';
+import { LegacyService } from './legacy_service';
const MockKbnServer: jest.Mock = KbnServer as any;
@@ -89,6 +92,7 @@ beforeEach(() => {
browserConfigs: new Map(),
},
},
+ rendering: renderingServiceMock,
uuid: uuidSetup,
},
plugins: { 'plugin-id': 'plugin-value' },
@@ -138,7 +142,7 @@ describe('once LegacyService is set up with connection info', () => {
{ path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value
expect.objectContaining({ get: expect.any(Function) }),
expect.any(Object),
- { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] }
+ { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] }
);
expect(MockKbnServer.mock.calls[0][1].get()).toEqual({
path: { autoListen: true },
@@ -168,7 +172,7 @@ describe('once LegacyService is set up with connection info', () => {
{ path: { autoListen: false }, server: { autoListen: true } },
expect.objectContaining({ get: expect.any(Function) }),
expect.any(Object),
- { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] }
+ { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] }
);
expect(MockKbnServer.mock.calls[0][1].get()).toEqual({
path: { autoListen: false },
@@ -309,7 +313,7 @@ describe('once LegacyService is set up without connection info', () => {
{ path: {}, server: { autoListen: true } },
expect.objectContaining({ get: expect.any(Function) }),
expect.any(Object),
- { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] }
+ { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] }
);
expect(MockKbnServer.mock.calls[0][1].get()).toEqual({
path: {},
@@ -395,16 +399,18 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
});
});
-test('Cannot start without setup phase', async () => {
- const legacyService = new LegacyService({
- coreId,
- env,
- logger,
- configService: configService as any,
+describe('start', () => {
+ test('Cannot start without setup phase', async () => {
+ const legacyService = new LegacyService({
+ coreId,
+ env,
+ logger,
+ configService: configService as any,
+ });
+ await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Legacy service is not setup yet."`
+ );
});
- await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
- `"Legacy service is not setup yet."`
- );
});
describe('#discoverPlugins()', () => {
@@ -438,7 +444,8 @@ describe('#discoverPlugins()', () => {
],
pluginExtendedConfig: settings,
disabledPluginSpecs: [],
- uiExports: [],
+ uiExports: {},
+ navLinks: [],
}) as any
);
@@ -469,15 +476,16 @@ test('Sets the server.uuid property on the legacy configuration', async () => {
const configSetMock = jest.fn();
- findLegacyPluginSpecsMock.mockImplementation((settings: Record) => ({
+ findLegacyPluginSpecsMock.mockImplementation((settings: LegacyVars) => ({
pluginSpecs: [],
pluginExtendedConfig: {
has: jest.fn(),
- get: jest.fn(() => settings),
+ get: jest.fn().mockReturnValue(settings),
set: configSetMock,
},
disabledPluginSpecs: [],
- uiExports: [],
+ uiExports: {},
+ navLinks: [],
}));
await legacyService.discoverPlugins();
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index 2e8a467eff995..2ed87f4c6d488 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -19,24 +19,30 @@
import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs';
import { first, map, publishReplay, tap } from 'rxjs/operators';
+
import { CoreService } from '../../types';
-import { CoreSetup, CoreStart } from '../';
-import { InternalCoreSetup, InternalCoreStart } from '../internal_types';
-import { SavedObjectsLegacyUiExports } from '../types';
import { Config, ConfigDeprecationProvider } from '../config';
import { CoreContext } from '../core_context';
import { CspConfigType, config as cspConfig } from '../csp';
import { DevConfig, DevConfigType, config as devConfig } from '../dev';
import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http';
import { Logger } from '../logging';
-import { PluginsServiceSetup, PluginsServiceStart } from '../plugins';
-import { findLegacyPluginSpecs } from './plugins';
-import { LegacyPluginSpec } from './plugins/find_legacy_plugin_specs';
import { PathConfigType } from '../path';
-import { LegacyConfig, convertLegacyDeprecationProvider } from './config';
+import { findLegacyPluginSpecs } from './plugins';
+import { convertLegacyDeprecationProvider } from './config';
+import {
+ LegacyServiceSetupDeps,
+ LegacyServiceStartDeps,
+ LegacyPlugins,
+ LegacyServiceDiscoverPlugins,
+ LegacyConfig,
+ LegacyVars,
+} from './types';
+import { LegacyInternals } from './legacy_internals';
+import { CoreSetup, CoreStart } from '..';
interface LegacyKbnServer {
- applyLoggingConfiguration: (settings: Readonly>) => void;
+ applyLoggingConfiguration: (settings: Readonly) => void;
listen: () => Promise;
ready: () => Promise;
close: () => Promise;
@@ -53,43 +59,14 @@ function getLegacyRawConfig(config: Config, pathConfig: PathConfigType) {
return {
...rawConfig,
- path: pathConfig, // We rely heavily in the default value of 'path.data' in the legacy world and, since it has been moved to NP, it won't show up in RawConfig
- };
-}
-
-/**
- * @public
- * @deprecated
- */
-export interface LegacyServiceSetupDeps {
- core: InternalCoreSetup & {
- plugins: PluginsServiceSetup;
- };
- plugins: Record;
-}
-
-/**
- * @public
- * @deprecated
- */
-export interface LegacyServiceStartDeps {
- core: InternalCoreStart & {
- plugins: PluginsServiceStart;
+ // We rely heavily in the default value of 'path.data' in the legacy world and,
+ // since it has been moved to NP, it won't show up in RawConfig.
+ path: pathConfig,
};
- plugins: Record;
}
/** @internal */
-export interface LegacyServiceDiscoverPlugins {
- pluginSpecs: LegacyPluginSpec[];
- disabledPluginSpecs: LegacyPluginSpec[];
- uiExports: SavedObjectsLegacyUiExports;
- pluginExtendedConfig: LegacyConfig;
- settings: Record;
-}
-
-/** @internal */
-export type ILegacyService = Pick;
+export type ILegacyService = PublicMethodsOf;
/** @internal */
export class LegacyService implements CoreService {
@@ -101,16 +78,10 @@ export class LegacyService implements CoreService {
private kbnServer?: LegacyKbnServer;
private configSubscription?: Subscription;
private setupDeps?: LegacyServiceSetupDeps;
- private update$: ConnectableObservable<[Config, PathConfigType]> | undefined;
- private legacyRawConfig: LegacyConfig | undefined;
- private legacyPlugins:
- | {
- pluginSpecs: LegacyPluginSpec[];
- disabledPluginSpecs: LegacyPluginSpec[];
- uiExports: SavedObjectsLegacyUiExports;
- }
- | undefined;
- private settings: Record | undefined;
+ private update$?: ConnectableObservable<[Config, PathConfigType]>;
+ private legacyRawConfig?: LegacyConfig;
+ private legacyPlugins?: LegacyPlugins;
+ private settings?: LegacyVars;
constructor(private readonly coreContext: CoreContext) {
const { logger, configService, env } = coreContext;
@@ -153,12 +124,14 @@ export class LegacyService implements CoreService {
pluginExtendedConfig,
disabledPluginSpecs,
uiExports,
+ navLinks,
} = await findLegacyPluginSpecs(this.settings, this.coreContext.logger);
this.legacyPlugins = {
pluginSpecs,
disabledPluginSpecs,
uiExports,
+ navLinks,
};
const deprecationProviders = await pluginSpecs
@@ -188,6 +161,7 @@ export class LegacyService implements CoreService {
pluginSpecs,
disabledPluginSpecs,
uiExports,
+ navLinks,
pluginExtendedConfig,
settings: this.settings,
};
@@ -195,35 +169,37 @@ export class LegacyService implements CoreService {
public async setup(setupDeps: LegacyServiceSetupDeps) {
this.log.debug('setting up legacy service');
- if (!this.legacyRawConfig || !this.legacyPlugins || !this.settings) {
+
+ if (!this.legacyPlugins) {
throw new Error(
'Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()'
);
}
- // propagate the instance uuid to the legacy config, as it was the legacy way to access it.
- this.legacyRawConfig.set('server.uuid', setupDeps.core.uuid.getInstanceUuid());
+ // propagate the instance uuid to the legacy config, as it was the legacy way to access it.
+ this.legacyRawConfig!.set('server.uuid', setupDeps.core.uuid.getInstanceUuid());
this.setupDeps = setupDeps;
}
public async start(startDeps: LegacyServiceStartDeps) {
const { setupDeps } = this;
- if (!setupDeps || !this.legacyRawConfig || !this.legacyPlugins || !this.settings) {
+
+ if (!setupDeps || !this.legacyPlugins) {
throw new Error('Legacy service is not setup yet.');
}
+
this.log.debug('starting legacy service');
// Receive initial config and create kbnServer/ClusterManager.
-
if (this.coreContext.env.isDevClusterMaster) {
- await this.createClusterManager(this.legacyRawConfig);
+ await this.createClusterManager(this.legacyRawConfig!);
} else {
this.kbnServer = await this.createKbnServer(
- this.settings,
- this.legacyRawConfig,
+ this.settings!,
+ this.legacyRawConfig!,
setupDeps,
startDeps,
- this.legacyPlugins
+ this.legacyPlugins!
);
}
}
@@ -263,15 +239,11 @@ export class LegacyService implements CoreService {
}
private async createKbnServer(
- settings: Record,
+ settings: LegacyVars,
config: LegacyConfig,
setupDeps: LegacyServiceSetupDeps,
startDeps: LegacyServiceStartDeps,
- legacyPlugins: {
- pluginSpecs: LegacyPluginSpec[];
- disabledPluginSpecs: LegacyPluginSpec[];
- uiExports: SavedObjectsLegacyUiExports;
- }
+ legacyPlugins: LegacyPlugins
) {
const coreSetup: CoreSetup = {
capabilities: setupDeps.core.capabilities,
@@ -338,8 +310,10 @@ export class LegacyService implements CoreService {
kibanaMigrator: startDeps.core.savedObjects.migrator,
uiPlugins: setupDeps.core.plugins.uiPlugins,
elasticsearch: setupDeps.core.elasticsearch,
+ rendering: setupDeps.core.rendering,
uiSettings: setupDeps.core.uiSettings,
savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider,
+ legacy: new LegacyInternals(legacyPlugins.uiExports, config, setupDeps.core.http.server),
},
logger: this.coreContext.logger,
},
diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts
index 011dfae8a5cef..6d82d929e7daa 100644
--- a/src/core/server/legacy/logging/appenders/legacy_appender.ts
+++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts
@@ -21,6 +21,7 @@ import { schema } from '@kbn/config-schema';
import { DisposableAppender } from '../../../logging/appenders/appenders';
import { LogRecord } from '../../../logging/log_record';
import { LegacyLoggingServer } from '../legacy_logging_server';
+import { LegacyVars } from '../../types';
/**
* Simple appender that just forwards `LogRecord` to the legacy KbnServer log.
@@ -34,7 +35,7 @@ export class LegacyAppender implements DisposableAppender {
private readonly loggingServer: LegacyLoggingServer;
- constructor(legacyLoggingConfig: Readonly>) {
+ constructor(legacyLoggingConfig: Readonly) {
this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig);
}
diff --git a/src/core/server/legacy/logging/legacy_logging_server.ts b/src/core/server/legacy/logging/legacy_logging_server.ts
index 57706bcac2232..85a8686b4eded 100644
--- a/src/core/server/legacy/logging/legacy_logging_server.ts
+++ b/src/core/server/legacy/logging/legacy_logging_server.ts
@@ -25,9 +25,10 @@ import { Config } from '../../../../legacy/server/config';
import { setupLogging } from '../../../../legacy/server/logging';
import { LogLevel } from '../../logging/log_level';
import { LogRecord } from '../../logging/log_record';
+import { LegacyVars } from '../../types';
export const metadataSymbol = Symbol('log message with metadata');
-export function attachMetaData(message: string, metadata: Record = {}) {
+export function attachMetaData(message: string, metadata: LegacyVars = {}) {
return {
[metadataSymbol]: {
message,
@@ -50,7 +51,7 @@ interface PluginRegisterParams {
options: PluginRegisterParams['options']
) => Promise;
};
- options: Record;
+ options: LegacyVars;
}
/**
@@ -84,7 +85,7 @@ export class LegacyLoggingServer {
private onPostStopCallback?: () => void;
- constructor(legacyLoggingConfig: Readonly>) {
+ constructor(legacyLoggingConfig: Readonly) {
// We set `ops.interval` to max allowed number and `ops` filter to value
// that doesn't exist to avoid logging of ops at all, if turned on it will be
// logged by the "legacy" Kibana.
diff --git a/src/legacy/ui/ui_render/lib/merge_variables.test.ts b/src/core/server/legacy/merge_vars.test.ts
similarity index 58%
rename from src/legacy/ui/ui_render/lib/merge_variables.test.ts
rename to src/core/server/legacy/merge_vars.test.ts
index 4d69216bc0bfd..d977ee292d039 100644
--- a/src/legacy/ui/ui_render/lib/merge_variables.test.ts
+++ b/src/core/server/legacy/merge_vars.test.ts
@@ -17,29 +17,26 @@
* under the License.
*/
-import { mergeVariables } from './merge_variables';
+import { mergeVars } from './merge_vars';
-describe('mergeVariables', () => {
+describe('mergeVars', () => {
it('merges two objects together', () => {
- const someVariables = {
- name: 'value',
- canFoo: true,
- nested: {
- anotherVariable: 'ok',
- },
- };
-
- const otherVariables = {
+ const first = {
otherName: 'value',
otherCanFoo: true,
otherNested: {
otherAnotherVariable: 'ok',
},
};
+ const second = {
+ name: 'value',
+ canFoo: true,
+ nested: {
+ anotherVariable: 'ok',
+ },
+ };
- const result = mergeVariables(someVariables, otherVariables);
-
- expect(result).toEqual({
+ expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
nested: {
@@ -54,86 +51,76 @@ describe('mergeVariables', () => {
});
it('does not mutate the source objects', () => {
- const original = {
- var1: 'original',
+ const first = {
+ var1: 'first',
};
-
- const set1 = {
- var1: 'value1',
- var2: 'value1',
+ const second = {
+ var1: 'second',
+ var2: 'second',
};
-
- const set2 = {
- var1: 'value2',
- var2: 'value2',
- var3: 'value2',
+ const third = {
+ var1: 'third',
+ var2: 'third',
+ var3: 'third',
};
-
- const set3 = {
- var1: 'value3',
- var2: 'value3',
- var3: 'value3',
- var4: 'value3',
+ const fourth = {
+ var1: 'fourth',
+ var2: 'fourth',
+ var3: 'fourth',
+ var4: 'fourth',
};
- mergeVariables(original, set1, set2, set3);
+ mergeVars(first, second, third, fourth);
- expect(original).toEqual({ var1: 'original' });
- expect(set1).toEqual({ var1: 'value1', var2: 'value1' });
- expect(set2).toEqual({ var1: 'value2', var2: 'value2', var3: 'value2' });
- expect(set3).toEqual({ var1: 'value3', var2: 'value3', var3: 'value3', var4: 'value3' });
+ expect(first).toEqual({ var1: 'first' });
+ expect(second).toEqual({ var1: 'second', var2: 'second' });
+ expect(third).toEqual({ var1: 'third', var2: 'third', var3: 'third' });
+ expect(fourth).toEqual({ var1: 'fourth', var2: 'fourth', var3: 'fourth', var4: 'fourth' });
});
- it('merges multiple objects together, preferring the leftmost values', () => {
- const original = {
- var1: 'original',
+ it('merges multiple objects together with precedence increasing from left-to-right', () => {
+ const first = {
+ var1: 'first',
+ var2: 'first',
+ var3: 'first',
+ var4: 'first',
};
-
- const set1 = {
- var1: 'value1',
- var2: 'value1',
+ const second = {
+ var1: 'second',
+ var2: 'second',
+ var3: 'second',
};
-
- const set2 = {
- var1: 'value2',
- var2: 'value2',
- var3: 'value2',
+ const third = {
+ var1: 'third',
+ var2: 'third',
};
-
- const set3 = {
- var1: 'value3',
- var2: 'value3',
- var3: 'value3',
- var4: 'value3',
+ const fourth = {
+ var1: 'fourth',
};
- const result = mergeVariables(original, set1, set2, set3);
-
- expect(result).toEqual({
- var1: 'original',
- var2: 'value1',
- var3: 'value2',
- var4: 'value3',
+ expect(mergeVars(first, second, third, fourth)).toEqual({
+ var1: 'fourth',
+ var2: 'third',
+ var3: 'second',
+ var4: 'first',
});
});
- it('retains the original variable value if a duplicate entry is found', () => {
- const someVariables = {
- name: 'value',
- canFoo: true,
+ it('overwrites the original variable value if a duplicate entry is found', () => {
+ const first = {
nested: {
- anotherVariable: 'ok',
+ otherAnotherVariable: 'ok',
},
};
-
- const otherVariables = {
+ const second = {
+ name: 'value',
+ canFoo: true,
nested: {
- otherAnotherVariable: 'ok',
+ anotherVariable: 'ok',
},
};
- const result = mergeVariables(someVariables, otherVariables);
- expect(result).toEqual({
+ expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
nested: {
@@ -143,55 +130,61 @@ describe('mergeVariables', () => {
});
it('combines entries within "uiCapabilities"', () => {
- const someVariables = {
- name: 'value',
- canFoo: true,
+ const first = {
uiCapabilities: {
firstCapability: 'ok',
+ sharedCapability: 'shared',
},
};
-
- const otherVariables = {
+ const second = {
+ name: 'value',
+ canFoo: true,
uiCapabilities: {
secondCapability: 'ok',
},
};
+ const third = {
+ name: 'value',
+ canFoo: true,
+ uiCapabilities: {
+ thirdCapability: 'ok',
+ sharedCapability: 'blocked',
+ },
+ };
- const result = mergeVariables(someVariables, otherVariables);
-
- expect(result).toEqual({
+ expect(mergeVars(first, second, third)).toEqual({
name: 'value',
canFoo: true,
uiCapabilities: {
firstCapability: 'ok',
secondCapability: 'ok',
+ thirdCapability: 'ok',
+ sharedCapability: 'blocked',
},
});
});
it('does not deeply combine entries within "uiCapabilities"', () => {
- const someVariables = {
- name: 'value',
- canFoo: true,
+ const first = {
uiCapabilities: {
firstCapability: 'ok',
nestedCapability: {
- nestedProp: 'nestedValue',
+ otherNestedProp: 'otherNestedValue',
},
},
};
-
- const otherVariables = {
+ const second = {
+ name: 'value',
+ canFoo: true,
uiCapabilities: {
secondCapability: 'ok',
nestedCapability: {
- otherNestedProp: 'otherNestedValue',
+ nestedProp: 'nestedValue',
},
},
};
- const result = mergeVariables(someVariables, otherVariables);
- expect(result).toEqual({
+ expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
uiCapabilities: {
diff --git a/src/legacy/ui/ui_render/lib/merge_variables.ts b/src/core/server/legacy/merge_vars.ts
similarity index 65%
rename from src/legacy/ui/ui_render/lib/merge_variables.ts
rename to src/core/server/legacy/merge_vars.ts
index 0f65c7825bdba..a1d43af2f861d 100644
--- a/src/legacy/ui/ui_render/lib/merge_variables.ts
+++ b/src/core/server/legacy/merge_vars.ts
@@ -17,23 +17,18 @@
* under the License.
*/
-const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities'];
-
-export function mergeVariables(...sources: Array>) {
- const result: Record = {};
+import { LegacyVars } from './types';
- for (const source of sources) {
- Object.entries(source).forEach(([key, value]) => {
- if (ELIGIBLE_FLAT_MERGE_KEYS.includes(key)) {
- result[key] = {
- ...value,
- ...result[key],
- };
- } else if (!result.hasOwnProperty(key)) {
- result[key] = value;
- }
- });
- }
+const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities'];
- return result;
+export function mergeVars(...sources: LegacyVars[]): LegacyVars {
+ return Object.assign(
+ {},
+ ...sources,
+ ...ELIGIBLE_FLAT_MERGE_KEYS.flatMap(key =>
+ sources.some(source => key in source)
+ ? [{ [key]: Object.assign({}, ...sources.map(source => source[key] || {})) }]
+ : []
+ )
+ );
}
diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
index 0a49154801e56..d2e7a39236d0a 100644
--- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
+++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
@@ -19,25 +19,77 @@
import { Observable, merge, forkJoin } from 'rxjs';
import { toArray, tap, distinct, map } from 'rxjs/operators';
+
import {
findPluginSpecs,
defaultConfig,
// @ts-ignore
} from '../../../../legacy/plugin_discovery/find_plugin_specs.js';
-import { LoggerFactory } from '../../logging';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports';
-import { LegacyConfig, LegacyConfigDeprecationProvider } from '../config';
-export interface LegacyPluginPack {
- getPath(): string;
+import { LoggerFactory } from '../../logging';
+import {
+ LegacyUiExports,
+ LegacyNavLink,
+ LegacyPluginSpec,
+ LegacyPluginPack,
+ LegacyConfig,
+} from '../types';
+
+const REMOVE_FROM_ARRAY: LegacyNavLink[] = [];
+
+function getUiAppsNavLinks({ uiAppSpecs = [] }: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) {
+ return uiAppSpecs.flatMap(spec => {
+ if (!spec) {
+ return REMOVE_FROM_ARRAY;
+ }
+
+ const id = spec.pluginId || spec.id;
+
+ if (!id) {
+ throw new Error('Every app must specify an id');
+ }
+
+ if (spec.pluginId && !pluginSpecs.some(plugin => plugin.getId() === spec.pluginId)) {
+ throw new Error(`Unknown plugin id "${spec.pluginId}"`);
+ }
+
+ const listed = typeof spec.listed === 'boolean' ? spec.listed : true;
+
+ if (spec.hidden || !listed) {
+ return REMOVE_FROM_ARRAY;
+ }
+
+ return {
+ id,
+ title: spec.title,
+ order: typeof spec.order === 'number' ? spec.order : 0,
+ icon: spec.icon,
+ euiIconType: spec.euiIconType,
+ url: spec.url || `/app/${id}`,
+ linkToLastSubUrl: spec.linkToLastSubUrl,
+ };
+ });
}
-export interface LegacyPluginSpec {
- getId: () => unknown;
- getExpectedKibanaVersion: () => string;
- getConfigPrefix: () => string;
- getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined;
+function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) {
+ return (uiExports.navLinkSpecs || [])
+ .map(spec => ({
+ id: spec.id,
+ title: spec.title,
+ order: typeof spec.order === 'number' ? spec.order : 0,
+ url: spec.url,
+ subUrlBase: spec.subUrlBase || spec.url,
+ icon: spec.icon,
+ euiIconType: spec.euiIconType,
+ linkToLastSub: 'linkToLastSubUrl' in spec ? spec.linkToLastSubUrl : false,
+ hidden: 'hidden' in spec ? spec.hidden : false,
+ disabled: 'disabled' in spec ? spec.disabled : false,
+ tooltip: spec.tooltip || '',
+ }))
+ .concat(getUiAppsNavLinks(uiExports, pluginSpecs))
+ .sort((a, b) => a.order - b.order);
}
export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) {
@@ -128,11 +180,14 @@ export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: Lo
spec$.pipe(toArray()),
log$.pipe(toArray())
).toPromise();
+ const uiExports = collectLegacyUiExports(pluginSpecs);
+ const navLinks = getNavLinks(uiExports, pluginSpecs);
return {
disabledPluginSpecs,
pluginSpecs,
pluginExtendedConfig: configToMutate,
- uiExports: collectLegacyUiExports(pluginSpecs),
+ uiExports,
+ navLinks,
};
}
diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts
index 7c69546f0c4de..a6d55e1da7839 100644
--- a/src/core/server/legacy/plugins/index.ts
+++ b/src/core/server/legacy/plugins/index.ts
@@ -16,4 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
+
export { findLegacyPluginSpecs } from './find_legacy_plugin_specs';
diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts
new file mode 100644
index 0000000000000..6ec893be9b310
--- /dev/null
+++ b/src/core/server/legacy/types.ts
@@ -0,0 +1,222 @@
+/*
+ * 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 { Server } from 'hapi';
+
+import { ChromeNavLink } from '../../public';
+import { LegacyRequest } from '../http';
+import { InternalCoreSetup, InternalCoreStart } from '../internal_types';
+import { PluginsServiceSetup, PluginsServiceStart } from '../plugins';
+import { RenderingServiceSetup } from '../rendering';
+import { SavedObjectsLegacyUiExports } from '../types';
+
+/**
+ * @internal
+ * @deprecated
+ */
+export type LegacyVars = Record;
+
+type LegacyCoreSetup = InternalCoreSetup & {
+ plugins: PluginsServiceSetup;
+ rendering: RenderingServiceSetup;
+};
+type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart };
+
+/**
+ * New platform representation of the legacy configuration (KibanaConfig)
+ *
+ * @internal
+ * @deprecated
+ */
+export interface LegacyConfig {
+ get(key?: string): T;
+ has(key: string): boolean;
+ set(key: string, value: any): void;
+ set(config: LegacyVars): void;
+}
+
+/**
+ * Representation of a legacy configuration deprecation factory used for
+ * legacy plugin deprecations.
+ *
+ * @internal
+ * @deprecated
+ */
+export interface LegacyConfigDeprecationFactory {
+ rename(oldKey: string, newKey: string): LegacyConfigDeprecation;
+ unused(unusedKey: string): LegacyConfigDeprecation;
+}
+
+/**
+ * Representation of a legacy configuration deprecation.
+ *
+ * @internal
+ * @deprecated
+ */
+export type LegacyConfigDeprecation = (settings: LegacyVars, log: (msg: string) => void) => void;
+
+/**
+ * Representation of a legacy configuration deprecation provider.
+ *
+ * @internal
+ * @deprecated
+ */
+export type LegacyConfigDeprecationProvider = (
+ factory: LegacyConfigDeprecationFactory
+) => LegacyConfigDeprecation[] | Promise;
+
+/**
+ * @internal
+ * @deprecated
+ */
+export interface LegacyPluginPack {
+ getPath(): string;
+}
+
+/**
+ * @internal
+ * @deprecated
+ */
+export interface LegacyPluginSpec {
+ getId: () => unknown;
+ getExpectedKibanaVersion: () => string;
+ getConfigPrefix: () => string;
+ getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined;
+}
+
+/**
+ * @internal
+ * @deprecated
+ */
+export interface VarsProvider {
+ fn: (server: Server, configValue: any) => LegacyVars;
+ pluginSpec: {
+ readConfigValue(config: any, key: string | string[]): any;
+ };
+}
+
+/**
+ * @internal
+ * @deprecated
+ */
+export type VarsInjector = () => LegacyVars;
+
+/**
+ * @internal
+ * @deprecated
+ */
+export type VarsReplacer = (
+ vars: LegacyVars,
+ request: LegacyRequest,
+ server: Server
+) => LegacyVars | Promise;
+
+/**
+ * @internal
+ * @deprecated
+ */
+export type LegacyNavLinkSpec = Record & ChromeNavLink;
+
+/**
+ * @internal
+ * @deprecated
+ */
+export type LegacyAppSpec = Pick<
+ ChromeNavLink,
+ 'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden'
+> & { pluginId?: string; id?: string; listed?: boolean };
+
+/**
+ * @internal
+ * @deprecated
+ */
+export type LegacyNavLink = Omit & {
+ order: number;
+};
+
+/**
+ * @internal
+ * @deprecated
+ */
+export type LegacyUiExports = SavedObjectsLegacyUiExports & {
+ defaultInjectedVarProviders?: VarsProvider[];
+ injectedVarsReplacers?: VarsReplacer[];
+ navLinkSpecs?: LegacyNavLinkSpec[] | null;
+ uiAppSpecs?: Array;
+ unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown }];
+};
+
+/**
+ * @public
+ * @deprecated
+ */
+export interface LegacyServiceSetupDeps {
+ core: LegacyCoreSetup;
+ plugins: Record;
+}
+
+/**
+ * @public
+ * @deprecated
+ */
+export interface LegacyServiceStartDeps {
+ core: LegacyCoreStart;
+ plugins: Record;
+}
+
+/**
+ * @internal
+ * @deprecated
+ */
+export interface ILegacyInternals {
+ /**
+ * Inject UI app vars for a particular plugin
+ */
+ injectUiAppVars(id: string, injector: VarsInjector): void;
+
+ /**
+ * Get all the merged injected UI app vars for a particular plugin
+ */
+ getInjectedUiAppVars(id: string): Promise;
+
+ /**
+ * Get the metadata vars for a particular plugin
+ */
+ getVars(id: string, request: LegacyRequest, injected?: LegacyVars): Promise;
+}
+
+/**
+ * @internal
+ * @deprecated
+ */
+export interface LegacyPlugins {
+ disabledPluginSpecs: LegacyPluginSpec[];
+ pluginSpecs: LegacyPluginSpec[];
+ uiExports: LegacyUiExports;
+ navLinks: LegacyNavLink[];
+}
+
+/**
+ * @internal
+ * @deprecated
+ */
+export interface LegacyServiceDiscoverPlugins extends LegacyPlugins {
+ pluginExtendedConfig: LegacyConfig;
+ settings: LegacyVars;
+}
diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts
index 8d3c6a8c909a2..5a52ebccbd472 100644
--- a/src/core/server/plugins/plugins_service.mock.ts
+++ b/src/core/server/plugins/plugins_service.mock.ts
@@ -17,28 +17,28 @@
* under the License.
*/
-import { PluginsService } from './plugins_service';
+import { PluginsService, PluginsServiceSetup } from './plugins_service';
-type ServiceContract = PublicMethodsOf;
-const createServiceMock = () => {
- const mocked: jest.Mocked = {
- discover: jest.fn(),
- setup: jest.fn(),
- start: jest.fn(),
- stop: jest.fn(),
- };
- mocked.setup.mockResolvedValue({
- contracts: new Map(),
- uiPlugins: {
- browserConfigs: new Map(),
- internal: new Map(),
- public: new Map(),
- },
- });
- mocked.start.mockResolvedValue({ contracts: new Map() });
- return mocked;
-};
+type PluginsServiceMock = jest.Mocked>;
+
+const createSetupContractMock = (): PluginsServiceSetup => ({
+ contracts: new Map(),
+ uiPlugins: {
+ browserConfigs: new Map(),
+ internal: new Map(),
+ public: new Map(),
+ },
+});
+const createStartContractMock = () => ({ contracts: new Map() });
+const createServiceMock = (): PluginsServiceMock => ({
+ discover: jest.fn(),
+ setup: jest.fn().mockResolvedValue(createSetupContractMock()),
+ start: jest.fn().mockResolvedValue(createStartContractMock()),
+ stop: jest.fn(),
+});
export const pluginServiceMock = {
create: createServiceMock,
+ createSetupContract: createSetupContractMock,
+ createStartContract: createStartContractMock,
};
diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts
new file mode 100644
index 0000000000000..392b2f0c5e2a4
--- /dev/null
+++ b/src/core/server/rendering/__mocks__/params.ts
@@ -0,0 +1,35 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { mockCoreContext } from '../../core_context.mock';
+import { httpServiceMock } from '../../http/http_service.mock';
+import { pluginServiceMock } from '../../plugins/plugins_service.mock';
+import { legacyServiceMock } from '../../legacy/legacy_service.mock';
+
+const context = mockCoreContext.create();
+const http = httpServiceMock.createSetupContract();
+const plugins = pluginServiceMock.createSetupContract();
+const legacyPlugins = legacyServiceMock.createDiscoverPlugins();
+
+export const mockRenderingServiceParams = context;
+export const mockRenderingSetupDeps = {
+ http,
+ legacyPlugins,
+ plugins,
+};
diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts
new file mode 100644
index 0000000000000..33dca7cc0d30e
--- /dev/null
+++ b/src/core/server/rendering/__mocks__/rendering_service.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { RenderingService as Service } from '../rendering_service';
+import { RenderingServiceSetup } from '../types';
+import { mockRenderingServiceParams } from './params';
+
+type IRenderingService = PublicMethodsOf;
+
+export const setupMock: jest.Mocked = {
+ render: jest.fn(),
+};
+export const mockSetup = jest.fn().mockResolvedValue(setupMock);
+export const mockStart = jest.fn();
+export const mockStop = jest.fn();
+export const mockRenderingService: jest.Mocked = {
+ setup: mockSetup,
+ start: mockStart,
+ stop: mockStop,
+};
+export const RenderingService = jest.fn(
+ () => mockRenderingService
+);
diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap
new file mode 100644
index 0000000000000..edde1dee85f4f
--- /dev/null
+++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap
@@ -0,0 +1,719 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RenderingService setup() render() renders "core" from legacy request 1`] = `
+Object {
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNumber": Any,
+ "csp": Object {
+ "warnLegacyBrowsers": true,
+ },
+ "env": Object {
+ "binDir": Any,
+ "cliArgs": Object {
+ "basePath": false,
+ "dev": true,
+ "open": false,
+ "optimize": false,
+ "oss": false,
+ "quiet": false,
+ "repl": false,
+ "silent": false,
+ "watch": false,
+ },
+ "configDir": Any,
+ "configs": Array [],
+ "homeDir": Any,
+ "isDevClusterMaster": false,
+ "logDir": Any,
+ "mode": Object {
+ "dev": true,
+ "name": "development",
+ "prod": false,
+ },
+ "packageInfo": Object {
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "dist": false,
+ "version": Any,
+ },
+ "pluginSearchPaths": Any,
+ "staticFilesDir": Any,
+ },
+ "i18n": Object {
+ "translationsUrl": "/mock-server-basepath/translations/en.json",
+ },
+ "legacyMetadata": Object {
+ "app": Object {},
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "bundleId": "app:core",
+ "devMode": true,
+ "nav": Array [],
+ "serverName": "http-server-test",
+ "uiSettings": Object {
+ "defaults": Object {
+ "registered": Object {
+ "name": "title",
+ },
+ },
+ "user": Object {},
+ },
+ "version": Any,
+ },
+ "legacyMode": false,
+ "uiPlugins": Array [],
+ "vars": Object {},
+ "version": Any,
+}
+`;
+
+exports[`RenderingService setup() render() renders "core" page 1`] = `
+Object {
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNumber": Any,
+ "csp": Object {
+ "warnLegacyBrowsers": true,
+ },
+ "env": Object {
+ "binDir": Any,
+ "cliArgs": Object {
+ "basePath": false,
+ "dev": true,
+ "open": false,
+ "optimize": false,
+ "oss": false,
+ "quiet": false,
+ "repl": false,
+ "silent": false,
+ "watch": false,
+ },
+ "configDir": Any,
+ "configs": Array [],
+ "homeDir": Any,
+ "isDevClusterMaster": false,
+ "logDir": Any,
+ "mode": Object {
+ "dev": true,
+ "name": "development",
+ "prod": false,
+ },
+ "packageInfo": Object {
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "dist": false,
+ "version": Any,
+ },
+ "pluginSearchPaths": Any,
+ "staticFilesDir": Any,
+ },
+ "i18n": Object {
+ "translationsUrl": "/mock-server-basepath/translations/en.json",
+ },
+ "legacyMetadata": Object {
+ "app": Object {},
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "bundleId": "app:core",
+ "devMode": true,
+ "nav": Array [],
+ "serverName": "http-server-test",
+ "uiSettings": Object {
+ "defaults": Object {
+ "registered": Object {
+ "name": "title",
+ },
+ },
+ "user": Object {},
+ },
+ "version": Any,
+ },
+ "legacyMode": false,
+ "uiPlugins": Array [],
+ "vars": Object {},
+ "version": Any,
+}
+`;
+
+exports[`RenderingService setup() render() renders "core" page driven by settings 1`] = `
+Object {
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNumber": Any,
+ "csp": Object {
+ "warnLegacyBrowsers": true,
+ },
+ "env": Object {
+ "binDir": Any,
+ "cliArgs": Object {
+ "basePath": false,
+ "dev": true,
+ "open": false,
+ "optimize": false,
+ "oss": false,
+ "quiet": false,
+ "repl": false,
+ "silent": false,
+ "watch": false,
+ },
+ "configDir": Any,
+ "configs": Array [],
+ "homeDir": Any,
+ "isDevClusterMaster": false,
+ "logDir": Any,
+ "mode": Object {
+ "dev": true,
+ "name": "development",
+ "prod": false,
+ },
+ "packageInfo": Object {
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "dist": false,
+ "version": Any,
+ },
+ "pluginSearchPaths": Any,
+ "staticFilesDir": Any,
+ },
+ "i18n": Object {
+ "translationsUrl": "/mock-server-basepath/translations/en.json",
+ },
+ "legacyMetadata": Object {
+ "app": Object {},
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "bundleId": "app:core",
+ "devMode": true,
+ "nav": Array [],
+ "serverName": "http-server-test",
+ "uiSettings": Object {
+ "defaults": Object {
+ "registered": Object {
+ "name": "title",
+ },
+ },
+ "user": Object {
+ "theme:darkMode": Object {
+ "userValue": true,
+ },
+ },
+ },
+ "version": Any,
+ },
+ "legacyMode": false,
+ "uiPlugins": Array [],
+ "vars": Object {},
+ "version": Any,
+}
+`;
+
+exports[`RenderingService setup() render() renders "core" page for blank basepath 1`] = `
+Object {
+ "basePath": "",
+ "branch": Any,
+ "buildNumber": Any,
+ "csp": Object {
+ "warnLegacyBrowsers": true,
+ },
+ "env": Object {
+ "binDir": Any,
+ "cliArgs": Object {
+ "basePath": false,
+ "dev": true,
+ "open": false,
+ "optimize": false,
+ "oss": false,
+ "quiet": false,
+ "repl": false,
+ "silent": false,
+ "watch": false,
+ },
+ "configDir": Any,
+ "configs": Array [],
+ "homeDir": Any,
+ "isDevClusterMaster": false,
+ "logDir": Any,
+ "mode": Object {
+ "dev": true,
+ "name": "development",
+ "prod": false,
+ },
+ "packageInfo": Object {
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "dist": false,
+ "version": Any,
+ },
+ "pluginSearchPaths": Any,
+ "staticFilesDir": Any,
+ },
+ "i18n": Object {
+ "translationsUrl": "/translations/en.json",
+ },
+ "legacyMetadata": Object {
+ "app": Object {},
+ "basePath": "",
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "bundleId": "app:core",
+ "devMode": true,
+ "nav": Array [],
+ "serverName": "http-server-test",
+ "uiSettings": Object {
+ "defaults": Object {
+ "registered": Object {
+ "name": "title",
+ },
+ },
+ "user": Object {},
+ },
+ "version": Any,
+ },
+ "legacyMode": false,
+ "uiPlugins": Array [],
+ "vars": Object {},
+ "version": Any,
+}
+`;
+
+exports[`RenderingService setup() render() renders "core" with excluded user settings 1`] = `
+Object {
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNumber": Any,
+ "csp": Object {
+ "warnLegacyBrowsers": true,
+ },
+ "env": Object {
+ "binDir": Any,
+ "cliArgs": Object {
+ "basePath": false,
+ "dev": true,
+ "open": false,
+ "optimize": false,
+ "oss": false,
+ "quiet": false,
+ "repl": false,
+ "silent": false,
+ "watch": false,
+ },
+ "configDir": Any,
+ "configs": Array [],
+ "homeDir": Any,
+ "isDevClusterMaster": false,
+ "logDir": Any,
+ "mode": Object {
+ "dev": true,
+ "name": "development",
+ "prod": false,
+ },
+ "packageInfo": Object {
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "dist": false,
+ "version": Any,
+ },
+ "pluginSearchPaths": Any,
+ "staticFilesDir": Any,
+ },
+ "i18n": Object {
+ "translationsUrl": "/mock-server-basepath/translations/en.json",
+ },
+ "legacyMetadata": Object {
+ "app": Object {},
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNum": Any,
+ "buildSha": Any,
+ "bundleId": "app:core",
+ "devMode": true,
+ "nav": Array [],
+ "serverName": "http-server-test",
+ "uiSettings": Object {
+ "defaults": Object {
+ "registered": Object {
+ "name": "title",
+ },
+ },
+ "user": Object {},
+ },
+ "version": Any,
+ },
+ "legacyMode": false,
+ "uiPlugins": Array [],
+ "vars": Object {},
+ "version": Any,
+}
+`;
+
+exports[`RenderingService setup() render() renders "legacy" page 1`] = `
+Object {
+ "basePath": "/mock-server-basepath",
+ "branch": Any,
+ "buildNumber": Any,
+ "csp": Object {
+ "warnLegacyBrowsers": true,
+ },
+ "env": Object {
+ "binDir": Any