Add server rendering service to enable standalone route rendering (#52161)

* Add server rendering service to enable standalone route rendering

* Update renovate config

* Move legacy rendering functionality to legacy service

* Use config for exposed variable in new platform

* Lint changes from rebase

* Rebase artifact

* Remove RenderingProvider, add tests for legacy vars implementation, review notes

* Add UI app functionality to legacy service

* Update rendering snapshots

* Update docs

* Fix up functional tests

* Clean up legacy types

* Revise types from reverting injected metadata changes

* Update translations and broken tests

* Mock legacy internals in legacy tests

* Add missing doc types

* Rename InternalRenderOptions to LegacyRenderOptions

* Remove extraneous legacy exports, review nits

* Functional tests fixes

* Rebase, attempt CI test fixes

* Only allow specified appIds in testbed rendering integration test

* Update snapshot names

* Review nits
This commit is contained in:
Eli Perelman 2019-12-29 02:10:55 -06:00 committed by GitHub
parent 3ed5264cc3
commit 054ec7036d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
95 changed files with 3399 additions and 1397 deletions

View file

@ -9,5 +9,5 @@ Search for objects
<b>Signature:</b>
```typescript
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>>;
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>>;
```

View file

@ -20,7 +20,7 @@ export declare class SavedObjectsClient
| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | <code>(objects?: {</code><br/><code> id: string;</code><br/><code> type: string;</code><br/><code> }[]) =&gt; Promise&lt;SavedObjectsBatchResponse&lt;SavedObjectAttributes&gt;&gt;</code> | Returns an array of objects by id |
| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(type: string, attributes: T, options?: SavedObjectsCreateOptions) =&gt; Promise&lt;SimpleSavedObject&lt;T&gt;&gt;</code> | Persists an object |
| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | <code>(type: string, id: string) =&gt; Promise&lt;{}&gt;</code> | Deletes an object |
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(options: Pick&lt;SavedObjectFindOptionsServer, &quot;search&quot; &#124; &quot;filter&quot; &#124; &quot;type&quot; &#124; &quot;page&quot; &#124; &quot;perPage&quot; &#124; &quot;sortField&quot; &#124; &quot;fields&quot; &#124; &quot;searchFields&quot; &#124; &quot;hasReference&quot; &#124; &quot;defaultSearchOperator&quot;&gt;) =&gt; Promise&lt;SavedObjectsFindResponsePublic&lt;T&gt;&gt;</code> | Search for objects |
| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(options: Pick&lt;SavedObjectFindOptionsServer, &quot;search&quot; &#124; &quot;filter&quot; &#124; &quot;type&quot; &#124; &quot;page&quot; &#124; &quot;fields&quot; &#124; &quot;searchFields&quot; &#124; &quot;defaultSearchOperator&quot; &#124; &quot;hasReference&quot; &#124; &quot;sortField&quot; &#124; &quot;perPage&quot;&gt;) =&gt; Promise&lt;SavedObjectsFindResponsePublic&lt;T&gt;&gt;</code> | Search for objects |
| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <code>&lt;T extends SavedObjectAttributes&gt;(type: string, id: string) =&gt; Promise&lt;SimpleSavedObject&lt;T&gt;&gt;</code> | Fetches a single object |
## Methods

View file

@ -9,5 +9,5 @@ returns `basePath` value, specific for an incoming request.
<b>Signature:</b>
```typescript
(request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest) => string;
get: (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest) => string;
```

View file

@ -9,5 +9,5 @@ sets `basePath` value, specific for an incoming request.
<b>Signature:</b>
```typescript
(request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest, requestSpecificBasePath: string) => void;
set: (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest, requestSpecificBasePath: string) => void;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IRenderOptions](./kibana-plugin-server.irenderoptions.md) &gt; [includeUserSettings](./kibana-plugin-server.irenderoptions.includeusersettings.md)
## IRenderOptions.includeUserSettings property
Set whether to output user settings in the page metadata. `true` by default.
<b>Signature:</b>
```typescript
includeUserSettings?: boolean;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IRenderOptions](./kibana-plugin-server.irenderoptions.md)
## IRenderOptions interface
<b>Signature:</b>
```typescript
export interface IRenderOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [includeUserSettings](./kibana-plugin-server.irenderoptions.includeusersettings.md) | <code>boolean</code> | Set whether to output user settings in the page metadata. <code>true</code> by default. |

View file

@ -9,5 +9,5 @@ Wrap a router handler to catch and converts legacy boom errors to proper custom
<b>Signature:</b>
```typescript
<P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
handleLegacyErrors: <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md)
## IScopedRenderingClient interface
<b>Signature:</b>
```typescript
export interface IScopedRenderingClient
```
## Methods
| Method | Description |
| --- | --- |
| [render(options)](./kibana-plugin-server.iscopedrenderingclient.render.md) | Generate a <code>KibanaResponse</code> which renders an HTML page bootstrapped with the <code>core</code> bundle. Intended as a response body for HTTP route handlers. |

View file

@ -0,0 +1,41 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) &gt; [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.
<b>Signature:</b>
```typescript
render(options?: IRenderOptions): Promise<string>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>IRenderOptions</code> | |
<b>Returns:</b>
`Promise<string>`
## 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,
},
})
);
```

View file

@ -7,7 +7,5 @@
<b>Signature:</b>
```typescript
core: InternalCoreSetup & {
plugins: PluginsServiceSetup;
};
core: LegacyCoreSetup;
```

View file

@ -18,6 +18,6 @@ export interface LegacyServiceSetupDeps
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-server.legacyservicesetupdeps.core.md) | <code>InternalCoreSetup &amp; {</code><br/><code> plugins: PluginsServiceSetup;</code><br/><code> }</code> | |
| [core](./kibana-plugin-server.legacyservicesetupdeps.core.md) | <code>LegacyCoreSetup</code> | |
| [plugins](./kibana-plugin-server.legacyservicesetupdeps.plugins.md) | <code>Record&lt;string, unknown&gt;</code> | |

View file

@ -7,7 +7,5 @@
<b>Signature:</b>
```typescript
core: InternalCoreStart & {
plugins: PluginsServiceStart;
};
core: LegacyCoreStart;
```

View file

@ -18,6 +18,6 @@ export interface LegacyServiceStartDeps
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-server.legacyservicestartdeps.core.md) | <code>InternalCoreStart &amp; {</code><br/><code> plugins: PluginsServiceStart;</code><br/><code> }</code> | |
| [core](./kibana-plugin-server.legacyservicestartdeps.core.md) | <code>LegacyCoreStart</code> | |
| [plugins](./kibana-plugin-server.legacyservicestartdeps.plugins.md) | <code>Record&lt;string, unknown&gt;</code> | |

View file

@ -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 |

View file

@ -8,6 +8,7 @@
```typescript
core: {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
};

View file

@ -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
<b>Signature:</b>
@ -18,5 +18,5 @@ export interface RequestHandlerContext
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-server.requesthandlercontext.core.md) | <code>{</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> dataClient: IScopedClusterClient;</code><br/><code> adminClient: IScopedClusterClient;</code><br/><code> };</code><br/><code> uiSettings: {</code><br/><code> client: IUiSettingsClient;</code><br/><code> };</code><br/><code> }</code> | |
| [core](./kibana-plugin-server.requesthandlercontext.core.md) | <code>{</code><br/><code> rendering: IScopedRenderingClient;</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> dataClient: IScopedClusterClient;</code><br/><code> adminClient: IScopedClusterClient;</code><br/><code> };</code><br/><code> uiSettings: {</code><br/><code> client: IUiSettingsClient;</code><br/><code> };</code><br/><code> }</code> | |

View file

@ -9,7 +9,7 @@ A schema created with `@kbn/config-schema` that every request will be validated
<b>Signature:</b>
```typescript
RouteValidatorFullConfig<P, Q, B> | false;
validate: RouteValidatorFullConfig<P, Q, B> | false;
```
## Remarks

View file

@ -9,7 +9,7 @@ Constructs a new instance of the `RouteValidationError` class
<b>Signature:</b>
```typescript
constructor(error;: Error | string, path?: string[];)
constructor(error: Error | string, path?: string[]);
```
## Parameters

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
(error: Error | string, path?: string[]) => {
RouteValidationError;
badRequest: (error: Error | string, path?: string[]) => {
error: RouteValidationError;
};
```

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
<T>(value: T) => {
T;
ok: <T>(value: T) => {
value: T;
};
```

View file

@ -13,6 +13,5 @@ unsafe?: {
params?: boolean;
query?: boolean;
body?: boolean;
}
};
```

View file

@ -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

View file

@ -80,9 +80,6 @@ export interface InjectedMetadataParams {
user?: Record<string, UserProvidedValues>;
};
};
apm: {
[key: string]: unknown;
};
};
}

View file

@ -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<Config>;
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,
};

View file

@ -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<InternalHttpServiceSetup['basePath']>;
export type HttpServiceSetupMock = jest.Mocked<InternalHttpServiceSetup> & {
basePath: jest.Mocked<InternalHttpServiceSetup['basePath']>;
basePath: BasePathMocked;
};
const createBasePathMock = (): jest.Mocked<InternalHttpServiceSetup['basePath']> => ({
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<Server>,
createCookieSessionStorageFactory: jest.fn(),
registerOnPreAuth: jest.fn(),

View file

@ -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,

View file

@ -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 */

View file

@ -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(

View file

@ -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<string, any> = {}): LegacyConfig =>
const getConfig = (values: LegacyVars = {}): LegacyConfig =>
({
get: () => values as any,
} as LegacyConfig);

View file

@ -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<string, any>;
settings: LegacyVars;
legacyConfig: LegacyConfig;
}) {
// transform deprecated plugin settings

View file

@ -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';

View file

@ -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');

View file

@ -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

View file

@ -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<string, any>) {
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<string, any>);
return LegacyObjectToConfigAdapter.transformPlugins(configValue as LegacyVars);
default:
return configValue;
}

View file

@ -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<T>(key?: string): T;
has(key: string): boolean;
set(key: string, value: any): void;
set(config: Record<string, any>): 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<string, any>,
log: (msg: string) => void
) => void;
/**
* Representation of a legacy configuration deprecation provider.
*
* @internal
*/
export type LegacyConfigDeprecationProvider = (
factory: LegacyConfigDeprecationFactory
) => LegacyConfigDeprecation[] | Promise<LegacyConfigDeprecation[]>;

View file

@ -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';

View file

@ -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",
}
`);
});
});
});

View file

@ -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<string, Set<VarsInjector>>();
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<LegacyVars>({})
);
}
public async getVars(id: string, request: LegacyRequest, injected: LegacyVars = {}) {
return this.replaceVars(
mergeVars(this.defaultVars, await this.getInjectedUiAppVars(id), injected),
request
);
}
}

View file

@ -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<LegacyServiceDiscoverPlugins> = {
type LegacyServiceMock = jest.Mocked<PublicMethodsOf<LegacyService> & { legacyId: symbol }>;
const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({
pluginSpecs: [],
disabledPluginSpecs: [],
uiExports: {} as any,
settings: {},
navLinks: [],
pluginExtendedConfig: {
get: jest.fn(),
has: jest.fn(),
set: jest.fn(),
} as any,
};
return setupContract;
};
},
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,
};

View file

@ -17,18 +17,19 @@
* under the License.
*/
export const findLegacyPluginSpecsMock = jest
.fn()
.mockImplementation((settings: Record<string, any>) => ({
import { LegacyVars } from './types';
export const findLegacyPluginSpecsMock = jest.fn().mockImplementation((settings: LegacyVars) => ({
pluginSpecs: [],
pluginExtendedConfig: {
has: jest.fn(),
get: jest.fn(() => settings),
get: jest.fn().mockReturnValue(settings),
set: jest.fn(),
},
disabledPluginSpecs: [],
uiExports: [],
}));
uiExports: {},
navLinks: [],
}));
jest.doMock('./plugins/find_legacy_plugin_specs.ts', () => ({
findLegacyPluginSpecs: findLegacyPluginSpecsMock,
}));

View file

@ -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> = 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,7 +399,8 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => {
});
});
test('Cannot start without setup phase', async () => {
describe('start', () => {
test('Cannot start without setup phase', async () => {
const legacyService = new LegacyService({
coreId,
env,
@ -405,6 +410,7 @@ test('Cannot start without setup phase', async () => {
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<string, any>) => ({
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();

View file

@ -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<Record<string, any>>) => void;
applyLoggingConfiguration: (settings: Readonly<LegacyVars>) => void;
listen: () => Promise<void>;
ready: () => Promise<void>;
close: () => Promise<void>;
@ -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
// 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,
};
}
/**
* @public
* @deprecated
*/
export interface LegacyServiceSetupDeps {
core: InternalCoreSetup & {
plugins: PluginsServiceSetup;
};
plugins: Record<string, unknown>;
}
/**
* @public
* @deprecated
*/
export interface LegacyServiceStartDeps {
core: InternalCoreStart & {
plugins: PluginsServiceStart;
};
plugins: Record<string, unknown>;
}
/** @internal */
export interface LegacyServiceDiscoverPlugins {
pluginSpecs: LegacyPluginSpec[];
disabledPluginSpecs: LegacyPluginSpec[];
uiExports: SavedObjectsLegacyUiExports;
pluginExtendedConfig: LegacyConfig;
settings: Record<string, any>;
}
/** @internal */
export type ILegacyService = Pick<LegacyService, keyof LegacyService>;
export type ILegacyService = PublicMethodsOf<LegacyService>;
/** @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<string, any> | 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<string, any>,
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,
},

View file

@ -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<Record<string, any>>) {
constructor(legacyLoggingConfig: Readonly<LegacyVars>) {
this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig);
}

View file

@ -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<string, any> = {}) {
export function attachMetaData(message: string, metadata: LegacyVars = {}) {
return {
[metadataSymbol]: {
message,
@ -50,7 +51,7 @@ interface PluginRegisterParams {
options: PluginRegisterParams['options']
) => Promise<void>;
};
options: Record<string, any>;
options: LegacyVars;
}
/**
@ -84,7 +85,7 @@ export class LegacyLoggingServer {
private onPostStopCallback?: () => void;
constructor(legacyLoggingConfig: Readonly<Record<string, any>>) {
constructor(legacyLoggingConfig: Readonly<LegacyVars>) {
// 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.

View file

@ -17,11 +17,18 @@
* under the License.
*/
import { mergeVariables } from './merge_variables';
import { mergeVars } from './merge_vars';
describe('mergeVariables', () => {
describe('mergeVars', () => {
it('merges two objects together', () => {
const someVariables = {
const first = {
otherName: 'value',
otherCanFoo: true,
otherNested: {
otherAnotherVariable: 'ok',
},
};
const second = {
name: 'value',
canFoo: true,
nested: {
@ -29,17 +36,7 @@ describe('mergeVariables', () => {
},
};
const otherVariables = {
otherName: 'value',
otherCanFoo: true,
otherNested: {
otherAnotherVariable: 'ok',
},
};
const result = mergeVariables(someVariables, otherVariables);
expect(result).toEqual({
expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
nested: {
@ -54,71 +51,68 @@ describe('mergeVariables', () => {
});
it('does not mutate the source objects', () => {
const original = {
var1: 'original',
const first = {
var1: 'first',
};
const second = {
var1: 'second',
var2: 'second',
};
const third = {
var1: 'third',
var2: 'third',
var3: 'third',
};
const fourth = {
var1: 'fourth',
var2: 'fourth',
var3: 'fourth',
var4: 'fourth',
};
const set1 = {
var1: 'value1',
var2: 'value1',
};
mergeVars(first, second, third, fourth);
const set2 = {
var1: 'value2',
var2: 'value2',
var3: 'value2',
};
const set3 = {
var1: 'value3',
var2: 'value3',
var3: 'value3',
var4: 'value3',
};
mergeVariables(original, set1, set2, set3);
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 second = {
var1: 'second',
var2: 'second',
var3: 'second',
};
const third = {
var1: 'third',
var2: 'third',
};
const fourth = {
var1: 'fourth',
};
const set1 = {
var1: 'value1',
var2: 'value1',
};
const set2 = {
var1: 'value2',
var2: 'value2',
var3: 'value2',
};
const set3 = {
var1: 'value3',
var2: 'value3',
var3: 'value3',
var4: 'value3',
};
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 = {
it('overwrites the original variable value if a duplicate entry is found', () => {
const first = {
nested: {
otherAnotherVariable: 'ok',
},
};
const second = {
name: 'value',
canFoo: true,
nested: {
@ -126,14 +120,7 @@ describe('mergeVariables', () => {
},
};
const otherVariables = {
nested: {
otherAnotherVariable: '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 = {
const first = {
uiCapabilities: {
firstCapability: 'ok',
nestedCapability: {
otherNestedProp: 'otherNestedValue',
},
},
};
const second = {
name: 'value',
canFoo: true,
uiCapabilities: {
firstCapability: 'ok',
secondCapability: 'ok',
nestedCapability: {
nestedProp: 'nestedValue',
},
},
};
const otherVariables = {
uiCapabilities: {
secondCapability: 'ok',
nestedCapability: {
otherNestedProp: 'otherNestedValue',
},
},
};
const result = mergeVariables(someVariables, otherVariables);
expect(result).toEqual({
expect(mergeVars(first, second)).toEqual({
name: 'value',
canFoo: true,
uiCapabilities: {

View file

@ -17,23 +17,18 @@
* under the License.
*/
import { LegacyVars } from './types';
const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities'];
export function mergeVariables(...sources: Array<Record<string, any>>) {
const result: Record<string, any> = {};
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;
}
});
}
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] || {})) }]
: []
)
);
}

View file

@ -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<LegacyNavLink>(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,
};
}

View file

@ -16,4 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
export { findLegacyPluginSpecs } from './find_legacy_plugin_specs';

View file

@ -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<string, any>;
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<T>(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<LegacyConfigDeprecation[]>;
/**
* @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<LegacyVars>;
/**
* @internal
* @deprecated
*/
export type LegacyNavLinkSpec = Record<string, unknown> & 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<ChromeNavLink, 'baseUrl' | 'legacy' | 'order'> & {
order: number;
};
/**
* @internal
* @deprecated
*/
export type LegacyUiExports = SavedObjectsLegacyUiExports & {
defaultInjectedVarProviders?: VarsProvider[];
injectedVarsReplacers?: VarsReplacer[];
navLinkSpecs?: LegacyNavLinkSpec[] | null;
uiAppSpecs?: Array<LegacyAppSpec | undefined>;
unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown }];
};
/**
* @public
* @deprecated
*/
export interface LegacyServiceSetupDeps {
core: LegacyCoreSetup;
plugins: Record<string, unknown>;
}
/**
* @public
* @deprecated
*/
export interface LegacyServiceStartDeps {
core: LegacyCoreStart;
plugins: Record<string, unknown>;
}
/**
* @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<LegacyVars>;
/**
* Get the metadata vars for a particular plugin
*/
getVars(id: string, request: LegacyRequest, injected?: LegacyVars): Promise<LegacyVars>;
}
/**
* @internal
* @deprecated
*/
export interface LegacyPlugins {
disabledPluginSpecs: LegacyPluginSpec[];
pluginSpecs: LegacyPluginSpec[];
uiExports: LegacyUiExports;
navLinks: LegacyNavLink[];
}
/**
* @internal
* @deprecated
*/
export interface LegacyServiceDiscoverPlugins extends LegacyPlugins {
pluginExtendedConfig: LegacyConfig;
settings: LegacyVars;
}

View file

@ -17,28 +17,28 @@
* under the License.
*/
import { PluginsService } from './plugins_service';
import { PluginsService, PluginsServiceSetup } from './plugins_service';
type ServiceContract = PublicMethodsOf<PluginsService>;
const createServiceMock = () => {
const mocked: jest.Mocked<ServiceContract> = {
discover: jest.fn(),
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockResolvedValue({
type PluginsServiceMock = jest.Mocked<PublicMethodsOf<PluginsService>>;
const createSetupContractMock = (): PluginsServiceSetup => ({
contracts: new Map(),
uiPlugins: {
browserConfigs: new Map(),
internal: new Map(),
public: new Map(),
},
});
mocked.start.mockResolvedValue({ contracts: new Map() });
return mocked;
};
});
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,
};

View file

@ -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,
};

View file

@ -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<Service>;
export const setupMock: jest.Mocked<RenderingServiceSetup> = {
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<IRenderingService> = {
setup: mockSetup,
start: mockStart,
stop: mockStop,
};
export const RenderingService = jest.fn<IRenderingService, [typeof mockRenderingServiceParams]>(
() => mockRenderingService
);

View file

@ -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<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:core",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": false,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "core" page 1`] = `
Object {
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:core",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": false,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "core" page driven by settings 1`] = `
Object {
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"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<String>,
},
"legacyMode": false,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "core" page for blank basepath 1`] = `
Object {
"basePath": "",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:core",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": false,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "core" with excluded user settings 1`] = `
Object {
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:core",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": false,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "legacy" page 1`] = `
Object {
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:legacy",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": true,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "legacy" page for blank basepath 1`] = `
Object {
"basePath": "",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:legacy",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": true,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "legacy" with custom vars 1`] = `
Object {
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:legacy",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": true,
"uiPlugins": Array [],
"vars": Object {
"fake": "__TEST_TOKEN__",
},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "legacy" with excluded user settings 1`] = `
Object {
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:legacy",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": true,
"uiPlugins": Array [],
"vars": Object {},
"version": Any<String>,
}
`;
exports[`RenderingService setup() render() renders "legacy" with excluded user settings and custom vars 1`] = `
Object {
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNumber": Any<Number>,
"csp": Object {
"warnLegacyBrowsers": true,
},
"env": Object {
"binDir": Any<String>,
"cliArgs": Object {
"basePath": false,
"dev": true,
"open": false,
"optimize": false,
"oss": false,
"quiet": false,
"repl": false,
"silent": false,
"watch": false,
},
"configDir": Any<String>,
"configs": Array [],
"homeDir": Any<String>,
"isDevClusterMaster": false,
"logDir": Any<String>,
"mode": Object {
"dev": true,
"name": "development",
"prod": false,
},
"packageInfo": Object {
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"dist": false,
"version": Any<String>,
},
"pluginSearchPaths": Any<Array>,
"staticFilesDir": Any<String>,
},
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/en.json",
},
"legacyMetadata": Object {
"app": Object {},
"basePath": "/mock-server-basepath",
"branch": Any<String>,
"buildNum": Any<Number>,
"buildSha": Any<String>,
"bundleId": "app:legacy",
"devMode": true,
"nav": Array [],
"serverName": "http-server-test",
"uiSettings": Object {
"defaults": Object {
"registered": Object {
"name": "title",
},
},
"user": Object {},
},
"version": Any<String>,
},
"legacyMode": true,
"uiPlugins": Array [],
"vars": Object {
"fake": "__TEST_TOKEN__",
},
"version": Any<String>,
}
`;

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { RenderingService } from './rendering_service';
export * from './types';

View file

@ -0,0 +1,185 @@
/*
* 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 { load } from 'cheerio';
import { httpServerMock } from '../http/http_server.mocks';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { mockRenderingServiceParams, mockRenderingSetupDeps } from './__mocks__/params';
import { RenderingServiceSetup } from './types';
import { RenderingService } from './rendering_service';
const INJECTED_METADATA = {
version: expect.any(String),
branch: expect.any(String),
buildNumber: expect.any(Number),
env: {
binDir: expect.any(String),
configDir: expect.any(String),
homeDir: expect.any(String),
logDir: expect.any(String),
packageInfo: {
branch: expect.any(String),
buildNum: expect.any(Number),
buildSha: expect.any(String),
version: expect.any(String),
},
pluginSearchPaths: expect.any(Array),
staticFilesDir: expect.any(String),
},
legacyMetadata: {
branch: expect.any(String),
buildNum: expect.any(Number),
buildSha: expect.any(String),
version: expect.any(String),
},
};
const { createKibanaRequest, createRawRequest } = httpServerMock;
const legacyApp = { getId: () => 'legacy' };
describe('RenderingService', () => {
let service: RenderingService;
beforeEach(() => {
jest.clearAllMocks();
service = new RenderingService(mockRenderingServiceParams);
});
describe('setup()', () => {
it('creates instance of RenderingServiceSetup', async () => {
const rendering = await service.setup(mockRenderingSetupDeps);
expect(rendering.render).toBeInstanceOf(Function);
});
describe('render()', () => {
let uiSettings: ReturnType<typeof uiSettingsServiceMock.createClient>;
let render: RenderingServiceSetup['render'];
beforeEach(async () => {
uiSettings = uiSettingsServiceMock.createClient();
uiSettings.getRegistered.mockReturnValue({
registered: { name: 'title' },
});
render = (await service.setup(mockRenderingSetupDeps)).render;
});
it('renders "core" page', async () => {
const content = await render(createKibanaRequest(), uiSettings);
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "core" page for blank basepath', async () => {
mockRenderingSetupDeps.http.basePath.get.mockReturnValueOnce('');
const content = await render(createKibanaRequest(), uiSettings);
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "core" page driven by settings', async () => {
uiSettings.getUserProvided.mockResolvedValue({ 'theme:darkMode': { userValue: true } });
const content = await render(createKibanaRequest(), uiSettings);
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "core" with excluded user settings', async () => {
const content = await render(createKibanaRequest(), uiSettings, {
includeUserSettings: false,
});
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "core" from legacy request', async () => {
const content = await render(createRawRequest(), uiSettings);
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "legacy" page', async () => {
const content = await render(createRawRequest(), uiSettings, { app: legacyApp });
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "legacy" page for blank basepath', async () => {
mockRenderingSetupDeps.http.basePath.get.mockReturnValueOnce('');
const content = await render(createRawRequest(), uiSettings, { app: legacyApp });
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "legacy" with custom vars', async () => {
const content = await render(createRawRequest(), uiSettings, {
app: legacyApp,
vars: {
fake: '__TEST_TOKEN__',
},
});
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "legacy" with excluded user settings', async () => {
const content = await render(createRawRequest(), uiSettings, {
app: legacyApp,
includeUserSettings: false,
});
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
it('renders "legacy" with excluded user settings and custom vars', async () => {
const content = await render(createRawRequest(), uiSettings, {
app: legacyApp,
includeUserSettings: false,
vars: {
fake: '__TEST_TOKEN__',
},
});
const dom = load(content);
const data = JSON.parse(dom('kbn-injected-metadata').attr('data'));
expect(data).toMatchSnapshot(INJECTED_METADATA);
});
});
});
});

View file

@ -0,0 +1,120 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { take } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { CoreService } from '../../types';
import { CoreContext } from '../core_context';
import { Template } from './views';
import {
RenderingSetupDeps,
RenderingServiceSetup,
RenderingMetadata,
LegacyRenderOptions,
} from './types';
/** @internal */
export class RenderingService implements CoreService<RenderingServiceSetup> {
constructor(private readonly coreContext: CoreContext) {}
public async setup({
http,
legacyPlugins,
plugins,
}: RenderingSetupDeps): Promise<RenderingServiceSetup> {
async function getUiConfig(pluginId: string) {
const browserConfig = plugins.uiPlugins.browserConfigs.get(pluginId);
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record<string, any>;
}
return {
render: async (
request,
uiSettings,
{
app = { getId: () => 'core' },
includeUserSettings = true,
vars = {},
}: LegacyRenderOptions = {}
) => {
const { env } = this.coreContext;
const basePath = http.basePath.get(request);
const settings = {
defaults: uiSettings.getRegistered(),
user: includeUserSettings ? await uiSettings.getUserProvided() : {},
};
const appId = app.getId();
const metadata: RenderingMetadata = {
strictCsp: http.csp.strict,
uiPublicUrl: `${basePath}/ui`,
bootstrapScriptUrl: `${basePath}/bundles/app/${appId}/bootstrap.js`,
i18n: i18n.translate,
locale: i18n.getLocale(),
darkMode: settings.user?.['theme:darkMode']?.userValue
? Boolean(settings.user['theme:darkMode'].userValue)
: false,
injectedMetadata: {
version: env.packageInfo.version,
buildNumber: env.packageInfo.buildNum,
branch: env.packageInfo.branch,
basePath,
env,
legacyMode: appId !== 'core',
i18n: {
translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`,
},
csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers },
vars,
uiPlugins: await Promise.all(
[...plugins.uiPlugins.public].map(async ([id, plugin]) => ({
id,
plugin,
config: await getUiConfig(id),
}))
),
legacyMetadata: {
app,
bundleId: `app:${appId}`,
nav: legacyPlugins.navLinks,
version: env.packageInfo.version,
branch: env.packageInfo.branch,
buildNum: env.packageInfo.buildNum,
buildSha: env.packageInfo.buildSha,
serverName: http.server.name,
devMode: env.mode.dev,
basePath,
uiSettings: settings,
},
},
};
return `<!DOCTYPE html>${renderToStaticMarkup(<Template metadata={metadata} />)}`;
},
};
}
public async start() {}
public async stop() {}
}

View file

@ -0,0 +1,145 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { Env } from '../config';
import { ICspConfig } from '../csp';
import { InternalHttpServiceSetup, KibanaRequest, LegacyRequest } from '../http';
import { LegacyNavLink, LegacyServiceDiscoverPlugins } from '../legacy';
import { PluginsServiceSetup, DiscoveredPlugin } from '../plugins';
import { IUiSettingsClient, UserProvidedValues } from '../ui_settings';
/** @internal */
export interface RenderingMetadata {
strictCsp: ICspConfig['strict'];
uiPublicUrl: string;
bootstrapScriptUrl: string;
i18n: typeof i18n.translate;
locale: string;
darkMode: boolean;
injectedMetadata: {
version: string;
buildNumber: number;
branch: string;
basePath: string;
env: Env;
legacyMode: boolean;
i18n: {
translationsUrl: string;
};
csp: Pick<ICspConfig, 'warnLegacyBrowsers'>;
vars: Record<string, any>;
uiPlugins: Array<{
id: string;
plugin: DiscoveredPlugin;
config?: Record<string, unknown>;
}>;
legacyMetadata: {
app: { getId(): string };
bundleId: string;
nav: LegacyNavLink[];
version: string;
branch: string;
buildNum: number;
buildSha: string;
serverName: string;
devMode: boolean;
basePath: string;
uiSettings: {
defaults: Record<string, any>;
user: Record<string, UserProvidedValues<any>>;
};
};
};
}
/** @internal */
export interface RenderingSetupDeps {
http: InternalHttpServiceSetup;
legacyPlugins: LegacyServiceDiscoverPlugins;
plugins: PluginsServiceSetup;
}
/** @public */
export interface IRenderOptions {
/**
* Set whether to output user settings in the page metadata.
* `true` by default.
*/
includeUserSettings?: boolean;
}
/**
* @internal
* @deprecated for legacy use only, remove with ui_render_mixin
*/
export interface LegacyRenderOptions extends IRenderOptions {
/**
* Render the bootstrapped HTML content for an optional legacy application.
* Defaults to `core`.
*/
app?: { getId(): string };
/**
* Inject custom vars into the page metadata.
*/
vars?: Record<string, any>;
}
/** @public */
export interface IScopedRenderingClient {
/**
* Generate a `KibanaResponse` which renders an HTML page bootstrapped
* with the `core` bundle. Intended as a response body for HTTP route handlers.
*
* @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,
* },
* })
* );
* ```
*/
render(options?: IRenderOptions): Promise<string>;
}
/** @internal */
export interface RenderingServiceSetup {
/**
* Generate a `KibanaResponse` which renders an HTML page bootstrapped
* with the `core` bundle or the ID of another specified legacy bundle.
*
* @example
* ```ts
* const html = await rendering.render(request, uiSettings);
* ```
*/
render<R extends KibanaRequest | LegacyRequest>(
request: R,
uiSettings: IUiSettingsClient,
options?: R extends LegacyRequest ? LegacyRenderOptions : IRenderOptions
): Promise<string>;
}

View file

@ -0,0 +1,336 @@
/*
* 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.
*/
/* eslint-disable react/no-danger */
import React, { FunctionComponent } from 'react';
import { RenderingMetadata } from '../types';
interface Props {
url: RenderingMetadata['uiPublicUrl'];
}
interface FontFace {
family: string;
variants: Array<{
style: 'normal' | 'italic';
weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
sources: string[];
unicodeRange?: string;
format?: string;
}>;
}
export const Fonts: FunctionComponent<Props> = ({ url }) => {
const interUi: FontFace = {
family: 'Inter UI',
variants: [
{
style: 'normal',
weight: 100,
sources: [
`${url}/fonts/inter_ui/Inter-UI-Thin-BETA.woff2`,
`${url}/fonts/inter_ui/Inter-UI-Thin-BETA.woff`,
],
},
{
style: 'italic',
weight: 100,
sources: [
`${url}/fonts/inter_ui/Inter-UI-ThinItalic-BETA.woff2`,
`${url}/fonts/inter_ui/Inter-UI-ThinItalic-BETA.woff`,
],
},
{
style: 'normal',
weight: 200,
sources: [
`${url}/fonts/inter_ui/Inter-UI-ExtraLight-BETA.woff2`,
`${url}/fonts/inter_ui/Inter-UI-ExtraLight-BETA.woff`,
],
},
{
style: 'italic',
weight: 200,
sources: [
`${url}/fonts/inter_ui/Inter-UI-ExtraLightItalic-BETA.woff2`,
`${url}/fonts/inter_ui/Inter-UI-ExtraLightItalic-BETA.woff`,
],
},
{
style: 'normal',
weight: 300,
sources: [
`${url}/fonts/inter_ui/Inter-UI-Light-BETA.woff2`,
`${url}/fonts/inter_ui/Inter-UI-Light-BETA.woff`,
],
},
{
style: 'italic',
weight: 300,
sources: [
`${url}/fonts/inter_ui/Inter-UI-LightItalic-BETA.woff2`,
`${url}/fonts/inter_ui/Inter-UI-LightItalic-BETA.woff`,
],
},
{
style: 'normal',
weight: 400,
sources: [
`${url}/fonts/inter_ui/Inter-UI-Regular.woff2`,
`${url}/fonts/inter_ui/Inter-UI-Regular.woff`,
],
},
{
style: 'italic',
weight: 400,
sources: [
`${url}/fonts/inter_ui/Inter-UI-Italic.woff2`,
`${url}/fonts/inter_ui/Inter-UI-Italic.woff`,
],
},
{
style: 'normal',
weight: 500,
sources: [
`${url}/fonts/inter_ui/Inter-UI-Medium.woff2`,
`${url}/fonts/inter_ui/Inter-UI-Medium.woff`,
],
},
{
style: 'italic',
weight: 500,
sources: [
`${url}/fonts/inter_ui/Inter-UI-MediumItalic.woff2`,
`${url}/fonts/inter_ui/Inter-UI-MediumItalic.woff`,
],
},
{
style: 'normal',
weight: 600,
sources: [
`${url}/fonts/inter_ui/Inter-UI-SemiBold.woff2`,
`${url}/fonts/inter_ui/Inter-UI-SemiBold.woff`,
],
},
{
style: 'italic',
weight: 600,
sources: [
`${url}/fonts/inter_ui/Inter-UI-SemiBoldItalic.woff2`,
`${url}/fonts/inter_ui/Inter-UI-SemiBoldItalic.woff`,
],
},
{
style: 'normal',
weight: 700,
sources: [
`${url}/fonts/inter_ui/Inter-UI-Bold.woff2`,
`${url}/fonts/inter_ui/Inter-UI-Bold.woff`,
],
},
{
style: 'italic',
weight: 700,
sources: [
`${url}/fonts/inter_ui/Inter-UI-BoldItalic.woff2`,
`${url}/fonts/inter_ui/Inter-UI-BoldItalic.woff`,
],
},
{
style: 'normal',
weight: 800,
sources: [
`${url}/fonts/inter_ui/Inter-UI-ExtraBold.woff2`,
`${url}/fonts/inter_ui/Inter-UI-ExtraBold.woff`,
],
},
{
style: 'italic',
weight: 800,
sources: [
`${url}/fonts/inter_ui/Inter-UI-ExtraBoldItalic.woff2`,
`${url}/fonts/inter_ui/Inter-UI-ExtraBoldItalic.woff`,
],
},
{
style: 'normal',
weight: 900,
sources: [
`${url}/fonts/inter_ui/Inter-UI-Black.woff2`,
`${url}/fonts/inter_ui/Inter-UI-Black.woff`,
],
},
{
style: 'italic',
weight: 900,
sources: [
`${url}/fonts/inter_ui/Inter-UI-BlackItalic.woff2`,
`${url}/fonts/inter_ui/Inter-UI-BlackItalic.woff`,
],
},
],
};
const roboto: FontFace = {
family: 'Roboto Mono',
variants: [
{
style: 'normal',
weight: 400,
format: 'woff2',
sources: [
'Roboto Mono',
'RobotoMono-Regular',
`${url}/fonts/roboto_mono/RobotoMono-Regular.ttf`,
],
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
{
style: 'italic',
weight: 400,
sources: [
'Roboto Mono Italic',
'RobotoMono-Italic',
`${url}/fonts/roboto_mono/RobotoMono-Italic.ttf`,
],
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
{
style: 'normal',
weight: 700,
format: 'woff2',
sources: [
'Roboto Mono Bold',
'RobotoMono-Bold',
`${url}/fonts/roboto_mono/RobotoMono-Bold.ttf`,
],
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
{
style: 'italic',
weight: 700,
format: 'woff2',
sources: [
'Roboto Mono Bold Italic',
'RobotoMono-BoldItalic',
`${url}/fonts/roboto_mono/RobotoMono-BoldItalic.ttf`,
],
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
],
};
return (
<style
dangerouslySetInnerHTML={{
__html: `
${[interUi, roboto]
.flatMap(({ family, variants }) =>
variants.map(({ style, weight, format, sources, unicodeRange }) => {
const src = sources
.map(source =>
source.startsWith(url)
? `url('${source}') format('${format || source.split('.').pop()}')`
: `local('${source}')`
)
.join(', ');
return `
@font-face {
font-family: '${family}';
font-style: ${style};
font-weight: ${weight};
src: ${src};${
unicodeRange
? `
unicode-range: ${unicodeRange};`
: ''
}
}`;
})
)
.join('\n')}
/*
Single variable font.
Note that you may want to do something like this to make sure you're serving
constant fonts to older browsers:
html {
font-family: 'Inter UI', sans-serif;
}
@supports (font-variation-settings: normal) {
html {
font-family: 'Inter UI var', sans-serif;
}
}
BUGS:
- Safari 12.0 will default to italic instead of regular when font-weight
is provided in a @font-face declaration.
Workaround: Use 'Inter UI var alt' for Safari, or explicitly set
\`font-variation-settings: 'slnt' DEGREE\`.
@font-face {
font-family: 'Inter UI var';
font-weight: 100 900;
font-style: oblique 0deg 10deg;
src:
url('${url}/fonts/inter_ui/Inter-UI.var.woff2') format('woff2-variations'),
url('${url}/fonts/inter_ui/Inter-UI.var.woff2') format('woff2');
}
'Inter UI var alt' is recommended for Safari and Edge, for reliable italics.
@supports (font-variation-settings: normal) {
html {
font-family: 'Inter UI var alt', sans-serif;
}
}
@font-face {
font-family: 'Inter UI var alt';
font-weight: 100 900;
font-style: normal;
font-named-instance: 'Regular';
src:
url('${url}/fonts/inter_ui/Inter-UI-upright.var.woff2') format('woff2 supports variations(gvar)'),
url('${url}/fonts/inter_ui/Inter-UI-upright.var.woff2') format('woff2-variations'),
url('${url}/fonts/inter_ui/Inter-UI-upright.var.woff2') format('woff2');
}
@font-face {
font-family: 'Inter UI var alt';
font-weight: 100 900;
font-style: italic;
font-named-instance: 'Italic';
src:
url('${url}/fonts/inter_ui/Inter-UI-italic.var.woff2') format('woff2 supports variations(gvar)'),
url('${url}/fonts/inter_ui/Inter-UI-italic.var.woff2') format('woff2-variations'),
url('${url}/fonts/inter_ui/Inter-UI-italic.var.woff2') format('woff2');
}
*/
`,
}}
/>
);
};

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { mergeVariables } from './merge_variables';
export { Template } from './template';

View file

@ -0,0 +1,175 @@
/*
* 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.
*/
/* eslint-disable react/no-danger */
import React, { FunctionComponent } from 'react';
import { RenderingMetadata } from '../types';
interface Props {
darkMode: RenderingMetadata['darkMode'];
}
export const Styles: FunctionComponent<Props> = ({ darkMode }) => {
const themeBackground = darkMode ? '#25262e' : '#f5f7fa';
return (
<style
dangerouslySetInnerHTML={{
__html: `
* {
box-sizing: border-box;
}
body, html {
width: 100%;
height: 100%;
margin: 0;
background-color: ${themeBackground};
}
.kibanaWelcomeView {
background-color: ${themeBackground};
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.kibanaWelcomeLogo {
width: 60px;
height: 60px;
margin: 10px 0 10px 20px;
background-repeat: no-repeat;
background-size: contain;
/* SVG optimized according to http://codepen.io/tigt/post/optimizing-svgs-in-data-uris */
background-image: url'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzOSIgdmlld0JveD0iMCAwIDMwIDM5Ij4gIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+ICAgIDxwb2x5Z29uIGZpbGw9IiNGMDRFOTgiIHBvaW50cz0iMCAwIDAgMzQuNTQ3IDI5LjkyMiAuMDIiLz4gICAgPHBhdGggZmlsbD0iIzM0Mzc0MSIgZD0iTTAsMTQuNCBMMCwzNC41NDY4IEwxNC4yODcyLDE4LjA2MTIgQzEwLjA0MTYsMTUuNzM4IDUuMTgwNCwxNC40IDAsMTQuNCIvPiAgICA8cGF0aCBmaWxsPSIjMDBCRkIzIiBkPSJNMTcuMzc0MiwxOS45OTY4IEwyLjcyMSwzNi45MDQ4IEwxLjQzMzQsMzguMzg5MiBMMjkuMjYzOCwzOC4zODkyIEMyNy43NjE0LDMwLjgzODggMjMuNDA0MiwyNC4zMjY0IDE3LjM3NDIsMTkuOTk2OCIvPiAgPC9nPjwvc3ZnPg==');
}
.kibanaWelcomeTitle {
color: #000;
font-size: 20px;
font-family: sans-serif;
margin-top: 20px;
animation: fadeIn 1s ease-in-out;
animation-fill-mode: forwards;
opacity: 0;
animation-delay: 1.0s;
}
.kibanaWelcomeText {
font-size: 14px;
font-family: sans-serif;
color: #98a2b3;
animation: fadeIn 1s ease-in-out;
animation-fill-mode: forwards;
opacity: 0;
animation-delay: 1.0s;
}
.kibanaLoaderWrap {
height: 128px;
width: 128px;
position: relative;
margin-top: 40px;
}
.kibanaLoaderWrap + * {
margin-top: 24px;
}
.kibanaLoader {
height: 128px;
width: 128px;
margin: 0 auto;
position: relative;
border: 2px solid transparent;
border-top: 2px solid #017d73;
border-radius: 100%;
display: block;
opacity: 0;
animation: rotation .75s .5s infinite linear, fadeIn 1s .5s ease-in-out forwards;
}
.kibanaWelcomeLogoCircle {
position: absolute;
left: 4px;
top: 4px;
width: 120px;
height: 120px;
padding: 20px;
background-color: #fff;
border-radius: 50%;
animation: bounceIn .5s ease-in-out;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(.1);
}
80% {
opacity: .5;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
`,
}}
/>
);
};

View file

@ -0,0 +1,144 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { FunctionComponent, createElement } from 'react';
import { RenderingMetadata } from '../types';
import { Fonts } from './fonts';
import { Styles } from './styles';
interface Props {
metadata: RenderingMetadata;
}
export const Template: FunctionComponent<Props> = ({
metadata: {
uiPublicUrl,
locale,
darkMode,
injectedMetadata,
i18n,
bootstrapScriptUrl,
strictCsp,
},
}) => {
return (
<html lang={locale}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width" />
<title>Kibana</title>
<Fonts url={uiPublicUrl} />
{/* Favicons (generated from http://realfavicongenerator.net/) */}
<link
rel="apple-touch-icon"
sizes="180x180"
href={`${uiPublicUrl}/favicons/apple-touch-icon.png`}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href={`${uiPublicUrl}/favicons/favicon-32x32.png`}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href={`${uiPublicUrl}/favicons/favicon-16x16.png`}
/>
<link rel="manifest" href={`${uiPublicUrl}/favicons/manifest.json`} />
<link
rel="mask-icon"
color="#e8488b"
href={`${uiPublicUrl}/favicons/safari-pinned-tab.svg`}
/>
<link rel="shortcut icon" href={`${uiPublicUrl}/favicons/favicon.ico`} />
<meta name="msapplication-config" content={`${uiPublicUrl}/favicons/browserconfig.xml`} />
<meta name="theme-color" content="#ffffff" />
<Styles darkMode={darkMode} />
</head>
<body>
{createElement('kbn-csp', {
data: JSON.stringify({ strictCsp }),
})}
{createElement('kbn-injected-metadata', { data: JSON.stringify(injectedMetadata) })}
<div
className="kibanaWelcomeView"
id="kbn_loading_message"
style={{ display: 'none' }}
data-test-subj="kbnLoadingMessage"
>
<div className="kibanaLoaderWrap">
<div className="kibanaLoader" />
<div className="kibanaWelcomeLogoCircle">
<div className="kibanaWelcomeLogo" />
</div>
</div>
<div
className="kibanaWelcomeText"
data-error-message={i18n('core.ui.welcomeErrorMessage', {
defaultMessage:
'Kibana did not load properly. Check the server output for more information.',
})}
>
{i18n('core.ui.welcomeMessage', { defaultMessage: 'Loading Kibana' })}
</div>
</div>
<div
className="kibanaWelcomeView"
id="kbn_legacy_browser_error"
style={{ display: 'none' }}
>
<div className="kibanaLoaderWrap">
<div className="kibanaWelcomeLogoCircle">
<div className="kibanaWelcomeLogo" />
</div>
</div>
<h2 className="kibanaWelcomeTitle">
{i18n('core.ui.legacyBrowserTitle', {
defaultMessage: 'Please upgrade your browser',
})}
</h2>
<div className="kibanaWelcomeText">
{i18n('core.ui.legacyBrowserMessage', {
defaultMessage:
'This Kibana installation has strict security requirements enabled that your current browser does not meet.',
})}
</div>
</div>
<script>
{`
// Since this is an unsafe inline script, this code will not run
// in browsers that support content security policy(CSP). This is
// intentional as we check for the existence of __kbnCspNotEnforced__ in
// bootstrap.
window.__kbnCspNotEnforced__ = true;
`}
</script>
<script src={bootstrapScriptUrl} />
</body>
</html>
);
};

View file

@ -20,7 +20,7 @@
import { createIndexMap } from './build_index_map';
import { ObjectToConfigAdapter } from '../../../config';
import { SavedObjectsSchema } from '../../schema';
import { LegacyConfig } from '../../../legacy/config';
import { LegacyConfig } from '../../../legacy';
const config = (new ObjectToConfigAdapter({}) as unknown) as LegacyConfig;

View file

@ -19,7 +19,7 @@
import { MappingProperties } from '../../mappings';
import { SavedObjectsSchema } from '../../schema';
import { LegacyConfig } from '../../../legacy/config';
import { LegacyConfig } from '../../../legacy';
export interface CreateIndexMapOptions {
config: LegacyConfig;

View file

@ -36,7 +36,7 @@ import {
} from '../core/document_migrator';
import { createIndexMap } from '../core/build_index_map';
import { SavedObjectsConfigType } from '../../saved_objects_config';
import { LegacyConfig } from '../../../legacy/config';
import { LegacyConfig } from '../../../legacy';
export interface KibanaMigratorOptions {
callCluster: CallCluster;

View file

@ -28,7 +28,7 @@ import {
} from './';
import { KibanaMigrator, IKibanaMigrator } from './migrations';
import { CoreContext } from '../core_context';
import { LegacyServiceDiscoverPlugins } from '../legacy/legacy_service';
import { LegacyServiceDiscoverPlugins } from '../legacy';
import { ElasticsearchServiceSetup, APICaller } from '../elasticsearch';
import { KibanaConfigType } from '../kibana_config';
import { migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster';

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { LegacyConfig } from '../../legacy/config';
import { LegacyConfig } from '../../legacy';
interface SavedObjectsSchemaTypeDefinition {
isNamespaceAgnostic: boolean;

View file

@ -52,7 +52,7 @@ import {
MutatingOperationRefreshSetting,
} from '../../types';
import { validateConvertFilterToKueryNode } from './filter_utils';
import { LegacyConfig } from '../../../legacy/config';
import { LegacyConfig } from '../../../legacy';
// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.

View file

@ -20,7 +20,7 @@ import { SavedObjectsRepository } from './repository';
import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock';
import { SavedObjectsSchema } from '../../schema';
import { KibanaMigrator } from '../../migrations';
import { LegacyConfig } from '../../../legacy/config';
import { LegacyConfig } from '../../../legacy';
jest.mock('./repository');
const { SavedObjectsRepository: originalRepository } = jest.requireActual('./repository');

View file

@ -236,7 +236,6 @@ export type SavedObjectsClientContract = Pick<SavedObjectsClient, keyof SavedObj
* @deprecated
*/
export interface SavedObjectsLegacyUiExports {
unknown: [{ pluginSpec: { getId: () => unknown }; type: unknown }] | undefined;
savedObjectMappings: SavedObjectsMapping[];
savedObjectMigrations: MigrationDefinition;
savedObjectSchemas: SavedObjectsSchemaDefinition;

View file

@ -798,6 +798,11 @@ export interface IndexSettingsDeprecationInfo {
[indexName: string]: DeprecationInfo[];
}
// @public (undocumented)
export interface IRenderOptions {
includeUserSettings?: boolean;
}
// @public
export interface IRouter {
delete: RouteRegistrar<'delete'>;
@ -822,6 +827,11 @@ export type ISavedObjectsRepository = Pick<SavedObjectsRepository, keyof SavedOb
// @public
export type IScopedClusterClient = Pick<ScopedClusterClient, 'callAsCurrentUser' | 'callAsInternalUser'>;
// @public (undocumented)
export interface IScopedRenderingClient {
render(options?: IRenderOptions): Promise<string>;
}
// @public
export interface IUiSettingsClient {
get: <T = any>(key: string) => Promise<T>;
@ -897,34 +907,92 @@ export const kibanaResponseFactory: {
// @public
export type KnownHeaders = KnownKeys<IncomingHttpHeaders>;
// @internal @deprecated
export interface LegacyConfig {
// (undocumented)
get<T>(key?: string): T;
// (undocumented)
has(key: string): boolean;
// (undocumented)
set(key: string, value: any): void;
// Warning: (ae-forgotten-export) The symbol "LegacyVars" needs to be exported by the entry point index.d.ts
//
// (undocumented)
set(config: LegacyVars): void;
}
// Warning: (ae-forgotten-export) The symbol "ILegacyInternals" needs to be exported by the entry point index.d.ts
//
// @internal @deprecated (undocumented)
export class LegacyInternals implements ILegacyInternals {
constructor(uiExports: LegacyUiExports, config: LegacyConfig, server: Server);
// (undocumented)
getInjectedUiAppVars(id: string): Promise<Record<string, any>>;
// (undocumented)
getVars(id: string, request: LegacyRequest, injected?: LegacyVars): Promise<Record<string, any>>;
private get defaultVars();
// Warning: (ae-forgotten-export) The symbol "VarsInjector" needs to be exported by the entry point index.d.ts
//
// (undocumented)
injectUiAppVars(id: string, injector: VarsInjector): void;
}
// @internal @deprecated (undocumented)
export interface LegacyRenderOptions extends IRenderOptions {
app?: {
getId(): string;
};
vars?: Record<string, any>;
}
// @public @deprecated (undocumented)
export interface LegacyRequest extends Request {
}
// Warning: (ae-forgotten-export) The symbol "LegacyPlugins" needs to be exported by the entry point index.d.ts
//
// @internal @deprecated (undocumented)
export interface LegacyServiceDiscoverPlugins extends LegacyPlugins {
// (undocumented)
pluginExtendedConfig: LegacyConfig;
// (undocumented)
settings: LegacyVars;
}
// @public @deprecated (undocumented)
export interface LegacyServiceSetupDeps {
// Warning: (ae-forgotten-export) The symbol "InternalCoreSetup" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "LegacyCoreSetup" needs to be exported by the entry point index.d.ts
//
// (undocumented)
core: InternalCoreSetup & {
plugins: PluginsServiceSetup;
};
core: LegacyCoreSetup;
// (undocumented)
plugins: Record<string, unknown>;
}
// @public @deprecated (undocumented)
export interface LegacyServiceStartDeps {
// Warning: (ae-forgotten-export) The symbol "InternalCoreStart" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "LegacyCoreStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
core: InternalCoreStart & {
plugins: PluginsServiceStart;
};
core: LegacyCoreStart;
// (undocumented)
plugins: Record<string, unknown>;
}
// Warning: (ae-forgotten-export) The symbol "SavedObjectsLegacyUiExports" needs to be exported by the entry point index.d.ts
//
// @internal @deprecated (undocumented)
export type LegacyUiExports = SavedObjectsLegacyUiExports & {
defaultInjectedVarProviders?: VarsProvider[];
injectedVarsReplacers?: VarsReplacer[];
navLinkSpecs?: LegacyNavLinkSpec[] | null;
uiAppSpecs?: Array<LegacyAppSpec | undefined>;
unknown?: [{
pluginSpec: LegacyPluginSpec;
type: unknown;
}];
};
// Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts
//
// @public
@ -1161,6 +1229,11 @@ export type RedirectResponseOptions = HttpResponseOptions & {
};
};
// @internal (undocumented)
export interface RenderingServiceSetup {
render<R extends KibanaRequest | LegacyRequest>(request: R, uiSettings: IUiSettingsClient, options?: R extends LegacyRequest ? LegacyRenderOptions : IRenderOptions): Promise<string>;
}
// @public
export type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<P, Q, B, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
@ -1168,6 +1241,7 @@ export type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends
export interface RequestHandlerContext {
// (undocumented)
core: {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
};
@ -1718,7 +1792,6 @@ export class SavedObjectsRepository {
bulkUpdate<T extends SavedObjectAttributes = any>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
create<T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
// Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "LegacyConfig" needs to be exported by the entry point index.d.ts
//
// @internal
static createRepository(migrator: KibanaMigrator, schema: SavedObjectsSchema, config: LegacyConfig, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): any;
@ -1893,6 +1966,11 @@ export const validBodyOutput: readonly ["data", "stream"];
// Warnings were encountered during analysis:
//
// src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:158:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:159:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:160:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:161:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:162:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts

View file

@ -35,14 +35,8 @@ jest.doMock('./elasticsearch/elasticsearch_service', () => ({
ElasticsearchService: jest.fn(() => mockElasticsearchService),
}));
import { ILegacyService } from './legacy/legacy_service';
export const mockLegacyService: ILegacyService = {
legacyId: Symbol(),
discoverPlugins: jest.fn().mockReturnValue({ uiExports: {} }),
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
import { legacyServiceMock } from './legacy/legacy_service.mock';
export const mockLegacyService = legacyServiceMock.create();
jest.mock('./legacy/legacy_service', () => ({
LegacyService: jest.fn(() => mockLegacyService),
}));
@ -76,6 +70,10 @@ jest.doMock('./legacy/config/ensure_valid_configuration', () => ({
ensureValidConfiguration: mockEnsureValidConfiguration,
}));
import { RenderingService, mockRenderingService } from './rendering/__mocks__/rendering_service';
export { mockRenderingService };
jest.doMock('./rendering/rendering_service', () => ({ RenderingService }));
import { uuidServiceMock } from './uuid/uuid_service.mock';
export const mockUuidService = uuidServiceMock.create();
jest.doMock('./uuid/uuid_service', () => ({

View file

@ -27,6 +27,7 @@ import {
mockContextService,
mockEnsureValidConfiguration,
mockUiSettingsService,
mockRenderingService,
} from './server.test.mocks';
import { BehaviorSubject } from 'rxjs';
@ -59,6 +60,7 @@ test('sets up services on "setup"', async () => {
expect(mockLegacyService.setup).not.toHaveBeenCalled();
expect(mockSavedObjectsService.setup).not.toHaveBeenCalled();
expect(mockUiSettingsService.setup).not.toHaveBeenCalled();
expect(mockRenderingService.setup).not.toHaveBeenCalled();
await server.setup();
@ -68,6 +70,7 @@ test('sets up services on "setup"', async () => {
expect(mockLegacyService.setup).toHaveBeenCalledTimes(1);
expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1);
expect(mockRenderingService.setup).toHaveBeenCalledTimes(1);
});
test('injects legacy dependency to context#setup()', async () => {
@ -155,6 +158,7 @@ test(`doesn't setup core services if config validation fails`, async () => {
expect(mockPluginsService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).not.toHaveBeenCalled();
expect(mockUiSettingsService.setup).not.toHaveBeenCalled();
expect(mockRenderingService.setup).not.toHaveBeenCalled();
});
test(`doesn't setup core services if legacy config validation fails`, async () => {

View file

@ -29,6 +29,7 @@ import {
} from './config';
import { ElasticsearchService } from './elasticsearch';
import { HttpService, InternalHttpServiceSetup } from './http';
import { RenderingService, RenderingServiceSetup } from './rendering';
import { LegacyService, ensureValidConfiguration } from './legacy';
import { Logger, LoggerFactory } from './logging';
import { UiSettingsService } from './ui_settings';
@ -44,7 +45,7 @@ import { config as pathConfig } from './path';
import { config as kibanaConfig } from './kibana_config';
import { config as savedObjectsConfig } from './saved_objects';
import { config as uiSettingsConfig } from './ui_settings';
import { mapToObject } from '../utils/';
import { mapToObject } from '../utils';
import { ContextService } from './context';
import { RequestHandlerContext } from '.';
import { InternalCoreSetup } from './internal_types';
@ -60,6 +61,7 @@ export class Server {
private readonly context: ContextService;
private readonly elasticsearch: ElasticsearchService;
private readonly http: HttpService;
private readonly rendering: RenderingService;
private readonly legacy: LegacyService;
private readonly log: Logger;
private readonly plugins: PluginsService;
@ -78,6 +80,7 @@ export class Server {
const core = { coreId, configService: this.configService, env, logger };
this.context = new ContextService(core);
this.http = new HttpService(core);
this.rendering = new RenderingService(core);
this.plugins = new PluginsService(core);
this.legacy = new LegacyService(core);
this.elasticsearch = new ElasticsearchService(core);
@ -144,12 +147,18 @@ export class Server {
const pluginsSetup = await this.plugins.setup(coreSetup);
const renderingSetup = await this.rendering.setup({
http: httpSetup,
legacyPlugins,
plugins: pluginsSetup,
});
await this.legacy.setup({
core: { ...coreSetup, plugins: pluginsSetup },
core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup },
plugins: mapToObject(pluginsSetup.contracts),
});
this.registerCoreContext(coreSetup);
this.registerCoreContext(coreSetup, renderingSetup);
return coreSetup;
}
@ -178,6 +187,7 @@ export class Server {
});
await this.http.start();
await this.rendering.start();
return coreStart;
}
@ -191,6 +201,7 @@ export class Server {
await this.elasticsearch.stop();
await this.http.stop();
await this.uiSettings.stop();
await this.rendering.stop();
}
private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) {
@ -200,16 +211,20 @@ export class Server {
);
}
private registerCoreContext(coreSetup: InternalCoreSetup) {
private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) {
coreSetup.http.registerRouteHandlerContext(
coreId,
'core',
async (context, req): Promise<RequestHandlerContext['core']> => {
async (context, req, res): Promise<RequestHandlerContext['core']> => {
const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise();
const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise();
const savedObjectsClient = coreSetup.savedObjects.getScopedClient(req);
const uiSettingsClient = coreSetup.uiSettings.asScopedToClient(savedObjectsClient);
return {
rendering: {
render: rendering.render.bind(rendering, req, uiSettingsClient),
},
savedObjects: {
// Note: the client provider doesn't support new ES clients
// emitted from adminClient$
@ -220,7 +235,7 @@ export class Server {
dataClient: dataClient.asScoped(req),
},
uiSettings: {
client: coreSetup.uiSettings.asScopedToClient(savedObjectsClient),
client: uiSettingsClient,
},
};
}

View file

@ -21,5 +21,6 @@
export { PluginOpaqueId } from './plugins/types';
export * from './saved_objects/types';
export * from './ui_settings/types';
export * from './legacy/types';
export { EnvironmentMode, PackageInfo } from './config/types';
export { ICspConfig } from './csp';

View file

@ -37,6 +37,9 @@ const createClientMock = () => {
isOverridden: jest.fn(),
};
mocked.get.mockResolvedValue(false);
mocked.getAll.mockResolvedValue({});
mocked.getRegistered.mockReturnValue({});
mocked.getUserProvided.mockResolvedValue({});
return mocked;
};

View file

@ -91,8 +91,8 @@ const telemetry = (kibana: any) => {
isNamespaceAgnostic: true,
},
},
async replaceInjectedVars(originalInjectedVars: any, request: any) {
const telemetryInjectedVars = await replaceTelemetryInjectedVars(request);
async replaceInjectedVars(originalInjectedVars: any, request: any, server: any) {
const telemetryInjectedVars = await replaceTelemetryInjectedVars(request, server);
return Object.assign({}, originalInjectedVars, telemetryInjectedVars);
},
injectDefaultVars(server: Server) {

View file

@ -23,8 +23,8 @@ import { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
import { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
import { getNotifyUserAboutOptInDefault } from './get_telemetry_notify_user_about_optin_default';
export async function replaceTelemetryInjectedVars(request: any) {
const config = request.server.config();
export async function replaceTelemetryInjectedVars(request: any, server: any) {
const config = server.config();
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
const configTelemetryOptIn = config.get('telemetry.optIn');
const configTelemetryAllowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
@ -38,7 +38,7 @@ export async function replaceTelemetryInjectedVars(request: any) {
}
const currentKibanaVersion = config.get('pkg.version');
const savedObjectsClient = request.getSavedObjectsClient();
const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient(request);
const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsClient);
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus,

View file

@ -20,24 +20,29 @@
import { ResponseObject, Server } from 'hapi';
import { UnwrapPromise } from '@kbn/utility-types';
import { SavedObjectsClientProviderOptions, CoreSetup, CoreStart } from 'src/core/server';
import {
ConfigService,
CoreSetup,
CoreStart,
ElasticsearchServiceSetup,
EnvironmentMode,
LoggerFactory,
SavedObjectsClientContract,
SavedObjectsLegacyService,
SavedObjectsClientProviderOptions,
IUiSettingsClient,
PackageInfo,
LegacyRequest,
LegacyServiceSetupDeps,
LegacyServiceStartDeps,
LegacyServiceDiscoverPlugins,
} from '../../core/server';
import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from '../../core/server/';
// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsManagement } from '../../core/server/saved_objects/management';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LegacyConfig } from '../../core/server/legacy';
import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy';
import { ApmOssPlugin } from '../core_plugins/apm_oss';
import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
import { UsageCollectionSetup } from '../../plugins/usage_collection/server';
@ -104,38 +109,61 @@ export interface PluginsSetup {
[key: string]: object;
}
// eslint-disable-next-line import/no-default-export
export default class KbnServer {
public readonly newPlatform: {
export interface KibanaCore {
__internals: {
hapiServer: LegacyServiceSetupDeps['core']['http']['server'];
uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins'];
elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch'];
uiSettings: LegacyServiceSetupDeps['core']['uiSettings'];
hapiServer: LegacyServiceSetupDeps['core']['http']['server'];
kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator'];
legacy: ILegacyInternals;
rendering: LegacyServiceSetupDeps['core']['rendering'];
uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins'];
uiSettings: LegacyServiceSetupDeps['core']['uiSettings'];
savedObjectsClientProvider: LegacyServiceStartDeps['core']['savedObjects']['clientProvider'];
};
env: {
mode: Readonly<EnvironmentMode>;
packageInfo: Readonly<PackageInfo>;
};
coreContext: {
logger: LoggerFactory;
};
setup: {
setupDeps: {
core: CoreSetup;
plugins: PluginsSetup;
};
start: {
core: CoreStart;
startDeps: {
core: CoreSetup;
plugins: Record<string, object>;
};
stop: null;
logger: LoggerFactory;
}
export interface NewPlatform {
__internals: KibanaCore['__internals'];
env: KibanaCore['env'];
coreContext: {
logger: KibanaCore['logger'];
};
setup: KibanaCore['setupDeps'];
start: KibanaCore['startDeps'];
stop: null;
}
export type LegacyPlugins = Pick<
LegacyServiceDiscoverPlugins,
'pluginSpecs' | 'disabledPluginSpecs' | 'uiExports'
>;
// eslint-disable-next-line import/no-default-export
export default class KbnServer {
public readonly newPlatform: NewPlatform;
public server: Server;
public inject: Server['inject'];
public pluginSpecs: any[];
constructor(settings: any, core: any);
constructor(
settings: Record<string, any>,
config: KibanaConfig,
core: KibanaCore,
legacyPlugins: LegacyPlugins
);
public ready(): Promise<void>;
public mixin(...fns: KbnMixinFunc[]): Promise<void>;

View file

@ -42,9 +42,21 @@ import { uiMixin } from '../ui';
import { sassMixin } from './sass';
import { i18nMixin } from './i18n';
/**
* @typedef {import('./kbn_server').KibanaConfig} KibanaConfig
* @typedef {import('./kbn_server').KibanaCore} KibanaCore
* @typedef {import('./kbn_server').LegacyPlugins} LegacyPlugins
*/
const rootDir = fromRoot('.');
export default class KbnServer {
/**
* @param {Record<string, any>} settings
* @param {KibanaConfig} config
* @param {KibanaCore} core
* @param {LegacyPlugins} legacyPlugins
*/
constructor(settings, config, core, legacyPlugins) {
this.name = pkg.name;
this.version = pkg.version;

View file

@ -1,160 +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 { resolve } from 'path';
import { delay } from 'bluebird';
import expect from '@kbn/expect';
import sinon from 'sinon';
import cheerio from 'cheerio';
import { noop } from 'lodash';
import * as getUiSettingsServiceForRequestNS from '../ui_settings/ui_settings_service_for_request';
import { createRoot, getKbnServer, request } from '../../../test_utils/kbn_server';
const getInjectedVarsFromResponse = resp => {
expect(resp.statusCode).to.be(200);
const $ = cheerio.load(resp.text);
const data = $('kbn-injected-metadata').attr('data');
return JSON.parse(data).vars;
};
const injectReplacer = (kbnServer, replacer) => {
// normally the replacer would be defined in a plugin's uiExports,
// but that requires stubbing out an entire plugin directory for
// each test, so we fake it and jam the replacer into uiExports
const { injectedVarsReplacers = [] } = kbnServer.uiExports;
kbnServer.uiExports.injectedVarsReplacers = [...injectedVarsReplacers, replacer];
};
describe('UiExports', function() {
const sandbox = sinon.createSandbox();
let root;
let kbnServer;
before(async () => {
this.slow(2000);
this.timeout(30000);
root = root = createRoot({
// inject an app so we can hit /app/{id}
plugins: { paths: [resolve(__dirname, './fixtures/test_app')] },
});
await root.setup();
await root.start();
kbnServer = getKbnServer(root);
// Mock out the ui settings which depends on ES
sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest').returns({
getRegistered: noop,
getUserProvided: noop,
});
});
after(async () => {
await root.shutdown();
sandbox.restore();
});
let originalInjectedVarsReplacers;
beforeEach(() => {
originalInjectedVarsReplacers = kbnServer.uiExports.injectedVarsReplacers;
});
afterEach(() => {
kbnServer.uiExports.injectedVarsReplacers = originalInjectedVarsReplacers;
});
describe('#replaceInjectedVars', function() {
it('allows sync replacing of injected vars', async () => {
injectReplacer(kbnServer, () => ({ a: 1 }));
const resp = await request.get(root, '/app/test_app').expect(200);
const injectedVars = getInjectedVarsFromResponse(resp);
expect(injectedVars).to.eql({ a: 1 });
});
it('allows async replacing of injected vars', async () => {
const asyncThing = () => delay(100).return('world');
injectReplacer(kbnServer, async () => {
return {
hello: await asyncThing(),
};
});
const resp = await request.get(root, '/app/test_app').expect(200);
const injectedVars = getInjectedVarsFromResponse(resp);
expect(injectedVars).to.eql({
hello: 'world',
});
});
it('passes originalInjectedVars, request, and server to replacer', async () => {
const stub = sinon.stub();
injectReplacer(kbnServer, () => ({ foo: 'bar' }));
injectReplacer(kbnServer, stub);
await await request.get(root, '/app/test_app').expect(200);
sinon.assert.calledOnce(stub);
expect(stub.firstCall.args[0]).to.eql({ foo: 'bar' }); // originalInjectedVars
expect(stub.firstCall.args[1]).to.have.property('path', '/app/test_app'); // request
expect(stub.firstCall.args[1]).to.have.property('server', kbnServer.server); // request
expect(stub.firstCall.args[2]).to.be(kbnServer.server);
});
it('calls the methods sequentially', async () => {
injectReplacer(kbnServer, () => ({ name: '' }));
injectReplacer(kbnServer, orig => ({ name: orig.name + 's' }));
injectReplacer(kbnServer, orig => ({ name: orig.name + 'a' }));
injectReplacer(kbnServer, orig => ({ name: orig.name + 'm' }));
const resp = await request.get(root, '/app/test_app').expect(200);
const injectedVars = getInjectedVarsFromResponse(resp);
expect(injectedVars).to.eql({ name: 'sam' });
});
it('propagates errors thrown in replacers', async () => {
injectReplacer(kbnServer, async () => {
await delay(100);
throw new Error('replacer failed');
});
await request.get(root, '/app/test_app').expect(500);
});
it('starts off with the injected vars for the app merged with the default injected vars', async () => {
const stub = sinon.stub();
injectReplacer(kbnServer, stub);
await request.get(root, '/app/test_app').expect(200);
sinon.assert.calledOnce(stub);
const args = stub.lastCall.args[0];
expect(args.from_defaults).to.be(true);
expect(args.from_test_app).to.be(true);
});
});
});

View file

@ -19,6 +19,15 @@
import { UiApp } from './ui_app';
/**
* @typedef {import('../../server/kbn_server').default} KbnServer
*/
/**
*
* @param {KbnServer} kbnServer
* @param {KbnServer['server']} server
*/
export function uiAppsMixin(kbnServer, server) {
const { uiAppSpecs = [] } = kbnServer.uiExports;
const existingIds = new Set();
@ -47,21 +56,12 @@ export function uiAppsMixin(kbnServer, server) {
server.decorate('server', 'getAllUiApps', () => kbnServer.uiApps.slice(0));
server.decorate('server', 'getUiAppById', id => appsById.get(id));
server.decorate('server', 'getHiddenUiAppById', id => hiddenAppsById.get(id));
const injectedVarProviders = [];
server.decorate('server', 'injectUiAppVars', (appId, provider) => {
injectedVarProviders.push({ appId, provider });
});
server.decorate('server', 'getInjectedUiAppVars', async appId => {
return await injectedVarProviders
.filter(p => p.appId === appId)
.reduce(
async (acc, { provider }) => ({
...(await acc),
...(await provider(server)),
}),
{}
server.decorate('server', 'injectUiAppVars', (appId, provider) =>
kbnServer.newPlatform.__internals.legacy.injectUiAppVars(appId, provider)
);
server.decorate(
'server',
'getInjectedUiAppVars',
async appId => await kbnServer.newPlatform.__internals.legacy.getInjectedUiAppVars(appId)
);
});
}

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { LegacyInternals } from '../../../core/server';
import { uiAppsMixin } from './ui_apps_mixin';
jest.mock('./ui_app', () => ({
@ -37,10 +39,10 @@ jest.mock('./ui_app', () => ({
describe('UiAppsMixin', () => {
let kbnServer;
let server;
let uiExports;
beforeEach(() => {
kbnServer = {
uiExports: {
uiExports = {
uiAppSpecs: [
{
id: 'foo',
@ -51,9 +53,7 @@ describe('UiAppsMixin', () => {
hidden: false,
},
],
},
};
server = {
decorate: jest.fn((type, name, value) => {
if (type !== 'server') {
@ -63,6 +63,14 @@ describe('UiAppsMixin', () => {
server[name] = value;
}),
};
kbnServer = {
uiExports,
newPlatform: {
__internals: {
legacy: new LegacyInternals(uiExports, {}, server),
},
},
};
uiAppsMixin(kbnServer, server);
});

View file

@ -42,7 +42,7 @@ import { CoreSystem } from '__kibanaCore__'
const injectedMetadata = JSON.parse(document.querySelector('kbn-injected-metadata').getAttribute('data'));
${apmInit('injectedMetadata.apm')}
${apmInit('injectedMetadata.vars.apmConfig')}
i18n.load(injectedMetadata.i18n.translationsUrl)
.catch(e => e)

View file

@ -17,8 +17,8 @@
* under the License.
*/
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsLegacyUiExports } from 'src/core/server/types';
import { LegacyUiExports } from '../../../core/server';
// @ts-ignore
import { UI_EXPORT_DEFAULTS } from './ui_export_defaults';
// @ts-ignore
@ -26,6 +26,6 @@ import * as uiExportTypeReducers from './ui_export_types';
// @ts-ignore
import { reduceExportSpecs } from '../../plugin_discovery';
export function collectUiExports(pluginSpecs: unknown[]): SavedObjectsLegacyUiExports {
export function collectUiExports(pluginSpecs: unknown[]): LegacyUiExports {
return reduceExportSpecs(pluginSpecs, uiExportTypeReducers, UI_EXPORT_DEFAULTS);
}

View file

@ -21,7 +21,6 @@ import { fieldFormatsMixin } from './field_formats';
import { tutorialsMixin } from './tutorials_mixin';
import { uiAppsMixin } from './ui_apps';
import { uiBundlesMixin } from './ui_bundles';
import { uiNavLinksMixin } from './ui_nav_links';
import { uiRenderMixin } from './ui_render';
import { uiSettingsMixin } from './ui_settings';
@ -31,6 +30,5 @@ export async function uiMixin(kbnServer) {
await kbnServer.mixin(uiSettingsMixin);
await kbnServer.mixin(fieldFormatsMixin);
await kbnServer.mixin(tutorialsMixin);
await kbnServer.mixin(uiNavLinksMixin);
await kbnServer.mixin(uiRenderMixin);
}

View file

@ -18,4 +18,3 @@
*/
export { UiNavLink } from './ui_nav_link';
export { uiNavLinksMixin } from './ui_nav_links_mixin';

View file

@ -17,43 +17,27 @@
* under the License.
*/
import { take } from 'rxjs/operators';
import { createHash } from 'crypto';
import { props, reduce as reduceAsync } from 'bluebird';
import Boom from 'boom';
import { resolve } from 'path';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { AppBootstrap } from './bootstrap';
import { mergeVariables } from './lib';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { fromRoot } from '../../../core/server/utils';
import { getApmConfig } from '../apm';
/**
* @typedef {import('../../server/kbn_server').default} KbnServer
* @typedef {import('../../server/kbn_server').ResponseToolkit} ResponseToolkit
*/
/**
*
* @param {KbnServer} kbnServer
* @param {KbnServer['server']} server
* @param {KbnServer['config']} config
*/
export function uiRenderMixin(kbnServer, server, config) {
function replaceInjectedVars(request, injectedVars) {
const { injectedVarsReplacers = [] } = kbnServer.uiExports;
return reduceAsync(
injectedVarsReplacers,
async (acc, replacer) => await replacer(acc, request, kbnServer.server),
injectedVars
);
}
let defaultInjectedVars = {};
kbnServer.afterPluginsInit(() => {
const { defaultInjectedVarProviders = [] } = kbnServer.uiExports;
defaultInjectedVars = defaultInjectedVarProviders.reduce(
(allDefaults, { fn, pluginSpec }) =>
mergeVariables(
allDefaults,
fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, []))
),
{}
);
});
// render all views from ./views
server.setupViews(resolve(__dirname, 'views'));
@ -192,121 +176,41 @@ export function uiRenderMixin(kbnServer, server, config) {
},
});
async function getUiSettings({ request, includeUserProvidedConfig }) {
const uiSettings = request.getUiSettingsService();
return props({
defaults: uiSettings.getRegistered(),
user: includeUserProvidedConfig && uiSettings.getUserProvided(),
});
}
function getLegacyKibanaPayload({ app, basePath, uiSettings }) {
return {
app,
bundleId: `app:${app.getId()}`,
nav: server.getUiNavLinks(),
version: kbnServer.version,
branch: config.get('pkg.branch'),
buildNum: config.get('pkg.buildNum'),
buildSha: config.get('pkg.buildSha'),
serverName: config.get('server.name'),
devMode: config.get('env.dev'),
basePath,
uiSettings,
};
}
async function renderApp({
app,
async function renderApp(
h,
includeUserProvidedConfig = true,
injectedVarsOverrides = {},
}) {
const request = h.request;
const basePath = request.getBasePath();
const uiSettings = await getUiSettings({ request, includeUserProvidedConfig });
app = app || { getId: () => 'core' };
const legacyMetadata = getLegacyKibanaPayload({
app = { getId: () => 'core' },
includeUserSettings = true,
overrides = {}
) {
const { http } = kbnServer.newPlatform.setup.core;
const {
rendering,
legacy,
savedObjectsClientProvider: savedObjects,
uiSettings: { asScopedToClient },
} = kbnServer.newPlatform.__internals;
const uiSettings = asScopedToClient(savedObjects.getClient(h.request));
const vars = await legacy.getVars(app.getId(), h.request, {
apmConfig: getApmConfig(app),
...overrides,
});
const content = await rendering.render(h.request, uiSettings, {
app,
basePath,
uiSettings,
includeUserSettings,
vars,
});
// Get the list of new platform plugins.
// Convert the Map into an array of objects so it is JSON serializable and order is preserved.
const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPlugins.browserConfigs;
const uiPlugins = await Promise.all(
[...kbnServer.newPlatform.__internals.uiPlugins.public.entries()].map(
async ([id, plugin]) => {
const config$ = uiPluginConfigs.get(id);
if (config$) {
return { id, plugin, config: await config$.pipe(take(1)).toPromise() };
} else {
return { id, plugin, config: {} };
}
}
)
);
const { strict, warnLegacyBrowsers, header } = kbnServer.newPlatform.setup.core.http.csp;
const response = h.view('ui_app', {
strictCsp: strict,
uiPublicUrl: `${basePath}/ui`,
bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`,
i18n: (id, options) => i18n.translate(id, options),
locale: i18n.getLocale(),
darkMode: get(uiSettings.user, ['theme:darkMode', 'userValue'], false),
injectedMetadata: {
version: kbnServer.version,
buildNumber: config.get('pkg.buildNum'),
branch: config.get('pkg.branch'),
basePath,
env: kbnServer.newPlatform.env,
legacyMode: app.getId() !== 'core',
i18n: {
translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`,
},
csp: {
warnLegacyBrowsers,
},
vars: await replaceInjectedVars(
request,
mergeVariables(
injectedVarsOverrides,
app ? await server.getInjectedUiAppVars(app.getId()) : {},
defaultInjectedVars
)
),
uiPlugins,
legacyMetadata,
apm: getApmConfig(legacyMetadata.app),
},
});
response.header('content-security-policy', header);
return response;
return h
.response(content)
.type('text/html')
.header('content-security-policy', http.csp.header);
}
server.decorate('toolkit', 'renderApp', function(app, injectedVarsOverrides) {
return renderApp({
app,
h: this,
includeUserProvidedConfig: true,
injectedVarsOverrides,
});
server.decorate('toolkit', 'renderApp', function(app, overrides) {
return renderApp(this, app, true, overrides);
});
server.decorate('toolkit', 'renderAppWithDefaultConfig', function(app) {
return renderApp({
app,
h: this,
includeUserProvidedConfig: false,
});
return renderApp(this, app, false);
});
}

View file

@ -1,305 +0,0 @@
block vars
doctype html
html(lang=locale)
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1')
meta(name='viewport', content='width=device-width')
title Kibana
style.
/* INTER UI FONT */
/* INTER UI FONT */
/* INTER UI FONT */
/* INTER UI FONT */
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 100;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Thin-BETA.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Thin-BETA.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 100;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ThinItalic-BETA.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ThinItalic-BETA.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 200;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraLight-BETA.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraLight-BETA.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 200;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraLightItalic-BETA.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraLightItalic-BETA.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 300;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Light-BETA.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Light-BETA.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 300;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-LightItalic-BETA.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-LightItalic-BETA.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 400;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Regular.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Regular.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 400;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Italic.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Italic.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 500;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Medium.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Medium.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 500;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-MediumItalic.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-MediumItalic.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 600;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-SemiBold.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-SemiBold.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 600;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-SemiBoldItalic.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-SemiBoldItalic.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 700;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Bold.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Bold.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 700;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-BoldItalic.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-BoldItalic.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 800;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraBold.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraBold.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 800;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraBoldItalic.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-ExtraBoldItalic.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: normal;
font-weight: 900;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Black.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-Black.woff") format("woff");
}
@font-face {
font-family: 'Inter UI';
font-style: italic;
font-weight: 900;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-BlackItalic.woff2") format("woff2"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-BlackItalic.woff") format("woff");
}
/* --------------------------------------------------------------------------
Single variable font.
Note that you may want to do something like this to make sure you're serving
constant fonts to older browsers:
html {
font-family: 'Inter UI', sans-serif;
}
@supports (font-variation-settings: normal) {
html {
font-family: 'Inter UI var', sans-serif;
}
}
BUGS:
- Safari 12.0 will default to italic instead of regular when font-weight
is provided in a @font-face declaration.
Workaround: Use "Inter UI var alt" for Safari, or explicitly set
`font-variation-settings:"slnt" DEGREE`.
@font-face {
font-family: 'Inter UI var';
font-weight: 100 900;
font-style: oblique 0deg 10deg;
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI.var.woff2") format("woff2-variations"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI.var.woff2") format("woff2");
}
"Inter UI var alt" is recommended for Safari and Edge, for reliable italics.
@supports (font-variation-settings: normal) {
html {
font-family: 'Inter UI var alt', sans-serif;
}
}
@font-face {
font-family: 'Inter UI var alt';
font-weight: 100 900;
font-style: normal;
font-named-instance: 'Regular';
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-upright.var.woff2") format("woff2 supports variations(gvar)"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-upright.var.woff2") format("woff2-variations"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-upright.var.woff2") format("woff2");
}
@font-face {
font-family: 'Inter UI var alt';
font-weight: 100 900;
font-style: italic;
font-named-instance: 'Italic';
src: url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-italic.var.woff2") format("woff2 supports variations(gvar)"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-italic.var.woff2") format("woff2-variations"),
url("#{uiPublicUrl}/fonts/inter_ui/Inter-UI-italic.var.woff2") format("woff2");
}
*/
/* ROBOTO MONO FONTS */
/* ROBOTO MONO FONTS */
/* ROBOTO MONO FONTS */
/* ROBOTO MONO FONTS */
/* ROBOTO MONO FONTS */
@font-face {
font-family: 'Roboto Mono';
font-style: italic;
font-weight: 400;
src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url("#{uiPublicUrl}/fonts/roboto_mono/RobotoMono-Italic.ttf") format('ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Roboto Mono';
font-style: italic;
font-weight: 700;
src: local('Roboto Mono Bold Italic'), local('RobotoMono-BoldItalic'), url("#{uiPublicUrl}/fonts/roboto_mono/RobotoMono-BoldItalic.ttf") format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: local('Roboto Mono'), local('RobotoMono-Regular'), url("#{uiPublicUrl}/fonts/roboto_mono/RobotoMono-Regular.ttf") format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url("#{uiPublicUrl}/fonts/roboto_mono/RobotoMono-Bold.ttf") format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
//- Favicons (generated from http://realfavicongenerator.net/)
link(
rel='apple-touch-icon' sizes='180x180' href=`${uiPublicUrl}/favicons/apple-touch-icon.png`
)
link(
rel='icon' type='image/png' href=`${uiPublicUrl}/favicons/favicon-32x32.png` sizes='32x32'
)
link(
rel='icon' type='image/png' href=`${uiPublicUrl}/favicons/favicon-16x16.png` sizes='16x16'
)
link(
rel='manifest' href=`${uiPublicUrl}/favicons/manifest.json`
)
link(
rel='mask-icon' href=`${uiPublicUrl}/favicons/safari-pinned-tab.svg` color='#e8488b'
)
link(
rel='shortcut icon' href=`${uiPublicUrl}/favicons/favicon.ico`
)
meta(
name='msapplication-config' content=`${uiPublicUrl}/favicons/browserconfig.xml`
)
meta(
name='theme-color' content='#ffffff'
)
style.
.kibanaWelcomeView {
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
background: #FFFFFF;
}
.kibanaWelcomeLogo {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: contain;
/* SVG optimized according to http://codepen.io/tigt/post/optimizing-svgs-in-data-uris */
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OSIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDQ5IDY0Ij4KICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgPHBhdGggZmlsbD0iIzNFQkVCMCIgZD0iTTEuNDE2MzE1NzMsNjQgTDQ4LjY0MDA4MTEsNjQgQzQ4LjY0MDA4MTEsNTEuMTI2NCA0MS4yMzYyNjI0LDM5LjY4NDggMjkuNzQ3NDk0NCwzMi4zNzA0IEwxLjQxNjMxNTczLDY0IFoiLz4KICAgIDxwYXRoIGZpbGw9IiMzN0E1OTUiIGQ9Ik0wLDQxLjYgTDAsNjQgTDMuMDM3NTY4LDY0IEwyOS43NTA2NTYsMzIuMzY1NiBDMjcuOTI1ODQ1MywzMS4yMDMyIDI1Ljk5MjQwNTMsMzAuMTUyIDIzLjk3NTQ2NjcsMjkuMjA4IEwwLDQxLjYgWiIvPgogICAgPHBhdGggZmlsbD0iIzM1MzUzNCIgZD0iTTAsMjAuOCBMMCw1Ny42IEwyMy45Nzk1MiwyOS4yMDMyIEMxNi45MDA3Nzg3LDI1Ljg5NzYgMjQuOTM1Mjk2LDIyLjQgMTYuMjEzMzMzMywyMi40IEwwLDIwLjggWiIvPgogICAgPHBhdGggZmlsbD0iI0U5NDc4QiIgZD0iTTQ4LjY0LDAgTDAsMCBMMCwyNCBDOC43MjE5NjI2NywyNCAxNi45MDA3Nzg3LDI1Ljg5NzYgMjMuOTc5NTIsMjkuMjAzMiBMNDguNjQsMCBaIi8+CiAgPC9nPgo8L3N2Zz4K");
}
block head
body
kbn-csp(data=JSON.stringify({ strictCsp }))
kbn-injected-metadata(data=JSON.stringify(injectedMetadata))
block content

View file

@ -1,140 +0,0 @@
extends ./chrome
block content
style.
* {
box-sizing: border-box;
}
body, html {
width: 100%;
height: 100%;
margin: 0;
background-color: #{darkMode ? '#25262E' : '#F5F7FA'};
}
.kibanaWelcomeView {
background-color: #{darkMode ? '#25262E' : '#F5F7FA'};
}
.kibanaWelcomeTitle {
color: #000;
font-size: 20px;
font-family: Sans-serif;
margin-top: 20px;
animation: fadeIn 1s ease-in-out;
animation-fill-mode: forwards;
opacity: 0;
animation-delay: 1.0s;
}
.kibanaWelcomeText {
font-size: 14px;
font-family: Sans-serif;
color: #98A2B3;
animation: fadeIn 1s ease-in-out;
animation-fill-mode: forwards;
opacity: 0;
animation-delay: 1.0s;
}
.kibanaLoaderWrap {
height: 128px;
width: 128px;
position: relative;
margin-top: 40px;
}
.kibanaLoaderWrap + * {
margin-top: 24px;
}
.kibanaLoader {
height: 128px;
width: 128px;
margin: 0 auto;
position: relative;
border: 2px solid transparent;
border-top: 2px solid #017D73;
border-radius: 100%;
display: block;
opacity: 0;
animation: rotation .75s .5s infinite linear, fadeIn 1s .5s ease-in-out forwards;
}
.kibanaWelcomeLogoCircle {
position: absolute;
left: 4px;
top: 4px;
width: 120px;
height: 120px;
padding: 20px;
background-color: #FFF;
border-radius: 50%;
animation: bounceIn .5s ease-in-out;
}
.kibanaWelcomeLogo {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzOSIgdmlld0JveD0iMCAwIDMwIDM5Ij4gIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+ICAgIDxwb2x5Z29uIGZpbGw9IiNGMDRFOTgiIHBvaW50cz0iMCAwIDAgMzQuNTQ3IDI5LjkyMiAuMDIiLz4gICAgPHBhdGggZmlsbD0iIzM0Mzc0MSIgZD0iTTAsMTQuNCBMMCwzNC41NDY4IEwxNC4yODcyLDE4LjA2MTIgQzEwLjA0MTYsMTUuNzM4IDUuMTgwNCwxNC40IDAsMTQuNCIvPiAgICA8cGF0aCBmaWxsPSIjMDBCRkIzIiBkPSJNMTcuMzc0MiwxOS45OTY4IEwyLjcyMSwzNi45MDQ4IEwxLjQzMzQsMzguMzg5MiBMMjkuMjYzOCwzOC4zODkyIEMyNy43NjE0LDMwLjgzODggMjMuNDA0MiwyNC4zMjY0IDE3LjM3NDIsMTkuOTk2OCIvPiAgPC9nPjwvc3ZnPg==");
background-repeat: no-repeat;
background-size: contain;
width: 60px;
height: 60px;
margin: 10px 0px 10px 20px;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(.1);
}
80% {
opacity: .5;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.kibanaWelcomeView(id="kbn_loading_message", style="display: none;", data-test-subj="kbnLoadingMessage")
.kibanaLoaderWrap
.kibanaLoader
.kibanaWelcomeLogoCircle
.kibanaWelcomeLogo
.kibanaWelcomeText(data-error-message=i18n('common.ui.welcomeErrorMessage', { defaultMessage: 'Kibana did not load properly. Check the server output for more information.' }))
| #{i18n('common.ui.welcomeMessage', { defaultMessage: 'Loading Kibana' })}
.kibanaWelcomeView(id="kbn_legacy_browser_error", style="display: none;")
.kibanaLoaderWrap
.kibanaWelcomeLogoCircle
.kibanaWelcomeLogo
h2.kibanaWelcomeTitle
| #{i18n('common.ui.legacyBrowserTitle', { defaultMessage: 'Please upgrade your browser' })}
.kibanaWelcomeText
| #{i18n('common.ui.legacyBrowserMessage', { defaultMessage: 'This Kibana installation has strict security requirements enabled that your current browser does not meet.' })}
script.
// Since this is an unsafe inline script, this code will not run
// in browsers that support content security policy(CSP). This is
// intentional as we check for the existence of __kbnCspNotEnforced__ in
// bootstrap.
window.__kbnCspNotEnforced__ = true;
script(src=bootstrapScriptUrl)

View file

@ -23,6 +23,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
import {
CoreSetup,
CoreStart,
LegacyRenderOptions,
Logger,
PluginInitializerContext,
PluginConfigDescriptor,
@ -77,6 +78,29 @@ class Plugin {
}
);
router.get(
{
path: '/requestcontext/render/{id}',
validate: {
params: schema.object({
id: schema.maybe(schema.string()),
}),
},
},
async (context, req, res) => {
const { id } = req.params;
const options: Partial<LegacyRenderOptions> = { app: { getId: () => id! } };
const body = await context.core.rendering.render(options);
return res.ok({
body,
headers: {
'content-securty-policy': core.http.csp.header,
},
});
}
);
return {
data$: this.initializerContext.config.create<ConfigType>().pipe(
map(configValue => {

View file

@ -33,6 +33,11 @@ export default function({ getService }) {
200,
'SavedObjects client: {"page":1,"per_page":20,"total":0,"saved_objects":[]}'
));
it('provides access to application rendering client', async () => {
await supertest.get('/requestcontext/render/core').expect(200, /app:core/);
await supertest.get('/requestcontext/render/testbed').expect(200, /app:testbed/);
});
});
describe('compression', () => {

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PluginFunctionalProviderContext } from '../../services';
// eslint-disable-next-line import/no-default-export
@ -53,5 +54,15 @@ export default function({ getService }: PluginFunctionalProviderContext) {
statusCode: 400,
});
});
it('renders core application explicitly', async () => {
await supertest.get('/requestcontext/render/core').expect(200, /app:core/);
});
it('renders legacy application', async () => {
await supertest
.get('/requestcontext/render/core_plugin_legacy')
.expect(200, /app:core_plugin_legacy/);
});
});
}

View file

@ -115,7 +115,7 @@ export class Plugin {
);
}
core.injectUiAppVars('monitoring', core => {
core.injectUiAppVars('monitoring', () => {
const config = core.config();
return {
maxBucketSize: config.get('xpack.monitoring.max_bucket_size'),

View file

@ -440,8 +440,6 @@
"common.ui.flotCharts.thuLabel": "木",
"common.ui.flotCharts.tueLabel": "火",
"common.ui.flotCharts.wedLabel": "水",
"common.ui.legacyBrowserMessage": "この Kibana インストレーションは、現在ご使用のブラウザが満たしていない厳格なセキュリティ要件が有効になっています。",
"common.ui.legacyBrowserTitle": "ブラウザをアップグレードしてください",
"common.ui.management.breadcrumb": "管理",
"management.connectDataDisplayName": "データに接続",
"management.displayName": "管理",
@ -521,8 +519,6 @@
"common.ui.vislib.colormaps.redsText": "赤",
"common.ui.vislib.colormaps.yellowToRedText": "黄色から赤",
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした",
"common.ui.welcomeErrorMessage": "Kibana が正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。",
"common.ui.welcomeMessage": "Kibana を読み込み中",
"common.ui.directives.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報図形",
"common.ui.vis.editors.agg.errorsAriaLabel": "集約にエラーがあります",
"common.ui.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr}).構成されている最高値は {max} です。",
@ -543,6 +539,10 @@
"core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近のアイテム",
"core.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近閲覧",
"core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel}、タイプ: {pageType}",
"core.ui.legacyBrowserMessage": "この Kibana インストレーションは、現在ご使用のブラウザが満たしていない厳格なセキュリティ要件が有効になっています。",
"core.ui.legacyBrowserTitle": "ブラウザをアップグレードしてください",
"core.ui.welcomeErrorMessage": "Kibana が正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。",
"core.ui.welcomeMessage": "Kibana を読み込み中",
"core.chrome.legacyBrowserWarning": "ご使用のブラウザが Kibana のセキュリティ要件を満たしていません。",
"core.euiBasicTable.selectAllRows": "すべての行を選択",
"core.euiBasicTable.selectThisRow": "この行を選択",

View file

@ -440,8 +440,6 @@
"common.ui.flotCharts.thuLabel": "周四",
"common.ui.flotCharts.tueLabel": "周二",
"common.ui.flotCharts.wedLabel": "周三",
"common.ui.legacyBrowserMessage": "此 Kibana 安装启用了当前浏览器未满足的严格安全要求。",
"common.ui.legacyBrowserTitle": "请升级您的浏览器",
"common.ui.management.breadcrumb": "管理",
"management.connectDataDisplayName": "连接数据",
"management.displayName": "管理",
@ -522,8 +520,6 @@
"common.ui.vislib.colormaps.redsText": "红色",
"common.ui.vislib.colormaps.yellowToRedText": "黄到红",
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界",
"common.ui.welcomeErrorMessage": "Kibana 未正确加载。检查服务器输出以了解详情。",
"common.ui.welcomeMessage": "正在加载 Kibana",
"common.ui.directives.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段",
"common.ui.vis.editors.agg.errorsAriaLabel": "聚合有错误",
"common.ui.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。",
@ -544,6 +540,10 @@
"core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近项",
"core.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近查看",
"core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel},类型:{pageType}",
"core.ui.legacyBrowserMessage": "此 Kibana 安装启用了当前浏览器未满足的严格安全要求。",
"core.ui.legacyBrowserTitle": "请升级您的浏览器",
"core.ui.welcomeErrorMessage": "Kibana 未正确加载。检查服务器输出以了解详情。",
"core.ui.welcomeMessage": "正在加载 Kibana",
"core.chrome.legacyBrowserWarning": "您的浏览器不满足 Kibana 的安全要求。",
"core.euiBasicTable.selectAllRows": "选择所有行",
"core.euiBasicTable.selectThisRow": "选择此行",