mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Core plugin system] Add dynamic contract resolving (#167113)
### Summary Fix https://github.com/elastic/kibana/issues/166688 Implements dynamic contract resolving for plugins, allowing to retrieve contracts after their respective lifecycle is completed, and therefore working around cyclic dependencies. In term of workflow execution, we're basically going from <img width="842" alt="Screenshot 2023-09-27 at 08 09 27" src="251637d1
-ec97-4071-a445-2f59512ce187"> to: <img width="1092" alt="Screenshot 2023-09-27 at 08 09 32" src="de466cda
-7e43-4fd3-81ec-4339d05d279d"> ### API This functionality is exposed by the now publicly exposed `plugins` service contracts: ```ts setup(core) { core.plugins.onSetup<{pluginA: SetupContractA, pluginB: SetupContractA}>('pluginA', 'pluginB') .then(({ pluginA, pluginB }) => { if(pluginA.found && pluginB.found) { // do something with pluginA.contract and pluginB.contract } }); } ``` ```ts start(core) { core.plugins.onStart<{pluginA: StartContractA, pluginB: StartContractA}>('pluginA', 'pluginB') .then(({ pluginA, pluginB }) => { if(pluginA.found && pluginB.found) { // do something with pluginA.contract and pluginB.contract } }); } ``` **remark:** the `setup` contract exposed both `onSetup` and `onStart`, while the `start` contract only exposed `onStart`. The intent is to avoid fully disrupting the concept of lifecycle stages. ### Guardrails To prevent developer from abusing this new API, or at least to add some visibility on its adoption, plugins can only perforn dynamic contract resolving against dependencies explicitly defined in their manifest: - any required dependencies (*existing concept*) - any optional dependencies (*existing concept*) - any runtime dependencies (**new concept**) Runtime dependencies must be specified using the new `runtimePluginDependencies` field of a plugin's manifest. ```json { "type": "plugin", "id": "@kbn/some-id", "owner": "@elastic/kibana-core", "plugin": { "id": "some-id", "...": "...", "runtimePluginDependencies" : ["someOtherPluginId"] } } ``` Using the contract resolving API will throw at call time when trying to resolve the contract for an undeclared dependency. E.g this would throw at invocation time (not returning a rejected promise - throw). ```ts setup(core) { core.plugins.onSetup<{undeclaredDependency: SomeContract}>('undeclaredDependency'); } ``` The reasoning behind throwing is that these errors should only occur during the development process, and an hard fail is way more visible than a promise rejection that should be more easily shallowed. ### Code reviews This PR defines @elastic/kibana-core as codeowner of all `kibana.jsonc` files in the `src/plugins` and `x-pack/plugins` directories, so that a code review will be triggered whenever anyone changes something in any manifest. The intent is to be able to monitor new usages of the feature, via the addition of entries in the `runtimePluginDependencies` option of the manifest. ### Remarks Exposing this API, and therefore making possible cyclic dependencies between plugins, opens the door to other questions. For instance, cross-plugin type imports are not technically possible at the moment, given that plugins are referencing each others via TS refs, and refs forbid cyclic dependencies. Which means that to leverage this to address cyclic dependency issues, the public types of **at least one of the two** plugins will have to be extracted to a shared place (likely a package). Resolving, or trying to improve the developer experience around this issue, is absolutely out of scope of the current PR (and the issue it addresses). --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b3c5646195
commit
e398e7bc51
95 changed files with 2391 additions and 133 deletions
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
|
@ -226,6 +226,8 @@ test/plugin_functional/plugins/core_plugin_b @elastic/kibana-core
|
|||
test/plugin_functional/plugins/core_plugin_chromeless @elastic/kibana-core
|
||||
test/plugin_functional/plugins/core_plugin_deep_links @elastic/kibana-core
|
||||
test/plugin_functional/plugins/core_plugin_deprecations @elastic/kibana-core
|
||||
test/plugin_functional/plugins/core_dynamic_resolving_a @elastic/kibana-core
|
||||
test/plugin_functional/plugins/core_dynamic_resolving_b @elastic/kibana-core
|
||||
test/plugin_functional/plugins/core_plugin_execution_context @elastic/kibana-core
|
||||
test/plugin_functional/plugins/core_plugin_helpmenu @elastic/kibana-core
|
||||
test/node_roles_functional/plugins/core_plugin_initializer_context @elastic/kibana-core
|
||||
|
@ -235,6 +237,8 @@ packages/core/plugins/core-plugins-base-server-internal @elastic/kibana-core
|
|||
packages/core/plugins/core-plugins-browser @elastic/kibana-core
|
||||
packages/core/plugins/core-plugins-browser-internal @elastic/kibana-core
|
||||
packages/core/plugins/core-plugins-browser-mocks @elastic/kibana-core
|
||||
packages/core/plugins/core-plugins-contracts-browser @elastic/kibana-core
|
||||
packages/core/plugins/core-plugins-contracts-server @elastic/kibana-core
|
||||
packages/core/plugins/core-plugins-server @elastic/kibana-core
|
||||
packages/core/plugins/core-plugins-server-internal @elastic/kibana-core
|
||||
packages/core/plugins/core-plugins-server-mocks @elastic/kibana-core
|
||||
|
@ -1454,6 +1458,10 @@ packages/react @elastic/appex-sharedux
|
|||
/packages/core/saved-objects/docs/openapi @elastic/platform-docs
|
||||
/plugins/data_views/docs/openapi @elastic/platform-docs
|
||||
|
||||
# Plugin manifests
|
||||
/src/plugins/**/kibana.jsonc @elastic/kibana-core
|
||||
/x-pack/plugins/**/kibana.jsonc @elastic/kibana-core
|
||||
|
||||
####
|
||||
## These rules are always last so they take ultimate priority over everything else
|
||||
####
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { BfetchDeps } from '../mount';
|
||||
|
||||
export const useDeps = () => useKibana().services as BfetchDeps;
|
||||
export const useDeps = () => useKibana().services as unknown as BfetchDeps;
|
||||
|
|
|
@ -10,4 +10,4 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
|
|||
|
||||
import type { ResponseStreamDeps } from '../mount';
|
||||
|
||||
export const useDeps = () => useKibana().services as ResponseStreamDeps;
|
||||
export const useDeps = () => useKibana().services as unknown as ResponseStreamDeps;
|
||||
|
|
|
@ -295,6 +295,8 @@
|
|||
"@kbn/core-plugin-chromeless-plugin": "link:test/plugin_functional/plugins/core_plugin_chromeless",
|
||||
"@kbn/core-plugin-deep-links-plugin": "link:test/plugin_functional/plugins/core_plugin_deep_links",
|
||||
"@kbn/core-plugin-deprecations-plugin": "link:test/plugin_functional/plugins/core_plugin_deprecations",
|
||||
"@kbn/core-plugin-dynamic-resolving-a": "link:test/plugin_functional/plugins/core_dynamic_resolving_a",
|
||||
"@kbn/core-plugin-dynamic-resolving-b": "link:test/plugin_functional/plugins/core_dynamic_resolving_b",
|
||||
"@kbn/core-plugin-execution-context-plugin": "link:test/plugin_functional/plugins/core_plugin_execution_context",
|
||||
"@kbn/core-plugin-helpmenu-plugin": "link:test/plugin_functional/plugins/core_plugin_helpmenu",
|
||||
"@kbn/core-plugin-initializer-context-plugin": "link:test/node_roles_functional/plugins/core_plugin_initializer_context",
|
||||
|
@ -303,6 +305,8 @@
|
|||
"@kbn/core-plugins-base-server-internal": "link:packages/core/plugins/core-plugins-base-server-internal",
|
||||
"@kbn/core-plugins-browser": "link:packages/core/plugins/core-plugins-browser",
|
||||
"@kbn/core-plugins-browser-internal": "link:packages/core/plugins/core-plugins-browser-internal",
|
||||
"@kbn/core-plugins-contracts-browser": "link:packages/core/plugins/core-plugins-contracts-browser",
|
||||
"@kbn/core-plugins-contracts-server": "link:packages/core/plugins/core-plugins-contracts-server",
|
||||
"@kbn/core-plugins-server": "link:packages/core/plugins/core-plugins-server",
|
||||
"@kbn/core-plugins-server-internal": "link:packages/core/plugins/core-plugins-server-internal",
|
||||
"@kbn/core-preboot-server": "link:packages/core/preboot/core-preboot-server",
|
||||
|
|
|
@ -135,6 +135,7 @@ describe('CoreApp', () => {
|
|||
optionalPlugins: [],
|
||||
requiredBundles: [],
|
||||
requiredPlugins: [],
|
||||
runtimePluginDependencies: [],
|
||||
});
|
||||
});
|
||||
it('calls `registerBundleRoutes` with the correct options', () => {
|
||||
|
|
|
@ -77,6 +77,12 @@ export interface DiscoveredPlugin {
|
|||
*/
|
||||
readonly requiredBundles: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* An optional list of plugin dependencies that can be resolved dynamically at runtime
|
||||
* using the dynamic contract resolving capabilities from the plugin service.
|
||||
*/
|
||||
readonly runtimePluginDependencies: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* Specifies whether this plugin - and its required dependencies - will be enabled for anonymous pages (login page, status page when
|
||||
* configured, etc.) Default is false.
|
||||
|
|
|
@ -11,7 +11,8 @@ import type { InternalApplicationSetup } from '@kbn/core-application-browser-int
|
|||
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
|
||||
|
||||
/** @internal */
|
||||
export interface InternalCoreSetup extends Omit<CoreSetup, 'application' | 'getStartServices'> {
|
||||
export interface InternalCoreSetup
|
||||
extends Omit<CoreSetup, 'application' | 'plugins' | 'getStartServices'> {
|
||||
application: InternalApplicationSetup;
|
||||
injectedMetadata: InternalInjectedMetadataSetup;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { InternalApplicationStart } from '@kbn/core-application-browser-int
|
|||
import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-browser-internal';
|
||||
|
||||
/** @internal */
|
||||
export interface InternalCoreStart extends Omit<CoreStart, 'application'> {
|
||||
export interface InternalCoreStart extends Omit<CoreStart, 'application' | 'plugins'> {
|
||||
application: InternalApplicationStart;
|
||||
injectedMetadata: InternalInjectedMetadataStart;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,10 @@ export function createCoreSetupMock({
|
|||
settings: settingsServiceMock.createSetupContract(),
|
||||
deprecations: deprecationsServiceMock.createSetupContract(),
|
||||
theme: themeServiceMock.createSetupContract(),
|
||||
plugins: {
|
||||
onSetup: jest.fn(),
|
||||
onStart: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
return mock;
|
||||
|
|
|
@ -40,6 +40,9 @@ export function createCoreStartMock({ basePath = '' } = {}) {
|
|||
deprecations: deprecationsServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
fatalErrors: fatalErrorsServiceMock.createStartContract(),
|
||||
plugins: {
|
||||
onStart: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
return mock;
|
||||
|
|
|
@ -15,6 +15,7 @@ import type { IUiSettingsClient, SettingsStart } from '@kbn/core-ui-settings-bro
|
|||
import type { NotificationsSetup } from '@kbn/core-notifications-browser';
|
||||
import type { ApplicationSetup } from '@kbn/core-application-browser';
|
||||
import type { CustomBrandingSetup } from '@kbn/core-custom-branding-browser';
|
||||
import type { PluginsServiceSetup } from '@kbn/core-plugins-contracts-browser';
|
||||
import type { CoreStart } from './core_start';
|
||||
|
||||
/**
|
||||
|
@ -53,6 +54,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
executionContext: ExecutionContextSetup;
|
||||
/** {@link ThemeServiceSetup} */
|
||||
theme: ThemeServiceSetup;
|
||||
/** {@link PluginsServiceSetup} */
|
||||
plugins: PluginsServiceSetup;
|
||||
/** {@link StartServicesAccessor} */
|
||||
getStartServices: StartServicesAccessor<TPluginsStart, TStart>;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { NotificationsStart } from '@kbn/core-notifications-browser';
|
|||
import type { ApplicationStart } from '@kbn/core-application-browser';
|
||||
import type { ChromeStart } from '@kbn/core-chrome-browser';
|
||||
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
|
||||
import type { PluginsServiceStart } from '@kbn/core-plugins-contracts-browser';
|
||||
|
||||
/**
|
||||
* Core services exposed to the `Plugin` start lifecycle
|
||||
|
@ -68,4 +69,6 @@ export interface CoreStart {
|
|||
deprecations: DeprecationsServiceStart;
|
||||
/** {@link ThemeServiceStart} */
|
||||
theme: ThemeServiceStart;
|
||||
/** {@link PluginsServiceStart} */
|
||||
plugins: PluginsServiceStart;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
"@kbn/core-overlays-browser",
|
||||
"@kbn/core-saved-objects-browser",
|
||||
"@kbn/core-chrome-browser",
|
||||
"@kbn/core-custom-branding-browser"
|
||||
"@kbn/core-custom-branding-browser",
|
||||
"@kbn/core-plugins-contracts-browser"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -70,6 +70,10 @@ export function createCoreSetupMock({
|
|||
coreUsageData: {
|
||||
registerUsageCounter: coreUsageDataServiceMock.createSetupContract().registerUsageCounter,
|
||||
},
|
||||
plugins: {
|
||||
onSetup: jest.fn(),
|
||||
onStart: jest.fn(),
|
||||
},
|
||||
getStartServices: jest
|
||||
.fn<Promise<[ReturnType<typeof createCoreStartMock>, object, any]>, []>()
|
||||
.mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]),
|
||||
|
|
|
@ -33,6 +33,9 @@ export function createCoreStartMock() {
|
|||
coreUsageData: coreUsageDataServiceMock.createStartContract(),
|
||||
executionContext: executionContextServiceMock.createInternalStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
plugins: {
|
||||
onStart: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
return mock;
|
||||
|
|
|
@ -24,6 +24,7 @@ import { UiSettingsServiceSetup } from '@kbn/core-ui-settings-server';
|
|||
import { CoreUsageDataSetup } from '@kbn/core-usage-data-server';
|
||||
import { CustomBrandingSetup } from '@kbn/core-custom-branding-server';
|
||||
import { UserSettingsServiceSetup } from '@kbn/core-user-settings-server';
|
||||
import { PluginsServiceSetup } from '@kbn/core-plugins-contracts-server';
|
||||
import { CoreStart } from './core_start';
|
||||
|
||||
/**
|
||||
|
@ -73,6 +74,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
getStartServices: StartServicesAccessor<TPluginsStart, TStart>;
|
||||
/** @internal {@link CoreUsageDataSetup} */
|
||||
coreUsageData: CoreUsageDataSetup;
|
||||
/** {@link PluginsServiceSetup} */
|
||||
plugins: PluginsServiceSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,6 +17,7 @@ import { SavedObjectsServiceStart } from '@kbn/core-saved-objects-server';
|
|||
import { UiSettingsServiceStart } from '@kbn/core-ui-settings-server';
|
||||
import { CoreUsageDataStart } from '@kbn/core-usage-data-server';
|
||||
import { CustomBrandingStart } from '@kbn/core-custom-branding-server';
|
||||
import { PluginsServiceStart } from '@kbn/core-plugins-contracts-server';
|
||||
|
||||
/**
|
||||
* Context passed to the plugins `start` method.
|
||||
|
@ -46,4 +47,6 @@ export interface CoreStart {
|
|||
uiSettings: UiSettingsServiceStart;
|
||||
/** @internal {@link CoreUsageDataStart} */
|
||||
coreUsageData: CoreUsageDataStart;
|
||||
/** {@link PluginsServiceStart} */
|
||||
plugins: PluginsServiceStart;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"@kbn/core-ui-settings-server",
|
||||
"@kbn/core-usage-data-server",
|
||||
"@kbn/core-custom-branding-server",
|
||||
"@kbn/core-user-settings-server"
|
||||
"@kbn/core-user-settings-server",
|
||||
"@kbn/core-plugins-contracts-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
export { PluginsService } from './src';
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
InternalPluginsServiceSetup,
|
||||
InternalPluginsServiceStart,
|
||||
PluginsServiceSetupDeps,
|
||||
PluginsServiceStartDeps,
|
||||
} from './src';
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
export { PluginsService } from './plugins_service';
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
InternalPluginsServiceSetup,
|
||||
InternalPluginsServiceStart,
|
||||
PluginsServiceSetupDeps,
|
||||
PluginsServiceStartDeps,
|
||||
} from './plugins_service';
|
||||
|
|
|
@ -24,6 +24,7 @@ function createManifest(
|
|||
requiredPlugins: required,
|
||||
optionalPlugins: optional,
|
||||
requiredBundles: [],
|
||||
runtimePluginDependencies: [],
|
||||
owner: {
|
||||
name: 'foo',
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ export class PluginWrapper<
|
|||
public readonly configPath: DiscoveredPlugin['configPath'];
|
||||
public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins'];
|
||||
public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins'];
|
||||
public readonly runtimePluginDependencies: DiscoveredPlugin['runtimePluginDependencies'];
|
||||
private instance?: Plugin<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
|
||||
private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>();
|
||||
|
@ -46,6 +47,7 @@ export class PluginWrapper<
|
|||
this.configPath = discoveredPlugin.configPath;
|
||||
this.requiredPlugins = discoveredPlugin.requiredPlugins;
|
||||
this.optionalPlugins = discoveredPlugin.optionalPlugins;
|
||||
this.runtimePluginDependencies = discoveredPlugin.runtimePluginDependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,7 @@ const createPluginManifest = (pluginName: string): DiscoveredPlugin => {
|
|||
requiredPlugins: [],
|
||||
optionalPlugins: [],
|
||||
requiredBundles: [],
|
||||
runtimePluginDependencies: [],
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -11,8 +11,9 @@ import type { CoreContext } from '@kbn/core-base-browser-internal';
|
|||
import type { DiscoveredPlugin, PluginOpaqueId } from '@kbn/core-base-common';
|
||||
import type { CoreSetup, CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import type { PluginInitializerContext } from '@kbn/core-plugins-browser';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
|
||||
import type { PluginWrapper } from './plugin';
|
||||
import type { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
|
||||
import type { IRuntimePluginContractResolver } from './plugin_contract_resolver';
|
||||
|
||||
/**
|
||||
* Provides a plugin-specific context passed to the plugin's constructor. This is currently
|
||||
|
@ -63,11 +64,15 @@ export function createPluginSetupContext<
|
|||
TStart,
|
||||
TPluginsSetup extends object,
|
||||
TPluginsStart extends object
|
||||
>(
|
||||
coreContext: CoreContext,
|
||||
deps: PluginsServiceSetupDeps,
|
||||
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
|
||||
): CoreSetup {
|
||||
>({
|
||||
deps,
|
||||
plugin,
|
||||
runtimeResolver,
|
||||
}: {
|
||||
deps: PluginsServiceSetupDeps;
|
||||
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
runtimeResolver: IRuntimePluginContractResolver;
|
||||
}): CoreSetup {
|
||||
return {
|
||||
analytics: deps.analytics,
|
||||
application: {
|
||||
|
@ -82,6 +87,10 @@ export function createPluginSetupContext<
|
|||
uiSettings: deps.uiSettings,
|
||||
settings: deps.settings,
|
||||
theme: deps.theme,
|
||||
plugins: {
|
||||
onSetup: (...dependencyNames) => runtimeResolver.onSetup(plugin.name, dependencyNames),
|
||||
onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames),
|
||||
},
|
||||
getStartServices: () => plugin.startDependencies,
|
||||
};
|
||||
}
|
||||
|
@ -101,11 +110,15 @@ export function createPluginStartContext<
|
|||
TStart,
|
||||
TPluginsSetup extends object,
|
||||
TPluginsStart extends object
|
||||
>(
|
||||
coreContext: CoreContext,
|
||||
deps: PluginsServiceStartDeps,
|
||||
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
|
||||
): CoreStart {
|
||||
>({
|
||||
deps,
|
||||
plugin,
|
||||
runtimeResolver,
|
||||
}: {
|
||||
deps: PluginsServiceStartDeps;
|
||||
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>;
|
||||
runtimeResolver: IRuntimePluginContractResolver;
|
||||
}): CoreStart {
|
||||
return {
|
||||
analytics: deps.analytics,
|
||||
application: {
|
||||
|
@ -131,5 +144,8 @@ export function createPluginStartContext<
|
|||
fatalErrors: deps.fatalErrors,
|
||||
deprecations: deps.deprecations,
|
||||
theme: deps.theme,
|
||||
plugins: {
|
||||
onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { RuntimePluginContractResolver } from './plugin_contract_resolver';
|
||||
|
||||
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 1));
|
||||
const fewTicks = () =>
|
||||
nextTick()
|
||||
.then(() => nextTick())
|
||||
.then(() => nextTick());
|
||||
|
||||
const toMap = (record: Record<string, unknown>): Map<string, unknown> => {
|
||||
return new Map(Object.entries(record));
|
||||
};
|
||||
|
||||
const pluginAContract = Symbol();
|
||||
|
||||
describe('RuntimePluginContractResolver', () => {
|
||||
const SOURCE_PLUGIN = 'sourcePlugin';
|
||||
let resolver: RuntimePluginContractResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
resolver = new RuntimePluginContractResolver();
|
||||
|
||||
const dependencyMap = new Map<string, Set<string>>();
|
||||
dependencyMap.set(SOURCE_PLUGIN, new Set(['pluginA', 'pluginB', 'pluginC']));
|
||||
resolver.setDependencyMap(dependencyMap);
|
||||
});
|
||||
|
||||
describe('setup contracts', () => {
|
||||
it('throws if onSetup is called before setDependencyMap', () => {
|
||||
resolver = new RuntimePluginContractResolver();
|
||||
|
||||
expect(() => resolver.onSetup(SOURCE_PLUGIN, ['pluginA'])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"onSetup cannot be called before setDependencyMap"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if resolveSetupRequests is called multiple times', async () => {
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"resolveSetupRequests can only be called once"`);
|
||||
});
|
||||
|
||||
it('resolves a single request', async () => {
|
||||
const handler = jest.fn();
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves multiple requests', async () => {
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
const handler3 = jest.fn();
|
||||
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
resolver
|
||||
.onSetup(SOURCE_PLUGIN, ['pluginA', 'pluginB'])
|
||||
.then((contracts) => handler3(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
expect(handler3).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler3).toHaveBeenCalledTimes(1);
|
||||
expect(handler3).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves requests instantly when called after resolveSetupRequests', async () => {
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when requesting a contract not defined in the dependency map', async () => {
|
||||
expect(() =>
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['undeclaredPlugin'])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when requesting a mixed defined/undefined dependencies', async () => {
|
||||
expect(() =>
|
||||
resolver.onSetup(SOURCE_PLUGIN, [
|
||||
'pluginA',
|
||||
'undeclaredPlugin1',
|
||||
'pluginB',
|
||||
'undeclaredPlugin2',
|
||||
])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin1, undeclaredPlugin2"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start contracts', () => {
|
||||
it('throws if onStart is called before setDependencyMap', () => {
|
||||
resolver = new RuntimePluginContractResolver();
|
||||
|
||||
expect(() => resolver.onStart(SOURCE_PLUGIN, ['pluginA'])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"onStart cannot be called before setDependencyMap"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if resolveStartRequests is called multiple times', async () => {
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"resolveStartRequests can only be called once"`);
|
||||
});
|
||||
|
||||
it('resolves a single request', async () => {
|
||||
const handler = jest.fn();
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves multiple requests', async () => {
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
const handler3 = jest.fn();
|
||||
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
resolver
|
||||
.onStart(SOURCE_PLUGIN, ['pluginA', 'pluginB'])
|
||||
.then((contracts) => handler3(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
expect(handler3).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler3).toHaveBeenCalledTimes(1);
|
||||
expect(handler3).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves requests instantly when called after resolveSetupRequests', async () => {
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when requesting a contract not defined in the dependency map', async () => {
|
||||
expect(() =>
|
||||
resolver.onStart(SOURCE_PLUGIN, ['undeclaredPlugin'])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when requesting a mixed defined/undefined dependencies', async () => {
|
||||
expect(() =>
|
||||
resolver.onStart(SOURCE_PLUGIN, [
|
||||
'pluginA',
|
||||
'undeclaredPlugin1',
|
||||
'pluginB',
|
||||
'undeclaredPlugin2',
|
||||
])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin1, undeclaredPlugin2"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { PluginName } from '@kbn/core-base-common';
|
||||
import type {
|
||||
PluginContractResolverResponse,
|
||||
PluginContractMap,
|
||||
PluginContractResolverResponseItem,
|
||||
} from '@kbn/core-plugins-contracts-browser';
|
||||
|
||||
export type IRuntimePluginContractResolver = PublicMethodsOf<RuntimePluginContractResolver>;
|
||||
|
||||
export class RuntimePluginContractResolver {
|
||||
private dependencyMap?: Map<PluginName, Set<PluginName>>;
|
||||
private setupContracts?: Map<PluginName, unknown>;
|
||||
private startContracts?: Map<PluginName, unknown>;
|
||||
|
||||
private readonly setupRequestQueue: PluginContractRequest[] = [];
|
||||
private readonly startRequestQueue: PluginContractRequest[] = [];
|
||||
|
||||
setDependencyMap(depMap: Map<PluginName, Set<PluginName>>) {
|
||||
this.dependencyMap = new Map(depMap.entries());
|
||||
}
|
||||
|
||||
onSetup = <T extends PluginContractMap>(
|
||||
pluginName: PluginName,
|
||||
dependencyNames: Array<keyof T>
|
||||
): Promise<PluginContractResolverResponse<T>> => {
|
||||
if (!this.dependencyMap) {
|
||||
throw new Error('onSetup cannot be called before setDependencyMap');
|
||||
}
|
||||
|
||||
const dependencyList = this.dependencyMap.get(pluginName) ?? new Set();
|
||||
const notDependencyPlugins = dependencyNames.filter(
|
||||
(name) => !dependencyList.has(name as PluginName)
|
||||
);
|
||||
if (notDependencyPlugins.length) {
|
||||
throw new Error(
|
||||
'Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.' +
|
||||
`Undeclared dependencies: ${notDependencyPlugins.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.setupContracts) {
|
||||
const response = createContractRequestResponse(
|
||||
dependencyNames as PluginName[],
|
||||
this.setupContracts
|
||||
);
|
||||
return Promise.resolve(response as PluginContractResolverResponse<T>);
|
||||
} else {
|
||||
const setupContractRequest = createPluginContractRequest<PluginContractResolverResponse<T>>(
|
||||
dependencyNames as PluginName[]
|
||||
);
|
||||
this.setupRequestQueue.push(setupContractRequest as PluginContractRequest);
|
||||
return setupContractRequest.contractPromise;
|
||||
}
|
||||
};
|
||||
|
||||
onStart = <T extends PluginContractMap>(
|
||||
pluginName: PluginName,
|
||||
dependencyNames: Array<keyof T>
|
||||
): Promise<PluginContractResolverResponse<T>> => {
|
||||
if (!this.dependencyMap) {
|
||||
throw new Error('onStart cannot be called before setDependencyMap');
|
||||
}
|
||||
|
||||
const dependencyList = this.dependencyMap.get(pluginName) ?? new Set();
|
||||
const notDependencyPlugins = dependencyNames.filter(
|
||||
(name) => !dependencyList.has(name as PluginName)
|
||||
);
|
||||
if (notDependencyPlugins.length) {
|
||||
throw new Error(
|
||||
'Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.' +
|
||||
`Undeclared dependencies: ${notDependencyPlugins.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.startContracts) {
|
||||
const response = createContractRequestResponse(
|
||||
dependencyNames as PluginName[],
|
||||
this.startContracts
|
||||
);
|
||||
return Promise.resolve(response as PluginContractResolverResponse<T>);
|
||||
} else {
|
||||
const startContractRequest = createPluginContractRequest<PluginContractResolverResponse<T>>(
|
||||
dependencyNames as PluginName[]
|
||||
);
|
||||
this.startRequestQueue.push(startContractRequest as PluginContractRequest);
|
||||
return startContractRequest.contractPromise;
|
||||
}
|
||||
};
|
||||
|
||||
resolveSetupRequests(setupContracts: Map<PluginName, unknown>) {
|
||||
if (this.setupContracts) {
|
||||
throw new Error('resolveSetupRequests can only be called once');
|
||||
}
|
||||
this.setupContracts = setupContracts;
|
||||
|
||||
for (const setupRequest of this.setupRequestQueue) {
|
||||
const response = createContractRequestResponse(setupRequest.pluginNames, setupContracts);
|
||||
setupRequest.resolve(response);
|
||||
}
|
||||
}
|
||||
|
||||
resolveStartRequests(startContracts: Map<PluginName, unknown>) {
|
||||
if (this.startContracts) {
|
||||
throw new Error('resolveStartRequests can only be called once');
|
||||
}
|
||||
this.startContracts = startContracts;
|
||||
|
||||
for (const startRequest of this.startRequestQueue) {
|
||||
const response = createContractRequestResponse(startRequest.pluginNames, startContracts);
|
||||
startRequest.resolve(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PluginContractRequest<T = unknown> {
|
||||
pluginNames: PluginName[];
|
||||
contractPromise: Promise<T>;
|
||||
resolve: (data?: T) => void;
|
||||
}
|
||||
|
||||
const createPluginContractRequest = <T = unknown>(
|
||||
pluginNames: PluginName[]
|
||||
): PluginContractRequest<T> => {
|
||||
let resolve!: (data?: T) => void;
|
||||
const contractPromise = new Promise<any>((_resolve) => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
|
||||
return {
|
||||
pluginNames,
|
||||
contractPromise,
|
||||
resolve,
|
||||
};
|
||||
};
|
||||
|
||||
const createContractRequestResponse = <T extends PluginContractMap>(
|
||||
pluginNames: PluginName[],
|
||||
contracts: Map<string, unknown>
|
||||
): PluginContractResolverResponse<T> => {
|
||||
const response = {} as Record<string, unknown>;
|
||||
for (const pluginName of pluginNames) {
|
||||
const pluginResponse: PluginContractResolverResponseItem = contracts.has(pluginName)
|
||||
? {
|
||||
found: true,
|
||||
contract: contracts.get(pluginName)!,
|
||||
}
|
||||
: { found: false };
|
||||
response[pluginName] = pluginResponse;
|
||||
}
|
||||
|
||||
return response as PluginContractResolverResponse<T>;
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import type { PluginName } from '@kbn/core-base-common';
|
||||
import type { Plugin } from '@kbn/core-plugins-browser';
|
||||
import { createRuntimePluginContractResolverMock } from './test_helpers';
|
||||
|
||||
export type MockedPluginInitializer = jest.Mock<Plugin<unknown, unknown>>;
|
||||
|
||||
|
@ -20,3 +21,11 @@ export const mockPluginInitializerProvider: jest.Mock<MockedPluginInitializer, [
|
|||
jest.mock('./plugin_reader', () => ({
|
||||
read: mockPluginInitializerProvider,
|
||||
}));
|
||||
|
||||
export const runtimeResolverMock = createRuntimePluginContractResolverMock();
|
||||
|
||||
jest.doMock('./plugin_contract_resolver', () => {
|
||||
return {
|
||||
RuntimePluginContractResolver: jest.fn().mockImplementation(() => runtimeResolverMock),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -11,9 +11,10 @@ import { omit } from 'lodash';
|
|||
import {
|
||||
type MockedPluginInitializer,
|
||||
mockPluginInitializerProvider,
|
||||
runtimeResolverMock,
|
||||
} from './plugins_service.test.mocks';
|
||||
|
||||
import { type PluginName, PluginType } from '@kbn/core-base-common';
|
||||
import { type PluginName, type DiscoveredPlugin, PluginType } from '@kbn/core-base-common';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { docLinksServiceMock } from '@kbn/core-doc-links-browser-mocks';
|
||||
import { executionContextServiceMock } from '@kbn/core-execution-context-browser-mocks';
|
||||
|
@ -60,24 +61,24 @@ let mockStartContext: DeeplyMocked<CoreStart>;
|
|||
function createManifest(
|
||||
id: string,
|
||||
{ required = [], optional = [] }: { required?: string[]; optional?: string[]; ui?: boolean } = {}
|
||||
) {
|
||||
): DiscoveredPlugin {
|
||||
return {
|
||||
id,
|
||||
version: 'some-version',
|
||||
type: PluginType.standard,
|
||||
configPath: ['path'],
|
||||
requiredPlugins: required,
|
||||
optionalPlugins: optional,
|
||||
requiredBundles: [],
|
||||
owner: {
|
||||
name: 'Core',
|
||||
githubTeam: 'kibana-core',
|
||||
},
|
||||
runtimePluginDependencies: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('PluginsService', () => {
|
||||
beforeEach(() => {
|
||||
runtimeResolverMock.setDependencyMap.mockReset();
|
||||
runtimeResolverMock.resolveSetupRequests.mockReset();
|
||||
runtimeResolverMock.resolveStartRequests.mockReset();
|
||||
|
||||
plugins = [
|
||||
{ id: 'pluginA', plugin: createManifest('pluginA') },
|
||||
{ id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) },
|
||||
|
@ -101,6 +102,7 @@ describe('PluginsService', () => {
|
|||
mockSetupContext = {
|
||||
...omit(mockSetupDeps, 'injectedMetadata'),
|
||||
application: expect.any(Object),
|
||||
plugins: expect.any(Object),
|
||||
getStartServices: expect.any(Function),
|
||||
};
|
||||
// @ts-expect-error this file was not being type checked properly in the past, error is legit
|
||||
|
@ -124,6 +126,7 @@ describe('PluginsService', () => {
|
|||
mockStartContext = {
|
||||
...omit(mockStartDeps, 'injectedMetadata'),
|
||||
application: expect.any(Object),
|
||||
plugins: expect.any(Object),
|
||||
chrome: omit(mockStartDeps.chrome, 'getComponent'),
|
||||
};
|
||||
|
||||
|
@ -248,6 +251,28 @@ describe('PluginsService', () => {
|
|||
expect(pluginDDeps).not.toHaveProperty('missing');
|
||||
});
|
||||
|
||||
it('setups the runtimeResolver', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
|
||||
expect(runtimeResolverMock.setDependencyMap).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeResolverMock.setDependencyMap).toHaveBeenCalledWith(expect.any(Map));
|
||||
|
||||
expect(runtimeResolverMock.resolveSetupRequests).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeResolverMock.resolveSetupRequests).toHaveBeenCalledWith(expect.any(Map));
|
||||
expect(
|
||||
Object.fromEntries([...runtimeResolverMock.resolveSetupRequests.mock.calls[0][0].entries()])
|
||||
).toEqual({
|
||||
pluginA: {
|
||||
setupValue: 1,
|
||||
},
|
||||
pluginB: {
|
||||
pluginAPlusB: 2,
|
||||
},
|
||||
pluginC: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns plugin setup contracts', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
const { contracts } = await pluginsService.setup(mockSetupDeps);
|
||||
|
@ -299,6 +324,26 @@ describe('PluginsService', () => {
|
|||
expect(pluginDDeps).not.toHaveProperty('missing');
|
||||
});
|
||||
|
||||
it('setups the runtimeResolver', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
await pluginsService.start(mockStartDeps);
|
||||
|
||||
expect(runtimeResolverMock.resolveStartRequests).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeResolverMock.resolveStartRequests).toHaveBeenCalledWith(expect.any(Map));
|
||||
expect(
|
||||
Object.fromEntries([...runtimeResolverMock.resolveStartRequests.mock.calls[0][0].entries()])
|
||||
).toEqual({
|
||||
pluginA: {
|
||||
startValue: 2,
|
||||
},
|
||||
pluginB: {
|
||||
pluginAPlusB: 3,
|
||||
},
|
||||
pluginC: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns plugin start contracts', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
createPluginSetupContext,
|
||||
createPluginStartContext,
|
||||
} from './plugin_context';
|
||||
import { RuntimePluginContractResolver } from './plugin_contract_resolver';
|
||||
|
||||
/** @internal */
|
||||
export type PluginsServiceSetupDeps = InternalCoreSetup;
|
||||
|
@ -23,12 +24,12 @@ export type PluginsServiceSetupDeps = InternalCoreSetup;
|
|||
export type PluginsServiceStartDeps = InternalCoreStart;
|
||||
|
||||
/** @internal */
|
||||
export interface PluginsServiceSetup {
|
||||
export interface InternalPluginsServiceSetup {
|
||||
contracts: ReadonlyMap<string, unknown>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface PluginsServiceStart {
|
||||
export interface InternalPluginsServiceStart {
|
||||
contracts: ReadonlyMap<string, unknown>;
|
||||
}
|
||||
|
||||
|
@ -38,7 +39,10 @@ export interface PluginsServiceStart {
|
|||
*
|
||||
* @internal
|
||||
*/
|
||||
export class PluginsService implements CoreService<PluginsServiceSetup, PluginsServiceStart> {
|
||||
export class PluginsService
|
||||
implements CoreService<InternalPluginsServiceSetup, InternalPluginsServiceStart>
|
||||
{
|
||||
private readonly runtimeResolver = new RuntimePluginContractResolver();
|
||||
/** Plugin wrappers in topological order. */
|
||||
private readonly plugins = new Map<PluginName, PluginWrapper<unknown, unknown>>();
|
||||
private readonly pluginDependencies = new Map<PluginName, PluginName[]>();
|
||||
|
@ -79,7 +83,10 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
);
|
||||
}
|
||||
|
||||
public async setup(deps: PluginsServiceSetupDeps): Promise<PluginsServiceSetup> {
|
||||
public async setup(deps: PluginsServiceSetupDeps): Promise<InternalPluginsServiceSetup> {
|
||||
const runtimeDependencies = buildPluginRuntimeDependencyMap(this.plugins);
|
||||
this.runtimeResolver.setDependencyMap(runtimeDependencies);
|
||||
|
||||
// Setup each plugin with required and optional plugin contracts
|
||||
const contracts = new Map<string, unknown>();
|
||||
for (const [pluginName, plugin] of this.plugins.entries()) {
|
||||
|
@ -97,7 +104,11 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
);
|
||||
|
||||
const contract = plugin.setup(
|
||||
createPluginSetupContext(this.coreContext, deps, plugin),
|
||||
createPluginSetupContext({
|
||||
deps,
|
||||
plugin,
|
||||
runtimeResolver: this.runtimeResolver,
|
||||
}),
|
||||
pluginDepContracts
|
||||
);
|
||||
|
||||
|
@ -105,11 +116,13 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
this.satupPlugins.push(pluginName);
|
||||
}
|
||||
|
||||
this.runtimeResolver.resolveSetupRequests(contracts);
|
||||
|
||||
// Expose setup contracts
|
||||
return { contracts };
|
||||
}
|
||||
|
||||
public async start(deps: PluginsServiceStartDeps): Promise<PluginsServiceStart> {
|
||||
public async start(deps: PluginsServiceStartDeps): Promise<InternalPluginsServiceStart> {
|
||||
// Setup each plugin with required and optional plugin contracts
|
||||
const contracts = new Map<string, unknown>();
|
||||
for (const [pluginName, plugin] of this.plugins.entries()) {
|
||||
|
@ -127,13 +140,19 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
);
|
||||
|
||||
const contract = plugin.start(
|
||||
createPluginStartContext(this.coreContext, deps, plugin),
|
||||
createPluginStartContext({
|
||||
deps,
|
||||
plugin,
|
||||
runtimeResolver: this.runtimeResolver,
|
||||
}),
|
||||
pluginDepContracts
|
||||
);
|
||||
|
||||
contracts.set(pluginName, contract);
|
||||
}
|
||||
|
||||
this.runtimeResolver.resolveStartRequests(contracts);
|
||||
|
||||
// Expose start contracts
|
||||
return { contracts };
|
||||
}
|
||||
|
@ -145,3 +164,18 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildPluginRuntimeDependencyMap = (
|
||||
pluginMap: Map<PluginName, PluginWrapper>
|
||||
): Map<PluginName, Set<PluginName>> => {
|
||||
const runtimeDependencies = new Map<PluginName, Set<PluginName>>();
|
||||
for (const [pluginName, pluginWrapper] of pluginMap.entries()) {
|
||||
const pluginRuntimeDeps = new Set([
|
||||
...pluginWrapper.optionalPlugins,
|
||||
...pluginWrapper.requiredPlugins,
|
||||
...pluginWrapper.runtimePluginDependencies,
|
||||
]);
|
||||
runtimeDependencies.set(pluginName, pluginRuntimeDeps);
|
||||
}
|
||||
return runtimeDependencies;
|
||||
};
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { createPluginInitializerContextMock } from './mocks';
|
||||
export { createRuntimePluginContractResolverMock } from './plugin_contract_resolver.mock';
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { IRuntimePluginContractResolver } from '../plugin_contract_resolver';
|
||||
|
||||
export const createRuntimePluginContractResolverMock =
|
||||
(): jest.Mocked<IRuntimePluginContractResolver> => {
|
||||
return {
|
||||
setDependencyMap: jest.fn(),
|
||||
onSetup: jest.fn(),
|
||||
onStart: jest.fn(),
|
||||
resolveSetupRequests: jest.fn(),
|
||||
resolveStartRequests: jest.fn(),
|
||||
};
|
||||
};
|
|
@ -35,6 +35,8 @@
|
|||
"@kbn/core-http-browser-mocks",
|
||||
"@kbn/core-saved-objects-browser-mocks",
|
||||
"@kbn/core-deprecations-browser-mocks",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/core-plugins-contracts-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -9,23 +9,27 @@
|
|||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import type { PluginInitializerContext } from '@kbn/core-plugins-browser';
|
||||
import type { PluginsService, PluginsServiceSetup } from '@kbn/core-plugins-browser-internal';
|
||||
import type {
|
||||
PluginsService,
|
||||
InternalPluginsServiceSetup,
|
||||
InternalPluginsServiceStart,
|
||||
} from '@kbn/core-plugins-browser-internal';
|
||||
import type { BuildFlavor } from '@kbn/config/src/types';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<PluginsServiceSetup> = {
|
||||
const createInternalSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<InternalPluginsServiceSetup> = {
|
||||
contracts: new Map(),
|
||||
};
|
||||
// we have to suppress type errors until decide how to mock es6 class
|
||||
return setupContract as PluginsServiceSetup;
|
||||
return setupContract as InternalPluginsServiceSetup;
|
||||
};
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const startContract: jest.Mocked<PluginsServiceSetup> = {
|
||||
const createInternalStartContractMock = () => {
|
||||
const startContract: jest.Mocked<InternalPluginsServiceStart> = {
|
||||
contracts: new Map(),
|
||||
};
|
||||
// we have to suppress type errors until decide how to mock es6 class
|
||||
return startContract as PluginsServiceSetup;
|
||||
return startContract as InternalPluginsServiceSetup;
|
||||
};
|
||||
|
||||
const createPluginInitializerContextMock = (
|
||||
|
@ -68,14 +72,14 @@ const createMock = () => {
|
|||
stop: jest.fn(),
|
||||
};
|
||||
|
||||
mocked.setup.mockResolvedValue(createSetupContractMock());
|
||||
mocked.start.mockResolvedValue(createStartContractMock());
|
||||
mocked.setup.mockResolvedValue(createInternalSetupContractMock());
|
||||
mocked.start.mockResolvedValue(createInternalStartContractMock());
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const pluginsServiceMock = {
|
||||
create: createMock,
|
||||
createSetupContract: createSetupContractMock,
|
||||
createStartContract: createStartContractMock,
|
||||
createInternalSetupContract: createInternalSetupContractMock,
|
||||
createInternalStartContract: createInternalStartContractMock,
|
||||
createPluginInitializerContext: createPluginInitializerContextMock,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# @kbn/core-plugins-contracts-browser
|
||||
|
||||
This package contains the public types for core's browser-side plugins service's contracts.
|
||||
|
||||
The dedicated package was required to avoid a cyclic dependencies between the `plugins` and `lifecycle` domains.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
PluginContractMap,
|
||||
PluginContractResolver,
|
||||
PluginContractResolverResponse,
|
||||
PluginContractResolverResponseItem,
|
||||
FoundPluginContractResolverResponseItem,
|
||||
NotFoundPluginContractResolverResponseItem,
|
||||
} from './src/contracts';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/packages/core/plugins/core-plugins-contracts-browser'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/core-plugins-contracts-browser",
|
||||
"owner": "@elastic/kibana-core"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/core-plugins-contracts-browser",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { PluginName } from '@kbn/core-base-common';
|
||||
|
||||
/**
|
||||
* Setup contract of Core's `plugins` service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginsServiceSetup {
|
||||
/**
|
||||
* Returns a promise that will resolve with the requested plugin setup contracts once all plugins have been set up.
|
||||
*
|
||||
* If called when plugins are already setup, the returned promise will resolve instantly.
|
||||
*
|
||||
* The API can only be used to resolve required dependencies, optional dependencies, or dependencies explicitly
|
||||
* defined as `runtimePluginDependencies` in the calling plugin's manifest, otherwise the API will throw at call time.
|
||||
*
|
||||
* **Important:** This API should only be used when trying to address cyclic dependency issues that can't easily
|
||||
* be solved otherwise. This is meant to be a temporary workaround only supposed to be used until a better solution
|
||||
* is made available.
|
||||
* Therefore, by using this API, you implicitly agree to:
|
||||
* - consider it as technical debt and open an issue to track the tech debt resolution
|
||||
* - accept that this is only a temporary solution, and will comply to switching to the long term solution when asked by the Core team
|
||||
*
|
||||
* @remark The execution order is not guaranteed to be consistent. Only guarantee is that the returned promise will be
|
||||
* resolved once all plugins are setup, and before Core's `start` is initiated.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* setup(core) {
|
||||
* core.plugins.onSetup<{pluginA: SetupContractA, pluginB: SetupContractA}>('pluginA', 'pluginB')
|
||||
* .then(({ pluginA, pluginB }) => {
|
||||
* if(pluginA.found && pluginB.found) {
|
||||
* // do something with pluginA.contract and pluginB.contract
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* @experimental
|
||||
* ```
|
||||
*/
|
||||
onSetup: PluginContractResolver;
|
||||
/**
|
||||
* Returns a promise that will resolve with the requested plugin start contracts once all plugins have been started.
|
||||
*
|
||||
* If called when plugins are already started, the returned promise will resolve instantly.
|
||||
*
|
||||
* The API can only be used to resolve required dependencies, optional dependencies, or dependencies explicitly
|
||||
* defined as `runtimePluginDependencies` in the calling plugin's manifest, otherwise the API will throw at call time.
|
||||
*
|
||||
* **Important:** This API should only be used when trying to address cyclic dependency issues that can't easily
|
||||
* be solved otherwise. This is meant to be a temporary workaround only supposed to be used until a better solution
|
||||
* is made available.
|
||||
* Therefore, by using this API, you implicitly agree to:
|
||||
* - consider it as technical debt and open an issue to track the tech debt resolution
|
||||
* - accept that this is only a temporary solution, and will comply to switching to the long term solution when asked by the Core team
|
||||
*
|
||||
* @remark The execution order is not guaranteed to be consistent. Only guarantee is that the returned promise will be
|
||||
* resolved once all plugins are started, and before Core's `start` lifecycle is resumed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* setup(core) {
|
||||
* core.plugins.onStart<{pluginA: StartContractA, pluginB: StartContractA}>('pluginA', 'pluginB')
|
||||
* .then(({ pluginA, pluginB }) => {
|
||||
* if(pluginA.found && pluginB.found) {
|
||||
* // do something with pluginA.contract and pluginB.contract
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
onStart: PluginContractResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start contract of Core's `plugins` service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginsServiceStart {
|
||||
/**
|
||||
* Returns a promise that will resolve with the requested plugin start contracts once all plugins have been started.
|
||||
*
|
||||
* If called when plugins are already started, the returned promise will resolve instantly.
|
||||
*
|
||||
* The API can only be used to resolve required dependencies, optional dependencies, or dependencies explicitly
|
||||
* defined as `runtimePluginDependencies` in the calling plugin's manifest, otherwise the API will throw at call time.
|
||||
*
|
||||
* **Important:** This API should only be used when trying to address cyclic dependency issues that can't easily
|
||||
* be solved otherwise. This is meant to be a temporary workaround only supposed to be used until a better solution
|
||||
* is made available.
|
||||
* Therefore, by using this API, you implicitly agree to:
|
||||
* - consider it as technical debt and open an issue to track the tech debt resolution
|
||||
* - accept that this is only a temporary solution, and will comply to switching to the long term solution when asked by the Core team
|
||||
*
|
||||
* @remark The execution order is not guaranteed to be consistent. Only guarantee is that the returned promise will be
|
||||
* resolved once all plugins are started, and before Core's `start` lifecycle is resumed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* start(core) {
|
||||
* core.plugins.onStart<{pluginA: StartContractA, pluginB: StartContractA}>('pluginA', 'pluginB')
|
||||
* .then(({ pluginA, pluginB }) => {
|
||||
* if(pluginA.found && pluginB.found) {
|
||||
* // do something with pluginA.contract and pluginB.contract
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
onStart: PluginContractResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract resolver response for found plugins.
|
||||
*
|
||||
* @see {@link PluginContractResolverResponseItem}
|
||||
* @public
|
||||
*/
|
||||
export interface FoundPluginContractResolverResponseItem<ContractType = unknown> {
|
||||
found: true;
|
||||
contract: ContractType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract resolver response for not found plugins.
|
||||
*
|
||||
* @see {@link PluginContractResolverResponseItem}
|
||||
* @public
|
||||
*/
|
||||
export interface NotFoundPluginContractResolverResponseItem {
|
||||
found: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract resolver response.
|
||||
*
|
||||
* @see {@link PluginContractResolver}
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractResolverResponseItem<ContractType = unknown> =
|
||||
| NotFoundPluginContractResolverResponseItem
|
||||
| FoundPluginContractResolverResponseItem<ContractType>;
|
||||
|
||||
/**
|
||||
* A record of plugin contracts.
|
||||
*
|
||||
* @see {@link PluginContractResolver}
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractMap = Record<PluginName, unknown>;
|
||||
|
||||
/**
|
||||
* Response from a plugin contract resolver request.
|
||||
*
|
||||
* @see {@link PluginContractResolver}
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractResolverResponse<ContractMap extends PluginContractMap> = {
|
||||
[Key in keyof ContractMap]: PluginContractResolverResponseItem<ContractMap[Key]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A plugin contract resolver, allowing to retrieve plugin contracts at runtime.
|
||||
*
|
||||
* Please refer to {@link PluginsServiceSetup} and {@link PluginsServiceStart} for more documentation and examples.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractResolver = <T extends PluginContractMap>(
|
||||
...pluginNames: Array<keyof T>
|
||||
) => Promise<PluginContractResolverResponse<T>>;
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-base-common",
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# @kbn/core-plugins-contracts-server
|
||||
|
||||
This package contains the public types for core's server-side plugins service's contracts.
|
||||
|
||||
The dedicated package was required to avoid a cyclic dependencies between the `plugins` and `lifecycle` domains.
|
18
packages/core/plugins/core-plugins-contracts-server/index.ts
Normal file
18
packages/core/plugins/core-plugins-contracts-server/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
PluginContractMap,
|
||||
PluginContractResolver,
|
||||
PluginContractResolverResponse,
|
||||
PluginContractResolverResponseItem,
|
||||
FoundPluginContractResolverResponseItem,
|
||||
NotFoundPluginContractResolverResponseItem,
|
||||
} from './src/contracts';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/packages/core/plugins/core-plugins-contracts-server'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/core-plugins-contracts-server",
|
||||
"owner": "@elastic/kibana-core"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/core-plugins-contracts-server",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { PluginName } from '@kbn/core-base-common';
|
||||
|
||||
/**
|
||||
* Setup contract of Core's `plugins` service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginsServiceSetup {
|
||||
/**
|
||||
* Returns a promise that will resolve with the requested plugin setup contracts once all plugins have been set up.
|
||||
*
|
||||
* If called when plugins are already setup, the returned promise will resolve instantly.
|
||||
*
|
||||
* The API can only be used to resolve required dependencies, optional dependencies, or dependencies explicitly
|
||||
* defined as `runtimePluginDependencies` in the calling plugin's manifest, otherwise the API will throw at call time.
|
||||
*
|
||||
* **Important:** This API should only be used when trying to address cyclic dependency issues that can't easily
|
||||
* be solved otherwise. This is meant to be a temporary workaround only supposed to be used until a better solution
|
||||
* is made available.
|
||||
* Therefore, by using this API, you implicitly agree to:
|
||||
* - consider it as technical debt and open an issue to track the tech debt resolution
|
||||
* - accept that this is only a temporary solution, and will comply to switching to the long term solution when asked by the Core team
|
||||
*
|
||||
* @remark The execution order is not guaranteed to be consistent. Only guarantee is that the returned promise will be
|
||||
* resolved once all plugins are started, and before Core's `start` lifecycle is resumed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* setup(core) {
|
||||
* core.plugins.onSetup<{pluginA: SetupContractA, pluginB: SetupContractA}>('pluginA', 'pluginB')
|
||||
* .then(({ pluginA, pluginB }) => {
|
||||
* if(pluginA.found && pluginB.found) {
|
||||
* // do something with pluginA.contract and pluginB.contract
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* @experimental
|
||||
* ```
|
||||
*/
|
||||
onSetup: PluginContractResolver;
|
||||
/**
|
||||
* Returns a promise that will resolve with the requested plugin start contracts once all plugins have been started.
|
||||
*
|
||||
* If called when plugins are already started, the returned promise will resolve instantly.
|
||||
*
|
||||
* The API can only be used to resolve required dependencies, optional dependencies, or dependencies explicitly
|
||||
* defined as `runtimePluginDependencies` in the calling plugin's manifest, otherwise the API will throw at call time.
|
||||
*
|
||||
* **Important:** This API should only be used when trying to address cyclic dependency issues that can't easily
|
||||
* be solved otherwise. This is meant to be a temporary workaround only supposed to be used until a better solution
|
||||
* is made available.
|
||||
* Therefore, by using this API, you implicitly agree to:
|
||||
* - consider it as technical debt and open an issue to track the tech debt resolution
|
||||
* - accept that this is only a temporary solution, and will comply to switching to the long term solution when asked by the Core team
|
||||
*
|
||||
* @remark The execution order is not guaranteed to be consistent. Only guarantee is that the returned promise will be
|
||||
* resolved once all plugins are started, and before Core's `start` lifecycle is resumed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* setup(core) {
|
||||
* core.plugins.onStart<{pluginA: StartContractA, pluginB: StartContractA}>('pluginA', 'pluginB')
|
||||
* .then(({ pluginA, pluginB }) => {
|
||||
* if(pluginA.found && pluginB.found) {
|
||||
* // do something with pluginA.contract and pluginB.contract
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* @experimental
|
||||
* ```
|
||||
*/
|
||||
onStart: PluginContractResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start contract of Core's `plugins` service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginsServiceStart {
|
||||
/**
|
||||
* Returns a promise that will resolve with the requested plugin start contracts once all plugins have been started.
|
||||
*
|
||||
* If called when plugins are already started, the returned promise will resolve instantly.
|
||||
*
|
||||
* The API can only be used to resolve required dependencies, optional dependencies, or dependencies explicitly
|
||||
* defined as `runtimePluginDependencies` in the calling plugin's manifest, otherwise the API will throw at call time.
|
||||
*
|
||||
* **Important:** This API should only be used when trying to address cyclic dependency issues that can't easily
|
||||
* be solved otherwise. This is meant to be a temporary workaround only supposed to be used until a better solution
|
||||
* is made available.
|
||||
* Therefore, by using this API, you implicitly agree to:
|
||||
* - consider it as technical debt and open an issue to track the tech debt resolution
|
||||
* - accept that this is only a temporary solution, and will comply to switching to the long term solution when asked by the Core team
|
||||
*
|
||||
* @remark The execution order is not guaranteed to be consistent. Only guarantee is that the returned promise will be
|
||||
* resolved once all plugins are started, and before Core's `start` lifecycle is resumed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* start(core) {
|
||||
* core.plugins.onStart<{pluginA: StartContractA, pluginB: StartContractA}>('pluginA', 'pluginB')
|
||||
* .then(({ pluginA, pluginB }) => {
|
||||
* if(pluginA.found && pluginB.found) {
|
||||
* // do something with pluginA.contract and pluginB.contract
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
onStart: PluginContractResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract resolver response for found plugins.
|
||||
*
|
||||
* @see {@link PluginContractResolverResponseItem}
|
||||
* @public
|
||||
*/
|
||||
export interface FoundPluginContractResolverResponseItem<ContractType = unknown> {
|
||||
found: true;
|
||||
contract: ContractType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract resolver response for not found plugins.
|
||||
*
|
||||
* @see {@link PluginContractResolverResponseItem}
|
||||
* @public
|
||||
*/
|
||||
export interface NotFoundPluginContractResolverResponseItem {
|
||||
found: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract resolver response.
|
||||
*
|
||||
* @see {@link PluginContractResolver}
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractResolverResponseItem<ContractType = unknown> =
|
||||
| NotFoundPluginContractResolverResponseItem
|
||||
| FoundPluginContractResolverResponseItem<ContractType>;
|
||||
|
||||
/**
|
||||
* A record of plugin contracts.
|
||||
*
|
||||
* @see {@link PluginContractResolver}
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractMap = Record<PluginName, unknown>;
|
||||
|
||||
/**
|
||||
* Response from a plugin contract resolver request.
|
||||
*
|
||||
* @see {@link PluginContractResolver}
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractResolverResponse<ContractMap extends PluginContractMap> = {
|
||||
[Key in keyof ContractMap]: PluginContractResolverResponseItem<ContractMap[Key]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A plugin contract resolver, allowing to retrieve plugin contracts at runtime.
|
||||
*
|
||||
* Please refer to {@link PluginsServiceSetup} and {@link PluginsServiceStart} for more documentation and examples.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PluginContractResolver = <T extends PluginContractMap>(
|
||||
...pluginNames: Array<keyof T>
|
||||
) => Promise<PluginContractResolverResponse<T>>;
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-base-common",
|
||||
]
|
||||
}
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
export { PluginsService, PluginWrapper, config, isNewPlatformPlugin } from './src';
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
InternalPluginsServiceSetup,
|
||||
InternalPluginsServiceStart,
|
||||
DiscoveredPlugins,
|
||||
PluginDependencies,
|
||||
} from './src';
|
||||
|
|
|
@ -32,6 +32,7 @@ const basic: PluginPackageManifest = {
|
|||
optionalPlugins: ['someOtherPlugin'],
|
||||
requiredBundles: ['someRequiresBundlePlugin'],
|
||||
requiredPlugins: ['someRequiredPlugin'],
|
||||
runtimePluginDependencies: ['someRuntimeDependencyPlugin'],
|
||||
},
|
||||
serviceFolders: ['foo', 'bar'],
|
||||
};
|
||||
|
@ -59,6 +60,9 @@ describe('pluginManifestFromPluginPackage()', () => {
|
|||
"requiredPlugins": Array [
|
||||
"someRequiredPlugin",
|
||||
],
|
||||
"runtimePluginDependencies": Array [
|
||||
"someRuntimeDependencyPlugin",
|
||||
],
|
||||
"server": true,
|
||||
"serviceFolders": Array [
|
||||
"foo",
|
||||
|
|
|
@ -25,6 +25,7 @@ export function pluginManifestFromPluginPackage(
|
|||
optionalPlugins: manifest.plugin.optionalPlugins ?? [],
|
||||
requiredBundles: manifest.plugin.requiredBundles ?? [],
|
||||
requiredPlugins: manifest.plugin.requiredPlugins ?? [],
|
||||
runtimePluginDependencies: manifest.plugin.runtimePluginDependencies ?? [],
|
||||
owner: {
|
||||
name: manifest.owner.join(' & '),
|
||||
},
|
||||
|
|
|
@ -388,6 +388,7 @@ test('set defaults for all missing optional fields', async () => {
|
|||
optionalPlugins: [],
|
||||
requiredPlugins: [],
|
||||
requiredBundles: [],
|
||||
runtimePluginDependencies: [],
|
||||
server: true,
|
||||
ui: false,
|
||||
owner: { name: 'foo' },
|
||||
|
@ -407,6 +408,7 @@ test('return all set optional fields as they are in manifest', async () => {
|
|||
type: 'preboot',
|
||||
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
|
||||
optionalPlugins: ['some-optional-plugin'],
|
||||
runtimePluginDependencies: ['runtime-plugin-dependency'],
|
||||
ui: true,
|
||||
owner: { name: 'foo' },
|
||||
enabledOnAnonymousPages: true,
|
||||
|
@ -424,6 +426,7 @@ test('return all set optional fields as they are in manifest', async () => {
|
|||
optionalPlugins: ['some-optional-plugin'],
|
||||
requiredBundles: [],
|
||||
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
|
||||
runtimePluginDependencies: ['runtime-plugin-dependency'],
|
||||
server: false,
|
||||
ui: true,
|
||||
owner: { name: 'foo' },
|
||||
|
@ -458,6 +461,7 @@ test('return manifest when plugin expected Kibana version matches actual version
|
|||
optionalPlugins: [],
|
||||
requiredPlugins: ['some-required-plugin'],
|
||||
requiredBundles: [],
|
||||
runtimePluginDependencies: [],
|
||||
server: true,
|
||||
ui: false,
|
||||
owner: { name: 'foo' },
|
||||
|
@ -491,6 +495,7 @@ test('return manifest when plugin expected Kibana version is `kibana`', async ()
|
|||
optionalPlugins: [],
|
||||
requiredPlugins: ['some-required-plugin'],
|
||||
requiredBundles: [],
|
||||
runtimePluginDependencies: [],
|
||||
server: true,
|
||||
ui: true,
|
||||
owner: { name: 'foo' },
|
||||
|
|
|
@ -45,6 +45,7 @@ const KNOWN_MANIFEST_FIELDS = (() => {
|
|||
configPath: true,
|
||||
requiredPlugins: true,
|
||||
optionalPlugins: true,
|
||||
runtimePluginDependencies: true,
|
||||
ui: true,
|
||||
server: true,
|
||||
extraPublicDirs: true,
|
||||
|
@ -209,6 +210,9 @@ export async function parseManifest(
|
|||
requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [],
|
||||
optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [],
|
||||
requiredBundles: Array.isArray(manifest.requiredBundles) ? manifest.requiredBundles : [],
|
||||
runtimePluginDependencies: Array.isArray(manifest.runtimePluginDependencies)
|
||||
? manifest.runtimePluginDependencies
|
||||
: [],
|
||||
ui: includesUiPlugin,
|
||||
server: includesServerPlugin,
|
||||
extraPublicDirs: manifest.extraPublicDirs,
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
export { PluginsService } from './plugins_service';
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
InternalPluginsServiceSetup,
|
||||
InternalPluginsServiceStart,
|
||||
DiscoveredPlugins,
|
||||
} from './plugins_service';
|
||||
export { config } from './plugins_config';
|
||||
|
|
|
@ -18,9 +18,10 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
|||
import type { NodeInfo } from '@kbn/core-node-server';
|
||||
import { nodeServiceMock } from '@kbn/core-node-server-mocks';
|
||||
import type { PluginManifest } from '@kbn/core-plugins-server';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginType } from '@kbn/core-base-common';
|
||||
import { coreInternalLifecycleMock } from '@kbn/core-lifecycle-server-mocks';
|
||||
import { createRuntimePluginContractResolverMock } from './test_helpers';
|
||||
import { PluginWrapper } from './plugin';
|
||||
|
||||
import {
|
||||
createPluginInitializerContext,
|
||||
|
@ -57,6 +58,7 @@ function createPluginManifest(manifestProps: Partial<PluginManifest> = {}): Plug
|
|||
requiredPlugins: ['some-required-dep'],
|
||||
optionalPlugins: ['some-optional-dep'],
|
||||
requiredBundles: [],
|
||||
runtimePluginDependencies: ['some-runtime-dep'],
|
||||
server: true,
|
||||
ui: true,
|
||||
owner: { name: 'Core' },
|
||||
|
@ -72,6 +74,7 @@ let env: Env;
|
|||
let coreContext: CoreContext;
|
||||
let instanceInfo: InstanceInfo;
|
||||
let nodeInfo: NodeInfo;
|
||||
let runtimeResolver: ReturnType<typeof createRuntimePluginContractResolverMock>;
|
||||
|
||||
const setupDeps = coreInternalLifecycleMock.createInternalSetup();
|
||||
|
||||
|
@ -82,7 +85,7 @@ beforeEach(() => {
|
|||
uuid: 'instance-uuid',
|
||||
};
|
||||
nodeInfo = nodeServiceMock.createInternalPrebootContract();
|
||||
|
||||
runtimeResolver = createRuntimePluginContractResolverMock();
|
||||
coreContext = { coreId, env, logger, configService: configService as any };
|
||||
});
|
||||
|
||||
|
@ -112,6 +115,7 @@ test('`constructor` correctly initializes plugin instance', () => {
|
|||
expect(plugin.source).toBe('external'); // see below for test cases for non-external sources (OSS and X-Pack)
|
||||
expect(plugin.requiredPlugins).toEqual(['some-required-dep']);
|
||||
expect(plugin.optionalPlugins).toEqual(['some-optional-dep']);
|
||||
expect(plugin.runtimePluginDependencies).toEqual(['some-runtime-dep']);
|
||||
});
|
||||
|
||||
describe('`constructor` correctly sets non-external source', () => {
|
||||
|
@ -170,7 +174,7 @@ test('`setup` fails if `plugin` initializer is not exported', () => {
|
|||
});
|
||||
|
||||
expect(() =>
|
||||
plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {})
|
||||
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Plugin \\"some-plugin-id\\" does not export \\"plugin\\" definition (plugin-without-initializer-path)."`
|
||||
);
|
||||
|
@ -193,7 +197,7 @@ test('`setup` fails if plugin initializer is not a function', () => {
|
|||
});
|
||||
|
||||
expect(() =>
|
||||
plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {})
|
||||
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Definition of plugin \\"some-plugin-id\\" should be a function (plugin-with-wrong-initializer-path)."`
|
||||
);
|
||||
|
@ -218,7 +222,7 @@ test('`setup` fails if initializer does not return object', () => {
|
|||
mockPluginInitializer.mockReturnValue(null);
|
||||
|
||||
expect(() =>
|
||||
plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {})
|
||||
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Initializer for plugin \\"some-plugin-id\\" is expected to return plugin instance, but returned \\"null\\"."`
|
||||
);
|
||||
|
@ -244,7 +248,7 @@ test('`setup` fails if object returned from initializer does not define `setup`
|
|||
mockPluginInitializer.mockReturnValue(mockPluginInstance);
|
||||
|
||||
expect(() =>
|
||||
plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {})
|
||||
plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Instance of plugin \\"some-plugin-id\\" does not define \\"setup\\" function."`
|
||||
);
|
||||
|
@ -270,7 +274,7 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async ()
|
|||
const mockPluginInstance = { setup: jest.fn().mockResolvedValue({ contract: 'yes' }) };
|
||||
mockPluginInitializer.mockReturnValue(mockPluginInstance);
|
||||
|
||||
const setupContext = createPluginSetupContext(coreContext, setupDeps, plugin);
|
||||
const setupContext = createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver });
|
||||
const setupDependencies = { 'some-required-dep': { contract: 'no' } };
|
||||
await expect(plugin.setup(setupContext, setupDependencies)).resolves.toEqual({ contract: 'yes' });
|
||||
|
||||
|
@ -450,7 +454,7 @@ test('`stop` does nothing if plugin does not define `stop` function', async () =
|
|||
});
|
||||
|
||||
mockPluginInitializer.mockReturnValue({ setup: jest.fn() });
|
||||
await plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {});
|
||||
await plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {});
|
||||
|
||||
await expect(plugin.stop()).resolves.toBeUndefined();
|
||||
});
|
||||
|
@ -473,7 +477,7 @@ test('`stop` calls `stop` defined by the plugin instance', async () => {
|
|||
|
||||
const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() };
|
||||
mockPluginInitializer.mockReturnValue(mockPluginInstance);
|
||||
await plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {});
|
||||
await plugin.setup(createPluginSetupContext({ deps: setupDeps, plugin, runtimeResolver }), {});
|
||||
|
||||
await expect(plugin.stop()).resolves.toBeUndefined();
|
||||
expect(mockPluginInstance.stop).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -47,6 +47,7 @@ export class PluginWrapper<
|
|||
public readonly configPath: PluginManifest['configPath'];
|
||||
public readonly requiredPlugins: PluginManifest['requiredPlugins'];
|
||||
public readonly optionalPlugins: PluginManifest['optionalPlugins'];
|
||||
public readonly runtimePluginDependencies: PluginManifest['runtimePluginDependencies'];
|
||||
public readonly requiredBundles: PluginManifest['requiredBundles'];
|
||||
public readonly includesServerPlugin: PluginManifest['server'];
|
||||
public readonly includesUiPlugin: PluginManifest['ui'];
|
||||
|
@ -81,6 +82,7 @@ export class PluginWrapper<
|
|||
this.requiredPlugins = params.manifest.requiredPlugins;
|
||||
this.optionalPlugins = params.manifest.optionalPlugins;
|
||||
this.requiredBundles = params.manifest.requiredBundles;
|
||||
this.runtimePluginDependencies = params.manifest.runtimePluginDependencies;
|
||||
this.includesServerPlugin = params.manifest.server;
|
||||
this.includesUiPlugin = params.manifest.ui;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ function createPluginManifest(manifestProps: Partial<PluginManifest> = {}): Plug
|
|||
requiredPlugins: ['some-required-dep'],
|
||||
requiredBundles: [],
|
||||
optionalPlugins: ['some-optional-dep'],
|
||||
runtimePluginDependencies: [],
|
||||
server: true,
|
||||
ui: true,
|
||||
owner: {
|
||||
|
@ -237,7 +238,7 @@ describe('createPluginPrebootSetupContext', () => {
|
|||
});
|
||||
|
||||
const corePreboot = coreInternalLifecycleMock.createInternalPreboot();
|
||||
const prebootSetupContext = createPluginPrebootSetupContext(coreContext, corePreboot, plugin);
|
||||
const prebootSetupContext = createPluginPrebootSetupContext({ deps: corePreboot, plugin });
|
||||
|
||||
const holdSetupPromise = Promise.resolve(undefined);
|
||||
prebootSetupContext.preboot.holdSetupUntilResolved('some-reason', holdSetupPromise);
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
PluginsServiceStartDeps,
|
||||
} from './plugins_service';
|
||||
import { getGlobalConfig, getGlobalConfig$ } from './legacy_config';
|
||||
import type { IRuntimePluginContractResolver } from './plugin_contract_resolver';
|
||||
|
||||
/** @internal */
|
||||
export interface InstanceInfo {
|
||||
|
@ -128,11 +129,13 @@ export function createPluginInitializerContext({
|
|||
* @param plugin The plugin we're building these values for.
|
||||
* @internal
|
||||
*/
|
||||
export function createPluginPrebootSetupContext(
|
||||
coreContext: CoreContext,
|
||||
deps: PluginsServicePrebootSetupDeps,
|
||||
plugin: PluginWrapper
|
||||
): CorePreboot {
|
||||
export function createPluginPrebootSetupContext({
|
||||
deps,
|
||||
plugin,
|
||||
}: {
|
||||
deps: PluginsServicePrebootSetupDeps;
|
||||
plugin: PluginWrapper;
|
||||
}): CorePreboot {
|
||||
return {
|
||||
analytics: {
|
||||
optIn: deps.analytics.optIn,
|
||||
|
@ -174,11 +177,15 @@ export function createPluginPrebootSetupContext(
|
|||
* @param deps Dependencies that Plugins services gets during setup.
|
||||
* @internal
|
||||
*/
|
||||
export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
||||
coreContext: CoreContext,
|
||||
deps: PluginsServiceSetupDeps,
|
||||
plugin: PluginWrapper<TPlugin, TPluginDependencies>
|
||||
): CoreSetup {
|
||||
export function createPluginSetupContext<TPlugin, TPluginDependencies>({
|
||||
deps,
|
||||
plugin,
|
||||
runtimeResolver,
|
||||
}: {
|
||||
deps: PluginsServiceSetupDeps;
|
||||
plugin: PluginWrapper<TPlugin, TPluginDependencies>;
|
||||
runtimeResolver: IRuntimePluginContractResolver;
|
||||
}): CoreSetup {
|
||||
const router = deps.http.createRouter('', plugin.opaqueId);
|
||||
|
||||
return {
|
||||
|
@ -268,6 +275,10 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
coreUsageData: {
|
||||
registerUsageCounter: deps.coreUsageData.registerUsageCounter,
|
||||
},
|
||||
plugins: {
|
||||
onSetup: (...dependencyNames) => runtimeResolver.onSetup(plugin.name, dependencyNames),
|
||||
onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -282,12 +293,16 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
* @param plugin The plugin we're building these values for.
|
||||
* @param deps Dependencies that Plugins services gets during start.
|
||||
* @internal
|
||||
*/
|
||||
export function createPluginStartContext<TPlugin, TPluginDependencies>(
|
||||
coreContext: CoreContext,
|
||||
deps: PluginsServiceStartDeps,
|
||||
plugin: PluginWrapper<TPlugin, TPluginDependencies>
|
||||
): CoreStart {
|
||||
*/ //
|
||||
export function createPluginStartContext<TPlugin, TPluginDependencies>({
|
||||
plugin,
|
||||
deps,
|
||||
runtimeResolver,
|
||||
}: {
|
||||
deps: PluginsServiceStartDeps;
|
||||
plugin: PluginWrapper<TPlugin, TPluginDependencies>;
|
||||
runtimeResolver: IRuntimePluginContractResolver;
|
||||
}): CoreStart {
|
||||
return {
|
||||
analytics: {
|
||||
optIn: deps.analytics.optIn,
|
||||
|
@ -332,5 +347,8 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>(
|
|||
globalAsScopedToClient: deps.uiSettings.globalAsScopedToClient,
|
||||
},
|
||||
coreUsageData: deps.coreUsageData,
|
||||
plugins: {
|
||||
onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { RuntimePluginContractResolver } from './plugin_contract_resolver';
|
||||
|
||||
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 1));
|
||||
const fewTicks = () =>
|
||||
nextTick()
|
||||
.then(() => nextTick())
|
||||
.then(() => nextTick());
|
||||
|
||||
const toMap = (record: Record<string, unknown>): Map<string, unknown> => {
|
||||
return new Map(Object.entries(record));
|
||||
};
|
||||
|
||||
const pluginAContract = Symbol();
|
||||
|
||||
describe('RuntimePluginContractResolver', () => {
|
||||
const SOURCE_PLUGIN = 'sourcePlugin';
|
||||
let resolver: RuntimePluginContractResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
resolver = new RuntimePluginContractResolver();
|
||||
|
||||
const dependencyMap = new Map<string, Set<string>>();
|
||||
dependencyMap.set(SOURCE_PLUGIN, new Set(['pluginA', 'pluginB', 'pluginC']));
|
||||
resolver.setDependencyMap(dependencyMap);
|
||||
});
|
||||
|
||||
describe('setup contracts', () => {
|
||||
it('throws if onSetup is called before setDependencyMap', () => {
|
||||
resolver = new RuntimePluginContractResolver();
|
||||
|
||||
expect(() => resolver.onSetup(SOURCE_PLUGIN, ['pluginA'])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"onSetup cannot be called before setDependencyMap"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if resolveSetupRequests is called multiple times', async () => {
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"resolveSetupRequests can only be called once"`);
|
||||
});
|
||||
|
||||
it('resolves a single request', async () => {
|
||||
const handler = jest.fn();
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves multiple requests', async () => {
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
const handler3 = jest.fn();
|
||||
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
resolver
|
||||
.onSetup(SOURCE_PLUGIN, ['pluginA', 'pluginB'])
|
||||
.then((contracts) => handler3(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
expect(handler3).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler3).toHaveBeenCalledTimes(1);
|
||||
expect(handler3).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves requests instantly when called after resolveSetupRequests', async () => {
|
||||
resolver.resolveSetupRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when requesting a contract not defined in the dependency map', async () => {
|
||||
expect(() =>
|
||||
resolver.onSetup(SOURCE_PLUGIN, ['undeclaredPlugin'])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when requesting a mixed defined/undefined dependencies', async () => {
|
||||
expect(() =>
|
||||
resolver.onSetup(SOURCE_PLUGIN, [
|
||||
'pluginA',
|
||||
'undeclaredPlugin1',
|
||||
'pluginB',
|
||||
'undeclaredPlugin2',
|
||||
])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin1, undeclaredPlugin2"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start contracts', () => {
|
||||
it('throws if onStart is called before setDependencyMap', () => {
|
||||
resolver = new RuntimePluginContractResolver();
|
||||
|
||||
expect(() => resolver.onStart(SOURCE_PLUGIN, ['pluginA'])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"onStart cannot be called before setDependencyMap"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if resolveStartRequests is called multiple times', async () => {
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"resolveStartRequests can only be called once"`);
|
||||
});
|
||||
|
||||
it('resolves a single request', async () => {
|
||||
const handler = jest.fn();
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves multiple requests', async () => {
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
const handler3 = jest.fn();
|
||||
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
resolver
|
||||
.onStart(SOURCE_PLUGIN, ['pluginA', 'pluginB'])
|
||||
.then((contracts) => handler3(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
expect(handler3).not.toHaveBeenCalled();
|
||||
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler3).toHaveBeenCalledTimes(1);
|
||||
expect(handler3).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves requests instantly when called after resolveSetupRequests', async () => {
|
||||
resolver.resolveStartRequests(
|
||||
toMap({
|
||||
pluginA: pluginAContract,
|
||||
})
|
||||
);
|
||||
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginA']).then((contracts) => handler1(contracts));
|
||||
resolver.onStart(SOURCE_PLUGIN, ['pluginB']).then((contracts) => handler2(contracts));
|
||||
|
||||
await fewTicks();
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler1).toHaveBeenCalledWith({
|
||||
pluginA: {
|
||||
found: true,
|
||||
contract: pluginAContract,
|
||||
},
|
||||
});
|
||||
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledWith({
|
||||
pluginB: {
|
||||
found: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when requesting a contract not defined in the dependency map', async () => {
|
||||
expect(() =>
|
||||
resolver.onStart(SOURCE_PLUGIN, ['undeclaredPlugin'])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when requesting a mixed defined/undefined dependencies', async () => {
|
||||
expect(() =>
|
||||
resolver.onStart(SOURCE_PLUGIN, [
|
||||
'pluginA',
|
||||
'undeclaredPlugin1',
|
||||
'pluginB',
|
||||
'undeclaredPlugin2',
|
||||
])
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.Undeclared dependencies: undeclaredPlugin1, undeclaredPlugin2"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { PluginName } from '@kbn/core-base-common';
|
||||
import type {
|
||||
PluginContractResolverResponse,
|
||||
PluginContractMap,
|
||||
PluginContractResolverResponseItem,
|
||||
} from '@kbn/core-plugins-contracts-server';
|
||||
|
||||
export type IRuntimePluginContractResolver = PublicMethodsOf<RuntimePluginContractResolver>;
|
||||
|
||||
export class RuntimePluginContractResolver {
|
||||
private dependencyMap?: Map<PluginName, Set<PluginName>>;
|
||||
private setupContracts?: Map<PluginName, unknown>;
|
||||
private startContracts?: Map<PluginName, unknown>;
|
||||
|
||||
private readonly setupRequestQueue: PluginContractRequest[] = [];
|
||||
private readonly startRequestQueue: PluginContractRequest[] = [];
|
||||
|
||||
setDependencyMap(depMap: Map<PluginName, Set<PluginName>>) {
|
||||
this.dependencyMap = new Map(depMap.entries());
|
||||
}
|
||||
|
||||
onSetup = <T extends PluginContractMap>(
|
||||
pluginName: PluginName,
|
||||
dependencyNames: Array<keyof T>
|
||||
): Promise<PluginContractResolverResponse<T>> => {
|
||||
if (!this.dependencyMap) {
|
||||
throw new Error('onSetup cannot be called before setDependencyMap');
|
||||
}
|
||||
|
||||
const dependencyList = this.dependencyMap.get(pluginName) ?? new Set();
|
||||
const notDependencyPlugins = dependencyNames.filter(
|
||||
(name) => !dependencyList.has(name as PluginName)
|
||||
);
|
||||
if (notDependencyPlugins.length) {
|
||||
throw new Error(
|
||||
'Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.' +
|
||||
`Undeclared dependencies: ${notDependencyPlugins.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.setupContracts) {
|
||||
const response = createContractRequestResponse(
|
||||
dependencyNames as PluginName[],
|
||||
this.setupContracts
|
||||
);
|
||||
return Promise.resolve(response as PluginContractResolverResponse<T>);
|
||||
} else {
|
||||
const setupContractRequest = createPluginContractRequest<PluginContractResolverResponse<T>>(
|
||||
dependencyNames as PluginName[]
|
||||
);
|
||||
this.setupRequestQueue.push(setupContractRequest as PluginContractRequest);
|
||||
return setupContractRequest.contractPromise;
|
||||
}
|
||||
};
|
||||
|
||||
onStart = <T extends PluginContractMap>(
|
||||
pluginName: PluginName,
|
||||
dependencyNames: Array<keyof T>
|
||||
): Promise<PluginContractResolverResponse<T>> => {
|
||||
if (!this.dependencyMap) {
|
||||
throw new Error('onStart cannot be called before setDependencyMap');
|
||||
}
|
||||
|
||||
const dependencyList = this.dependencyMap.get(pluginName) ?? new Set();
|
||||
const notDependencyPlugins = dependencyNames.filter(
|
||||
(name) => !dependencyList.has(name as PluginName)
|
||||
);
|
||||
if (notDependencyPlugins.length) {
|
||||
throw new Error(
|
||||
'Dynamic contract resolving requires the dependencies to be declared in the plugin manifest.' +
|
||||
`Undeclared dependencies: ${notDependencyPlugins.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.startContracts) {
|
||||
const response = createContractRequestResponse(
|
||||
dependencyNames as PluginName[],
|
||||
this.startContracts
|
||||
);
|
||||
return Promise.resolve(response as PluginContractResolverResponse<T>);
|
||||
} else {
|
||||
const startContractRequest = createPluginContractRequest<PluginContractResolverResponse<T>>(
|
||||
dependencyNames as PluginName[]
|
||||
);
|
||||
this.startRequestQueue.push(startContractRequest as PluginContractRequest);
|
||||
return startContractRequest.contractPromise;
|
||||
}
|
||||
};
|
||||
|
||||
resolveSetupRequests(setupContracts: Map<PluginName, unknown>) {
|
||||
if (this.setupContracts) {
|
||||
throw new Error('resolveSetupRequests can only be called once');
|
||||
}
|
||||
this.setupContracts = setupContracts;
|
||||
|
||||
for (const setupRequest of this.setupRequestQueue) {
|
||||
const response = createContractRequestResponse(setupRequest.pluginNames, setupContracts);
|
||||
setupRequest.resolve(response);
|
||||
}
|
||||
}
|
||||
|
||||
resolveStartRequests(startContracts: Map<PluginName, unknown>) {
|
||||
if (this.startContracts) {
|
||||
throw new Error('resolveStartRequests can only be called once');
|
||||
}
|
||||
this.startContracts = startContracts;
|
||||
|
||||
for (const startRequest of this.startRequestQueue) {
|
||||
const response = createContractRequestResponse(startRequest.pluginNames, startContracts);
|
||||
startRequest.resolve(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PluginContractRequest<T = unknown> {
|
||||
pluginNames: PluginName[];
|
||||
contractPromise: Promise<T>;
|
||||
resolve: (data?: T) => void;
|
||||
}
|
||||
|
||||
const createPluginContractRequest = <T = unknown>(
|
||||
pluginNames: PluginName[]
|
||||
): PluginContractRequest<T> => {
|
||||
let resolve!: (data?: T) => void;
|
||||
const contractPromise = new Promise<any>((_resolve) => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
|
||||
return {
|
||||
pluginNames,
|
||||
contractPromise,
|
||||
resolve,
|
||||
};
|
||||
};
|
||||
|
||||
const createContractRequestResponse = <T extends PluginContractMap>(
|
||||
pluginNames: PluginName[],
|
||||
contracts: Map<string, unknown>
|
||||
): PluginContractResolverResponse<T> => {
|
||||
const response = {} as Record<string, unknown>;
|
||||
for (const pluginName of pluginNames) {
|
||||
const pluginResponse: PluginContractResolverResponseItem = contracts.has(pluginName)
|
||||
? {
|
||||
found: true,
|
||||
contract: contracts.get(pluginName)!,
|
||||
}
|
||||
: { found: false };
|
||||
response[pluginName] = pluginResponse;
|
||||
}
|
||||
|
||||
return response as PluginContractResolverResponse<T>;
|
||||
};
|
|
@ -76,6 +76,7 @@ const createPlugin = (
|
|||
requiredPlugins = [],
|
||||
requiredBundles = [],
|
||||
optionalPlugins = [],
|
||||
runtimePluginDependencies = [],
|
||||
kibanaVersion = '7.0.0',
|
||||
configPath = [path],
|
||||
server = true,
|
||||
|
@ -88,6 +89,7 @@ const createPlugin = (
|
|||
requiredPlugins?: string[];
|
||||
requiredBundles?: string[];
|
||||
optionalPlugins?: string[];
|
||||
runtimePluginDependencies?: string[];
|
||||
kibanaVersion?: string;
|
||||
configPath?: ConfigPath;
|
||||
server?: boolean;
|
||||
|
@ -105,6 +107,7 @@ const createPlugin = (
|
|||
requiredPlugins,
|
||||
requiredBundles,
|
||||
optionalPlugins,
|
||||
runtimePluginDependencies,
|
||||
server,
|
||||
owner: {
|
||||
name: 'Core',
|
||||
|
@ -1018,6 +1021,7 @@ describe('PluginsService', () => {
|
|||
requiredPlugins: [],
|
||||
requiredBundles: [],
|
||||
optionalPlugins: [],
|
||||
runtimePluginDependencies: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export type DiscoveredPlugins = {
|
|||
};
|
||||
|
||||
/** @internal */
|
||||
export interface PluginsServiceSetup {
|
||||
export interface InternalPluginsServiceSetup {
|
||||
/** Indicates whether or not plugins were initialized. */
|
||||
initialized: boolean;
|
||||
/** Setup contracts returned by plugins. */
|
||||
|
@ -51,7 +51,7 @@ export interface PluginsServiceSetup {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
export interface PluginsServiceStart {
|
||||
export interface InternalPluginsServiceStart {
|
||||
/** Start contracts returned by plugins. */
|
||||
contracts: Map<PluginName, unknown>;
|
||||
}
|
||||
|
@ -72,7 +72,9 @@ export interface PluginsServiceDiscoverDeps {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
export class PluginsService implements CoreService<PluginsServiceSetup, PluginsServiceStart> {
|
||||
export class PluginsService
|
||||
implements CoreService<InternalPluginsServiceSetup, InternalPluginsServiceStart>
|
||||
{
|
||||
private readonly log: Logger;
|
||||
private readonly prebootPluginsSystem: PluginsSystem<PluginType.preboot>;
|
||||
private arePrebootPluginsStopped = false;
|
||||
|
|
|
@ -6,11 +6,22 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createRuntimePluginContractResolverMock } from './test_helpers';
|
||||
|
||||
export const mockCreatePluginPrebootSetupContext = jest.fn();
|
||||
export const mockCreatePluginSetupContext = jest.fn();
|
||||
export const mockCreatePluginStartContext = jest.fn();
|
||||
|
||||
jest.mock('./plugin_context', () => ({
|
||||
createPluginPrebootSetupContext: mockCreatePluginPrebootSetupContext,
|
||||
createPluginSetupContext: mockCreatePluginSetupContext,
|
||||
createPluginStartContext: mockCreatePluginStartContext,
|
||||
}));
|
||||
|
||||
export const runtimeResolverMock = createRuntimePluginContractResolverMock();
|
||||
|
||||
jest.doMock('./plugin_contract_resolver', () => {
|
||||
return {
|
||||
RuntimePluginContractResolver: jest.fn().mockImplementation(() => runtimeResolverMock),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
mockCreatePluginPrebootSetupContext,
|
||||
mockCreatePluginSetupContext,
|
||||
mockCreatePluginStartContext,
|
||||
runtimeResolverMock,
|
||||
} from './plugins_system.test.mocks';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
@ -52,6 +53,7 @@ function createPlugin(
|
|||
type,
|
||||
requiredPlugins: required,
|
||||
optionalPlugins: optional,
|
||||
runtimePluginDependencies: [],
|
||||
requiredBundles: [],
|
||||
server,
|
||||
ui,
|
||||
|
@ -73,6 +75,10 @@ let env: Env;
|
|||
let coreContext: CoreContext;
|
||||
|
||||
beforeEach(() => {
|
||||
runtimeResolverMock.setDependencyMap.mockReset();
|
||||
runtimeResolverMock.resolveSetupRequests.mockReset();
|
||||
runtimeResolverMock.resolveStartRequests.mockReset();
|
||||
|
||||
logger = loggingSystemMock.create();
|
||||
env = Env.createDefault(REPO_ROOT, getEnvOptions());
|
||||
|
||||
|
@ -197,6 +203,31 @@ test('`setupPlugins` ignores missing optional dependency', async () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('`setupPlugins` setups the runtimeResolver', async () => {
|
||||
const pluginA = createPlugin('pluginA', { required: [] });
|
||||
const pluginB = createPlugin('pluginB', { required: ['pluginA'] });
|
||||
|
||||
jest.spyOn(pluginA, 'setup').mockReturnValue('contractA');
|
||||
jest.spyOn(pluginB, 'setup').mockReturnValue('contractB');
|
||||
|
||||
pluginsSystem.addPlugin(pluginA);
|
||||
pluginsSystem.addPlugin(pluginB);
|
||||
|
||||
await pluginsSystem.setupPlugins(setupDeps);
|
||||
|
||||
expect(runtimeResolverMock.setDependencyMap).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeResolverMock.setDependencyMap).toHaveBeenCalledWith(expect.any(Map));
|
||||
|
||||
expect(runtimeResolverMock.resolveSetupRequests).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeResolverMock.resolveSetupRequests).toHaveBeenCalledWith(expect.any(Map));
|
||||
expect(
|
||||
Object.fromEntries([...runtimeResolverMock.resolveSetupRequests.mock.calls[0][0].entries()])
|
||||
).toEqual({
|
||||
pluginA: 'contractA',
|
||||
pluginB: 'contractB',
|
||||
});
|
||||
});
|
||||
|
||||
test('correctly orders plugins and returns exposed values for "setup" and "start"', async () => {
|
||||
interface Contracts {
|
||||
setup: Record<PluginName, unknown>;
|
||||
|
@ -254,13 +285,9 @@ test('correctly orders plugins and returns exposed values for "setup" and "start
|
|||
pluginsSystem.addPlugin(plugin);
|
||||
});
|
||||
|
||||
mockCreatePluginSetupContext.mockImplementation((context, deps, plugin) =>
|
||||
setupContextMap.get(plugin.name)
|
||||
);
|
||||
mockCreatePluginSetupContext.mockImplementation(({ plugin }) => setupContextMap.get(plugin.name));
|
||||
|
||||
mockCreatePluginStartContext.mockImplementation((context, deps, plugin) =>
|
||||
startContextMap.get(plugin.name)
|
||||
);
|
||||
mockCreatePluginStartContext.mockImplementation(({ plugin }) => startContextMap.get(plugin.name));
|
||||
|
||||
expect([...(await pluginsSystem.setupPlugins(setupDeps))]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -288,7 +315,11 @@ test('correctly orders plugins and returns exposed values for "setup" and "start
|
|||
`);
|
||||
|
||||
for (const [plugin, deps] of plugins) {
|
||||
expect(mockCreatePluginSetupContext).toHaveBeenCalledWith(coreContext, setupDeps, plugin);
|
||||
expect(mockCreatePluginSetupContext).toHaveBeenCalledWith({
|
||||
deps: setupDeps,
|
||||
plugin,
|
||||
runtimeResolver: expect.any(Object),
|
||||
});
|
||||
expect(plugin.setup).toHaveBeenCalledTimes(1);
|
||||
expect(plugin.setup).toHaveBeenCalledWith(setupContextMap.get(plugin.name), deps.setup);
|
||||
}
|
||||
|
@ -319,7 +350,11 @@ test('correctly orders plugins and returns exposed values for "setup" and "start
|
|||
`);
|
||||
|
||||
for (const [plugin, deps] of plugins) {
|
||||
expect(mockCreatePluginStartContext).toHaveBeenCalledWith(coreContext, startDeps, plugin);
|
||||
expect(mockCreatePluginStartContext).toHaveBeenCalledWith({
|
||||
deps: startDeps,
|
||||
plugin,
|
||||
runtimeResolver: expect.any(Object),
|
||||
});
|
||||
expect(plugin.start).toHaveBeenCalledTimes(1);
|
||||
expect(plugin.start).toHaveBeenCalledWith(startContextMap.get(plugin.name), deps.start);
|
||||
}
|
||||
|
@ -362,7 +397,7 @@ test('correctly orders preboot plugins and returns exposed values for "setup"',
|
|||
prebootPluginSystem.addPlugin(plugin);
|
||||
});
|
||||
|
||||
mockCreatePluginPrebootSetupContext.mockImplementation((context, deps, plugin) =>
|
||||
mockCreatePluginPrebootSetupContext.mockImplementation(({ plugin }) =>
|
||||
setupContextMap.get(plugin.name)
|
||||
);
|
||||
|
||||
|
@ -392,11 +427,10 @@ test('correctly orders preboot plugins and returns exposed values for "setup"',
|
|||
`);
|
||||
|
||||
for (const [plugin, deps] of plugins) {
|
||||
expect(mockCreatePluginPrebootSetupContext).toHaveBeenCalledWith(
|
||||
coreContext,
|
||||
prebootDeps,
|
||||
plugin
|
||||
);
|
||||
expect(mockCreatePluginPrebootSetupContext).toHaveBeenCalledWith({
|
||||
deps: prebootDeps,
|
||||
plugin,
|
||||
});
|
||||
expect(plugin.setup).toHaveBeenCalledTimes(1);
|
||||
expect(plugin.setup).toHaveBeenCalledWith(setupContextMap.get(plugin.name), deps);
|
||||
}
|
||||
|
@ -426,17 +460,21 @@ test('`setupPlugins` only setups plugins that have server side', async () => {
|
|||
]
|
||||
`);
|
||||
|
||||
expect(mockCreatePluginSetupContext).toHaveBeenCalledWith(
|
||||
coreContext,
|
||||
setupDeps,
|
||||
firstPluginToRun
|
||||
);
|
||||
expect(mockCreatePluginSetupContext).not.toHaveBeenCalledWith(coreContext, secondPluginNotToRun);
|
||||
expect(mockCreatePluginSetupContext).toHaveBeenCalledWith(
|
||||
coreContext,
|
||||
setupDeps,
|
||||
thirdPluginToRun
|
||||
);
|
||||
expect(mockCreatePluginSetupContext).toHaveBeenCalledWith({
|
||||
deps: setupDeps,
|
||||
plugin: firstPluginToRun,
|
||||
runtimeResolver: expect.any(Object),
|
||||
});
|
||||
expect(mockCreatePluginSetupContext).not.toHaveBeenCalledWith({
|
||||
deps: setupDeps,
|
||||
plugin: secondPluginNotToRun,
|
||||
runtimeResolver: expect.any(Object),
|
||||
});
|
||||
expect(mockCreatePluginSetupContext).toHaveBeenCalledWith({
|
||||
deps: setupDeps,
|
||||
plugin: thirdPluginToRun,
|
||||
runtimeResolver: expect.any(Object),
|
||||
});
|
||||
|
||||
expect(firstPluginToRun.setup).toHaveBeenCalledTimes(1);
|
||||
expect(secondPluginNotToRun.setup).not.toHaveBeenCalled();
|
||||
|
@ -627,6 +665,32 @@ describe('start', () => {
|
|||
const log = logger.get.mock.results[0].value as jest.Mocked<Logger>;
|
||||
expect(log.info).toHaveBeenCalledWith(`Starting [2] plugins: [order-1,order-0]`);
|
||||
});
|
||||
|
||||
it('setups the runtimeResolver', async () => {
|
||||
const pluginA = createPlugin('pluginA', { required: [] });
|
||||
const pluginB = createPlugin('pluginB', { required: ['pluginA'] });
|
||||
|
||||
jest.spyOn(pluginA, 'setup').mockReturnValue({});
|
||||
jest.spyOn(pluginB, 'setup').mockReturnValue({});
|
||||
|
||||
jest.spyOn(pluginA, 'start').mockReturnValue('contractA');
|
||||
jest.spyOn(pluginB, 'start').mockReturnValue('contractB');
|
||||
|
||||
pluginsSystem.addPlugin(pluginA);
|
||||
pluginsSystem.addPlugin(pluginB);
|
||||
|
||||
await pluginsSystem.setupPlugins(setupDeps);
|
||||
await pluginsSystem.startPlugins(startDeps);
|
||||
|
||||
expect(runtimeResolverMock.resolveStartRequests).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeResolverMock.resolveStartRequests).toHaveBeenCalledWith(expect.any(Map));
|
||||
expect(
|
||||
Object.fromEntries([...runtimeResolverMock.resolveStartRequests.mock.calls[0][0].entries()])
|
||||
).toEqual({
|
||||
pluginA: 'contractA',
|
||||
pluginB: 'contractB',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('asynchronous plugins', () => {
|
||||
|
|
|
@ -23,11 +23,13 @@ import type {
|
|||
PluginsServiceSetupDeps,
|
||||
PluginsServiceStartDeps,
|
||||
} from './plugins_service';
|
||||
import { RuntimePluginContractResolver } from './plugin_contract_resolver';
|
||||
|
||||
const Sec = 1000;
|
||||
|
||||
/** @internal */
|
||||
export class PluginsSystem<T extends PluginType> {
|
||||
private readonly runtimeResolver = new RuntimePluginContractResolver();
|
||||
private readonly plugins = new Map<PluginName, PluginWrapper>();
|
||||
private readonly log: Logger;
|
||||
// `satup`, the past-tense version of the noun `setup`.
|
||||
|
@ -90,6 +92,9 @@ export class PluginsSystem<T extends PluginType> {
|
|||
return contracts;
|
||||
}
|
||||
|
||||
const runtimeDependencies = buildPluginRuntimeDependencyMap(this.plugins);
|
||||
this.runtimeResolver.setDependencyMap(runtimeDependencies);
|
||||
|
||||
const sortedPlugins = new Map(
|
||||
[...this.getTopologicallySortedPluginNames()]
|
||||
.map((pluginName) => [pluginName, this.plugins.get(pluginName)!] as [string, PluginWrapper])
|
||||
|
@ -114,17 +119,16 @@ export class PluginsSystem<T extends PluginType> {
|
|||
|
||||
let pluginSetupContext;
|
||||
if (this.type === PluginType.preboot) {
|
||||
pluginSetupContext = createPluginPrebootSetupContext(
|
||||
this.coreContext,
|
||||
deps as PluginsServicePrebootSetupDeps,
|
||||
plugin
|
||||
);
|
||||
pluginSetupContext = createPluginPrebootSetupContext({
|
||||
deps: deps as PluginsServicePrebootSetupDeps,
|
||||
plugin,
|
||||
});
|
||||
} else {
|
||||
pluginSetupContext = createPluginSetupContext(
|
||||
this.coreContext,
|
||||
deps as PluginsServiceSetupDeps,
|
||||
plugin
|
||||
);
|
||||
pluginSetupContext = createPluginSetupContext({
|
||||
deps: deps as PluginsServiceSetupDeps,
|
||||
plugin,
|
||||
runtimeResolver: this.runtimeResolver,
|
||||
});
|
||||
}
|
||||
|
||||
let contract: unknown;
|
||||
|
@ -155,6 +159,8 @@ export class PluginsSystem<T extends PluginType> {
|
|||
this.satupPlugins.push(pluginName);
|
||||
}
|
||||
|
||||
this.runtimeResolver.resolveSetupRequests(contracts);
|
||||
|
||||
return contracts;
|
||||
}
|
||||
|
||||
|
@ -186,7 +192,7 @@ export class PluginsSystem<T extends PluginType> {
|
|||
|
||||
let contract: unknown;
|
||||
const contractOrPromise = plugin.start(
|
||||
createPluginStartContext(this.coreContext, deps, plugin),
|
||||
createPluginStartContext({ deps, plugin, runtimeResolver: this.runtimeResolver }),
|
||||
pluginDepContracts
|
||||
);
|
||||
if (isPromise(contractOrPromise)) {
|
||||
|
@ -214,6 +220,8 @@ export class PluginsSystem<T extends PluginType> {
|
|||
contracts.set(pluginName, contract);
|
||||
}
|
||||
|
||||
this.runtimeResolver.resolveStartRequests(contracts);
|
||||
|
||||
return contracts;
|
||||
}
|
||||
|
||||
|
@ -265,6 +273,7 @@ export class PluginsSystem<T extends PluginType> {
|
|||
const uiPluginNames = [...this.getTopologicallySortedPluginNames().keys()].filter(
|
||||
(pluginName) => this.plugins.get(pluginName)!.includesUiPlugin
|
||||
);
|
||||
const filterUiPlugins = (pluginName: string) => uiPluginNames.includes(pluginName);
|
||||
const publicPlugins = new Map<PluginName, DiscoveredPlugin>(
|
||||
uiPluginNames.map((pluginName) => {
|
||||
const plugin = this.plugins.get(pluginName)!;
|
||||
|
@ -274,12 +283,10 @@ export class PluginsSystem<T extends PluginType> {
|
|||
id: pluginName,
|
||||
type: plugin.manifest.type,
|
||||
configPath: plugin.manifest.configPath,
|
||||
requiredPlugins: plugin.manifest.requiredPlugins.filter((p) =>
|
||||
uiPluginNames.includes(p)
|
||||
),
|
||||
optionalPlugins: plugin.manifest.optionalPlugins.filter((p) =>
|
||||
uiPluginNames.includes(p)
|
||||
),
|
||||
requiredPlugins: plugin.manifest.requiredPlugins.filter(filterUiPlugins),
|
||||
optionalPlugins: plugin.manifest.optionalPlugins.filter(filterUiPlugins),
|
||||
runtimePluginDependencies:
|
||||
plugin.manifest.runtimePluginDependencies.filter(filterUiPlugins),
|
||||
requiredBundles: plugin.manifest.requiredBundles,
|
||||
enabledOnAnonymousPages: plugin.manifest.enabledOnAnonymousPages,
|
||||
},
|
||||
|
@ -371,3 +378,18 @@ const buildReverseDependencyMap = (
|
|||
}
|
||||
return reverseMap;
|
||||
};
|
||||
|
||||
const buildPluginRuntimeDependencyMap = (
|
||||
pluginMap: Map<PluginName, PluginWrapper>
|
||||
): Map<PluginName, Set<PluginName>> => {
|
||||
const runtimeDependencies = new Map<PluginName, Set<PluginName>>();
|
||||
for (const [pluginName, pluginWrapper] of pluginMap.entries()) {
|
||||
const pluginRuntimeDeps = new Set([
|
||||
...pluginWrapper.optionalPlugins,
|
||||
...pluginWrapper.requiredPlugins,
|
||||
...pluginWrapper.runtimePluginDependencies,
|
||||
]);
|
||||
runtimeDependencies.set(pluginName, pluginRuntimeDeps);
|
||||
}
|
||||
return runtimeDependencies;
|
||||
};
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { createCoreContextConfigServiceMock } from './create_core_context_config_service.mock';
|
||||
export { createRuntimePluginContractResolverMock } from './plugin_contract_resolver.mock';
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { IRuntimePluginContractResolver } from '../plugin_contract_resolver';
|
||||
|
||||
export const createRuntimePluginContractResolverMock =
|
||||
(): jest.Mocked<IRuntimePluginContractResolver> => {
|
||||
return {
|
||||
setDependencyMap: jest.fn(),
|
||||
onSetup: jest.fn(),
|
||||
onStart: jest.fn(),
|
||||
resolveSetupRequests: jest.fn(),
|
||||
resolveStartRequests: jest.fn(),
|
||||
};
|
||||
};
|
|
@ -38,6 +38,8 @@
|
|||
"@kbn/core-node-server-internal",
|
||||
"@kbn/core-plugins-base-server-internal",
|
||||
"@kbn/repo-packages",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/core-plugins-contracts-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -7,25 +7,48 @@
|
|||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { PluginsService, type PluginsServiceSetup } from '@kbn/core-plugins-server-internal';
|
||||
import type { PluginsServiceSetup, PluginsServiceStart } from '@kbn/core-plugins-contracts-server';
|
||||
import {
|
||||
PluginsService,
|
||||
type InternalPluginsServiceSetup,
|
||||
type InternalPluginsServiceStart,
|
||||
} from '@kbn/core-plugins-server-internal';
|
||||
|
||||
type PluginsServiceMock = jest.Mocked<PublicMethodsOf<PluginsService>>;
|
||||
|
||||
const createSetupContractMock = (): PluginsServiceSetup => ({
|
||||
const createInternalSetupContractMock = (): InternalPluginsServiceSetup => ({
|
||||
contracts: new Map(),
|
||||
initialized: true,
|
||||
});
|
||||
const createStartContractMock = () => ({ contracts: new Map() });
|
||||
const createInternalStartContractMock = (): InternalPluginsServiceStart => ({
|
||||
contracts: new Map(),
|
||||
});
|
||||
|
||||
const createServiceMock = (): PluginsServiceMock => ({
|
||||
discover: jest.fn(),
|
||||
getExposedPluginConfigsToUsage: jest.fn(),
|
||||
preboot: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue(createSetupContractMock()),
|
||||
start: jest.fn().mockResolvedValue(createStartContractMock()),
|
||||
setup: jest.fn().mockResolvedValue(createInternalSetupContractMock()),
|
||||
start: jest.fn().mockResolvedValue(createInternalStartContractMock()),
|
||||
stop: jest.fn(),
|
||||
});
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const contract: jest.Mocked<PluginsServiceSetup> = {
|
||||
onSetup: jest.fn(),
|
||||
onStart: jest.fn(),
|
||||
};
|
||||
|
||||
return contract;
|
||||
};
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const contract: jest.Mocked<PluginsServiceStart> = {
|
||||
onStart: jest.fn(),
|
||||
};
|
||||
return contract;
|
||||
};
|
||||
|
||||
function createUiPlugins() {
|
||||
return {
|
||||
browserConfigs: new Map(),
|
||||
|
@ -38,5 +61,7 @@ export const pluginServiceMock = {
|
|||
create: createServiceMock,
|
||||
createSetupContract: createSetupContractMock,
|
||||
createStartContract: createStartContractMock,
|
||||
createInternalSetupContract: createInternalSetupContractMock,
|
||||
createInternalStartContract: createInternalStartContractMock,
|
||||
createUiPlugins,
|
||||
};
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/utility-types",
|
||||
"@kbn/core-plugins-server-internal"
|
||||
"@kbn/core-plugins-server-internal",
|
||||
"@kbn/core-plugins-contracts-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -215,6 +215,12 @@ export interface PluginManifest {
|
|||
*/
|
||||
readonly optionalPlugins: readonly PluginName[];
|
||||
|
||||
/**
|
||||
* An optional list of plugin dependencies that can be resolved dynamically at runtime
|
||||
* using the dynamic contract resolving capabilities from the plugin service.
|
||||
*/
|
||||
readonly runtimePluginDependencies: readonly string[];
|
||||
|
||||
/**
|
||||
* Specifies whether plugin includes some client/browser specific functionality
|
||||
* that should be included into client bundle via `public/ui_plugin.js` file.
|
||||
|
|
|
@ -30,6 +30,7 @@ const createUiPlugins = (pluginDeps: Record<string, string[]>) => {
|
|||
type: PluginType.standard,
|
||||
optionalPlugins: [],
|
||||
requiredPlugins: [],
|
||||
runtimePluginDependencies: [],
|
||||
requiredBundles: deps,
|
||||
});
|
||||
|
||||
|
|
|
@ -71,6 +71,10 @@ function parseLegacyKibanaPlatformPlugin(manifestPath) {
|
|||
optionalPlugins: isValidDepsDeclaration(manifest.optionalPlugins, 'optionalPlugins'),
|
||||
requiredBundles: isValidDepsDeclaration(manifest.requiredBundles, 'requiredBundles'),
|
||||
extraPublicDirs: isValidDepsDeclaration(manifest.extraPublicDirs, 'extraPublicDirs'),
|
||||
runtimePluginDependencies: isValidDepsDeclaration(
|
||||
manifest.runtimePluginDependencies,
|
||||
'runtimePluginDependencies'
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface LegacyKibanaPlatformPluginManifest {
|
|||
serviceFolders: readonly string[];
|
||||
requiredPlugins: readonly string[];
|
||||
optionalPlugins: readonly string[];
|
||||
runtimePluginDependencies?: readonly string[];
|
||||
requiredBundles: readonly string[];
|
||||
extraPublicDirs: readonly string[];
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ function validatePackageManifestPlugin(plugin, repoRoot, path) {
|
|||
requiredPlugins,
|
||||
optionalPlugins,
|
||||
requiredBundles,
|
||||
runtimePluginDependencies,
|
||||
enabledOnAnonymousPages,
|
||||
type,
|
||||
__category__,
|
||||
|
@ -103,6 +104,14 @@ function validatePackageManifestPlugin(plugin, repoRoot, path) {
|
|||
);
|
||||
}
|
||||
|
||||
if (runtimePluginDependencies !== undefined && !isArrOfIds(runtimePluginDependencies)) {
|
||||
throw err(
|
||||
`plugin.runtimePluginDependencies`,
|
||||
runtimePluginDependencies,
|
||||
`must be an array of strings in camel or snake case`
|
||||
);
|
||||
}
|
||||
|
||||
if (requiredBundles !== undefined && !isArrOfIds(requiredBundles)) {
|
||||
throw err(
|
||||
`plugin.requiredBundles`,
|
||||
|
@ -154,6 +163,7 @@ function validatePackageManifestPlugin(plugin, repoRoot, path) {
|
|||
requiredPlugins,
|
||||
optionalPlugins,
|
||||
requiredBundles,
|
||||
runtimePluginDependencies,
|
||||
enabledOnAnonymousPages,
|
||||
extraPublicDirs,
|
||||
[PLUGIN_CATEGORY]: __category__,
|
||||
|
|
|
@ -104,6 +104,7 @@ export interface PluginPackageManifest extends PackageManifestBaseFields {
|
|||
requiredPlugins?: string[];
|
||||
optionalPlugins?: string[];
|
||||
requiredBundles?: string[];
|
||||
runtimePluginDependencies?: string[];
|
||||
enabledOnAnonymousPages?: boolean;
|
||||
type?: 'preboot';
|
||||
extraPublicDirs?: string[];
|
||||
|
|
|
@ -68,6 +68,16 @@ export type {
|
|||
PluginInitializer,
|
||||
PluginInitializerContext,
|
||||
} from '@kbn/core-plugins-browser';
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
PluginContractResolver,
|
||||
PluginContractMap,
|
||||
PluginContractResolverResponse,
|
||||
PluginContractResolverResponseItem,
|
||||
FoundPluginContractResolverResponseItem,
|
||||
NotFoundPluginContractResolverResponseItem,
|
||||
} from '@kbn/core-plugins-contracts-browser';
|
||||
export type { PluginOpaqueId } from '@kbn/core-base-common';
|
||||
|
||||
export type { PackageInfo, EnvironmentMode } from '@kbn/config';
|
||||
|
|
|
@ -46,7 +46,10 @@ import { configSchema as elasticsearchConfigSchema } from '@kbn/core-elasticsear
|
|||
import type { CapabilitiesSetup, CapabilitiesStart } from '@kbn/core-capabilities-server';
|
||||
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
|
||||
import type { HttpResources } from '@kbn/core-http-resources-server';
|
||||
import type { PluginsServiceSetup, PluginsServiceStart } from '@kbn/core-plugins-server-internal';
|
||||
import type {
|
||||
InternalPluginsServiceSetup,
|
||||
InternalPluginsServiceStart,
|
||||
} from '@kbn/core-plugins-server-internal';
|
||||
|
||||
export { bootstrap } from '@kbn/core-root-server-internal';
|
||||
|
||||
|
@ -234,6 +237,16 @@ export type {
|
|||
MakeUsageFromSchema,
|
||||
ExposedToBrowserDescriptor,
|
||||
} from '@kbn/core-plugins-server';
|
||||
export type {
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
NotFoundPluginContractResolverResponseItem,
|
||||
FoundPluginContractResolverResponseItem,
|
||||
PluginContractResolverResponseItem,
|
||||
PluginContractMap,
|
||||
PluginContractResolverResponse,
|
||||
PluginContractResolver,
|
||||
} from '@kbn/core-plugins-contracts-server';
|
||||
|
||||
export type { PluginName, DiscoveredPlugin } from '@kbn/core-base-common';
|
||||
|
||||
|
@ -472,8 +485,8 @@ export type {
|
|||
ExecutionContextSetup,
|
||||
ExecutionContextStart,
|
||||
HttpResources,
|
||||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
InternalPluginsServiceSetup,
|
||||
InternalPluginsServiceStart,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -153,6 +153,8 @@
|
|||
"@kbn/stdio-dev-helpers",
|
||||
"@kbn/safer-lodash-set",
|
||||
"@kbn/core-test-helpers-model-versions",
|
||||
"@kbn/core-plugins-contracts-browser",
|
||||
"@kbn/core-plugins-contracts-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreStart, ElasticsearchClient } from '@kbn/core/server';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { InternalCoreStart } from '@kbn/core-lifecycle-server-internal';
|
||||
import {
|
||||
createTestServers,
|
||||
createRootWithCorePlugins,
|
||||
|
@ -38,7 +39,7 @@ describe('FileService', () => {
|
|||
let fileService: FileServiceStart;
|
||||
let blobStorageService: BlobStorageService;
|
||||
let esClient: ElasticsearchClient;
|
||||
let coreStart: CoreStart;
|
||||
let coreStart: InternalCoreStart;
|
||||
let fileServiceFactory: FileServiceFactory;
|
||||
let security: ReturnType<typeof securityMock.createSetup>;
|
||||
let auditLogger: AuditLogger;
|
||||
|
@ -93,11 +94,13 @@ describe('FileService', () => {
|
|||
});
|
||||
|
||||
let disposables: File[] = [];
|
||||
|
||||
async function createDisposableFile<M = unknown>(args: CreateFileArgs<M>) {
|
||||
const file = await fileService.create(args);
|
||||
disposables.push(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await fileService.bulkDelete({ ids: disposables.map((d) => d.id) });
|
||||
const { files } = await fileService.find({ kind: [fileKind] });
|
||||
|
@ -326,6 +329,7 @@ describe('FileService', () => {
|
|||
interface CustomMeta {
|
||||
some: string;
|
||||
}
|
||||
|
||||
it('updates files', async () => {
|
||||
const file = await createDisposableFile<CustomMeta>({ fileKind, name: 'test' });
|
||||
const updatableFields = {
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"@kbn/core-saved-objects-server-mocks",
|
||||
"@kbn/logging",
|
||||
"@kbn/core-http-common",
|
||||
"@kbn/core-lifecycle-server-internal",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/core-plugin-dynamic-resolving-a",
|
||||
"owner": "@elastic/kibana-core",
|
||||
"plugin": {
|
||||
"id": "coreDynamicResolvingA",
|
||||
"server": true,
|
||||
"browser": false,
|
||||
"configPath": [
|
||||
"core_plugin_a"
|
||||
],
|
||||
"runtimePluginDependencies" : ["coreDynamicResolvingB"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "@kbn/core-plugin-dynamic-resolving-a",
|
||||
"version": "1.0.0",
|
||||
"main": "target/test/plugin_functional/plugins/core_dynamic_resolving_a",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreDynamicResolvingAPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new CoreDynamicResolvingAPlugin();
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Plugin, CoreSetup } from '@kbn/core/server';
|
||||
|
||||
interface GenericSetupContract {
|
||||
someSetupAPI: () => string;
|
||||
}
|
||||
|
||||
interface GenericStartContract {
|
||||
someStartAPI: () => string;
|
||||
}
|
||||
|
||||
export class CoreDynamicResolvingAPlugin implements Plugin {
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
const router = core.http.createRouter();
|
||||
router.get(
|
||||
{
|
||||
path: '/api/core_dynamic_resolving_a/test',
|
||||
validate: false,
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return Promise.all([
|
||||
core.plugins.onSetup<{
|
||||
coreDynamicResolvingB: GenericSetupContract;
|
||||
}>('coreDynamicResolvingB'),
|
||||
core.plugins.onStart<{
|
||||
coreDynamicResolvingB: GenericStartContract;
|
||||
}>('coreDynamicResolvingB'),
|
||||
]).then(
|
||||
([
|
||||
{ coreDynamicResolvingB: coreDynamicResolvingBSetup },
|
||||
{ coreDynamicResolvingB: coreDynamicResolvingBStart },
|
||||
]) => {
|
||||
if (coreDynamicResolvingBSetup.found && coreDynamicResolvingBStart.found) {
|
||||
return res.ok({
|
||||
body: {
|
||||
setup: coreDynamicResolvingBSetup.contract.someSetupAPI(),
|
||||
start: coreDynamicResolvingBStart.contract.someStartAPI(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.badRequest({
|
||||
body: {
|
||||
message: 'not found',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
someSetupAPI: () => 'pluginASetup',
|
||||
};
|
||||
}
|
||||
|
||||
public start() {
|
||||
return {
|
||||
someStartAPI: () => 'pluginAStart',
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/core-plugin-dynamic-resolving-b",
|
||||
"owner": "@elastic/kibana-core",
|
||||
"plugin": {
|
||||
"id": "coreDynamicResolvingB",
|
||||
"server": true,
|
||||
"browser": false,
|
||||
"configPath": [
|
||||
"core_plugin_b"
|
||||
],
|
||||
"runtimePluginDependencies" : ["coreDynamicResolvingA"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "@kbn/core-plugin-dynamic-resolving-b",
|
||||
"version": "1.0.0",
|
||||
"main": "target/test/plugin_functional/plugins/core_dynamic_resolving_b",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreDynamicResolvingBPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new CoreDynamicResolvingBPlugin();
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Plugin, CoreSetup } from '@kbn/core/server';
|
||||
|
||||
interface GenericSetupContract {
|
||||
someSetupAPI: () => string;
|
||||
}
|
||||
|
||||
interface GenericStartContract {
|
||||
someStartAPI: () => string;
|
||||
}
|
||||
|
||||
export class CoreDynamicResolvingBPlugin implements Plugin {
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
const router = core.http.createRouter();
|
||||
router.get(
|
||||
{
|
||||
path: '/api/core_dynamic_resolving_b/test',
|
||||
validate: false,
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return Promise.all([
|
||||
core.plugins.onSetup<{
|
||||
coreDynamicResolvingA: GenericSetupContract;
|
||||
}>('coreDynamicResolvingA'),
|
||||
core.plugins.onStart<{
|
||||
coreDynamicResolvingA: GenericStartContract;
|
||||
}>('coreDynamicResolvingA'),
|
||||
]).then(
|
||||
([
|
||||
{ coreDynamicResolvingA: coreDynamicResolvingASetup },
|
||||
{ coreDynamicResolvingA: coreDynamicResolvingAStart },
|
||||
]) => {
|
||||
if (coreDynamicResolvingASetup.found && coreDynamicResolvingAStart.found) {
|
||||
return res.ok({
|
||||
body: {
|
||||
setup: coreDynamicResolvingASetup.contract.someSetupAPI(),
|
||||
start: coreDynamicResolvingAStart.contract.someStartAPI(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.badRequest({
|
||||
body: {
|
||||
message: 'not found',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
someSetupAPI: () => 'pluginBSetup',
|
||||
};
|
||||
}
|
||||
|
||||
public start() {
|
||||
return {
|
||||
someStartAPI: () => 'pluginBStart',
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
export default function ({ getService }: PluginFunctionalProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('Dynamic plugin resolving', function describeIndexTests() {
|
||||
it('Plugin A can dynamically resolve plugin B contracts', async () => {
|
||||
await supertest
|
||||
.get('/api/core_dynamic_resolving_a/test')
|
||||
.set('kbn-xsrf', 'anything')
|
||||
.send()
|
||||
.expect(200)
|
||||
.expect({
|
||||
setup: 'pluginBSetup',
|
||||
start: 'pluginBStart',
|
||||
});
|
||||
});
|
||||
|
||||
it('Plugin B can dynamically resolve plugin A contracts', async () => {
|
||||
await supertest
|
||||
.get('/api/core_dynamic_resolving_b/test')
|
||||
.set('kbn-xsrf', 'anything')
|
||||
.send()
|
||||
.expect(200)
|
||||
.expect({
|
||||
setup: 'pluginASetup',
|
||||
start: 'pluginAStart',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -24,5 +24,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
|
|||
loadTestFile(require.resolve('./chrome_help_menu_links'));
|
||||
loadTestFile(require.resolve('./history_block'));
|
||||
loadTestFile(require.resolve('./http'));
|
||||
loadTestFile(require.resolve('./dynamic_contract_resolving'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -446,6 +446,10 @@
|
|||
"@kbn/core-plugin-deep-links-plugin/*": ["test/plugin_functional/plugins/core_plugin_deep_links/*"],
|
||||
"@kbn/core-plugin-deprecations-plugin": ["test/plugin_functional/plugins/core_plugin_deprecations"],
|
||||
"@kbn/core-plugin-deprecations-plugin/*": ["test/plugin_functional/plugins/core_plugin_deprecations/*"],
|
||||
"@kbn/core-plugin-dynamic-resolving-a": ["test/plugin_functional/plugins/core_dynamic_resolving_a"],
|
||||
"@kbn/core-plugin-dynamic-resolving-a/*": ["test/plugin_functional/plugins/core_dynamic_resolving_a/*"],
|
||||
"@kbn/core-plugin-dynamic-resolving-b": ["test/plugin_functional/plugins/core_dynamic_resolving_b"],
|
||||
"@kbn/core-plugin-dynamic-resolving-b/*": ["test/plugin_functional/plugins/core_dynamic_resolving_b/*"],
|
||||
"@kbn/core-plugin-execution-context-plugin": ["test/plugin_functional/plugins/core_plugin_execution_context"],
|
||||
"@kbn/core-plugin-execution-context-plugin/*": ["test/plugin_functional/plugins/core_plugin_execution_context/*"],
|
||||
"@kbn/core-plugin-helpmenu-plugin": ["test/plugin_functional/plugins/core_plugin_helpmenu"],
|
||||
|
@ -464,6 +468,10 @@
|
|||
"@kbn/core-plugins-browser-internal/*": ["packages/core/plugins/core-plugins-browser-internal/*"],
|
||||
"@kbn/core-plugins-browser-mocks": ["packages/core/plugins/core-plugins-browser-mocks"],
|
||||
"@kbn/core-plugins-browser-mocks/*": ["packages/core/plugins/core-plugins-browser-mocks/*"],
|
||||
"@kbn/core-plugins-contracts-browser": ["packages/core/plugins/core-plugins-contracts-browser"],
|
||||
"@kbn/core-plugins-contracts-browser/*": ["packages/core/plugins/core-plugins-contracts-browser/*"],
|
||||
"@kbn/core-plugins-contracts-server": ["packages/core/plugins/core-plugins-contracts-server"],
|
||||
"@kbn/core-plugins-contracts-server/*": ["packages/core/plugins/core-plugins-contracts-server/*"],
|
||||
"@kbn/core-plugins-server": ["packages/core/plugins/core-plugins-server"],
|
||||
"@kbn/core-plugins-server/*": ["packages/core/plugins/core-plugins-server/*"],
|
||||
"@kbn/core-plugins-server-internal": ["packages/core/plugins/core-plugins-server-internal"],
|
||||
|
|
|
@ -13,6 +13,7 @@ import { createBrowserHistory } from 'history';
|
|||
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
import type { PluginsServiceStart } from '@kbn/core/public';
|
||||
import { CoreScopedHistory } from '@kbn/core/public';
|
||||
import { getStorybookContextProvider } from '@kbn/custom-integrations-plugin/storybook';
|
||||
|
||||
|
@ -93,6 +94,7 @@ export const StorybookContext: React.FC<{ storyContext?: Parameters<DecoratorFn>
|
|||
theme: {
|
||||
theme$: EMPTY,
|
||||
},
|
||||
plugins: {} as unknown as PluginsServiceStart,
|
||||
authz: {
|
||||
fleet: {
|
||||
all: true,
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -3792,6 +3792,14 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-plugin-dynamic-resolving-a@link:test/plugin_functional/plugins/core_dynamic_resolving_a":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-plugin-dynamic-resolving-b@link:test/plugin_functional/plugins/core_dynamic_resolving_b":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-plugin-execution-context-plugin@link:test/plugin_functional/plugins/core_plugin_execution_context":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -3828,6 +3836,14 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-plugins-contracts-browser@link:packages/core/plugins/core-plugins-contracts-browser":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-plugins-contracts-server@link:packages/core/plugins/core-plugins-contracts-server":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-plugins-server-internal@link:packages/core/plugins/core-plugins-server-internal":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue