From fb2b28c3f8b138bd1669aac7782bcc8ecc8700ff Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 13 Nov 2019 23:17:08 +0100 Subject: [PATCH 01/17] introduce PluginConfigDescriptor type --- src/core/server/index.ts | 2 ++ src/core/server/plugins/plugin.test.ts | 15 ++++++++------- src/core/server/plugins/plugin.ts | 9 +++++---- src/core/server/plugins/plugins_service.ts | 9 ++++++--- src/core/server/plugins/types.ts | 7 ++++++- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2a5631ad1c380..987e4e64f9d5b 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -123,6 +123,8 @@ export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; export { DiscoveredPlugin, Plugin, + PluginConfigDescriptor, + PluginConfigSchema, PluginInitializer, PluginInitializerContext, PluginManifest, diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index e457f01a1941c..6aab03a01675d 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -291,12 +291,13 @@ test('`stop` calls `stop` defined by the plugin instance', async () => { describe('#getConfigSchema()', () => { it('reads config schema from plugin', () => { const pluginSchema = schema.any(); + const configDescriptor = { + schema: pluginSchema, + }; jest.doMock( 'plugin-with-schema/server', () => ({ - config: { - schema: pluginSchema, - }, + config: configDescriptor, }), { virtual: true } ); @@ -309,7 +310,7 @@ describe('#getConfigSchema()', () => { initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(plugin.getConfigSchema()).toBe(pluginSchema); + expect(plugin.getConfigDescriptor()).toBe(configDescriptor); }); it('returns null if config definition not specified', () => { @@ -322,7 +323,7 @@ describe('#getConfigSchema()', () => { opaqueId, initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(plugin.getConfigSchema()).toBe(null); + expect(plugin.getConfigDescriptor()).toBe(null); }); it('returns null for plugins without a server part', () => { @@ -334,7 +335,7 @@ describe('#getConfigSchema()', () => { opaqueId, initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(plugin.getConfigSchema()).toBe(null); + expect(plugin.getConfigDescriptor()).toBe(null); }); it('throws if plugin contains invalid schema', () => { @@ -357,7 +358,7 @@ describe('#getConfigSchema()', () => { opaqueId, initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), }); - expect(() => plugin.getConfigSchema()).toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.getConfigDescriptor()).toThrowErrorMatchingInlineSnapshot( `"Configuration schema expected to be an instance of Type"` ); }); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index ff61d8033a484..c0b484515ccce 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -27,9 +27,9 @@ import { Plugin, PluginInitializerContext, PluginManifest, - PluginConfigSchema, PluginInitializer, PluginOpaqueId, + PluginConfigDescriptor, } from './types'; import { CoreSetup, CoreStart } from '..'; @@ -128,7 +128,7 @@ export class PluginWrapper< this.instance = undefined; } - public getConfigSchema(): PluginConfigSchema { + public getConfigDescriptor(): PluginConfigDescriptor | null { if (!this.manifest.server) { return null; } @@ -141,10 +141,11 @@ export class PluginWrapper< return null; } - if (!(pluginDefinition.config.schema instanceof Type)) { + const configDescriptor = pluginDefinition.config; + if (!(configDescriptor.schema instanceof Type)) { throw new Error('Configuration schema expected to be an instance of Type'); } - return pluginDefinition.config.schema; + return configDescriptor; } private createPluginInstance() { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 38fe519567a63..ea985674ed472 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -138,9 +138,12 @@ export class PluginsService implements CoreService { - const schema = plugin.getConfigSchema(); - if (schema) { - await this.coreContext.configService.setSchema(plugin.configPath, schema); + const configDescriptor = plugin.getConfigDescriptor(); + if (configDescriptor) { + await this.coreContext.configService.setSchema( + plugin.configPath, + configDescriptor.schema + ); } const isEnabled = await this.coreContext.configService.isEnabledAtPath(plugin.configPath); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 9a3e922b3cb89..e47ab1064f643 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -24,7 +24,12 @@ import { ConfigPath, EnvironmentMode, PackageInfo } from '../config'; import { LoggerFactory } from '../logging'; import { CoreSetup, CoreStart } from '..'; -export type PluginConfigSchema = Type | null; +export type PluginConfigSchema = Type; + +export interface PluginConfigDescriptor { + exposeToBrowser?: Array; + schema: PluginConfigSchema; +} /** * Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays From daf09e3be50a80716baf2698d8a2275b4f8c792b Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 14 Nov 2019 12:10:11 +0100 Subject: [PATCH 02/17] inject client plugin configs in injectedMetadata --- .../injected_metadata_service.ts | 6 + src/core/server/legacy/legacy_service.test.ts | 1 + .../server/plugins/plugins_service.mock.ts | 1 + src/core/server/plugins/plugins_service.ts | 52 ++++++-- src/legacy/ui/ui_render/ui_render_mixin.js | 112 +++++++++++------- 5 files changed, 119 insertions(+), 53 deletions(-) diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index a5342aaa48b72..3d6858a5ecda4 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -58,6 +58,9 @@ export interface InjectedMetadataParams { uiPlugins: Array<{ id: PluginName; plugin: DiscoveredPlugin; + config?: { + [key: string]: unknown; + }; }>; capabilities: Capabilities; legacyMode: boolean; @@ -168,6 +171,9 @@ export interface InjectedMetadataSetup { getPlugins: () => Array<{ id: string; plugin: DiscoveredPlugin; + config?: { + [key: string]: unknown; + }; }>; /** Indicates whether or not we are rendering a known legacy app. */ getLegacyMode: () => boolean; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index e2aefd846d978..989c408b328d2 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -85,6 +85,7 @@ beforeEach(() => { uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), + config: new Map([['plugin-id', null]]), }, }, }, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index c8b6bed044fd7..4dcebdf673e0d 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -32,6 +32,7 @@ const createServiceMock = () => { uiPlugins: { public: new Map(), internal: new Map(), + config: new Map(), }, }); mocked.start.mockResolvedValue({ contracts: new Map() }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index ea985674ed472..efd7d4708b664 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; @@ -25,10 +25,17 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types'; +import { + DiscoveredPlugin, + DiscoveredPluginInternal, + PluginConfigDescriptor, + PluginName, +} from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; import { InternalCoreSetup } from '../internal_types'; +import { IConfigService } from '../config'; +import { pick } from '../../utils'; /** @public */ export interface PluginsServiceSetup { @@ -36,6 +43,7 @@ export interface PluginsServiceSetup { uiPlugins: { public: Map; internal: Map; + config: Map | null>; }; } @@ -54,11 +62,14 @@ export interface PluginsServiceStartDeps {} // eslint-disable-line @typescript-e export class PluginsService implements CoreService { private readonly log: Logger; private readonly pluginsSystem: PluginsSystem; + private readonly configService: IConfigService; private readonly config$: Observable; + private readonly pluginConfigDescriptors = new Map(); constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); this.pluginsSystem = new PluginsSystem(coreContext); + this.configService = coreContext.configService; this.config$ = coreContext.configService .atPath('plugins') .pipe(map(rawConfig => new PluginsConfig(rawConfig, coreContext.env))); @@ -82,17 +93,20 @@ export class PluginsService implements CoreService(); if (!config.initialize || this.coreContext.env.isDevClusterMaster) { this.log.info('Plugin initialization disabled.'); - return { - contracts: new Map(), - uiPlugins: this.pluginsSystem.uiPlugins(), - }; + } else { + contracts = await this.pluginsSystem.setupPlugins(deps); } + const uiPlugins = this.pluginsSystem.uiPlugins(); return { - contracts: await this.pluginsSystem.setupPlugins(deps), - uiPlugins: this.pluginsSystem.uiPlugins(), + contracts, + uiPlugins: { + ...uiPlugins, + config: this.generateUiPluginsConfigs(uiPlugins.public), + }, }; } @@ -107,6 +121,27 @@ export class PluginsService implements CoreService + ): Map> { + return new Map( + [...uiPlugins].map(([pluginId, plugin]) => { + const configDescriptor = this.pluginConfigDescriptors.get(pluginId); + if (configDescriptor && configDescriptor.exposeToBrowser) { + return [ + pluginId, + this.configService + .atPath(plugin.configPath) + .pipe( + map((config: any) => pick(config || {}, configDescriptor.exposeToBrowser || [])) + ), + ]; + } + return [pluginId, of({})]; + }) + ); + } + private async handleDiscoveryErrors(error$: Observable) { // At this stage we report only errors that can occur when new platform plugin // manifest is present, otherwise we can't be sure that the plugin is for the new @@ -140,6 +175,7 @@ export class PluginsService implements CoreService { const configDescriptor = plugin.getConfigDescriptor(); if (configDescriptor) { + this.pluginConfigDescriptors.set(plugin.name, configDescriptor); await this.coreContext.configService.setSchema( plugin.configPath, configDescriptor.schema diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 0d05ea259d1a1..80d71a57c3abb 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -17,6 +17,7 @@ * under the License. */ +import { take } from 'rxjs/operators'; import { createHash } from 'crypto'; import { props, reduce as reduceAsync } from 'bluebird'; import Boom from 'boom'; @@ -42,21 +43,31 @@ export function uiRenderMixin(kbnServer, server, config) { let defaultInjectedVars = {}; kbnServer.afterPluginsInit(() => { const { defaultInjectedVarProviders = [] } = kbnServer.uiExports; - defaultInjectedVars = defaultInjectedVarProviders - .reduce((allDefaults, { fn, pluginSpec }) => ( + 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')); - server.exposeStaticDir('/node_modules/@elastic/eui/dist/{path*}', fromRoot('node_modules/@elastic/eui/dist')); - server.exposeStaticDir('/node_modules/@kbn/ui-framework/dist/{path*}', fromRoot('node_modules/@kbn/ui-framework/dist')); - server.exposeStaticDir('/node_modules/@elastic/charts/dist/{path*}', fromRoot('node_modules/@elastic/charts/dist')); + server.exposeStaticDir( + '/node_modules/@elastic/eui/dist/{path*}', + fromRoot('node_modules/@elastic/eui/dist') + ); + server.exposeStaticDir( + '/node_modules/@kbn/ui-framework/dist/{path*}', + fromRoot('node_modules/@kbn/ui-framework/dist') + ); + server.exposeStaticDir( + '/node_modules/@elastic/charts/dist/{path*}', + fromRoot('node_modules/@elastic/charts/dist') + ); const translationsCache = { translations: null, hash: null }; server.route({ @@ -80,11 +91,12 @@ export function uiRenderMixin(kbnServer, server, config) { .digest('hex'); } - return h.response(translationsCache.translations) + return h + .response(translationsCache.translations) .header('cache-control', 'must-revalidate') .header('content-type', 'application/json') .etag(translationsCache.hash); - } + }, }); // register the bootstrap.js route after plugins are initialized so that we can @@ -105,42 +117,38 @@ export function uiRenderMixin(kbnServer, server, config) { const isCore = !app; const uiSettings = request.getUiSettingsService(); - const darkMode = !authEnabled || request.auth.isAuthenticated - ? await uiSettings.get('theme:darkMode') - : false; + const darkMode = + !authEnabled || request.auth.isAuthenticated + ? await uiSettings.get('theme:darkMode') + : false; const basePath = config.get('server.basePath'); const regularBundlePath = `${basePath}/bundles`; const dllBundlePath = `${basePath}/built_assets/dlls`; const styleSheetPaths = [ `${dllBundlePath}/vendors.style.dll.css`, - ...( - darkMode ? - [ - `${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, - `${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`, - ] : [ - `${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, - `${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`, - ] - ), + ...(darkMode + ? [ + `${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, + `${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`, + ] + : [ + `${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, + `${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`, + ]), `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, `${regularBundlePath}/commons.style.css`, - ...( - !isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : [] - ), + ...(!isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : []), ...kbnServer.uiExports.styleSheetPaths - .filter(path => ( - path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light') - )) - .map(path => ( + .filter(path => path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light')) + .map(path => path.localPath.endsWith('.scss') ? `${basePath}/built_assets/css/${path.publicPath}` : `${basePath}/${path.publicPath}` - )) - .reverse() + ) + .reverse(), ]; const bootstrap = new AppBootstrap({ @@ -149,17 +157,18 @@ export function uiRenderMixin(kbnServer, server, config) { regularBundlePath, dllBundlePath, styleSheetPaths, - } + }, }); const body = await bootstrap.getJsFile(); const etag = await bootstrap.getJsFileHash(); - return h.response(body) + return h + .response(body) .header('cache-control', 'must-revalidate') .header('content-type', 'application/javascript') .etag(etag); - } + }, }); }); @@ -179,14 +188,14 @@ export function uiRenderMixin(kbnServer, server, config) { } catch (err) { throw Boom.boomify(err); } - } + }, }); async function getUiSettings({ request, includeUserProvidedConfig }) { const uiSettings = request.getUiSettingsService(); return props({ defaults: uiSettings.getRegistered(), - user: includeUserProvidedConfig && uiSettings.getUserProvided() + user: includeUserProvidedConfig && uiSettings.getUserProvided(), }); } @@ -206,7 +215,12 @@ export function uiRenderMixin(kbnServer, server, config) { }; } - async function renderApp({ app, h, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) { + async function renderApp({ + app, + h, + includeUserProvidedConfig = true, + injectedVarsOverrides = {}, + }) { const request = h.request; const basePath = request.getBasePath(); const uiSettings = await getUiSettings({ request, includeUserProvidedConfig }); @@ -215,14 +229,22 @@ export function uiRenderMixin(kbnServer, server, config) { const legacyMetadata = getLegacyKibanaPayload({ app, basePath, - uiSettings + uiSettings, }); // 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 uiPlugins = [ - ...kbnServer.newPlatform.__internals.uiPlugins.public.entries() - ].map(([id, plugin]) => ({ id, plugin })); + const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPlugins.config; + 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 response = h.view('ui_app', { strictCsp: config.get('csp.strict'), @@ -250,8 +272,8 @@ export function uiRenderMixin(kbnServer, server, config) { mergeVariables( injectedVarsOverrides, app ? await server.getInjectedUiAppVars(app.getId()) : {}, - defaultInjectedVars, - ), + defaultInjectedVars + ) ), uiPlugins, From ac9d19b9509051194faacaf47091db34ac3a7dfc Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 14 Nov 2019 13:42:36 +0100 Subject: [PATCH 03/17] expose client config in PluginInitializerContext --- src/core/public/injected_metadata/index.ts | 1 + .../injected_metadata_service.ts | 24 ++++++++----------- src/core/public/mocks.ts | 4 ++++ src/core/public/plugins/plugin_context.ts | 21 ++++++++++++---- .../public/plugins/plugins_service.test.ts | 5 ++-- src/core/public/plugins/plugins_service.ts | 12 ++++------ .../legacy/plugins/siem/public/apps/index.ts | 8 +++++-- .../plugins/uptime/public/apps/index.ts | 6 ++++- 8 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/core/public/injected_metadata/index.ts b/src/core/public/injected_metadata/index.ts index dac9d5cea3565..cebd0f017de69 100644 --- a/src/core/public/injected_metadata/index.ts +++ b/src/core/public/injected_metadata/index.ts @@ -22,5 +22,6 @@ export { InjectedMetadataParams, InjectedMetadataSetup, InjectedMetadataStart, + InjectedPluginMetadata, LegacyNavLink, } from './injected_metadata_service'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 3d6858a5ecda4..002f83d9feac4 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -38,6 +38,14 @@ export interface LegacyNavLink { euiIconType?: string; } +export interface InjectedPluginMetadata { + id: PluginName; + plugin: DiscoveredPlugin; + config?: { + [key: string]: unknown; + }; +} + /** @internal */ export interface InjectedMetadataParams { injectedMetadata: { @@ -55,13 +63,7 @@ export interface InjectedMetadataParams { mode: Readonly; packageInfo: Readonly; }; - uiPlugins: Array<{ - id: PluginName; - plugin: DiscoveredPlugin; - config?: { - [key: string]: unknown; - }; - }>; + uiPlugins: InjectedPluginMetadata[]; capabilities: Capabilities; legacyMode: boolean; legacyMetadata: { @@ -168,13 +170,7 @@ export interface InjectedMetadataSetup { /** * An array of frontend plugins in topological order. */ - getPlugins: () => Array<{ - id: string; - plugin: DiscoveredPlugin; - config?: { - [key: string]: unknown; - }; - }>; + getPlugins: () => InjectedPluginMetadata[]; /** Indicates whether or not we are rendering a known legacy app. */ getLegacyMode: () => boolean; getLegacyMetadata: () => { diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index b9cd2577c2217..9089a6138b58a 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { of } from 'rxjs'; import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; import { CoreContext, CoreSetup, CoreStart, PluginInitializerContext, NotificationsSetup } from '.'; @@ -92,6 +93,9 @@ function pluginInitializerContextMock() { dist: false, }, }, + config: { + create: () => of({} as T), + }, }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index eae45654fce18..d1c05ebcd5e58 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -18,7 +18,7 @@ */ import { omit } from 'lodash'; - +import { Observable, of } from 'rxjs'; import { DiscoveredPlugin } from '../../server'; import { PluginOpaqueId, PackageInfo, EnvironmentMode } from '../../server/types'; import { CoreContext } from '../core_system'; @@ -31,7 +31,7 @@ import { CoreSetup, CoreStart } from '../'; * * @public */ -export interface PluginInitializerContext { +export interface PluginInitializerContext { /** * A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. */ @@ -40,6 +40,9 @@ export interface PluginInitializerContext { mode: Readonly; packageInfo: Readonly; }; + readonly config: { + create: () => Observable; + }; } /** @@ -47,17 +50,27 @@ export interface PluginInitializerContext { * empty but should provide static services in the future, such as config and logging. * * @param coreContext - * @param pluginManinfest + * @param opaqueId + * @param pluginManifest + * @param pluginConfig * @internal */ export function createPluginInitializerContext( coreContext: CoreContext, opaqueId: PluginOpaqueId, - pluginManifest: DiscoveredPlugin + pluginManifest: DiscoveredPlugin, + pluginConfig: { + [key: string]: unknown; + } ): PluginInitializerContext { return { opaqueId, env: coreContext.env, + config: { + create() { + return of((pluginConfig as unknown) as T); + }, + }, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 0d8887774e900..bd1133ee36521 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -25,13 +25,14 @@ import { mockPluginInitializerProvider, } from './plugins_service.test.mocks'; -import { PluginName, DiscoveredPlugin } from 'src/core/server'; +import { PluginName } from 'src/core/server'; import { coreMock } from '../mocks'; import { PluginsService, PluginsServiceStartDeps, PluginsServiceSetupDeps, } from './plugins_service'; +import { InjectedPluginMetadata } from '../injected_metadata'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { applicationServiceMock } from '../application/application_service.mock'; import { i18nServiceMock } from '../i18n/i18n_service.mock'; @@ -52,7 +53,7 @@ mockPluginInitializerProvider.mockImplementation( pluginName => mockPluginInitializers.get(pluginName)! ); -let plugins: Array<{ id: string; plugin: DiscoveredPlugin }>; +let plugins: InjectedPluginMetadata[]; type DeeplyMocked = { [P in keyof T]: jest.Mocked }; diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 1ab9d7f2fa9b2..0221762f8dd70 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { DiscoveredPlugin, PluginName, PluginOpaqueId } from '../../server'; +import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; import { PluginWrapper } from './plugin'; @@ -27,6 +27,7 @@ import { createPluginStartContext, } from './plugin_context'; import { InternalCoreSetup, InternalCoreStart } from '../core_system'; +import { InjectedPluginMetadata } from '../injected_metadata'; /** @internal */ export type PluginsServiceSetupDeps = InternalCoreSetup; @@ -55,15 +56,12 @@ export class PluginsService implements CoreService - ) { + constructor(private readonly coreContext: CoreContext, plugins: InjectedPluginMetadata[]) { // Generate opaque ids const opaqueIds = new Map(plugins.map(p => [p.id, Symbol(p.id)])); // Setup dependency map and plugin wrappers - plugins.forEach(({ id, plugin }) => { + plugins.forEach(({ id, plugin, config }) => { // Setup map of dependencies this.pluginDependencies.set(id, [ ...plugin.requiredPlugins, @@ -76,7 +74,7 @@ export class PluginsService implements CoreService of({} as any) } }, + chrome +).start(npStart); diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts index 3b328b3ff2326..20a8156d7b066 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/index.ts +++ b/x-pack/legacy/plugins/uptime/public/apps/index.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { of } from 'rxjs'; import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; -new Plugin({ opaqueId: Symbol('uptime'), env: {} as any }, chrome).start(npStart); +new Plugin( + { opaqueId: Symbol('uptime'), env: {} as any, config: { create: () => of({} as any) } }, + chrome +).start(npStart); From 99790dd9e80acff9429c2918f779fd4f12d3c82b Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 14 Nov 2019 13:43:04 +0100 Subject: [PATCH 04/17] add example implementation in testbed --- src/plugins/testbed/public/index.ts | 7 ++++--- src/plugins/testbed/public/plugin.ts | 18 +++++++++++++++--- src/plugins/testbed/server/index.ts | 25 ++++++++++++++++++------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/plugins/testbed/public/index.ts b/src/plugins/testbed/public/index.ts index 44eea308a31d9..601db10f6f8bb 100644 --- a/src/plugins/testbed/public/index.ts +++ b/src/plugins/testbed/public/index.ts @@ -17,8 +17,9 @@ * under the License. */ -import { PluginInitializer } from 'kibana/public'; +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin'; -export const plugin: PluginInitializer = () => - new TestbedPlugin(); +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => new TestbedPlugin(initializerContext); diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts index bf51dbf0b8e78..32f9be3498a11 100644 --- a/src/plugins/testbed/public/plugin.ts +++ b/src/plugins/testbed/public/plugin.ts @@ -17,12 +17,24 @@ * under the License. */ -import { Plugin, CoreSetup } from 'kibana/public'; +import { take } from 'rxjs/operators'; +import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/public'; + +interface ConfigType { + uiProp: string; +} export class TestbedPlugin implements Plugin { - public setup(core: CoreSetup, deps: {}) { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + const config = await this.initializerContext.config + .create() + .pipe(take(1)) + .toPromise(); + // eslint-disable-next-line no-console - console.log(`Testbed plugin set up`); + console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`); return { foo: 'bar', }; diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 4dd22d3dce1ef..75e8a740f595b 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -20,15 +20,26 @@ import { map, mergeMap } from 'rxjs/operators'; import { schema, TypeOf } from '@kbn/config-schema'; -import { CoreSetup, CoreStart, Logger, PluginInitializerContext, PluginName } from 'kibana/server'; +import { + CoreSetup, + CoreStart, + Logger, + PluginInitializerContext, + PluginConfigDescriptor, + PluginName, +} from 'kibana/server'; -export const config = { - schema: schema.object({ - secret: schema.string({ defaultValue: 'Not really a secret :/' }), - }), -}; +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Not really a secret :/' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; -type ConfigType = TypeOf; +export const config: PluginConfigDescriptor = { + exposeToBrowser: ['uiProp'], + schema: configSchema, +}; class Plugin { private readonly log: Logger; From 0df8a6f1177175c05bd11226e9f94f6a0860faa4 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 14 Nov 2019 13:46:05 +0100 Subject: [PATCH 05/17] update generated doc --- ...-public.plugininitializercontext.config.md | 13 +++++++++++++ ...-plugin-public.plugininitializercontext.md | 3 ++- .../core/server/kibana-plugin-server.md | 2 ++ ....pluginconfigdescriptor.exposetobrowser.md | 11 +++++++++++ ...na-plugin-server.pluginconfigdescriptor.md | 19 +++++++++++++++++++ ...in-server.pluginconfigdescriptor.schema.md | 11 +++++++++++ ...kibana-plugin-server.pluginconfigschema.md | 11 +++++++++++ ...ibana-plugin-server.pluginsservicesetup.md | 2 +- ...in-server.pluginsservicesetup.uiplugins.md | 1 + src/core/public/public.api.md | 6 +++++- src/core/server/server.api.md | 18 +++++++++++++++++- 11 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginconfigschema.md diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md new file mode 100644 index 0000000000000..219ebafb2d42d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) > [config](./kibana-plugin-public.plugininitializercontext.config.md) + +## PluginInitializerContext.config property + +Signature: + +```typescript +readonly config: { + create: () => Observable; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md index 87c39a502040d..08aba3f961b79 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md @@ -9,13 +9,14 @@ The available core services passed to a `PluginInitializer` Signature: ```typescript -export interface PluginInitializerContext +export interface PluginInitializerContext ``` ## Properties | Property | Type | Description | | --- | --- | --- | +| [config](./kibana-plugin-public.plugininitializercontext.config.md) | {
create: <T = ConfigSchema>() => Observable<T>;
} | | | [env](./kibana-plugin-public.plugininitializercontext.env.md) | {
mode: Readonly<EnvironmentMode>;
packageInfo: Readonly<PackageInfo>;
} | | | [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) | PluginOpaqueId | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 9907750b8742f..35d59b5ee7bea 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -75,6 +75,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | +| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [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) | | @@ -156,6 +157,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | +| [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md new file mode 100644 index 0000000000000..42e3e44f96d31 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) > [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) + +## PluginConfigDescriptor.exposeToBrowser property + +Signature: + +```typescript +exposeToBrowser?: Array; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md new file mode 100644 index 0000000000000..30206c808d471 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) + +## PluginConfigDescriptor interface + +Signature: + +```typescript +export interface PluginConfigDescriptor +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Array<keyof T> | | +| [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | | + diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md new file mode 100644 index 0000000000000..ee473a8f1f6ff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) > [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) + +## PluginConfigDescriptor.schema property + +Signature: + +```typescript +schema: PluginConfigSchema; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md new file mode 100644 index 0000000000000..8efb3ddfba989 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) + +## PluginConfigSchema type + +Signature: + +```typescript +export declare type PluginConfigSchema = Type; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md index 2b3ff9a2cd419..0ff702a2e7c31 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -16,5 +16,5 @@ export interface PluginsServiceSetup | Property | Type | Description | | --- | --- | --- | | [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | -| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
} | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
config: Map<PluginName, Observable<unknown> | null>;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md index fa286dfb59092..dbadd72fc583c 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -10,5 +10,6 @@ uiPlugins: { public: Map; internal: Map; + config: Map | null>; }; ``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d3ce86d76d7cc..616707c38b1ef 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -701,7 +701,11 @@ export interface Plugin = (core: PluginInitializerContext) => Plugin; // @public -export interface PluginInitializerContext { +export interface PluginInitializerContext { + // (undocumented) + readonly config: { + create: () => Observable; + }; // (undocumented) readonly env: { mode: Readonly; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 97a04a4a4efab..97ef6315f2193 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -959,6 +959,21 @@ export interface Plugin { + // (undocumented) + exposeToBrowser?: Array; + // (undocumented) + schema: PluginConfigSchema; +} + +// Warning: (ae-missing-release-tag) "PluginConfigSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PluginConfigSchema = Type; + // @public export type PluginInitializer = (core: PluginInitializerContext) => Plugin; @@ -1006,6 +1021,7 @@ export interface PluginsServiceSetup { uiPlugins: { public: Map; internal: Map; + config: Map | null>; }; } @@ -1615,6 +1631,6 @@ export interface UserProvidedValues { // 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/plugins/plugins_service.ts:38:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:45:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts ``` From 9b93964933542dfa09d45fb6148030755b2001c6 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 14:51:54 +0100 Subject: [PATCH 06/17] only generates ui config entry for plugins exposing properties to client --- src/core/server/plugins/plugins_service.ts | 24 +++++++++++-------- .../legacy/plugins/siem/public/apps/index.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index efd7d4708b664..1baf90b45e59d 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; @@ -125,20 +125,24 @@ export class PluginsService implements CoreService ): Map> { return new Map( - [...uiPlugins].map(([pluginId, plugin]) => { - const configDescriptor = this.pluginConfigDescriptors.get(pluginId); - if (configDescriptor && configDescriptor.exposeToBrowser) { + [...uiPlugins] + .filter(([pluginId, plugin]) => { + const configDescriptor = this.pluginConfigDescriptors.get(pluginId); + return ( + configDescriptor && + configDescriptor.exposeToBrowser && + configDescriptor.exposeToBrowser.length > 0 + ); + }) + .map(([pluginId, plugin]) => { + const configDescriptor = this.pluginConfigDescriptors.get(pluginId)!; return [ pluginId, this.configService .atPath(plugin.configPath) - .pipe( - map((config: any) => pick(config || {}, configDescriptor.exposeToBrowser || [])) - ), + .pipe(map((config: any) => pick(config || {}, configDescriptor.exposeToBrowser!))), ]; - } - return [pluginId, of({})]; - }) + }) ); } diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts index 0632a17a915db..1f53707cd973d 100644 --- a/x-pack/legacy/plugins/siem/public/apps/index.ts +++ b/x-pack/legacy/plugins/siem/public/apps/index.ts @@ -13,4 +13,4 @@ new Plugin( // eslint-disable-next-line @typescript-eslint/no-explicit-any { opaqueId: Symbol('siem'), env: {} as any, config: { create: () => of({} as any) } }, chrome -).start(npStart); +).start(npStart.core, npStart.plugins); From a87b7b061145ad03d12bb2569d59080943722e92 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 15:06:46 +0100 Subject: [PATCH 07/17] separate plugin configs from plugins --- .../kibana-plugin-server.pluginsservicesetup.md | 3 ++- ...ugin-server.pluginsservicesetup.uipluginconfigs.md | 11 +++++++++++ ...ana-plugin-server.pluginsservicesetup.uiplugins.md | 1 - src/core/server/legacy/legacy_service.ts | 1 + src/core/server/plugins/plugins_service.ts | 8 +++----- src/core/server/server.api.md | 3 ++- src/legacy/server/kbn_server.d.ts | 1 + src/legacy/ui/ui_render/ui_render_mixin.js | 2 +- 8 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md index 0ff702a2e7c31..36d803ddea618 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -16,5 +16,6 @@ export interface PluginsServiceSetup | Property | Type | Description | | --- | --- | --- | | [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | -| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
config: Map<PluginName, Observable<unknown> | null>;
} | | +| [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) | Map<PluginName, Observable<unknown>> | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md new file mode 100644 index 0000000000000..4bd57b873043e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) + +## PluginsServiceSetup.uiPluginConfigs property + +Signature: + +```typescript +uiPluginConfigs: Map>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md index dbadd72fc583c..fa286dfb59092 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -10,6 +10,5 @@ uiPlugins: { public: Map; internal: Map; - config: Map | null>; }; ``` diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 99963ad9ce3e8..e86e6cde6e927 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -278,6 +278,7 @@ export class LegacyService implements CoreService { hapiServer: setupDeps.core.http.server, kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.core.plugins.uiPlugins, + uiPluginConfigs: setupDeps.core.plugins.uiPluginConfigs, elasticsearch: setupDeps.core.elasticsearch, uiSettings: setupDeps.core.uiSettings, savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 1baf90b45e59d..1d6442457b0bf 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -43,8 +43,8 @@ export interface PluginsServiceSetup { uiPlugins: { public: Map; internal: Map; - config: Map | null>; }; + uiPluginConfigs: Map>; } /** @public */ @@ -103,10 +103,8 @@ export class PluginsService implements CoreService; // (undocumented) + uiPluginConfigs: Map>; + // (undocumented) uiPlugins: { public: Map; internal: Map; - config: Map | null>; }; } diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 9cc4e30d4252d..7399f2d08508f 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -107,6 +107,7 @@ export default class KbnServer { __internals: { hapiServer: LegacyServiceSetupDeps['core']['http']['server']; uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins']; + uiPluginConfigs: LegacyServiceSetupDeps['core']['plugins']['uiPluginConfigs']; elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 80d71a57c3abb..c0885cd5d3d13 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -234,7 +234,7 @@ export function uiRenderMixin(kbnServer, server, config) { // 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.config; + const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPluginConfigs; const uiPlugins = await Promise.all([ ...kbnServer.newPlatform.__internals.uiPlugins.public.entries(), ].map(async ([id, plugin]) => { From b6d9a832e5068e844e4fdbc9848e6eeaff5d5ffa Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 15:32:03 +0100 Subject: [PATCH 08/17] restructure plugin services tests --- .../public/plugins/plugins_service.test.ts | 459 ++++++++-------- .../server/plugins/plugins_service.test.ts | 502 +++++++++--------- 2 files changed, 493 insertions(+), 468 deletions(-) diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index bd1133ee36521..b074f5a655ef3 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -63,83 +63,6 @@ let mockSetupContext: DeeplyMocked; let mockStartDeps: DeeplyMocked; let mockStartContext: DeeplyMocked; -beforeEach(() => { - plugins = [ - { id: 'pluginA', plugin: createManifest('pluginA') }, - { id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) }, - { - id: 'pluginC', - plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }), - }, - ]; - mockSetupDeps = { - application: applicationServiceMock.createInternalSetupContract(), - context: contextServiceMock.createSetupContract(), - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - http: httpServiceMock.createSetupContract(), - injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), - notifications: notificationServiceMock.createSetupContract(), - uiSettings: uiSettingsServiceMock.createSetupContract(), - }; - mockSetupContext = { - ...mockSetupDeps, - application: expect.any(Object), - }; - mockStartDeps = { - application: applicationServiceMock.createInternalStartContract(), - docLinks: docLinksServiceMock.createStartContract(), - http: httpServiceMock.createStartContract(), - chrome: chromeServiceMock.createStartContract(), - i18n: i18nServiceMock.createStartContract(), - injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), - notifications: notificationServiceMock.createStartContract(), - overlays: overlayServiceMock.createStartContract(), - uiSettings: uiSettingsServiceMock.createStartContract(), - savedObjects: savedObjectsMock.createStartContract(), - }; - mockStartContext = { - ...mockStartDeps, - application: expect.any(Object), - chrome: omit(mockStartDeps.chrome, 'getComponent'), - }; - - // Reset these for each test. - mockPluginInitializers = new Map(([ - [ - 'pluginA', - jest.fn(() => ({ - setup: jest.fn(() => ({ setupValue: 1 })), - start: jest.fn(() => ({ startValue: 2 })), - stop: jest.fn(), - })), - ], - [ - 'pluginB', - jest.fn(() => ({ - setup: jest.fn((core, deps: any) => ({ - pluginAPlusB: deps.pluginA.setupValue + 1, - })), - start: jest.fn((core, deps: any) => ({ - pluginAPlusB: deps.pluginA.startValue + 1, - })), - stop: jest.fn(), - })), - ], - [ - 'pluginC', - jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - })), - ], - ] as unknown) as [[PluginName, any]]); -}); - -afterEach(() => { - mockLoadPluginBundle.mockClear(); -}); - function createManifest( id: string, { required = [], optional = [] }: { required?: string[]; optional?: string[]; ui?: boolean } = {} @@ -153,9 +76,88 @@ function createManifest( }; } -test('`PluginsService.getOpaqueIds` returns dependency tree of symbols', () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` +describe('PluginsService', () => { + beforeEach(() => { + plugins = [ + { id: 'pluginA', plugin: createManifest('pluginA') }, + { id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) }, + { + id: 'pluginC', + plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }), + }, + ]; + mockSetupDeps = { + application: applicationServiceMock.createInternalSetupContract(), + context: contextServiceMock.createSetupContract(), + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + http: httpServiceMock.createSetupContract(), + injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), + notifications: notificationServiceMock.createSetupContract(), + uiSettings: uiSettingsServiceMock.createSetupContract(), + }; + mockSetupContext = { + ...mockSetupDeps, + application: expect.any(Object), + }; + mockStartDeps = { + application: applicationServiceMock.createInternalStartContract(), + docLinks: docLinksServiceMock.createStartContract(), + http: httpServiceMock.createStartContract(), + chrome: chromeServiceMock.createStartContract(), + i18n: i18nServiceMock.createStartContract(), + injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'), + notifications: notificationServiceMock.createStartContract(), + overlays: overlayServiceMock.createStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract(), + savedObjects: savedObjectsMock.createStartContract(), + }; + mockStartContext = { + ...mockStartDeps, + application: expect.any(Object), + chrome: omit(mockStartDeps.chrome, 'getComponent'), + }; + + // Reset these for each test. + mockPluginInitializers = new Map(([ + [ + 'pluginA', + jest.fn(() => ({ + setup: jest.fn(() => ({ setupValue: 1 })), + start: jest.fn(() => ({ startValue: 2 })), + stop: jest.fn(), + })), + ], + [ + 'pluginB', + jest.fn(() => ({ + setup: jest.fn((core, deps: any) => ({ + pluginAPlusB: deps.pluginA.setupValue + 1, + })), + start: jest.fn((core, deps: any) => ({ + pluginAPlusB: deps.pluginA.startValue + 1, + })), + stop: jest.fn(), + })), + ], + [ + 'pluginC', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + })), + ], + ] as unknown) as [[PluginName, any]]); + }); + + afterEach(() => { + mockLoadPluginBundle.mockClear(); + }); + + describe('#getOpaqueIds()', () => { + it('returns dependency tree of symbols', () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` Map { Symbol(pluginA) => Array [], Symbol(pluginB) => Array [ @@ -166,152 +168,169 @@ test('`PluginsService.getOpaqueIds` returns dependency tree of symbols', () => { ], } `); -}); - -test('`PluginsService.setup` fails if any bundle cannot be loaded', async () => { - mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Could not load bundle"` - ); -}); - -test('`PluginsService.setup` fails if any plugin instance does not have a setup function', async () => { - mockPluginInitializers.set('pluginA', (() => ({})) as any); - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."` - ); -}); - -test('`PluginsService.setup` calls loadPluginBundles with http and plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginA'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginB'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginC'); -}); - -test('`PluginsService.setup` initalizes plugins with PluginIntitializerContext', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(expect.any(Object)); - expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(expect.any(Object)); - expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(expect.any(Object)); -}); - -test('`PluginsService.setup` exposes dependent setup contracts to plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; - const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; - const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; - - expect(pluginAInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); - expect(pluginBInstance.setup).toHaveBeenCalledWith(mockSetupContext, { - pluginA: { setupValue: 1 }, - }); - // Does not supply value for `nonexist` optional dep - expect(pluginCInstance.setup).toHaveBeenCalledWith(mockSetupContext, { - pluginA: { setupValue: 1 }, + }); }); -}); - -test('`PluginsService.setup` does not set missing dependent setup contracts', async () => { - plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; - mockPluginInitializers.set( - 'pluginD', - jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - })) as any - ); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - // If a dependency is missing it should not be in the deps at all, not even as undefined. - const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; - expect(pluginDInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); - const pluginDDeps = pluginDInstance.setup.mock.calls[0][1]; - expect(pluginDDeps).not.toHaveProperty('missing'); -}); -test('`PluginsService.setup` returns plugin setup contracts', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - const { contracts } = await pluginsService.setup(mockSetupDeps); - - // Verify that plugin contracts were available - expect((contracts.get('pluginA')! as any).setupValue).toEqual(1); - expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2); -}); - -test('`PluginsService.start` exposes dependent start contracts to plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - await pluginsService.start(mockStartDeps); - - const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; - const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; - const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; - - expect(pluginAInstance.start).toHaveBeenCalledWith(mockStartContext, {}); - expect(pluginBInstance.start).toHaveBeenCalledWith(mockStartContext, { - pluginA: { startValue: 2 }, - }); - // Does not supply value for `nonexist` optional dep - expect(pluginCInstance.start).toHaveBeenCalledWith(mockStartContext, { - pluginA: { startValue: 2 }, + describe('#setup()', () => { + it('fails if any bundle cannot be loaded', async () => { + mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not load bundle"` + ); + }); + + it('fails if any plugin instance does not have a setup function', async () => { + mockPluginInitializers.set('pluginA', (() => ({})) as any); + const pluginsService = new PluginsService(mockCoreContext, plugins); + await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."` + ); + }); + + it('calls loadPluginBundles with http and plugins', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); + expect(mockLoadPluginBundle).toHaveBeenCalledWith( + mockSetupDeps.http.basePath.prepend, + 'pluginA' + ); + expect(mockLoadPluginBundle).toHaveBeenCalledWith( + mockSetupDeps.http.basePath.prepend, + 'pluginB' + ); + expect(mockLoadPluginBundle).toHaveBeenCalledWith( + mockSetupDeps.http.basePath.prepend, + 'pluginC' + ); + }); + + it('initalizes plugins with PluginIntitializerContext', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(expect.any(Object)); + expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(expect.any(Object)); + expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(expect.any(Object)); + }); + + it('exposes dependent setup contracts to plugins', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; + const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; + const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; + + expect(pluginAInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); + expect(pluginBInstance.setup).toHaveBeenCalledWith(mockSetupContext, { + pluginA: { setupValue: 1 }, + }); + // Does not supply value for `nonexist` optional dep + expect(pluginCInstance.setup).toHaveBeenCalledWith(mockSetupContext, { + pluginA: { setupValue: 1 }, + }); + }); + + it('does not set missing dependent setup contracts', async () => { + plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; + mockPluginInitializers.set( + 'pluginD', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any + ); + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + // If a dependency is missing it should not be in the deps at all, not even as undefined. + const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; + expect(pluginDInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); + const pluginDDeps = pluginDInstance.setup.mock.calls[0][1]; + expect(pluginDDeps).not.toHaveProperty('missing'); + }); + + it('returns plugin setup contracts', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + const { contracts } = await pluginsService.setup(mockSetupDeps); + + // Verify that plugin contracts were available + expect((contracts.get('pluginA')! as any).setupValue).toEqual(1); + expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2); + }); }); -}); -test('`PluginsService.start` does not set missing dependent start contracts', async () => { - plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; - mockPluginInitializers.set( - 'pluginD', - jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - })) as any - ); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - await pluginsService.start(mockStartDeps); - - // If a dependency is missing it should not be in the deps at all, not even as undefined. - const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; - expect(pluginDInstance.start).toHaveBeenCalledWith(mockStartContext, {}); - const pluginDDeps = pluginDInstance.start.mock.calls[0][1]; - expect(pluginDDeps).not.toHaveProperty('missing'); -}); - -test('`PluginsService.start` returns plugin start contracts', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - const { contracts } = await pluginsService.start(mockStartDeps); - - // Verify that plugin contracts were available - expect((contracts.get('pluginA')! as any).startValue).toEqual(2); - expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3); -}); + describe('#start()', () => { + it('exposes dependent start contracts to plugins', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + + const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; + const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; + const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; + + expect(pluginAInstance.start).toHaveBeenCalledWith(mockStartContext, {}); + expect(pluginBInstance.start).toHaveBeenCalledWith(mockStartContext, { + pluginA: { startValue: 2 }, + }); + // Does not supply value for `nonexist` optional dep + expect(pluginCInstance.start).toHaveBeenCalledWith(mockStartContext, { + pluginA: { startValue: 2 }, + }); + }); + + it('does not set missing dependent start contracts', async () => { + plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; + mockPluginInitializers.set( + 'pluginD', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any + ); + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + + // If a dependency is missing it should not be in the deps at all, not even as undefined. + const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; + expect(pluginDInstance.start).toHaveBeenCalledWith(mockStartContext, {}); + const pluginDDeps = pluginDInstance.start.mock.calls[0][1]; + expect(pluginDDeps).not.toHaveProperty('missing'); + }); + + it('returns plugin start contracts', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + const { contracts } = await pluginsService.start(mockStartDeps); + + // Verify that plugin contracts were available + expect((contracts.get('pluginA')! as any).startValue).toEqual(2); + expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3); + }); + }); -test('`PluginService.stop` calls the stop function on each plugin', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); + describe('#stop()', () => { + it('calls the stop function on each plugin', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); - const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; - const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; - const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; + const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; + const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; + const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; - await pluginsService.stop(); + await pluginsService.stop(); - expect(pluginAInstance.stop).toHaveBeenCalled(); - expect(pluginBInstance.stop).toHaveBeenCalled(); - expect(pluginCInstance.stop).toHaveBeenCalled(); + expect(pluginAInstance.stop).toHaveBeenCalled(); + expect(pluginBInstance.stop).toHaveBeenCalled(); + expect(pluginCInstance.stop).toHaveBeenCalled(); + }); + }); }); diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 0b3bc0759463c..184e599aae73e 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -90,149 +90,151 @@ const createPlugin = ( }); }; -beforeEach(async () => { - mockPackage.raw = { - branch: 'feature-v1', - version: 'v1', - build: { - distributable: true, - number: 100, - sha: 'feature-v1-build-sha', - }, - }; +describe('PluginsService', () => { + beforeEach(async () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; - coreId = Symbol('core'); - env = Env.createDefault(getEnvOptions()); + coreId = Symbol('core'); + env = Env.createDefault(getEnvOptions()); - configService = new ConfigService( - new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })), - env, - logger - ); - await configService.setSchema(config.path, config.schema); - pluginsService = new PluginsService({ coreId, env, logger, configService }); + configService = new ConfigService( + new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })), + env, + logger + ); + await configService.setSchema(config.path, config.schema); + pluginsService = new PluginsService({ coreId, env, logger, configService }); - [mockPluginSystem] = MockPluginsSystem.mock.instances as any; -}); - -afterEach(() => { - jest.clearAllMocks(); -}); + [mockPluginSystem] = MockPluginsSystem.mock.instances as any; + }); -test('`discover` throws if plugin has an invalid manifest', async () => { - mockDiscover.mockReturnValue({ - error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]), - plugin$: from([]), + afterEach(() => { + jest.clearAllMocks(); }); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` + describe('#discover()', () => { + it('throws if plugin has an invalid manifest', async () => { + mockDiscover.mockReturnValue({ + error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]), + plugin$: from([]), + }); + + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Invalid JSON (invalid-manifest, path-1)] `); - expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ [Error: Invalid JSON (invalid-manifest, path-1)], ], ] `); -}); + }); -test('`discover` throws if plugin required Kibana version is incompatible with the current version', async () => { - mockDiscover.mockReturnValue({ - error$: from([ - PluginDiscoveryError.incompatibleVersion('path-3', new Error('Incompatible version')), - ]), - plugin$: from([]), - }); + it('throws if plugin required Kibana version is incompatible with the current version', async () => { + mockDiscover.mockReturnValue({ + error$: from([ + PluginDiscoveryError.incompatibleVersion('path-3', new Error('Incompatible version')), + ]), + plugin$: from([]), + }); - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Incompatible version (incompatible-version, path-3)] `); - expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ [Error: Incompatible version (incompatible-version, path-3)], ], ] `); -}); - -test('`discover` throws if discovered plugins with conflicting names', async () => { - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([ - createPlugin('conflicting-id', { - path: 'path-4', - version: 'some-version', - configPath: 'path', - requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], - optionalPlugins: ['some-optional-plugin'], - }), - createPlugin('conflicting-id', { - path: 'path-4', - version: 'some-version', - configPath: 'path', - requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], - optionalPlugins: ['some-optional-plugin'], - }), - ]), - }); - - await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot( - `[Error: Plugin with id "conflicting-id" is already registered!]` - ); - - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); - expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); -}); - -test('`discover` properly detects plugins that should be disabled.', async () => { - jest - .spyOn(configService, 'isEnabledAtPath') - .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); - - mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); - mockPluginSystem.uiPlugins.mockReturnValue({ public: new Map(), internal: new Map() }); - - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([ - createPlugin('explicitly-disabled-plugin', { - disabled: true, - path: 'path-1', - configPath: 'path-1', - }), - createPlugin('plugin-with-missing-required-deps', { - path: 'path-2', - configPath: 'path-2', - requiredPlugins: ['missing-plugin'], - }), - createPlugin('plugin-with-disabled-transitive-dep', { - path: 'path-3', - configPath: 'path-3', - requiredPlugins: ['another-explicitly-disabled-plugin'], - }), - createPlugin('another-explicitly-disabled-plugin', { - disabled: true, - path: 'path-4', - configPath: 'path-4-disabled', - }), - ]), - }); - - await pluginsService.discover(); - const setup = await pluginsService.setup(setupDeps); - - expect(setup.contracts).toBeInstanceOf(Map); - expect(setup.uiPlugins.public).toBeInstanceOf(Map); - expect(setup.uiPlugins.internal).toBeInstanceOf(Map); - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); - expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); - - expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(` + }); + + it('throws if discovered plugins with conflicting names', async () => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('conflicting-id', { + path: 'path-4', + version: 'some-version', + configPath: 'path', + requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], + optionalPlugins: ['some-optional-plugin'], + }), + createPlugin('conflicting-id', { + path: 'path-4', + version: 'some-version', + configPath: 'path', + requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], + optionalPlugins: ['some-optional-plugin'], + }), + ]), + }); + + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot( + `[Error: Plugin with id "conflicting-id" is already registered!]` + ); + + expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); + }); + + it('properly detects plugins that should be disabled.', async () => { + jest + .spyOn(configService, 'isEnabledAtPath') + .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); + + mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); + mockPluginSystem.uiPlugins.mockReturnValue({ public: new Map(), internal: new Map() }); + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('explicitly-disabled-plugin', { + disabled: true, + path: 'path-1', + configPath: 'path-1', + }), + createPlugin('plugin-with-missing-required-deps', { + path: 'path-2', + configPath: 'path-2', + requiredPlugins: ['missing-plugin'], + }), + createPlugin('plugin-with-disabled-transitive-dep', { + path: 'path-3', + configPath: 'path-3', + requiredPlugins: ['another-explicitly-disabled-plugin'], + }), + createPlugin('another-explicitly-disabled-plugin', { + disabled: true, + path: 'path-4', + configPath: 'path-4-disabled', + }), + ]), + }); + + await pluginsService.discover(); + const setup = await pluginsService.setup(setupDeps); + + expect(setup.contracts).toBeInstanceOf(Map); + expect(setup.uiPlugins.public).toBeInstanceOf(Map); + expect(setup.uiPlugins.internal).toBeInstanceOf(Map); + expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); + expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); + + expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(` Array [ Array [ "Plugin \\"explicitly-disabled-plugin\\" is disabled.", @@ -248,143 +250,147 @@ Array [ ], ] `); -}); - -test('`discover` does not throw in case of mutual plugin dependencies', async () => { - const firstPlugin = createPlugin('first-plugin', { - path: 'path-1', - requiredPlugins: ['second-plugin'], - }); - const secondPlugin = createPlugin('second-plugin', { - path: 'path-2', - requiredPlugins: ['first-plugin'], - }); - - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([firstPlugin, secondPlugin]), - }); + }); - await expect(pluginsService.discover()).resolves.toBeUndefined(); - - expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); -}); - -test('`discover` does not throw in case of cyclic plugin dependencies', async () => { - const firstPlugin = createPlugin('first-plugin', { - path: 'path-1', - requiredPlugins: ['second-plugin'], - }); - const secondPlugin = createPlugin('second-plugin', { - path: 'path-2', - requiredPlugins: ['third-plugin', 'last-plugin'], - }); - const thirdPlugin = createPlugin('third-plugin', { - path: 'path-3', - requiredPlugins: ['last-plugin', 'first-plugin'], - }); - const lastPlugin = createPlugin('last-plugin', { - path: 'path-4', - requiredPlugins: ['first-plugin'], - }); - const missingDepsPlugin = createPlugin('missing-deps-plugin', { - path: 'path-5', - requiredPlugins: ['not-a-plugin'], - }); + it('does not throw in case of mutual plugin dependencies', async () => { + const firstPlugin = createPlugin('first-plugin', { + path: 'path-1', + requiredPlugins: ['second-plugin'], + }); + const secondPlugin = createPlugin('second-plugin', { + path: 'path-2', + requiredPlugins: ['first-plugin'], + }); - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), - }); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([firstPlugin, secondPlugin]), + }); - await expect(pluginsService.discover()).resolves.toBeUndefined(); + await expect(pluginsService.discover()).resolves.toBeUndefined(); - expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(thirdPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(lastPlugin); -}); + expect(mockDiscover).toHaveBeenCalledTimes(1); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + }); -test('`discover` properly invokes plugin discovery and ignores non-critical errors.', async () => { - const firstPlugin = createPlugin('some-id', { - path: 'path-1', - configPath: 'path', - requiredPlugins: ['some-other-id'], - optionalPlugins: ['missing-optional-dep'], - }); - const secondPlugin = createPlugin('some-other-id', { - path: 'path-2', - version: 'some-other-version', - configPath: ['plugin', 'path'], - }); - - mockDiscover.mockReturnValue({ - error$: from([ - PluginDiscoveryError.missingManifest('path-2', new Error('No manifest')), - PluginDiscoveryError.invalidSearchPath('dir-1', new Error('No dir')), - PluginDiscoveryError.invalidPluginPath('path4-1', new Error('No path')), - ]), - plugin$: from([firstPlugin, secondPlugin]), + it('does not throw in case of cyclic plugin dependencies', async () => { + const firstPlugin = createPlugin('first-plugin', { + path: 'path-1', + requiredPlugins: ['second-plugin'], + }); + const secondPlugin = createPlugin('second-plugin', { + path: 'path-2', + requiredPlugins: ['third-plugin', 'last-plugin'], + }); + const thirdPlugin = createPlugin('third-plugin', { + path: 'path-3', + requiredPlugins: ['last-plugin', 'first-plugin'], + }); + const lastPlugin = createPlugin('last-plugin', { + path: 'path-4', + requiredPlugins: ['first-plugin'], + }); + const missingDepsPlugin = createPlugin('missing-deps-plugin', { + path: 'path-5', + requiredPlugins: ['not-a-plugin'], + }); + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), + }); + + await expect(pluginsService.discover()).resolves.toBeUndefined(); + + expect(mockDiscover).toHaveBeenCalledTimes(1); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(thirdPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(lastPlugin); + }); + + it('properly invokes plugin discovery and ignores non-critical errors.', async () => { + const firstPlugin = createPlugin('some-id', { + path: 'path-1', + configPath: 'path', + requiredPlugins: ['some-other-id'], + optionalPlugins: ['missing-optional-dep'], + }); + const secondPlugin = createPlugin('some-other-id', { + path: 'path-2', + version: 'some-other-version', + configPath: ['plugin', 'path'], + }); + + mockDiscover.mockReturnValue({ + error$: from([ + PluginDiscoveryError.missingManifest('path-2', new Error('No manifest')), + PluginDiscoveryError.invalidSearchPath('dir-1', new Error('No dir')), + PluginDiscoveryError.invalidPluginPath('path4-1', new Error('No path')), + ]), + plugin$: from([firstPlugin, secondPlugin]), + }); + + await pluginsService.discover(); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); + + expect(mockDiscover).toHaveBeenCalledTimes(1); + expect(mockDiscover).toHaveBeenCalledWith( + { + additionalPluginPaths: [], + initialize: true, + pluginSearchPaths: [ + resolve(process.cwd(), 'src', 'plugins'), + resolve(process.cwd(), 'x-pack', 'plugins'), + resolve(process.cwd(), 'plugins'), + resolve(process.cwd(), '..', 'kibana-extra'), + ], + }, + { coreId, env, logger, configService } + ); + + const logs = loggingServiceMock.collect(logger); + expect(logs.info).toHaveLength(0); + expect(logs.error).toHaveLength(0); + }); + + it('registers plugin config schema in config service', async () => { + const configSchema = schema.string(); + jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); + jest.doMock( + join('path-with-schema', 'server'), + () => ({ + config: { + schema: configSchema, + }, + }), + { + virtual: true, + } + ); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('some-id', { + path: 'path-with-schema', + configPath: 'path', + }), + ]), + }); + await pluginsService.discover(); + expect(configService.setSchema).toBeCalledWith('path', configSchema); + }); }); - await pluginsService.discover(); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); - expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); - - expect(mockDiscover).toHaveBeenCalledTimes(1); - expect(mockDiscover).toHaveBeenCalledWith( - { - additionalPluginPaths: [], - initialize: true, - pluginSearchPaths: [ - resolve(process.cwd(), 'src', 'plugins'), - resolve(process.cwd(), 'x-pack', 'plugins'), - resolve(process.cwd(), 'plugins'), - resolve(process.cwd(), '..', 'kibana-extra'), - ], - }, - { coreId, env, logger, configService } - ); - - const logs = loggingServiceMock.collect(logger); - expect(logs.info).toHaveLength(0); - expect(logs.error).toHaveLength(0); -}); - -test('`stop` stops plugins system', async () => { - await pluginsService.stop(); - expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); -}); - -test('`discover` registers plugin config schema in config service', async () => { - const configSchema = schema.string(); - jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); - jest.doMock( - join('path-with-schema', 'server'), - () => ({ - config: { - schema: configSchema, - }, - }), - { - virtual: true, - } - ); - mockDiscover.mockReturnValue({ - error$: from([]), - plugin$: from([ - createPlugin('some-id', { - path: 'path-with-schema', - configPath: 'path', - }), - ]), + describe('#stop()', () => { + it('`stop` stops plugins system', async () => { + await pluginsService.stop(); + expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); + }); }); - await pluginsService.discover(); - expect(configService.setSchema).toBeCalledWith('path', configSchema); }); From 95f5fc4e5be223088a9a1ce3a2607a650135cd25 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 15:37:04 +0100 Subject: [PATCH 09/17] fix test/mocks due to plugin configs api changes --- src/core/server/legacy/legacy_service.test.ts | 2 +- src/core/server/plugins/plugins_service.mock.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 989c408b328d2..1240518422e2f 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -85,8 +85,8 @@ beforeEach(() => { uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), - config: new Map([['plugin-id', null]]), }, + uiPluginConfigs: new Map(), }, }, plugins: { 'plugin-id': 'plugin-value' }, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 4dcebdf673e0d..e3be8fbb98309 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -32,8 +32,8 @@ const createServiceMock = () => { uiPlugins: { public: new Map(), internal: new Map(), - config: new Map(), }, + uiPluginConfigs: new Map(), }); mocked.start.mockResolvedValue({ contracts: new Map() }); return mocked; From e08312529a838c47ab37e90f3bc4e6e052761dc6 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 20:20:29 +0100 Subject: [PATCH 10/17] add unit tests --- .../injected_metadata_service.test.ts | 4 +- .../public/plugins/plugins_service.test.ts | 23 ++- .../server/plugins/plugins_service.test.ts | 157 ++++++++++++++---- 3 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index 1110097c1c92b..cf4b72114d5ac 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -69,7 +69,7 @@ describe('setup.getPlugins()', () => { const injectedMetadata = new InjectedMetadataService({ injectedMetadata: { uiPlugins: [ - { id: 'plugin-1', plugin: {} }, + { id: 'plugin-1', plugin: {}, config: { clientProp: 'clientValue' } }, { id: 'plugin-2', plugin: {} }, ], }, @@ -77,7 +77,7 @@ describe('setup.getPlugins()', () => { const plugins = injectedMetadata.setup().getPlugins(); expect(plugins).toEqual([ - { id: 'plugin-1', plugin: {} }, + { id: 'plugin-1', plugin: {}, config: { clientProp: 'clientValue' } }, { id: 'plugin-2', plugin: {} }, ]); }); diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index b074f5a655ef3..ef2a1410ddc6f 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -42,10 +42,11 @@ import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.moc import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { CoreSetup, CoreStart } from '..'; +import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; +import { take } from 'rxjs/operators'; export let mockPluginInitializers: Map; @@ -208,7 +209,7 @@ describe('PluginsService', () => { ); }); - it('initalizes plugins with PluginIntitializerContext', async () => { + it('initializes plugins with PluginInitializerContext', async () => { const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); @@ -217,6 +218,24 @@ describe('PluginsService', () => { expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(expect.any(Object)); }); + it('initializes plugins with associated client configuration', async () => { + const pluginConfig = { + clientProperty: 'some value', + }; + plugins[0].config = pluginConfig; + + const pluginsService = new PluginsService(mockCoreContext, plugins); + await pluginsService.setup(mockSetupDeps); + + const initializerContext = mockPluginInitializers.get('pluginA')!.mock + .calls[0][0] as PluginInitializerContext; + const config = await initializerContext.config + .create() + .pipe(take(1)) + .toPromise(); + expect(config).toMatchObject(pluginConfig); + }); + it('exposes dependent setup contracts to plugins', async () => { const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 184e599aae73e..78bd66c6e71ca 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -32,6 +32,8 @@ import { PluginWrapper } from './plugin'; import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; +import { take } from 'rxjs/operators'; +import { DiscoveredPluginInternal } from './types'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; @@ -128,16 +130,16 @@ describe('PluginsService', () => { }); await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` -[Error: Failed to initialize plugins: - Invalid JSON (invalid-manifest, path-1)] -`); + [Error: Failed to initialize plugins: + Invalid JSON (invalid-manifest, path-1)] + `); expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` -Array [ - Array [ - [Error: Invalid JSON (invalid-manifest, path-1)], - ], -] -`); + Array [ + Array [ + [Error: Invalid JSON (invalid-manifest, path-1)], + ], + ] + `); }); it('throws if plugin required Kibana version is incompatible with the current version', async () => { @@ -149,16 +151,16 @@ Array [ }); await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` -[Error: Failed to initialize plugins: - Incompatible version (incompatible-version, path-3)] -`); + [Error: Failed to initialize plugins: + Incompatible version (incompatible-version, path-3)] + `); expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` -Array [ - Array [ - [Error: Incompatible version (incompatible-version, path-3)], - ], -] -`); + Array [ + Array [ + [Error: Incompatible version (incompatible-version, path-3)], + ], + ] + `); }); it('throws if discovered plugins with conflicting names', async () => { @@ -235,21 +237,21 @@ Array [ expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(` -Array [ - Array [ - "Plugin \\"explicitly-disabled-plugin\\" is disabled.", - ], - Array [ - "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", - ], - Array [ - "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", - ], - Array [ - "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", - ], -] -`); + Array [ + Array [ + "Plugin \\"explicitly-disabled-plugin\\" is disabled.", + ], + Array [ + "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + ], + Array [ + "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + ], + Array [ + "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", + ], + ] + `); }); it('does not throw in case of mutual plugin dependencies', async () => { @@ -387,6 +389,95 @@ Array [ }); }); + describe('#generateUiPluginsConfigs()', () => { + const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPluginInternal] => [ + plugin.name, + { + id: plugin.name, + path: plugin.path, + configPath: plugin.manifest.configPath, + requiredPlugins: [], + optionalPlugins: [], + }, + ]; + + it('properly generates client configs for plugins according to `exposeToBrowser`', async () => { + jest.doMock( + join('plugin-with-expose', 'server'), + () => ({ + config: { + exposeToBrowser: ['sharedProp'], + schema: schema.object({ + serverProp: schema.string({ defaultValue: 'serverProp default value' }), + sharedProp: schema.string({ defaultValue: 'sharedProp default value' }), + }), + }, + }), + { + virtual: true, + } + ); + const plugin = createPlugin('plugin-with-expose', { + path: 'plugin-with-expose', + configPath: 'path', + }); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([plugin]), + }); + mockPluginSystem.uiPlugins.mockReturnValue({ + public: new Map([pluginToDiscoveredEntry(plugin)]), + internal: new Map([pluginToDiscoveredEntry(plugin)]), + }); + + await pluginsService.discover(); + const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + + const uiConfig$ = uiPluginConfigs.get('plugin-with-expose'); + expect(uiConfig$).toBeDefined(); + + const uiConfig = await uiConfig$.pipe(take(1)).toPromise(); + expect(uiConfig).toMatchInlineSnapshot(` + Object { + "sharedProp": "sharedProp default value", + } + `); + }); + + it('does not generate config for plugins not exposing to client', async () => { + jest.doMock( + join('plugin-without-expose', 'server'), + () => ({ + config: { + schema: schema.object({ + serverProp: schema.string({ defaultValue: 'serverProp default value' }), + }), + }, + }), + { + virtual: true, + } + ); + const plugin = createPlugin('plugin-without-expose', { + path: 'plugin-without-expose', + configPath: 'path', + }); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([plugin]), + }); + mockPluginSystem.uiPlugins.mockReturnValue({ + public: new Map([pluginToDiscoveredEntry(plugin)]), + internal: new Map([pluginToDiscoveredEntry(plugin)]), + }); + + await pluginsService.discover(); + const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + + expect([...uiPluginConfigs.entries()]).toHaveLength(0); + }); + }); + describe('#stop()', () => { it('`stop` stops plugins system', async () => { await pluginsService.stop(); From c3c926a8dafdd544a6bc8be283ceef0fb5af7fbe Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 21:08:15 +0100 Subject: [PATCH 11/17] update migration guide --- src/core/MIGRATION.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 366a5b65fbb99..3182998dbca4b 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1112,6 +1112,7 @@ import { npStart: { core } } from 'ui/new_platform'; | `ui/routes` | -- | There is no global routing mechanism. Each app [configures its own routing](/rfcs/text/0004_application_service_mounting.md#complete-example). | | `ui/saved_objects` | [`core.savedObjects`](/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md) | Client API is the same | | `ui/doc_title` | [`core.chrome.docTitle`](/docs/development/core/public/kibana-plugin-public.chromedoctitle.md) | | +| `uiExports/injectedVars` | [Configure plugin](#configure-plugin) and [`PluginConfigDescriptor.exposeToBrowser`](/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Can only be used to expose configuration properties | _See also: [Public's CoreStart API Docs](/docs/development/core/public/kibana-plugin-public.corestart.md)_ @@ -1243,6 +1244,43 @@ class MyPlugin { } ``` +If your plugin also have a client-side part, you can also expose configuration properties to it using a whitelisting mechanism with the configuration `exposeToBrowser` property. +```typescript +// my_plugin/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Only on server' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: ['uiProp'], + schema: configSchema, +}; + +export type ClientConfigType = Pick +``` + +Configuration containing only the exposed properties will be then available on the client-side plugin using the same API as the server-side: +```typescript +// my_plugin/public/index.ts +import { ClientConfigType } from '../server'; + +export class Plugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + const config = await this.initializerContext.config + .create() + .pipe(take(1)) + .toPromise(); + } +``` + ### Mock new platform services in tests #### Writing mocks for your plugin From 93ba77325f807c52585f208a5913e5d8d57c982a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 21:25:40 +0100 Subject: [PATCH 12/17] update tsdoc --- .../core/server/kibana-plugin-server.md | 4 +- ....pluginconfigdescriptor.exposetobrowser.md | 2 + ...na-plugin-server.pluginconfigdescriptor.md | 28 +++++++++++++- ...in-server.pluginconfigdescriptor.schema.md | 4 ++ ...kibana-plugin-server.pluginconfigschema.md | 2 + src/core/server/plugins/types.ts | 37 +++++++++++++++++++ src/core/server/server.api.md | 10 +---- 7 files changed, 75 insertions(+), 12 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 35d59b5ee7bea..c6ab8502acbd2 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -75,7 +75,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | -| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | | +| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [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) | | @@ -157,7 +157,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | -| [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | | +| [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | Dedicated type for plugin configuration schema. | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md index 42e3e44f96d31..d572aab2d8359 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md @@ -4,6 +4,8 @@ ## PluginConfigDescriptor.exposeToBrowser property +List of configuration properties that will be available on the client-side plugin. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md index 30206c808d471..b6bbfa70acb8c 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -4,6 +4,8 @@ ## PluginConfigDescriptor interface +Describes a plugin configuration schema and capabilities. + Signature: ```typescript @@ -14,6 +16,28 @@ export interface PluginConfigDescriptor | Property | Type | Description | | --- | --- | --- | -| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Array<keyof T> | | -| [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | | +| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Array<keyof T> | List of configuration properties that will be available on the client-side plugin. | +| [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | + +## Example + + +```typescript +// my_plugin/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Only on server' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: ['uiProp'], + schema: configSchema, +}; + +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md index ee473a8f1f6ff..c4845d52ff212 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md @@ -4,6 +4,10 @@ ## PluginConfigDescriptor.schema property +Schema to use to validate the plugin configuration. + +[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md index 8efb3ddfba989..80de34f39b87b 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md @@ -4,6 +4,8 @@ ## PluginConfigSchema type +Dedicated type for plugin configuration schema. + Signature: ```typescript diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index e47ab1064f643..4f7330a0a2940 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -24,10 +24,47 @@ import { ConfigPath, EnvironmentMode, PackageInfo } from '../config'; import { LoggerFactory } from '../logging'; import { CoreSetup, CoreStart } from '..'; +/** + * Dedicated type for plugin configuration schema. + * + * @public + */ export type PluginConfigSchema = Type; +/** + * Describes a plugin configuration schema and capabilities. + * + * @example + * ```typescript + * // my_plugin/server/index.ts + * import { schema, TypeOf } from '@kbn/config-schema'; + * import { PluginConfigDescriptor } from 'kibana/server'; + * + * const configSchema = schema.object({ + * secret: schema.string({ defaultValue: 'Only on server' }), + * uiProp: schema.string({ defaultValue: 'Accessible from client' }), + * }); + * + * type ConfigType = TypeOf; + * + * export const config: PluginConfigDescriptor = { + * exposeToBrowser: ['uiProp'], + * schema: configSchema, + * }; + * ``` + * + * @public + */ export interface PluginConfigDescriptor { + /** + * List of configuration properties that will be available on the client-side plugin. + */ exposeToBrowser?: Array; + /** + * Schema to use to validate the plugin configuration. + * + * {@link PluginConfigSchema} + */ schema: PluginConfigSchema; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0c7da88f07a8a..3d064010d124b 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -959,19 +959,13 @@ export interface Plugin { - // (undocumented) exposeToBrowser?: Array; - // (undocumented) schema: PluginConfigSchema; } -// Warning: (ae-missing-release-tag) "PluginConfigSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PluginConfigSchema = Type; // @public From bd1e3bcb521a6733accf999572de6aa603f80c63 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 18 Nov 2019 21:57:20 +0100 Subject: [PATCH 13/17] fix typecheck --- src/core/server/plugins/plugins_service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 78bd66c6e71ca..8f03ca9709bca 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -436,7 +436,7 @@ describe('PluginsService', () => { const uiConfig$ = uiPluginConfigs.get('plugin-with-expose'); expect(uiConfig$).toBeDefined(); - const uiConfig = await uiConfig$.pipe(take(1)).toPromise(); + const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); expect(uiConfig).toMatchInlineSnapshot(` Object { "sharedProp": "sharedProp default value", From 1fe445f01b3289eab0bf9deeb50c05eef9f19731 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 19 Nov 2019 18:12:43 +0100 Subject: [PATCH 14/17] use sync getter for config on client side instead of observable --- src/core/MIGRATION.md | 14 ++++++-------- src/core/public/mocks.ts | 3 +-- src/core/public/plugins/plugin_context.ts | 7 +++---- src/core/public/plugins/plugins_service.test.ts | 6 +----- src/plugins/testbed/public/plugin.ts | 6 +----- x-pack/legacy/plugins/siem/public/apps/index.ts | 3 +-- x-pack/legacy/plugins/uptime/public/apps/index.ts | 3 +-- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 3182998dbca4b..e9e599f520009 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1261,23 +1261,21 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: ['uiProp'], schema: configSchema, }; - -export type ClientConfigType = Pick ``` -Configuration containing only the exposed properties will be then available on the client-side plugin using the same API as the server-side: +Configuration containing only the exposed properties will be then available on the client-side using the plugin's `initializerContext`: ```typescript // my_plugin/public/index.ts -import { ClientConfigType } from '../server'; +interface ClientConfigType { + uiProp: string; +} export class Plugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup(core: CoreSetup, deps: {}) { - const config = await this.initializerContext.config - .create() - .pipe(take(1)) - .toPromise(); + const config = this.initializerContext.config.get(); + // ... } ``` diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 9089a6138b58a..2b09bec433660 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { of } from 'rxjs'; import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; import { CoreContext, CoreSetup, CoreStart, PluginInitializerContext, NotificationsSetup } from '.'; @@ -94,7 +93,7 @@ function pluginInitializerContextMock() { }, }, config: { - create: () => of({} as T), + get: () => ({} as T), }, }; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index d1c05ebcd5e58..dbb76b30554a7 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -18,7 +18,6 @@ */ import { omit } from 'lodash'; -import { Observable, of } from 'rxjs'; import { DiscoveredPlugin } from '../../server'; import { PluginOpaqueId, PackageInfo, EnvironmentMode } from '../../server/types'; import { CoreContext } from '../core_system'; @@ -41,7 +40,7 @@ export interface PluginInitializerContext { packageInfo: Readonly; }; readonly config: { - create: () => Observable; + get: () => T; }; } @@ -67,8 +66,8 @@ export function createPluginInitializerContext( opaqueId, env: coreContext.env, config: { - create() { - return of((pluginConfig as unknown) as T); + get() { + return (pluginConfig as unknown) as T; }, }, }; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index ef2a1410ddc6f..2983d7583cb49 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -46,7 +46,6 @@ import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; -import { take } from 'rxjs/operators'; export let mockPluginInitializers: Map; @@ -229,10 +228,7 @@ describe('PluginsService', () => { const initializerContext = mockPluginInitializers.get('pluginA')!.mock .calls[0][0] as PluginInitializerContext; - const config = await initializerContext.config - .create() - .pipe(take(1)) - .toPromise(); + const config = initializerContext.config.get(); expect(config).toMatchObject(pluginConfig); }); diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts index 32f9be3498a11..8c70485d9ee8b 100644 --- a/src/plugins/testbed/public/plugin.ts +++ b/src/plugins/testbed/public/plugin.ts @@ -17,7 +17,6 @@ * under the License. */ -import { take } from 'rxjs/operators'; import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/public'; interface ConfigType { @@ -28,10 +27,7 @@ export class TestbedPlugin implements Plugin() - .pipe(take(1)) - .toPromise(); + const config = this.initializerContext.config.get(); // eslint-disable-next-line no-console console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`); diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts index 1f53707cd973d..73f9b65ba3546 100644 --- a/x-pack/legacy/plugins/siem/public/apps/index.ts +++ b/x-pack/legacy/plugins/siem/public/apps/index.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { of } from 'rxjs'; import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; new Plugin( // eslint-disable-next-line @typescript-eslint/no-explicit-any - { opaqueId: Symbol('siem'), env: {} as any, config: { create: () => of({} as any) } }, + { opaqueId: Symbol('siem'), env: {} as any, config: { get: () => ({} as any) } }, chrome ).start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts index 20a8156d7b066..53a74022778f4 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/index.ts +++ b/x-pack/legacy/plugins/uptime/public/apps/index.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { of } from 'rxjs'; import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; new Plugin( - { opaqueId: Symbol('uptime'), env: {} as any, config: { create: () => of({} as any) } }, + { opaqueId: Symbol('uptime'), env: {} as any, config: { get: () => ({} as any) } }, chrome ).start(npStart); From 5e34e91e8a86b8500ba11dfc4bd9ce63d8fa6ae2 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 20 Nov 2019 08:46:25 +0100 Subject: [PATCH 15/17] change type of exposeToBrowser prop --- src/core/MIGRATION.md | 4 +++- src/core/public/plugins/plugins_service.ts | 4 ++-- src/core/server/plugins/plugins_service.test.ts | 4 +++- src/core/server/plugins/plugins_service.ts | 17 ++++++++++++----- src/core/server/plugins/types.ts | 2 +- src/plugins/testbed/server/index.ts | 4 +++- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index e9e599f520009..fa57a6e389e09 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1258,7 +1258,9 @@ const configSchema = schema.object({ type ConfigType = TypeOf; export const config: PluginConfigDescriptor = { - exposeToBrowser: ['uiProp'], + exposeToBrowser: { + uiProp: true, + }, schema: configSchema, }; ``` diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 0221762f8dd70..c1939a3397647 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -61,7 +61,7 @@ export class PluginsService implements CoreService(plugins.map(p => [p.id, Symbol(p.id)])); // Setup dependency map and plugin wrappers - plugins.forEach(({ id, plugin, config }) => { + plugins.forEach(({ id, plugin, config = {} }) => { // Setup map of dependencies this.pluginDependencies.set(id, [ ...plugin.requiredPlugins, @@ -74,7 +74,7 @@ export class PluginsService implements CoreService { join('plugin-with-expose', 'server'), () => ({ config: { - exposeToBrowser: ['sharedProp'], + exposeToBrowser: { + sharedProp: true, + }, schema: schema.object({ serverProp: schema.string({ defaultValue: 'serverProp default value' }), sharedProp: schema.string({ defaultValue: 'sharedProp default value' }), diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 1d6442457b0bf..79c9489a8b4c0 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -124,21 +124,28 @@ export class PluginsService implements CoreService> { return new Map( [...uiPlugins] - .filter(([pluginId, plugin]) => { + .filter(([pluginId, _]) => { const configDescriptor = this.pluginConfigDescriptors.get(pluginId); return ( configDescriptor && configDescriptor.exposeToBrowser && - configDescriptor.exposeToBrowser.length > 0 + Object.values(configDescriptor?.exposeToBrowser).some(exposed => exposed) ); }) .map(([pluginId, plugin]) => { const configDescriptor = this.pluginConfigDescriptors.get(pluginId)!; return [ pluginId, - this.configService - .atPath(plugin.configPath) - .pipe(map((config: any) => pick(config || {}, configDescriptor.exposeToBrowser!))), + this.configService.atPath(plugin.configPath).pipe( + map((config: any) => + pick( + config || {}, + Object.entries(configDescriptor.exposeToBrowser!) + .filter(([_, exposed]) => exposed) + .map(([key, _]) => key) + ) + ) + ), ]; }) ); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 4f7330a0a2940..8b89a583043a7 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -59,7 +59,7 @@ export interface PluginConfigDescriptor { /** * List of configuration properties that will be available on the client-side plugin. */ - exposeToBrowser?: Array; + exposeToBrowser?: { [P in keyof T]?: boolean }; /** * Schema to use to validate the plugin configuration. * diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 75e8a740f595b..60c6f560869bb 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -37,7 +37,9 @@ const configSchema = schema.object({ type ConfigType = TypeOf; export const config: PluginConfigDescriptor = { - exposeToBrowser: ['uiProp'], + exposeToBrowser: { + uiProp: true, + }, schema: configSchema, }; From b1470188cd0a34a4aefe945138c051ed1f8ef0e0 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 20 Nov 2019 09:25:00 +0100 Subject: [PATCH 16/17] updates generated doc --- .../kibana-plugin-public.plugininitializercontext.config.md | 2 +- .../public/kibana-plugin-public.plugininitializercontext.md | 2 +- ...na-plugin-server.pluginconfigdescriptor.exposetobrowser.md | 4 +++- .../server/kibana-plugin-server.pluginconfigdescriptor.md | 2 +- src/core/public/public.api.md | 2 +- src/core/server/server.api.md | 4 +++- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md index 219ebafb2d42d..2a6ff23ec9b28 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md @@ -8,6 +8,6 @@ ```typescript readonly config: { - create: () => Observable; + get: () => T; }; ``` diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md index 08aba3f961b79..ffd56bbed7519 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md @@ -16,7 +16,7 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-public.plugininitializercontext.config.md) | {
create: <T = ConfigSchema>() => Observable<T>;
} | | +| [config](./kibana-plugin-public.plugininitializercontext.config.md) | {
get: <T = ConfigSchema>() => T;
} | | | [env](./kibana-plugin-public.plugininitializercontext.env.md) | {
mode: Readonly<EnvironmentMode>;
packageInfo: Readonly<PackageInfo>;
} | | | [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) | PluginOpaqueId | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md index d572aab2d8359..d62b2457e9d9a 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md @@ -9,5 +9,7 @@ List of configuration properties that will be available on the client-side plugi Signature: ```typescript -exposeToBrowser?: Array; +exposeToBrowser?: { + [P in keyof T]?: boolean; + }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md index b6bbfa70acb8c..6cdfc008ad5ed 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -16,7 +16,7 @@ export interface PluginConfigDescriptor | Property | Type | Description | | --- | --- | --- | -| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Array<keyof T> | List of configuration properties that will be available on the client-side plugin. | +| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | {
[P in keyof T]?: boolean;
} | List of configuration properties that will be available on the client-side plugin. | | [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | ## Example diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 5634ca13c9b9d..efce3f457419e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -698,7 +698,7 @@ export type PluginInitializer { // (undocumented) readonly config: { - create: () => Observable; + get: () => T; }; // (undocumented) readonly env: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 3d064010d124b..9b7be92e75468 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -961,7 +961,9 @@ export interface Plugin { - exposeToBrowser?: Array; + exposeToBrowser?: { + [P in keyof T]?: boolean; + }; schema: PluginConfigSchema; } From cfa27e876d6b0852f57ab4947ae6eed6ce5e90f6 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 21 Nov 2019 10:50:57 +0100 Subject: [PATCH 17/17] fix doc and address nits --- .../kibana-plugin-public.plugininitializercontext.config.md | 2 +- .../public/kibana-plugin-public.plugininitializercontext.md | 4 ++-- .../server/kibana-plugin-server.pluginconfigdescriptor.md | 4 +++- .../core/server/kibana-plugin-server.pluginconfigschema.md | 2 +- src/core/public/plugins/plugin_context.ts | 4 ++-- src/core/public/public.api.md | 4 ++-- src/core/server/plugins/types.ts | 6 ++++-- src/core/server/server.api.md | 2 +- src/plugins/testbed/server/index.ts | 2 +- 9 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md index 2a6ff23ec9b28..28141c9e13749 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.config.md @@ -8,6 +8,6 @@ ```typescript readonly config: { - get: () => T; + get: () => T; }; ``` diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md index ffd56bbed7519..64eaabb28646d 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md @@ -9,14 +9,14 @@ The available core services passed to a `PluginInitializer` Signature: ```typescript -export interface PluginInitializerContext +export interface PluginInitializerContext ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-public.plugininitializercontext.config.md) | {
get: <T = ConfigSchema>() => T;
} | | +| [config](./kibana-plugin-public.plugininitializercontext.config.md) | {
get: <T extends object = ConfigSchema>() => T;
} | | | [env](./kibana-plugin-public.plugininitializercontext.env.md) | {
mode: Readonly<EnvironmentMode>;
packageInfo: Readonly<PackageInfo>;
} | | | [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) | PluginOpaqueId | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md index 6cdfc008ad5ed..41fdcfe5df45d 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -35,7 +35,9 @@ const configSchema = schema.object({ type ConfigType = TypeOf; export const config: PluginConfigDescriptor = { - exposeToBrowser: ['uiProp'], + exposeToBrowser: { + uiProp: true, + }, schema: configSchema, }; diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md index 80de34f39b87b..6528798ec8e01 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md @@ -9,5 +9,5 @@ Dedicated type for plugin configuration schema. Signature: ```typescript -export declare type PluginConfigSchema = Type; +export declare type PluginConfigSchema = Type; ``` diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index dbb76b30554a7..f77ddd8f2f696 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -30,7 +30,7 @@ import { CoreSetup, CoreStart } from '../'; * * @public */ -export interface PluginInitializerContext { +export interface PluginInitializerContext { /** * A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. */ @@ -40,7 +40,7 @@ export interface PluginInitializerContext { packageInfo: Readonly; }; readonly config: { - get: () => T; + get: () => T; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index efce3f457419e..330b89e1184ef 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -695,10 +695,10 @@ export interface Plugin = (core: PluginInitializerContext) => Plugin; // @public -export interface PluginInitializerContext { +export interface PluginInitializerContext { // (undocumented) readonly config: { - get: () => T; + get: () => T; }; // (undocumented) readonly env: { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 8b89a583043a7..17704ce687b92 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -29,7 +29,7 @@ import { CoreSetup, CoreStart } from '..'; * * @public */ -export type PluginConfigSchema = Type; +export type PluginConfigSchema = Type; /** * Describes a plugin configuration schema and capabilities. @@ -48,7 +48,9 @@ export type PluginConfigSchema = Type; * type ConfigType = TypeOf; * * export const config: PluginConfigDescriptor = { - * exposeToBrowser: ['uiProp'], + * exposeToBrowser: { + * uiProp: true, + * }, * schema: configSchema, * }; * ``` diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9b7be92e75468..7ecb9053a4bcf 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -968,7 +968,7 @@ export interface PluginConfigDescriptor { } // @public -export type PluginConfigSchema = Type; +export type PluginConfigSchema = Type; // @public export type PluginInitializer = (core: PluginInitializerContext) => Plugin; diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 60c6f560869bb..07fda4eb98727 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -36,7 +36,7 @@ const configSchema = schema.object({ type ConfigType = TypeOf; -export const config: PluginConfigDescriptor = { +export const config: PluginConfigDescriptor = { exposeToBrowser: { uiProp: true, },