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..28141c9e13749
--- /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: {
+ 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 87c39a502040d..64eaabb28646d 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) | {
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.md b/docs/development/core/server/kibana-plugin-server.md
index 9907750b8742f..c6ab8502acbd2 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) | 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) | |
@@ -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) | 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
new file mode 100644
index 0000000000000..d62b2457e9d9a
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md
@@ -0,0 +1,15 @@
+
+
+[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
+
+List of configuration properties that will be available on the client-side plugin.
+
+Signature:
+
+```typescript
+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
new file mode 100644
index 0000000000000..41fdcfe5df45d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md
@@ -0,0 +1,45 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md)
+
+## PluginConfigDescriptor interface
+
+Describes a plugin configuration schema and capabilities.
+
+Signature:
+
+```typescript
+export interface PluginConfigDescriptor
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [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
+
+
+```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: true,
+ },
+ 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
new file mode 100644
index 0000000000000..c4845d52ff212
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.schema.md
@@ -0,0 +1,15 @@
+
+
+[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
+
+Schema to use to validate the plugin configuration.
+
+[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md)
+
+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..6528798ec8e01
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.pluginconfigschema.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md)
+
+## PluginConfigSchema type
+
+Dedicated type for plugin configuration schema.
+
+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..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>
| |
+| [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/src/core/MIGRATION.md b/src/core/MIGRATION.md
new file mode 100644
index 0000000000000..e69de29bb2d1d
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.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/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts
index a5342aaa48b72..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,10 +63,7 @@ export interface InjectedMetadataParams {
mode: Readonly;
packageInfo: Readonly;
};
- uiPlugins: Array<{
- id: PluginName;
- plugin: DiscoveredPlugin;
- }>;
+ uiPlugins: InjectedPluginMetadata[];
capabilities: Capabilities;
legacyMode: boolean;
legacyMetadata: {
@@ -165,10 +170,7 @@ export interface InjectedMetadataSetup {
/**
* An array of frontend plugins in topological order.
*/
- getPlugins: () => Array<{
- id: string;
- plugin: DiscoveredPlugin;
- }>;
+ 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 afd0825ec986c..695f0454f8b65 100644
--- a/src/core/public/mocks.ts
+++ b/src/core/public/mocks.ts
@@ -92,6 +92,9 @@ function pluginInitializerContextMock() {
dist: false,
},
},
+ config: {
+ get: () => ({} as T),
+ },
};
return mock;
diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts
index eae45654fce18..f77ddd8f2f696 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 { DiscoveredPlugin } from '../../server';
import { PluginOpaqueId, PackageInfo, EnvironmentMode } from '../../server/types';
import { CoreContext } from '../core_system';
@@ -31,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,6 +39,9 @@ export interface PluginInitializerContext {
mode: Readonly;
packageInfo: Readonly;
};
+ readonly config: {
+ get: () => T;
+ };
}
/**
@@ -47,17 +49,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: {
+ 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 0d8887774e900..2983d7583cb49 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';
@@ -41,7 +42,7 @@ 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';
@@ -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 };
@@ -62,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 } = {}
@@ -152,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 [
@@ -165,152 +168,184 @@ 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('initializes plugins with PluginInitializerContext', 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('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 = initializerContext.config.get();
+ expect(config).toMatchObject(pluginConfig);
+ });
+
+ 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/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts
index 1ab9d7f2fa9b2..c1939a3397647 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 = (core: PluginInitializerContext) => Plugin;
// @public
-export interface PluginInitializerContext {
+export interface PluginInitializerContext {
+ // (undocumented)
+ readonly config: {
+ get: () => T;
+ };
// (undocumented)
readonly env: {
mode: Readonly;
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/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index e2aefd846d978..1240518422e2f 100644
--- a/src/core/server/legacy/legacy_service.test.ts
+++ b/src/core/server/legacy/legacy_service.test.ts
@@ -86,6 +86,7 @@ beforeEach(() => {
public: new Map([['plugin-id', {} as DiscoveredPlugin]]),
internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]),
},
+ uiPluginConfigs: new Map(),
},
},
plugins: { 'plugin-id': 'plugin-value' },
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/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.mock.ts b/src/core/server/plugins/plugins_service.mock.ts
index c8b6bed044fd7..e3be8fbb98309 100644
--- a/src/core/server/plugins/plugins_service.mock.ts
+++ b/src/core/server/plugins/plugins_service.mock.ts
@@ -33,6 +33,7 @@ const createServiceMock = () => {
public: new Map(),
internal: new Map(),
},
+ uiPluginConfigs: new Map(),
});
mocked.start.mockResolvedValue({ contracts: new Map() });
return mocked;
diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts
index 0b3bc0759463c..da6d1d5a010e7 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;
@@ -90,301 +92,398 @@ const createPlugin = (
});
};
-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());
-
- 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 });
+describe('PluginsService', () => {
+ beforeEach(async () => {
+ mockPackage.raw = {
+ branch: 'feature-v1',
+ version: 'v1',
+ build: {
+ distributable: true,
+ number: 100,
+ sha: 'feature-v1-build-sha',
+ },
+ };
- [mockPluginSystem] = MockPluginsSystem.mock.instances as any;
-});
+ coreId = Symbol('core');
+ env = Env.createDefault(getEnvOptions());
-afterEach(() => {
- jest.clearAllMocks();
-});
+ 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 });
-test('`discover` throws if plugin has an invalid manifest', async () => {
- mockDiscover.mockReturnValue({
- error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]),
- plugin$: from([]),
+ [mockPluginSystem] = MockPluginsSystem.mock.instances as any;
});
- await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(`
-[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)],
- ],
-]
-`);
-});
-
-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([]),
+ afterEach(() => {
+ jest.clearAllMocks();
});
- await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(`
-[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)],
- ],
-]
-`);
-});
-
-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!]`
- );
+ 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(`
+ Array [
+ Array [
+ [Error: Invalid JSON (invalid-manifest, path-1)],
+ ],
+ ]
+ `);
+ });
+
+ 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(`
+ [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)],
+ ],
+ ]
+ `);
+ });
+
+ 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.",
+ ],
+ 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 () => {
+ const firstPlugin = createPlugin('first-plugin', {
+ path: 'path-1',
+ requiredPlugins: ['second-plugin'],
+ });
+ const secondPlugin = createPlugin('second-plugin', {
+ path: 'path-2',
+ requiredPlugins: ['first-plugin'],
+ });
- expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled();
- expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled();
-});
+ mockDiscover.mockReturnValue({
+ error$: from([]),
+ plugin$: from([firstPlugin, secondPlugin]),
+ });
-test('`discover` properly detects plugins that should be disabled.', async () => {
- jest
- .spyOn(configService, 'isEnabledAtPath')
- .mockImplementation(path => Promise.resolve(!path.includes('disabled')));
+ await expect(pluginsService.discover()).resolves.toBeUndefined();
- mockPluginSystem.setupPlugins.mockResolvedValue(new Map());
- mockPluginSystem.uiPlugins.mockReturnValue({ public: new Map(), internal: new Map() });
+ expect(mockDiscover).toHaveBeenCalledTimes(1);
+ expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
+ expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin);
+ expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin);
+ });
- mockDiscover.mockReturnValue({
- error$: from([]),
- plugin$: from([
- createPlugin('explicitly-disabled-plugin', {
- disabled: true,
+ it('does not throw in case of cyclic plugin dependencies', async () => {
+ const firstPlugin = createPlugin('first-plugin', {
path: 'path-1',
- configPath: 'path-1',
- }),
- createPlugin('plugin-with-missing-required-deps', {
+ requiredPlugins: ['second-plugin'],
+ });
+ const secondPlugin = createPlugin('second-plugin', {
path: 'path-2',
- configPath: 'path-2',
- requiredPlugins: ['missing-plugin'],
- }),
- createPlugin('plugin-with-disabled-transitive-dep', {
+ requiredPlugins: ['third-plugin', 'last-plugin'],
+ });
+ const thirdPlugin = createPlugin('third-plugin', {
path: 'path-3',
- configPath: 'path-3',
- requiredPlugins: ['another-explicitly-disabled-plugin'],
- }),
- createPlugin('another-explicitly-disabled-plugin', {
- disabled: true,
+ requiredPlugins: ['last-plugin', 'first-plugin'],
+ });
+ const lastPlugin = createPlugin('last-plugin', {
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.",
- ],
- 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.",
- ],
-]
-`);
-});
-
-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'],
- });
-
- 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);
-});
-
-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]),
+ 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,
+ describe('#generateUiPluginsConfigs()', () => {
+ const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPluginInternal] => [
+ plugin.name,
+ {
+ id: plugin.name,
+ path: plugin.path,
+ configPath: plugin.manifest.configPath,
+ requiredPlugins: [],
+ optionalPlugins: [],
},
- }),
- {
- virtual: true,
- }
- );
- mockDiscover.mockReturnValue({
- error$: from([]),
- plugin$: from([
- createPlugin('some-id', {
- path: 'path-with-schema',
+ ];
+
+ it('properly generates client configs for plugins according to `exposeToBrowser`', async () => {
+ jest.doMock(
+ join('plugin-with-expose', 'server'),
+ () => ({
+ config: {
+ exposeToBrowser: {
+ sharedProp: true,
+ },
+ 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();
+ expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1);
+ });
});
- await pluginsService.discover();
- expect(configService.setSchema).toBeCalledWith('path', configSchema);
});
diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts
index 38fe519567a63..79c9489a8b4c0 100644
--- a/src/core/server/plugins/plugins_service.ts
+++ b/src/core/server/plugins/plugins_service.ts
@@ -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 {
@@ -37,6 +44,7 @@ export interface PluginsServiceSetup {
public: Map;
internal: Map;
};
+ uiPluginConfigs: Map>;
}
/** @public */
@@ -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,18 @@ 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,
+ uiPluginConfigs: this.generateUiPluginsConfigs(uiPlugins.public),
};
}
@@ -107,6 +119,38 @@ export class PluginsService implements CoreService
+ ): Map> {
+ return new Map(
+ [...uiPlugins]
+ .filter(([pluginId, _]) => {
+ const configDescriptor = this.pluginConfigDescriptors.get(pluginId);
+ return (
+ configDescriptor &&
+ configDescriptor.exposeToBrowser &&
+ 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 || {},
+ Object.entries(configDescriptor.exposeToBrowser!)
+ .filter(([_, exposed]) => exposed)
+ .map(([key, _]) => key)
+ )
+ )
+ ),
+ ];
+ })
+ );
+ }
+
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
@@ -138,9 +182,13 @@ 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) {
+ this.pluginConfigDescriptors.set(plugin.name, 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..17704ce687b92 100644
--- a/src/core/server/plugins/types.ts
+++ b/src/core/server/plugins/types.ts
@@ -24,7 +24,51 @@ import { ConfigPath, EnvironmentMode, PackageInfo } from '../config';
import { LoggerFactory } from '../logging';
import { CoreSetup, CoreStart } from '..';
-export type PluginConfigSchema = Type | null;
+/**
+ * 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: true,
+ * },
+ * schema: configSchema,
+ * };
+ * ```
+ *
+ * @public
+ */
+export interface PluginConfigDescriptor {
+ /**
+ * List of configuration properties that will be available on the client-side plugin.
+ */
+ exposeToBrowser?: { [P in keyof T]?: boolean };
+ /**
+ * Schema to use to validate the plugin configuration.
+ *
+ * {@link PluginConfigSchema}
+ */
+ schema: PluginConfigSchema;
+}
/**
* Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 97a04a4a4efab..7ecb9053a4bcf 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -959,6 +959,17 @@ export interface Plugin {
+ exposeToBrowser?: {
+ [P in keyof T]?: boolean;
+ };
+ schema: PluginConfigSchema;
+}
+
+// @public
+export type PluginConfigSchema = Type;
+
// @public
export type PluginInitializer = (core: PluginInitializerContext) => Plugin;
@@ -1003,6 +1014,8 @@ export interface PluginsServiceSetup {
// (undocumented)
contracts: Map;
// (undocumented)
+ uiPluginConfigs: Map>;
+ // (undocumented)
uiPlugins: {
public: Map;
internal: Map;
@@ -1615,6 +1628,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
```
diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts
index 448a418abcb8a..2bff1d707c951 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 0d05ea259d1a1..c0885cd5d3d13 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.uiPluginConfigs;
+ 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,
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..8c70485d9ee8b 100644
--- a/src/plugins/testbed/public/plugin.ts
+++ b/src/plugins/testbed/public/plugin.ts
@@ -17,12 +17,20 @@
* under the License.
*/
-import { Plugin, CoreSetup } from 'kibana/public';
+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 = this.initializerContext.config.get();
+
// 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..07fda4eb98727 100644
--- a/src/plugins/testbed/server/index.ts
+++ b/src/plugins/testbed/server/index.ts
@@ -20,15 +20,28 @@
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: true,
+ },
+ schema: configSchema,
+};
class Plugin {
private readonly log: Logger;
diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts
index b71c4fe699860..73f9b65ba3546 100644
--- a/x-pack/legacy/plugins/siem/public/apps/index.ts
+++ b/x-pack/legacy/plugins/siem/public/apps/index.ts
@@ -8,8 +8,8 @@ import chrome from 'ui/chrome';
import { npStart } from 'ui/new_platform';
import { Plugin } from './plugin';
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start(
- npStart.core,
- npStart.plugins
-);
+new Plugin(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-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 3b328b3ff2326..53a74022778f4 100644
--- a/x-pack/legacy/plugins/uptime/public/apps/index.ts
+++ b/x-pack/legacy/plugins/uptime/public/apps/index.ts
@@ -8,4 +8,7 @@ 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: { get: () => ({} as any) } },
+ chrome
+).start(npStart);