mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* introduce PluginConfigDescriptor type * inject client plugin configs in injectedMetadata * expose client config in PluginInitializerContext * add example implementation in testbed * update generated doc * only generates ui config entry for plugins exposing properties to client * separate plugin configs from plugins * restructure plugin services tests * fix test/mocks due to plugin configs api changes * add unit tests * update migration guide * update tsdoc * fix typecheck * use sync getter for config on client side instead of observable * change type of exposeToBrowser prop * updates generated doc * fix doc and address nits
This commit is contained in:
parent
92c31d1dd2
commit
f0257a8de3
35 changed files with 1064 additions and 634 deletions
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly config: {
|
||||
get: <T extends object = ConfigSchema>() => T;
|
||||
};
|
||||
```
|
|
@ -9,13 +9,14 @@ The available core services passed to a `PluginInitializer`
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface PluginInitializerContext
|
||||
export interface PluginInitializerContext<ConfigSchema extends object = object>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [config](./kibana-plugin-public.plugininitializercontext.config.md) | <code>{</code><br/><code> get: <T extends object = ConfigSchema>() => T;</code><br/><code> }</code> | |
|
||||
| [env](./kibana-plugin-public.plugininitializercontext.env.md) | <code>{</code><br/><code> mode: Readonly<EnvironmentMode>;</code><br/><code> packageInfo: Readonly<PackageInfo>;</code><br/><code> }</code> | |
|
||||
| [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) | <code>PluginOpaqueId</code> | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. |
|
||||
|
||||
|
|
|
@ -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 <code>PluginInitializer</code>. |
|
||||
| [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 <code>plugin</code> export at the root of a plugin's <code>server</code> 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) | |
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
exposeToBrowser?: {
|
||||
[P in keyof T]?: boolean;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,45 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface PluginConfigDescriptor<T = any>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | <code>{</code><br/><code> [P in keyof T]?: boolean;</code><br/><code> }</code> | List of configuration properties that will be available on the client-side plugin. |
|
||||
| [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | <code>PluginConfigSchema<T></code> | 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<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<ConfigType> = {
|
||||
exposeToBrowser: {
|
||||
uiProp: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
schema: PluginConfigSchema<T>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md)
|
||||
|
||||
## PluginConfigSchema type
|
||||
|
||||
Dedicated type for plugin configuration schema.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PluginConfigSchema<T> = Type<T>;
|
||||
```
|
|
@ -16,5 +16,6 @@ export interface PluginsServiceSetup
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | <code>Map<PluginName, unknown></code> | |
|
||||
| [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) | <code>Map<PluginName, Observable<unknown>></code> | |
|
||||
| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | <code>{</code><br/><code> public: Map<PluginName, DiscoveredPlugin>;</code><br/><code> internal: Map<PluginName, DiscoveredPluginInternal>;</code><br/><code> }</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
uiPluginConfigs: Map<PluginName, Observable<unknown>>;
|
||||
```
|
0
src/core/MIGRATION.md
Normal file
0
src/core/MIGRATION.md
Normal file
|
@ -22,5 +22,6 @@ export {
|
|||
InjectedMetadataParams,
|
||||
InjectedMetadataSetup,
|
||||
InjectedMetadataStart,
|
||||
InjectedPluginMetadata,
|
||||
LegacyNavLink,
|
||||
} from './injected_metadata_service';
|
||||
|
|
|
@ -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: {} },
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -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<EnvironmentMode>;
|
||||
packageInfo: Readonly<PackageInfo>;
|
||||
};
|
||||
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: () => {
|
||||
|
|
|
@ -92,6 +92,9 @@ function pluginInitializerContextMock() {
|
|||
dist: false,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
get: <T>() => ({} as T),
|
||||
},
|
||||
};
|
||||
|
||||
return mock;
|
||||
|
|
|
@ -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<ConfigSchema extends object = object> {
|
||||
/**
|
||||
* 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<EnvironmentMode>;
|
||||
packageInfo: Readonly<PackageInfo>;
|
||||
};
|
||||
readonly config: {
|
||||
get: <T extends object = ConfigSchema>() => 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<T>() {
|
||||
return (pluginConfig as unknown) as T;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T> = { [P in keyof T]: jest.Mocked<T[P]> };
|
||||
|
||||
|
@ -62,83 +63,6 @@ let mockSetupContext: DeeplyMocked<CoreSetup>;
|
|||
let mockStartDeps: DeeplyMocked<PluginsServiceStartDeps>;
|
||||
let mockStartContext: DeeplyMocked<CoreStart>;
|
||||
|
||||
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<PluginName, MockedPluginInitializer>(([
|
||||
[
|
||||
'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<PluginName, MockedPluginInitializer>(([
|
||||
[
|
||||
'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 },
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
await pluginsService.stop();
|
||||
|
||||
expect(pluginAInstance.stop).toHaveBeenCalled();
|
||||
expect(pluginBInstance.stop).toHaveBeenCalled();
|
||||
expect(pluginCInstance.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 },
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
test('`PluginService.stop` 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;
|
||||
|
||||
await pluginsService.stop();
|
||||
|
||||
expect(pluginAInstance.stop).toHaveBeenCalled();
|
||||
expect(pluginBInstance.stop).toHaveBeenCalled();
|
||||
expect(pluginCInstance.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -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<PluginsServiceSetup, PluginsS
|
|||
|
||||
private readonly satupPlugins: PluginName[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly coreContext: CoreContext,
|
||||
plugins: Array<{ id: PluginName; plugin: DiscoveredPlugin }>
|
||||
) {
|
||||
constructor(private readonly coreContext: CoreContext, plugins: InjectedPluginMetadata[]) {
|
||||
// Generate opaque ids
|
||||
const opaqueIds = new Map<PluginName, PluginOpaqueId>(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<PluginsServiceSetup, PluginsS
|
|||
new PluginWrapper(
|
||||
plugin,
|
||||
opaqueIds.get(id)!,
|
||||
createPluginInitializerContext(this.coreContext, opaqueIds.get(id)!, plugin)
|
||||
createPluginInitializerContext(this.coreContext, opaqueIds.get(id)!, plugin, config)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -695,7 +695,11 @@ export interface Plugin<TSetup = void, TStart = void, TPluginsSetup extends obje
|
|||
export type PluginInitializer<TSetup, TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
|
||||
// @public
|
||||
export interface PluginInitializerContext {
|
||||
export interface PluginInitializerContext<ConfigSchema extends object = object> {
|
||||
// (undocumented)
|
||||
readonly config: {
|
||||
get: <T extends object = ConfigSchema>() => T;
|
||||
};
|
||||
// (undocumented)
|
||||
readonly env: {
|
||||
mode: Readonly<EnvironmentMode>;
|
||||
|
|
|
@ -123,6 +123,8 @@ export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
|
|||
export {
|
||||
DiscoveredPlugin,
|
||||
Plugin,
|
||||
PluginConfigDescriptor,
|
||||
PluginConfigSchema,
|
||||
PluginInitializer,
|
||||
PluginInitializerContext,
|
||||
PluginManifest,
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -278,6 +278,7 @@ export class LegacyService implements CoreService<LegacyServiceSetup> {
|
|||
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,
|
||||
|
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -33,6 +33,7 @@ const createServiceMock = () => {
|
|||
public: new Map(),
|
||||
internal: new Map(),
|
||||
},
|
||||
uiPluginConfigs: new Map(),
|
||||
});
|
||||
mocked.start.mockResolvedValue({ contracts: new Map() });
|
||||
return mocked;
|
||||
|
|
|
@ -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> = 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<Config>(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();
|
||||
});
|
||||
|
||||
test('`discover` 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)],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
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([]),
|
||||
});
|
||||
|
||||
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!]`
|
||||
);
|
||||
|
||||
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(`
|
||||
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]),
|
||||
});
|
||||
|
||||
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('PluginsService', () => {
|
||||
beforeEach(async () => {
|
||||
mockPackage.raw = {
|
||||
branch: 'feature-v1',
|
||||
version: 'v1',
|
||||
build: {
|
||||
distributable: true,
|
||||
number: 100,
|
||||
sha: 'feature-v1-build-sha',
|
||||
},
|
||||
}),
|
||||
{
|
||||
virtual: true,
|
||||
}
|
||||
);
|
||||
mockDiscover.mockReturnValue({
|
||||
error$: from([]),
|
||||
plugin$: from([
|
||||
createPlugin('some-id', {
|
||||
path: 'path-with-schema',
|
||||
configPath: 'path',
|
||||
}),
|
||||
]),
|
||||
};
|
||||
|
||||
coreId = Symbol('core');
|
||||
env = Env.createDefault(getEnvOptions());
|
||||
|
||||
configService = new ConfigService(
|
||||
new BehaviorSubject<Config>(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();
|
||||
});
|
||||
|
||||
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'],
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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: 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);
|
||||
});
|
||||
|
|
|
@ -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<PluginName, DiscoveredPlugin>;
|
||||
internal: Map<PluginName, DiscoveredPluginInternal>;
|
||||
};
|
||||
uiPluginConfigs: Map<PluginName, Observable<unknown>>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -54,11 +62,14 @@ export interface PluginsServiceStartDeps {} // eslint-disable-line @typescript-e
|
|||
export class PluginsService implements CoreService<PluginsServiceSetup, PluginsServiceStart> {
|
||||
private readonly log: Logger;
|
||||
private readonly pluginsSystem: PluginsSystem;
|
||||
private readonly configService: IConfigService;
|
||||
private readonly config$: Observable<PluginsConfig>;
|
||||
private readonly pluginConfigDescriptors = new Map<PluginName, PluginConfigDescriptor>();
|
||||
|
||||
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<PluginsConfigType>('plugins')
|
||||
.pipe(map(rawConfig => new PluginsConfig(rawConfig, coreContext.env)));
|
||||
|
@ -82,17 +93,18 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
|
||||
let contracts = new Map<PluginName, unknown>();
|
||||
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<PluginsServiceSetup, PluginsS
|
|||
await this.pluginsSystem.stopPlugins();
|
||||
}
|
||||
|
||||
private generateUiPluginsConfigs(
|
||||
uiPlugins: Map<string, DiscoveredPlugin>
|
||||
): Map<PluginName, Observable<unknown>> {
|
||||
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<PluginDiscoveryError>) {
|
||||
// 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<PluginsServiceSetup, PluginsS
|
|||
await plugin$
|
||||
.pipe(
|
||||
mergeMap(async plugin => {
|
||||
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);
|
||||
|
||||
|
|
|
@ -24,7 +24,51 @@ import { ConfigPath, EnvironmentMode, PackageInfo } from '../config';
|
|||
import { LoggerFactory } from '../logging';
|
||||
import { CoreSetup, CoreStart } from '..';
|
||||
|
||||
export type PluginConfigSchema = Type<unknown> | null;
|
||||
/**
|
||||
* Dedicated type for plugin configuration schema.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginConfigSchema<T> = Type<T>;
|
||||
|
||||
/**
|
||||
* 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<typeof configSchema>;
|
||||
*
|
||||
* export const config: PluginConfigDescriptor<ConfigType> = {
|
||||
* exposeToBrowser: {
|
||||
* uiProp: true,
|
||||
* },
|
||||
* schema: configSchema,
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginConfigDescriptor<T = any> {
|
||||
/**
|
||||
* 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<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays
|
||||
|
|
|
@ -959,6 +959,17 @@ export interface Plugin<TSetup = void, TStart = void, TPluginsSetup extends obje
|
|||
stop?(): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface PluginConfigDescriptor<T = any> {
|
||||
exposeToBrowser?: {
|
||||
[P in keyof T]?: boolean;
|
||||
};
|
||||
schema: PluginConfigSchema<T>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type PluginConfigSchema<T> = Type<T>;
|
||||
|
||||
// @public
|
||||
export type PluginInitializer<TSetup, TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object> = (core: PluginInitializerContext) => Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
|
||||
|
@ -1003,6 +1014,8 @@ export interface PluginsServiceSetup {
|
|||
// (undocumented)
|
||||
contracts: Map<PluginName, unknown>;
|
||||
// (undocumented)
|
||||
uiPluginConfigs: Map<PluginName, Observable<unknown>>;
|
||||
// (undocumented)
|
||||
uiPlugins: {
|
||||
public: Map<PluginName, DiscoveredPlugin>;
|
||||
internal: Map<PluginName, DiscoveredPluginInternal>;
|
||||
|
@ -1615,6 +1628,6 @@ export interface UserProvidedValues<T extends SavedObjectAttribute = any> {
|
|||
// 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
|
||||
|
||||
```
|
||||
|
|
1
src/legacy/server/kbn_server.d.ts
vendored
1
src/legacy/server/kbn_server.d.ts
vendored
|
@ -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'];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<TestbedPluginSetup, TestbedPluginStart> = () =>
|
||||
new TestbedPlugin();
|
||||
export const plugin: PluginInitializer<TestbedPluginSetup, TestbedPluginStart> = (
|
||||
initializerContext: PluginInitializerContext
|
||||
) => new TestbedPlugin(initializerContext);
|
||||
|
|
|
@ -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<TestbedPluginSetup, TestbedPluginStart> {
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public async setup(core: CoreSetup, deps: {}) {
|
||||
const config = this.initializerContext.config.get<ConfigType>();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Testbed plugin set up`);
|
||||
console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`);
|
||||
return {
|
||||
foo: 'bar',
|
||||
};
|
||||
|
|
|
@ -20,16 +20,29 @@
|
|||
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<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor = {
|
||||
exposeToBrowser: {
|
||||
uiProp: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
type ConfigType = TypeOf<typeof config.schema>;
|
||||
|
||||
class Plugin {
|
||||
private readonly log: Logger;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue