From a62c4cfbf1d660ab96d06e10984816e539ab422f Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 23 Apr 2019 13:39:06 -0500 Subject: [PATCH] Introduce `start` lifecycle event to client (#35269) (#35488) --- .../capabilities/capabilities_service.mock.ts | 16 +- .../capabilities/capabilities_service.test.ts | 4 +- .../capabilities/capabilities_service.tsx | 10 +- src/core/public/capabilities/index.ts | 2 +- src/core/public/core_system.test.mocks.ts | 9 + src/core/public/core_system.test.ts | 238 ++++++++++-------- src/core/public/core_system.ts | 54 ++-- .../__snapshots__/i18n_service.test.tsx.snap | 59 +++++ src/core/public/i18n/i18n_service.mock.ts | 6 + src/core/public/i18n/i18n_service.test.tsx | 10 + src/core/public/i18n/i18n_service.tsx | 6 + src/core/public/i18n/index.ts | 2 +- src/core/public/index.ts | 46 +++- src/core/public/injected_metadata/index.ts | 1 + .../injected_metadata_service.mock.ts | 18 +- .../injected_metadata_service.ts | 7 + .../__snapshots__/legacy_service.test.ts.snap | 42 +++- src/core/public/legacy/legacy_service.mock.ts | 1 + src/core/public/legacy/legacy_service.test.ts | 150 +++++++---- src/core/public/legacy/legacy_service.ts | 79 ++++-- src/core/public/notifications/index.ts | 8 +- .../notifications_service.mock.ts | 20 +- .../notifications/notifications_service.ts | 64 ++--- .../toasts_service.test.tsx.snap | 2 +- src/core/public/notifications/toasts/index.ts | 2 +- ...oasts_start.test.ts => toasts_api.test.ts} | 32 +-- .../{toasts_start.tsx => toasts_api.tsx} | 2 +- .../toasts/toasts_service.mock.ts | 13 +- .../toasts/toasts_service.test.tsx | 40 +-- .../notifications/toasts/toasts_service.tsx | 56 ++--- src/core/public/overlays/index.ts | 2 +- .../public/overlays/overlay_service.mock.ts | 14 +- src/core/public/overlays/overlay_service.ts | 10 +- src/core/public/plugins/plugin.test.mocks.ts | 1 + src/core/public/plugins/plugin.test.ts | 29 ++- src/core/public/plugins/plugin.ts | 49 +++- src/core/public/plugins/plugin_context.ts | 47 +++- src/core/public/plugins/plugin_loader.ts | 95 +++---- .../public/plugins/plugins_service.mock.ts | 17 +- .../public/plugins/plugins_service.test.ts | 167 +++++++++--- src/core/public/plugins/plugins_service.ts | 45 +++- src/core/public/public.api.md | 47 +++- .../public/dashboard/dashboard_config.js | 4 +- .../public/dashboard/top_nav/add_panel.js | 4 +- .../dashboard/top_nav/add_panel.test.js | 12 +- .../field_chooser/discover_field.js | 4 +- .../embeddable/search_embeddable_factory.ts | 4 +- .../management/sections/settings/index.js | 4 +- .../kibana/public/visualize/editor/editor.js | 6 +- .../visualize_embeddable_factory.ts | 4 +- .../listing/visualize_listing_table.js | 6 +- .../tests_bundle/tests_entry_template.js | 10 +- .../core_plugins/timelion/public/app.js | 4 +- src/legacy/ui/public/capabilities/index.ts | 21 +- .../react/inject_ui_capabilities.test.tsx | 12 +- .../legacy/inject_ui_capabilities.test.tsx | 12 +- .../react/legacy/ui_capabilities_provider.tsx | 4 +- .../react/ui_capabilities_provider.tsx | 4 +- .../ui/public/chrome/api/base_path.test.ts | 4 +- src/legacy/ui/public/chrome/api/base_path.ts | 2 +- .../ui/public/chrome/api/breadcrumbs.ts | 2 +- .../ui/public/chrome/api/controls.test.ts | 4 +- src/legacy/ui/public/chrome/api/controls.ts | 2 +- .../ui/public/chrome/api/help_extension.ts | 2 +- .../public/chrome/api/injected_vars.test.ts | 4 +- .../ui/public/chrome/api/injected_vars.ts | 2 +- .../ui/public/chrome/api/loading_count.js | 2 +- src/legacy/ui/public/chrome/api/theme.test.ts | 4 +- src/legacy/ui/public/chrome/api/theme.ts | 2 +- .../ui/public/chrome/api/ui_settings.js | 2 +- .../chrome/services/global_nav_state.js | 2 +- src/legacy/ui/public/i18n/index.test.tsx | 4 +- src/legacy/ui/public/i18n/index.tsx | 2 +- src/legacy/ui/public/inspector/inspector.tsx | 2 +- src/legacy/ui/public/management/section.js | 4 +- .../ui/public/management/section.test.js | 16 +- src/legacy/ui/public/metadata.js | 2 +- src/legacy/ui/public/new_platform/index.ts | 2 +- .../ui/public/new_platform/new_platform.ts | 21 +- src/legacy/ui/public/notify/fatal_error.ts | 2 +- src/legacy/ui/public/notify/toasts/index.ts | 2 +- .../notify/toasts/toast_notifications.test.ts | 4 +- .../notify/toasts/toast_notifications.ts | 4 +- src/legacy/ui/public/notify/toasts/toasts.ts | 4 +- .../ui/public/registry/feature_catalogue.js | 4 +- .../public/registry/feature_catalogue.test.js | 18 +- .../ui/ui_bundles/app_entry_template.js | 2 + src/plugins/testbed/public/index.ts | 5 +- src/plugins/testbed/public/plugin.ts | 10 +- .../plugins/core_plugin_a/public/index.ts | 5 +- .../plugins/core_plugin_a/public/plugin.tsx | 5 +- .../plugins/core_plugin_b/public/index.ts | 14 +- .../plugins/core_plugin_b/public/plugin.tsx | 6 +- .../public/sample_panel_action.tsx | 2 +- x-pack/plugins/__mocks__/ui/capabilities.ts | 19 +- .../canvas/public/state/initial_state.js | 4 +- x-pack/plugins/graph/public/app.js | 6 +- .../maps/public/angular/map_controller.js | 6 +- x-pack/plugins/maps/public/index.js | 4 +- .../views/management/edit_role/index.js | 4 +- .../components/manage_spaces_button.tsx | 4 +- .../secure_space_message.tsx | 4 +- .../edit_space/manage_space_page.tsx | 6 +- .../spaces_grid/spaces_grid_page.tsx | 4 +- 104 files changed, 1244 insertions(+), 628 deletions(-) rename src/core/public/notifications/toasts/{toasts_start.test.ts => toasts_api.test.ts} (87%) rename src/core/public/notifications/toasts/{toasts_start.tsx => toasts_api.tsx} (98%) diff --git a/src/core/public/capabilities/capabilities_service.mock.ts b/src/core/public/capabilities/capabilities_service.mock.ts index 980755302f383..b65bc7816a2b5 100644 --- a/src/core/public/capabilities/capabilities_service.mock.ts +++ b/src/core/public/capabilities/capabilities_service.mock.ts @@ -16,30 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -import { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service'; +import { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { +const createStartContractMock = () => { + const startContract: jest.Mocked = { getCapabilities: jest.fn(), }; - setupContract.getCapabilities.mockReturnValue({ + startContract.getCapabilities.mockReturnValue({ catalogue: {}, management: {}, navLinks: {}, } as Capabilities); - return setupContract; + return startContract; }; type CapabilitiesServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { - setup: jest.fn(), + start: jest.fn(), }; - mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const capabilitiesServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/capabilities/capabilities_service.test.ts b/src/core/public/capabilities/capabilities_service.test.ts index 9c138b780f563..69cf7f5d18dbc 100644 --- a/src/core/public/capabilities/capabilities_service.test.ts +++ b/src/core/public/capabilities/capabilities_service.test.ts @@ -32,7 +32,7 @@ describe('#start', () => { } as any, }); const service = new CapabilitiesService(); - const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() }); + const startContract = service.start({ injectedMetadata: injectedMetadata.start() }); expect(startContract.getCapabilities()).toEqual({ foo: 'bar', bar: 'baz', @@ -51,7 +51,7 @@ describe('#start', () => { } as any, }); const service = new CapabilitiesService(); - const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() }); + const startContract = service.start({ injectedMetadata: injectedMetadata.start() }); const capabilities = startContract.getCapabilities(); // @ts-ignore TypeScript knows this shouldn't be possible diff --git a/src/core/public/capabilities/capabilities_service.tsx b/src/core/public/capabilities/capabilities_service.tsx index 560b8f628ee37..2dc322f9027c3 100644 --- a/src/core/public/capabilities/capabilities_service.tsx +++ b/src/core/public/capabilities/capabilities_service.tsx @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { InjectedMetadataSetup } from '../injected_metadata'; +import { InjectedMetadataStart } from '../injected_metadata'; import { deepFreeze } from '../utils/deep_freeze'; interface StartDeps { - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InjectedMetadataStart; } /** @@ -50,7 +50,7 @@ export interface Capabilities { * Capabilities Setup. * @public */ -export interface CapabilitiesSetup { +export interface CapabilitiesStart { /** * Gets the read-only capabilities. */ @@ -63,10 +63,10 @@ export interface CapabilitiesSetup { * Service that is responsible for UI Capabilities. */ export class CapabilitiesService { - public setup({ injectedMetadata }: StartDeps): CapabilitiesSetup { + public start({ injectedMetadata }: StartDeps): CapabilitiesStart { return { getCapabilities: () => - deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities), + deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities), }; } } diff --git a/src/core/public/capabilities/index.ts b/src/core/public/capabilities/index.ts index dd95d8530a5bd..4cabc3770ec26 100644 --- a/src/core/public/capabilities/index.ts +++ b/src/core/public/capabilities/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service'; +export { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service'; diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index 27eafb2a4e9c9..d618af88852a7 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -18,6 +18,7 @@ */ import { basePathServiceMock } from './base_path/base_path_service.mock'; +import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from './http/http_service.mock'; @@ -104,3 +105,11 @@ export const PluginsServiceConstructor = jest.fn().mockImplementation(() => Mock jest.doMock('./plugins', () => ({ PluginsService: PluginsServiceConstructor, })); + +export const MockCapabilitiesService = capabilitiesServiceMock.create(); +export const CapabilitiesServiceConstructor = jest + .fn() + .mockImplementation(() => MockCapabilitiesService); +jest.doMock('./capabilities', () => ({ + CapabilitiesService: CapabilitiesServiceConstructor, +})); diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 9a5840829453e..a68fac44bbf54 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -17,9 +17,6 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; - import { BasePathServiceConstructor, ChromeServiceConstructor, @@ -42,6 +39,7 @@ import { NotificationServiceConstructor, OverlayServiceConstructor, UiSettingsServiceConstructor, + MockCapabilitiesService, } from './core_system.test.mocks'; import { CoreSystem } from './core_system'; @@ -110,21 +108,11 @@ describe('constructor', () => { expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1); expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({ - targetDomElement: expect.any(HTMLElement), requireLegacyFiles, useLegacyTestHarness, }); }); - it('passes a dom element to NotificationsService', () => { - createCoreSystem(); - - expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1); - expect(NotificationServiceConstructor).toHaveBeenCalledWith({ - targetDomElement$: expect.any(Observable), - }); - }); - it('passes browserSupportsCsp to ChromeService', () => { createCoreSystem(); @@ -157,7 +145,121 @@ describe('constructor', () => { }); }); -describe('#stop', () => { +describe('#setup()', () => { + function setupCore(rootDomElement = defaultCoreSystemParams.rootDomElement) { + const core = createCoreSystem({ + ...defaultCoreSystemParams, + rootDomElement, + }); + + return core.setup(); + } + + it('calls injectedMetadata#setup()', async () => { + await setupCore(); + expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls http#setup()', async () => { + await setupCore(); + expect(MockHttpService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls basePath#setup()', async () => { + await setupCore(); + expect(MockBasePathService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls uiSettings#setup()', async () => { + await setupCore(); + expect(MockUiSettingsService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls i18n#setup()', async () => { + await setupCore(); + expect(MockI18nService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls fatalErrors#setup()', async () => { + await setupCore(); + expect(MockFatalErrorsService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls notifications#setup()', async () => { + await setupCore(); + expect(MockNotificationsService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls chrome#setup()', async () => { + await setupCore(); + expect(MockChromeService.setup).toHaveBeenCalledTimes(1); + }); + + it('calls plugin#setup()', async () => { + await setupCore(); + expect(MockPluginsService.setup).toHaveBeenCalledTimes(1); + }); +}); + +describe('#start()', () => { + async function startCore(rootDomElement = defaultCoreSystemParams.rootDomElement) { + const core = createCoreSystem({ + ...defaultCoreSystemParams, + rootDomElement, + }); + + await core.setup(); + await core.start(); + } + + it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', async () => { + const root = document.createElement('div'); + root.innerHTML = '

foo bar

'; + await startCore(root); + expect(root.innerHTML).toBe('
'); + }); + + it('calls capabilities#start()', async () => { + await startCore(); + expect(MockCapabilitiesService.start).toHaveBeenCalledTimes(1); + }); + + it('calls i18n#start()', async () => { + await startCore(); + expect(MockI18nService.start).toHaveBeenCalledTimes(1); + }); + + it('calls injectedMetadata#start()', async () => { + await startCore(); + expect(MockInjectedMetadataService.start).toHaveBeenCalledTimes(1); + }); + + it('calls notifications#start() with a dom element', async () => { + await startCore(); + expect(MockNotificationsService.start).toHaveBeenCalledTimes(1); + expect(MockNotificationsService.start).toHaveBeenCalledWith({ + i18n: expect.any(Object), + targetDomElement: expect.any(HTMLElement), + }); + }); + + it('calls plugins#start()', async () => { + await startCore(); + expect(MockPluginsService.start).toHaveBeenCalledTimes(1); + }); + + it('calls legacyPlatform#start()', async () => { + await startCore(); + expect(MockLegacyPlatformService.start).toHaveBeenCalledTimes(1); + }); + + it('calls overlays#start()', async () => { + await startCore(); + expect(MockOverlayService.start).toHaveBeenCalledTimes(1); + }); +}); + +describe('#stop()', () => { it('calls legacyPlatform.stop()', () => { const coreSystem = createCoreSystem(); @@ -212,126 +314,50 @@ describe('#stop', () => { }); await coreSystem.setup(); + await coreSystem.start(); expect(rootDomElement.innerHTML).not.toBe(''); await coreSystem.stop(); expect(rootDomElement.innerHTML).toBe(''); }); }); -describe('#setup()', () => { - function setupCore(rootDomElement = defaultCoreSystemParams.rootDomElement) { - const core = createCoreSystem({ - ...defaultCoreSystemParams, - rootDomElement, - }); - - return core.setup(); - } - - it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', async () => { - const root = document.createElement('div'); - root.innerHTML = '

foo bar

'; - await setupCore(root); - expect(root.innerHTML).toBe('
'); - }); - - it('calls injectedMetadata#setup()', async () => { - await setupCore(); - expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls http#setup()', async () => { - await setupCore(); - expect(MockHttpService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls basePath#setup()', async () => { - await setupCore(); - expect(MockBasePathService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls uiSettings#setup()', async () => { - await setupCore(); - expect(MockUiSettingsService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls i18n#setup()', async () => { - await setupCore(); - expect(MockI18nService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls fatalErrors#setup()', async () => { - await setupCore(); - expect(MockFatalErrorsService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls notifications#setup()', async () => { - await setupCore(); - expect(MockNotificationsService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls chrome#setup()', async () => { - await setupCore(); - expect(MockChromeService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls overlays#setup()', () => { - setupCore(); - expect(MockOverlayService.setup).toHaveBeenCalledTimes(1); - }); - - it('calls plugin#setup()', async () => { - await setupCore(); - expect(MockPluginsService.setup).toHaveBeenCalledTimes(1); - }); -}); - describe('LegacyPlatform targetDomElement', () => { - it('only mounts the element when set up, before setting up the legacyPlatformService', async () => { + it('only mounts the element when start, after setting up the legacyPlatformService', async () => { const rootDomElement = document.createElement('div'); const core = createCoreSystem({ rootDomElement, }); - let targetDomElementParentInSetup: HTMLElement; - MockLegacyPlatformService.setup.mockImplementation(() => { - targetDomElementParentInSetup = targetDomElement.parentElement; + let targetDomElementParentInStart: HTMLElement | null; + MockLegacyPlatformService.start.mockImplementation(async ({ targetDomElement }) => { + targetDomElementParentInStart = targetDomElement.parentElement; }); - // targetDomElement should not have a parent element when the LegacyPlatformService is constructed - const [[{ targetDomElement }]] = LegacyPlatformServiceConstructor.mock.calls; - expect(targetDomElement).toHaveProperty('parentElement', null); - // setting up the core system should mount the targetDomElement as a child of the rootDomElement await core.setup(); - expect(targetDomElementParentInSetup!).toBe(rootDomElement); + await core.start(); + expect(targetDomElementParentInStart!).toBe(rootDomElement); }); }); describe('Notifications targetDomElement', () => { - it('only mounts the element when set up, before setting up the notificationsService', async () => { + it('only mounts the element when started, after setting up the notificationsService', async () => { const rootDomElement = document.createElement('div'); const core = createCoreSystem({ rootDomElement, }); - const [[{ targetDomElement$ }]] = NotificationServiceConstructor.mock.calls; - - let targetDomElementParentInSetup: HTMLElement | null; - MockNotificationsService.setup.mockImplementation( - (): any => { - (targetDomElement$ as Observable).pipe(take(1)).subscribe({ - next: targetDomElement => { - // The targetDomElement should already be a child once it's received by the NotificationsService - expect(targetDomElement.parentElement).not.toBeNull(); - targetDomElementParentInSetup = targetDomElement.parentElement; - }, - }); + let targetDomElementParentInStart: HTMLElement | null; + MockNotificationsService.start.mockImplementation( + ({ targetDomElement }): any => { + expect(targetDomElement.parentElement).not.toBeNull(); + targetDomElementParentInStart = targetDomElement.parentElement; } ); - // setting up the core system should mount the targetDomElement as a child of the rootDomElement + // setting up and starting the core system should mount the targetDomElement as a child of the rootDomElement await core.setup(); - expect(targetDomElementParentInSetup!).toBe(rootDomElement); + await core.start(); + expect(targetDomElementParentInStart!).toBe(rootDomElement); }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 9923790672283..5d301dfd7f99f 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -19,9 +19,7 @@ import './core.css'; -import { Subject } from 'rxjs'; - -import { CoreSetup } from '.'; +import { CoreSetup, CoreStart } from '.'; import { BasePathService } from './base_path'; import { CapabilitiesService } from './capabilities'; import { ChromeService } from './chrome'; @@ -70,8 +68,6 @@ export class CoreSystem { private readonly plugins: PluginsService; private readonly rootDomElement: HTMLElement; - private readonly notificationsTargetDomElement$: Subject; - private readonly legacyPlatformTargetDomElement: HTMLDivElement; private readonly overlayTargetDomElement: HTMLDivElement; constructor(params: Params) { @@ -101,10 +97,7 @@ export class CoreSystem { }, }); - this.notificationsTargetDomElement$ = new Subject(); - this.notifications = new NotificationsService({ - targetDomElement$: this.notificationsTargetDomElement$.asObservable(), - }); + this.notifications = new NotificationsService(); this.http = new HttpService(); this.basePath = new BasePathService(); this.uiSettings = new UiSettingsService(); @@ -115,9 +108,7 @@ export class CoreSystem { const core: CoreContext = {}; this.plugins = new PluginsService(core); - this.legacyPlatformTargetDomElement = document.createElement('div'); this.legacyPlatform = new LegacyPlatformService({ - targetDomElement: this.legacyPlatformTargetDomElement, requireLegacyFiles, useLegacyTestHarness, }); @@ -129,15 +120,13 @@ export class CoreSystem { const injectedMetadata = this.injectedMetadata.setup(); const fatalErrors = this.fatalErrors.setup({ i18n }); const http = this.http.setup({ fatalErrors }); - const overlays = this.overlay.setup({ i18n }); const basePath = this.basePath.setup({ injectedMetadata }); - const capabilities = this.capabilities.setup({ injectedMetadata }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata, basePath, }); - const notifications = this.notifications.setup({ i18n, uiSettings }); + const notifications = this.notifications.setup({ uiSettings }); const chrome = this.chrome.setup({ injectedMetadata, notifications, @@ -149,31 +138,52 @@ export class CoreSystem { fatalErrors, http, i18n, - capabilities, injectedMetadata, notifications, uiSettings, - overlays, }; + // Services that do not expose contracts at setup await this.plugins.setup(core); + await this.legacyPlatform.setup({ core }); + + return { fatalErrors }; + } catch (error) { + this.fatalErrors.add(error); + } + } + public async start() { + try { // ensure the rootDomElement is empty this.rootDomElement.textContent = ''; this.rootDomElement.classList.add('coreSystemRootDomElement'); const notificationsTargetDomElement = document.createElement('div'); + const legacyPlatformTargetDomElement = document.createElement('div'); this.rootDomElement.appendChild(notificationsTargetDomElement); - this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement); + this.rootDomElement.appendChild(legacyPlatformTargetDomElement); this.rootDomElement.appendChild(this.overlayTargetDomElement); - // Only provide the DOM element to notifications once it's attached to the page. - // This prevents notifications from timing out before being displayed. - this.notificationsTargetDomElement$.next(notificationsTargetDomElement); + const injectedMetadata = this.injectedMetadata.start(); + const i18n = this.i18n.start(); + const capabilities = this.capabilities.start({ injectedMetadata }); + const notifications = this.notifications.start({ + i18n, + targetDomElement: notificationsTargetDomElement, + }); + const overlays = this.overlay.start({ i18n }); - this.legacyPlatform.setup(core); + const core: CoreStart = { + capabilities, + i18n, + injectedMetadata, + notifications, + overlays, + }; - return { fatalErrors }; + await this.plugins.start(core); + await this.legacyPlatform.start({ core, targetDomElement: legacyPlatformTargetDomElement }); } catch (error) { this.fatalErrors.add(error); } diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index 0ea5624f19d27..5bb4d2323f486 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -58,3 +58,62 @@ exports[`#setup() returns \`Context\` component 1`] = ` `; + +exports[`#start() returns \`Context\` component 1`] = ` + + + content + + +`; diff --git a/src/core/public/i18n/i18n_service.mock.ts b/src/core/public/i18n/i18n_service.mock.ts index af6eb0f2f8ff9..c165535b4982b 100644 --- a/src/core/public/i18n/i18n_service.mock.ts +++ b/src/core/public/i18n/i18n_service.mock.ts @@ -30,17 +30,23 @@ const createSetupContractMock = () => { return setupContract; }; +// Start contract is identical to setup +const createStartContractMock = createSetupContractMock; + type I18nServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), }; mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const i18nServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/i18n/i18n_service.test.tsx b/src/core/public/i18n/i18n_service.test.tsx index c457a93417d71..bf0f1a03c7899 100644 --- a/src/core/public/i18n/i18n_service.test.tsx +++ b/src/core/public/i18n/i18n_service.test.tsx @@ -53,3 +53,13 @@ describe('#setup()', () => { expect(shallow(content)).toMatchSnapshot(); }); }); + +describe('#start()', () => { + it('returns `Context` component', () => { + const i18nService = new I18nService(); + + const i18n = i18nService.start(); + + expect(shallow(content)).toMatchSnapshot(); + }); +}); diff --git a/src/core/public/i18n/i18n_service.tsx b/src/core/public/i18n/i18n_service.tsx index 8d770b5c5f284..9ab70c989ee7f 100644 --- a/src/core/public/i18n/i18n_service.tsx +++ b/src/core/public/i18n/i18n_service.tsx @@ -267,6 +267,10 @@ export class I18nService { return setup; } + public start() { + return this.setup(); + } + public stop() { // nothing to do here currently } @@ -285,3 +289,5 @@ export interface I18nSetup { */ Context: ({ children }: { children: React.ReactNode }) => JSX.Element; } + +export type I18nStart = I18nSetup; diff --git a/src/core/public/i18n/index.ts b/src/core/public/i18n/index.ts index b18ba4d84a81e..b1dd108b01b17 100644 --- a/src/core/public/i18n/index.ts +++ b/src/core/public/i18n/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { I18nService, I18nSetup } from './i18n_service'; +export { I18nService, I18nSetup, I18nStart } from './i18n_service'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 08f1a248cabc2..32eb75d7fea0a 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -18,14 +18,24 @@ */ import { BasePathSetup } from './base_path'; -import { Capabilities, CapabilitiesSetup } from './capabilities'; +import { Capabilities, CapabilitiesStart } from './capabilities'; import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeSetup } from './chrome'; import { FatalErrorsSetup } from './fatal_errors'; import { HttpSetup } from './http'; -import { I18nSetup } from './i18n'; -import { InjectedMetadataParams, InjectedMetadataSetup } from './injected_metadata'; -import { NotificationsSetup, Toast, ToastInput, ToastsSetup } from './notifications'; -import { FlyoutRef, OverlaySetup } from './overlays'; +import { I18nSetup, I18nStart } from './i18n'; +import { + InjectedMetadataParams, + InjectedMetadataSetup, + InjectedMetadataStart, +} from './injected_metadata'; +import { + NotificationsSetup, + Toast, + ToastInput, + ToastsApi, + NotificationsStart, +} from './notifications'; +import { FlyoutRef, OverlayStart } from './overlays'; import { Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext } from './plugins'; import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings'; @@ -54,39 +64,51 @@ export interface CoreSetup { http: HttpSetup; /** {@link BasePathSetup} */ basePath: BasePathSetup; - /** {@link CapabilitiesSetup} */ - capabilities: CapabilitiesSetup; /** {@link UiSettingsSetup} */ uiSettings: UiSettingsSetup; /** {@link ChromeSetup} */ chrome: ChromeSetup; - /** {@link OverlaySetup} */ - overlays: OverlaySetup; +} + +export interface CoreStart { + /** {@link CapabilitiesStart} */ + capabilities: CapabilitiesStart; + /** {@link I18nStart} */ + i18n: I18nStart; + /** {@link InjectedMetadataStart} */ + injectedMetadata: InjectedMetadataStart; + /** {@link NotificationsStart} */ + notifications: NotificationsStart; + /** {@link OverlayStart} */ + overlays: OverlayStart; } export { BasePathSetup, HttpSetup, FatalErrorsSetup, - CapabilitiesSetup, Capabilities, + CapabilitiesStart, ChromeSetup, ChromeBreadcrumb, ChromeBrand, ChromeHelpExtension, I18nSetup, + I18nStart, InjectedMetadataSetup, + InjectedMetadataStart, InjectedMetadataParams, Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext, NotificationsSetup, - OverlaySetup, + NotificationsStart, + OverlayStart, FlyoutRef, Toast, ToastInput, - ToastsSetup, + ToastsApi, UiSettingsClient, UiSettingsState, UiSettingsSetup, diff --git a/src/core/public/injected_metadata/index.ts b/src/core/public/injected_metadata/index.ts index 42d82ee2d346e..93a6d27de1a6f 100644 --- a/src/core/public/injected_metadata/index.ts +++ b/src/core/public/injected_metadata/index.ts @@ -21,4 +21,5 @@ export { InjectedMetadataService, InjectedMetadataParams, InjectedMetadataSetup, + InjectedMetadataStart, } from './injected_metadata_service'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 26d88e8e07d2d..fbe4a2c625c16 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -40,18 +40,18 @@ const createSetupContractMock = () => { return setupContract; }; +const createStartContractMock = createSetupContractMock; + type InjectedMetadataServiceContract = PublicMethodsOf; -const createMock = () => { - const mocked: jest.Mocked = { - setup: jest.fn(), - getKibanaVersion: jest.fn(), - getKibanaBuildNumber: jest.fn(), - }; - mocked.setup.mockReturnValue(createSetupContractMock()); - return mocked; -}; +const createMock = (): jest.Mocked => ({ + setup: jest.fn().mockReturnValue(createSetupContractMock()), + start: jest.fn().mockReturnValue(createStartContractMock()), + getKibanaVersion: jest.fn(), + getKibanaBuildNumber: jest.fn(), +}); export const injectedMetadataServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index e77ff901040e0..ffe2689945f2a 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -105,6 +105,10 @@ export class InjectedMetadataService { }; } + public start(): InjectedMetadataStart { + return this.setup(); + } + public getKibanaVersion() { return this.state.version; } @@ -154,3 +158,6 @@ export interface InjectedMetadataSetup { [key: string]: unknown; }; } + +/** @public */ +export type InjectedMetadataStart = InjectedMetadataSetup; diff --git a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap index 096ef3addfedd..e76bcf7725d17 100644 --- a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap @@ -6,7 +6,6 @@ Array [ "ui/i18n", "ui/notify/fatal_error", "ui/notify/toasts", - "ui/capabilities", "ui/chrome/api/loading_count", "ui/chrome/api/base_path", "ui/chrome/api/ui_settings", @@ -27,7 +26,47 @@ Array [ "ui/i18n", "ui/notify/fatal_error", "ui/notify/toasts", + "ui/chrome/api/loading_count", + "ui/chrome/api/base_path", + "ui/chrome/api/ui_settings", + "ui/chrome/api/injected_vars", + "ui/chrome/api/controls", + "ui/chrome/api/help_extension", + "ui/chrome/api/theme", + "ui/chrome/api/breadcrumbs", + "ui/chrome/services/global_nav_state", + "ui/test_harness", + "legacy files", +] +`; + +exports[`#start() load order useLegacyTestHarness = false loads ui/modules before ui/chrome, and both before legacy files 1`] = ` +Array [ + "ui/metadata", + "ui/i18n", + "ui/notify/fatal_error", + "ui/notify/toasts", + "ui/chrome/api/loading_count", + "ui/chrome/api/base_path", + "ui/chrome/api/ui_settings", + "ui/chrome/api/injected_vars", + "ui/chrome/api/controls", + "ui/chrome/api/help_extension", + "ui/chrome/api/theme", + "ui/chrome/api/breadcrumbs", + "ui/chrome/services/global_nav_state", + "ui/chrome", + "legacy files", "ui/capabilities", +] +`; + +exports[`#start() load order useLegacyTestHarness = true loads ui/modules before ui/test_harness, and both before legacy files 1`] = ` +Array [ + "ui/metadata", + "ui/i18n", + "ui/notify/fatal_error", + "ui/notify/toasts", "ui/chrome/api/loading_count", "ui/chrome/api/base_path", "ui/chrome/api/ui_settings", @@ -39,6 +78,7 @@ Array [ "ui/chrome/services/global_nav_state", "ui/test_harness", "legacy files", + "ui/capabilities", ] `; diff --git a/src/core/public/legacy/legacy_service.mock.ts b/src/core/public/legacy/legacy_service.mock.ts index 9a89104ebf805..bf5d4f67059cb 100644 --- a/src/core/public/legacy/legacy_service.mock.ts +++ b/src/core/public/legacy/legacy_service.mock.ts @@ -22,6 +22,7 @@ type LegacyPlatformServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), }; return mocked; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 7ee06f76b80df..9c411f02782c1 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -25,7 +25,7 @@ const mockUiMetadataInit = jest.fn(); jest.mock('ui/metadata', () => { mockLoadOrder.push('ui/metadata'); return { - __newPlatformInit__: mockUiMetadataInit, + __newPlatformSetup__: mockUiMetadataInit, }; }); @@ -49,7 +49,7 @@ const mockI18nContextInit = jest.fn(); jest.mock('ui/i18n', () => { mockLoadOrder.push('ui/i18n'); return { - __newPlatformInit__: mockI18nContextInit, + __newPlatformSetup__: mockI18nContextInit, }; }); @@ -57,7 +57,7 @@ const mockUICapabilitiesInit = jest.fn(); jest.mock('ui/capabilities', () => { mockLoadOrder.push('ui/capabilities'); return { - __newPlatformInit__: mockUICapabilitiesInit, + __newPlatformStart__: mockUICapabilitiesInit, }; }); @@ -65,7 +65,7 @@ const mockFatalErrorInit = jest.fn(); jest.mock('ui/notify/fatal_error', () => { mockLoadOrder.push('ui/notify/fatal_error'); return { - __newPlatformInit__: mockFatalErrorInit, + __newPlatformSetup__: mockFatalErrorInit, }; }); @@ -73,7 +73,7 @@ const mockNotifyToastsInit = jest.fn(); jest.mock('ui/notify/toasts', () => { mockLoadOrder.push('ui/notify/toasts'); return { - __newPlatformInit__: mockNotifyToastsInit, + __newPlatformSetup__: mockNotifyToastsInit, }; }); @@ -81,7 +81,7 @@ const mockHttpInit = jest.fn(); jest.mock('ui/chrome/api/loading_count', () => { mockLoadOrder.push('ui/chrome/api/loading_count'); return { - __newPlatformInit__: mockHttpInit, + __newPlatformSetup__: mockHttpInit, }; }); @@ -89,7 +89,7 @@ const mockBasePathInit = jest.fn(); jest.mock('ui/chrome/api/base_path', () => { mockLoadOrder.push('ui/chrome/api/base_path'); return { - __newPlatformInit__: mockBasePathInit, + __newPlatformSetup__: mockBasePathInit, }; }); @@ -97,7 +97,7 @@ const mockUiSettingsInit = jest.fn(); jest.mock('ui/chrome/api/ui_settings', () => { mockLoadOrder.push('ui/chrome/api/ui_settings'); return { - __newPlatformInit__: mockUiSettingsInit, + __newPlatformSetup__: mockUiSettingsInit, }; }); @@ -105,7 +105,7 @@ const mockInjectedVarsInit = jest.fn(); jest.mock('ui/chrome/api/injected_vars', () => { mockLoadOrder.push('ui/chrome/api/injected_vars'); return { - __newPlatformInit__: mockInjectedVarsInit, + __newPlatformSetup__: mockInjectedVarsInit, }; }); @@ -113,7 +113,7 @@ const mockChromeControlsInit = jest.fn(); jest.mock('ui/chrome/api/controls', () => { mockLoadOrder.push('ui/chrome/api/controls'); return { - __newPlatformInit__: mockChromeControlsInit, + __newPlatformSetup__: mockChromeControlsInit, }; }); @@ -121,7 +121,7 @@ const mockChromeHelpExtensionInit = jest.fn(); jest.mock('ui/chrome/api/help_extension', () => { mockLoadOrder.push('ui/chrome/api/help_extension'); return { - __newPlatformInit__: mockChromeHelpExtensionInit, + __newPlatformSetup__: mockChromeHelpExtensionInit, }; }); @@ -129,7 +129,7 @@ const mockChromeThemeInit = jest.fn(); jest.mock('ui/chrome/api/theme', () => { mockLoadOrder.push('ui/chrome/api/theme'); return { - __newPlatformInit__: mockChromeThemeInit, + __newPlatformSetup__: mockChromeThemeInit, }; }); @@ -137,7 +137,7 @@ const mockChromeBreadcrumbsInit = jest.fn(); jest.mock('ui/chrome/api/breadcrumbs', () => { mockLoadOrder.push('ui/chrome/api/breadcrumbs'); return { - __newPlatformInit__: mockChromeBreadcrumbsInit, + __newPlatformSetup__: mockChromeBreadcrumbsInit, }; }); @@ -145,7 +145,7 @@ const mockGlobalNavStateInit = jest.fn(); jest.mock('ui/chrome/services/global_nav_state', () => { mockLoadOrder.push('ui/chrome/services/global_nav_state'); return { - __newPlatformInit__: mockGlobalNavStateInit, + __newPlatformSetup__: mockGlobalNavStateInit, }; }); @@ -168,28 +168,42 @@ const httpSetup = httpServiceMock.createSetupContract(); const i18nSetup = i18nServiceMock.createSetupContract(); const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); const notificationsSetup = notificationServiceMock.createSetupContract(); -const capabilitiesSetup = capabilitiesServiceMock.createSetupContract(); const uiSettingsSetup = uiSettingsServiceMock.createSetupContract(); -const overlaySetup = overlayServiceMock.createSetupContract(); const defaultParams = { - targetDomElement: document.createElement('div'), requireLegacyFiles: jest.fn(() => { mockLoadOrder.push('legacy files'); }), }; const defaultSetupDeps = { - i18n: i18nSetup, - fatalErrors: fatalErrorsSetup, - injectedMetadata: injectedMetadataSetup, - notifications: notificationsSetup, - http: httpSetup, - basePath: basePathSetup, - capabilities: capabilitiesSetup, - uiSettings: uiSettingsSetup, - chrome: chromeSetup, - overlays: overlaySetup, + core: { + i18n: i18nSetup, + fatalErrors: fatalErrorsSetup, + injectedMetadata: injectedMetadataSetup, + notifications: notificationsSetup, + http: httpSetup, + basePath: basePathSetup, + uiSettings: uiSettingsSetup, + chrome: chromeSetup, + }, +}; + +const capabilitiesStart = capabilitiesServiceMock.createStartContract(); +const i18nStart = i18nServiceMock.createStartContract(); +const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); +const notificationsStart = notificationServiceMock.createStartContract(); +const overlayStart = overlayServiceMock.createStartContract(); + +const defaultStartDeps = { + core: { + capabilities: capabilitiesStart, + i18n: i18nStart, + injectedMetadata: injectedMetadataStart, + notifications: notificationsStart, + overlays: overlayStart, + }, + targetDomElement: document.createElement('div'), }; afterEach(() => { @@ -226,17 +240,6 @@ describe('#setup()', () => { expect(mockI18nContextInit).toHaveBeenCalledWith(i18nSetup.Context); }); - it('passes uiCapabilities to ui/capabilities', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1); - expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesSetup); - }); - it('passes fatalErrors service to ui/notify/fatal_errors', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, @@ -357,34 +360,81 @@ describe('#setup()', () => { expect(mockGlobalNavStateInit).toHaveBeenCalledTimes(1); expect(mockGlobalNavStateInit).toHaveBeenCalledWith(chromeSetup); }); + }); + describe('load order', () => { describe('useLegacyTestHarness = false', () => { - it('passes the targetDomElement to ui/chrome', () => { + it('loads ui/modules before ui/chrome, and both before legacy files', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, }); + expect(mockLoadOrder).toEqual([]); + legacyPlatform.setup(defaultSetupDeps); - expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled(); - expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultParams.targetDomElement); + expect(mockLoadOrder).toMatchSnapshot(); }); }); describe('useLegacyTestHarness = true', () => { - it('passes the targetDomElement to ui/test_harness', () => { + it('loads ui/modules before ui/test_harness, and both before legacy files', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, useLegacyTestHarness: true, }); + expect(mockLoadOrder).toEqual([]); + legacyPlatform.setup(defaultSetupDeps); - expect(mockUiChromeBootstrap).not.toHaveBeenCalled(); - expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultParams.targetDomElement); + expect(mockLoadOrder).toMatchSnapshot(); + }); + }); + }); +}); + +describe('#start()', () => { + it('passes uiCapabilities to ui/capabilities', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); + + expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1); + expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesStart); + }); + + describe('useLegacyTestHarness = false', () => { + it('passes the targetDomElement to ui/chrome', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); + + expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled(); + expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); + expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); + }); + }); + + describe('useLegacyTestHarness = true', () => { + it('passes the targetDomElement to ui/test_harness', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + useLegacyTestHarness: true, }); + + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); + + expect(mockUiChromeBootstrap).not.toHaveBeenCalled(); + expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1); + expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); }); }); @@ -398,6 +448,7 @@ describe('#setup()', () => { expect(mockLoadOrder).toEqual([]); legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); expect(mockLoadOrder).toMatchSnapshot(); }); @@ -413,6 +464,7 @@ describe('#setup()', () => { expect(mockLoadOrder).toEqual([]); legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); expect(mockLoadOrder).toMatchSnapshot(); }); @@ -429,20 +481,18 @@ describe('#stop()', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, - targetDomElement, }); legacyPlatform.stop(); expect(targetDomElement).toMatchSnapshot(); }); - it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', () => { + it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', async () => { const targetDomElement = document.createElement('div'); const scopeDestroySpy = jest.fn(); const legacyPlatform = new LegacyPlatformService({ ...defaultParams, - targetDomElement, }); // simulate bootstrapping with a module "foo" @@ -459,6 +509,8 @@ describe('#stop()', () => { angular.bootstrap(targetDomElement, ['foo']); + await legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start({ ...defaultStartDeps, targetDomElement }); legacyPlatform.stop(); expect(targetDomElement).toMatchSnapshot(); diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index e503b1b05013a..3fa1bc5aa7118 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -18,14 +18,27 @@ */ import angular from 'angular'; -import { CoreSetup } from '../'; +import { CoreSetup, CoreStart } from '../'; +/** @internal */ export interface LegacyPlatformParams { - targetDomElement: HTMLElement; requireLegacyFiles: () => void; useLegacyTestHarness?: boolean; } +interface SetupDeps { + core: CoreSetup; +} + +interface StartDeps { + core: CoreStart; + targetDomElement: HTMLElement; +} + +interface BootstrapModule { + bootstrap: (targetDomElement: HTMLElement) => void; +} + /** * The LegacyPlatformService is responsible for initializing * the legacy platform by injecting parts of the new platform @@ -34,9 +47,12 @@ export interface LegacyPlatformParams { * setup either the app or browser tests. */ export class LegacyPlatformService { + private bootstrapModule?: BootstrapModule; + private targetDomElement?: HTMLElement; + constructor(private readonly params: LegacyPlatformParams) {} - public setup(core: CoreSetup) { + public async setup({ core }: SetupDeps) { const { i18n, injectedMetadata, @@ -44,40 +60,53 @@ export class LegacyPlatformService { notifications, http, basePath, - capabilities, uiSettings, chrome, } = core; // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts - require('ui/new_platform').__newPlatformInit__(core); - require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata()); - require('ui/i18n').__newPlatformInit__(i18n.Context); - require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors); - require('ui/notify/toasts').__newPlatformInit__(notifications.toasts); - require('ui/capabilities').__newPlatformInit__(capabilities); - require('ui/chrome/api/loading_count').__newPlatformInit__(http); - require('ui/chrome/api/base_path').__newPlatformInit__(basePath); - require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings); - require('ui/chrome/api/injected_vars').__newPlatformInit__(injectedMetadata); - require('ui/chrome/api/controls').__newPlatformInit__(chrome); - require('ui/chrome/api/help_extension').__newPlatformInit__(chrome); - require('ui/chrome/api/theme').__newPlatformInit__(chrome); - require('ui/chrome/api/breadcrumbs').__newPlatformInit__(chrome); - require('ui/chrome/services/global_nav_state').__newPlatformInit__(chrome); + require('ui/new_platform').__newPlatformSetup__(core); + require('ui/metadata').__newPlatformSetup__(injectedMetadata.getLegacyMetadata()); + require('ui/i18n').__newPlatformSetup__(i18n.Context); + require('ui/notify/fatal_error').__newPlatformSetup__(fatalErrors); + require('ui/notify/toasts').__newPlatformSetup__(notifications.toasts); + require('ui/chrome/api/loading_count').__newPlatformSetup__(http); + require('ui/chrome/api/base_path').__newPlatformSetup__(basePath); + require('ui/chrome/api/ui_settings').__newPlatformSetup__(uiSettings); + require('ui/chrome/api/injected_vars').__newPlatformSetup__(injectedMetadata); + require('ui/chrome/api/controls').__newPlatformSetup__(chrome); + require('ui/chrome/api/help_extension').__newPlatformSetup__(chrome); + require('ui/chrome/api/theme').__newPlatformSetup__(chrome); + require('ui/chrome/api/breadcrumbs').__newPlatformSetup__(chrome); + require('ui/chrome/services/global_nav_state').__newPlatformSetup__(chrome); // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first - const bootstrapModule = this.loadBootstrapModule(); + this.bootstrapModule = this.loadBootstrapModule(); // require the files that will tie into the legacy platform this.params.requireLegacyFiles(); + } + + public start({ core, targetDomElement }: StartDeps) { + if (!this.bootstrapModule) { + throw new Error('Bootstrap module must be loaded before `start`'); + } + + this.targetDomElement = targetDomElement; - bootstrapModule.bootstrap(this.params.targetDomElement); + require('ui/new_platform').__newPlatformStart__(core); + require('ui/capabilities').__newPlatformStart__(core.capabilities); + + this.bootstrapModule.bootstrap(this.targetDomElement); } public stop() { - const angularRoot = angular.element(this.params.targetDomElement); + if (!this.targetDomElement) { + return; + } + + const angularRoot = angular.element(this.targetDomElement); const injector$ = angularRoot.injector(); // if we haven't gotten to the point of bootstraping @@ -90,12 +119,10 @@ export class LegacyPlatformService { injector$.get('$rootScope').$destroy(); // clear the inner html of the root angular element - this.params.targetDomElement.textContent = ''; + this.targetDomElement.textContent = ''; } - private loadBootstrapModule(): { - bootstrap: (targetDomElement: HTMLElement) => void; - } { + private loadBootstrapModule(): BootstrapModule { if (this.params.useLegacyTestHarness) { // wrapped in NODE_ENV check so the `ui/test_harness` module // is not included in the distributable diff --git a/src/core/public/notifications/index.ts b/src/core/public/notifications/index.ts index 91b81e4f95ccd..9800b5154d0bc 100644 --- a/src/core/public/notifications/index.ts +++ b/src/core/public/notifications/index.ts @@ -17,5 +17,9 @@ * under the License. */ -export { Toast, ToastInput, ToastsSetup } from './toasts'; -export { NotificationsService, NotificationsSetup } from './notifications_service'; +export { Toast, ToastInput, ToastsApi } from './toasts'; +export { + NotificationsService, + NotificationsSetup, + NotificationsStart, +} from './notifications_service'; diff --git a/src/core/public/notifications/notifications_service.mock.ts b/src/core/public/notifications/notifications_service.mock.ts index 87509d3232f80..e1901f55331ae 100644 --- a/src/core/public/notifications/notifications_service.mock.ts +++ b/src/core/public/notifications/notifications_service.mock.ts @@ -16,22 +16,35 @@ * specific language governing permissions and limitations * under the License. */ -import { NotificationsService, NotificationsSetup } from './notifications_service'; +import { + NotificationsService, + NotificationsSetup, + NotificationsStart, +} from './notifications_service'; import { toastsServiceMock } from './toasts/toasts_service.mock'; -import { ToastsSetup } from './toasts/toasts_start'; +import { ToastsApi } from './toasts/toasts_api'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { // we have to suppress type errors until decide how to mock es6 class - toasts: (toastsServiceMock.createSetupContract() as unknown) as ToastsSetup, + toasts: (toastsServiceMock.createSetupContract() as unknown) as ToastsApi, }; return setupContract; }; +const createStartContractMock = () => { + const startContract: jest.Mocked = { + // we have to suppress type errors until decide how to mock es6 class + toasts: (toastsServiceMock.createStartContract() as unknown) as ToastsApi, + }; + return startContract; +}; + type NotificationsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), }; mocked.setup.mockReturnValue(createSetupContractMock()); @@ -41,4 +54,5 @@ const createMock = () => { export const notificationServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts index a6a2b97f41a0b..09a527e90ae38 100644 --- a/src/core/public/notifications/notifications_service.ts +++ b/src/core/public/notifications/notifications_service.ts @@ -19,49 +19,33 @@ import { i18n } from '@kbn/i18n'; -import { Observable, Subject, Subscription } from 'rxjs'; -import { I18nSetup } from '../i18n'; +import { Subscription } from 'rxjs'; +import { I18nStart } from '../i18n'; import { ToastsService } from './toasts'; +import { ToastsApi } from './toasts/toasts_api'; import { UiSettingsSetup } from '../ui_settings'; -interface NotificationServiceParams { - targetDomElement$: Observable; +interface SetupDeps { + uiSettings: UiSettingsSetup; } -interface NotificationsServiceDeps { - i18n: I18nSetup; - uiSettings: UiSettingsSetup; +interface StartDeps { + i18n: I18nStart; + targetDomElement: HTMLElement; } /** @public */ export class NotificationsService { private readonly toasts: ToastsService; - - private readonly toastsContainer$: Subject; - private domElemSubscription?: Subscription; private uiSettingsErrorSubscription?: Subscription; private targetDomElement?: HTMLElement; - constructor(private readonly params: NotificationServiceParams) { - this.toastsContainer$ = new Subject(); - this.toasts = new ToastsService({ - targetDomElement$: this.toastsContainer$.asObservable(), - }); + constructor() { + this.toasts = new ToastsService(); } - public setup({ i18n: i18nDep, uiSettings }: NotificationsServiceDeps) { - this.domElemSubscription = this.params.targetDomElement$.subscribe({ - next: targetDomElement => { - this.cleanupTargetDomElement(); - this.targetDomElement = targetDomElement; - - const toastsContainer = document.createElement('div'); - targetDomElement.appendChild(toastsContainer); - this.toastsContainer$.next(toastsContainer); - }, - }); - - const notificationSetup = { toasts: this.toasts.setup({ i18n: i18nDep }) }; + public setup({ uiSettings }: SetupDeps): NotificationsSetup { + const notificationSetup = { toasts: this.toasts.setup() }; this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => { notificationSetup.toasts.addDanger({ @@ -75,25 +59,31 @@ export class NotificationsService { return notificationSetup; } + public start({ i18n: i18nDep, targetDomElement }: StartDeps): NotificationsStart { + this.targetDomElement = targetDomElement; + const toastsContainer = document.createElement('div'); + targetDomElement.appendChild(toastsContainer); + + return { toasts: this.toasts.start({ i18n: i18nDep, targetDomElement: toastsContainer }) }; + } + public stop() { this.toasts.stop(); - this.cleanupTargetDomElement(); - if (this.domElemSubscription) { - this.domElemSubscription.unsubscribe(); + if (this.targetDomElement) { + this.targetDomElement.textContent = ''; } if (this.uiSettingsErrorSubscription) { this.uiSettingsErrorSubscription.unsubscribe(); } } +} - private cleanupTargetDomElement() { - if (this.targetDomElement) { - this.targetDomElement.textContent = ''; - } - } +/** @public */ +export interface NotificationsSetup { + toasts: ToastsApi; } /** @public */ -export type NotificationsSetup = ReturnType; +export type NotificationsStart = NotificationsSetup; diff --git a/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap b/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap index efe33c050e4ab..75d456dd87f78 100644 --- a/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap +++ b/src/core/public/notifications/toasts/__snapshots__/toasts_service.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#setup() renders the GlobalToastList into the targetDomElement param 1`] = ` +exports[`#start() renders the GlobalToastList into the targetDomElement param 1`] = ` Array [ Array [ diff --git a/src/core/public/notifications/toasts/index.ts b/src/core/public/notifications/toasts/index.ts index 4481c29130670..8615bfd792e19 100644 --- a/src/core/public/notifications/toasts/index.ts +++ b/src/core/public/notifications/toasts/index.ts @@ -18,5 +18,5 @@ */ export { ToastsService } from './toasts_service'; -export { ToastsSetup, ToastInput } from './toasts_start'; +export { ToastsApi, ToastInput } from './toasts_api'; export { Toast } from '@elastic/eui'; diff --git a/src/core/public/notifications/toasts/toasts_start.test.ts b/src/core/public/notifications/toasts/toasts_api.test.ts similarity index 87% rename from src/core/public/notifications/toasts/toasts_start.test.ts rename to src/core/public/notifications/toasts/toasts_api.test.ts index 7dce1f8bb81ce..d57b84b876c13 100644 --- a/src/core/public/notifications/toasts/toasts_start.test.ts +++ b/src/core/public/notifications/toasts/toasts_api.test.ts @@ -19,9 +19,9 @@ import { take } from 'rxjs/operators'; -import { ToastsSetup } from './toasts_start'; +import { ToastsApi } from './toasts_api'; -async function getCurrentToasts(toasts: ToastsSetup) { +async function getCurrentToasts(toasts: ToastsApi) { return await toasts .get$() .pipe(take(1)) @@ -30,7 +30,7 @@ async function getCurrentToasts(toasts: ToastsSetup) { describe('#get$()', () => { it('returns observable that emits NEW toast list when something added or removed', () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); const onToasts = jest.fn(); toasts.get$().subscribe(onToasts); @@ -57,7 +57,7 @@ describe('#get$()', () => { }); it('does not emit a new toast list when unknown toast is passed to remove()', () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); const onToasts = jest.fn(); toasts.get$().subscribe(onToasts); @@ -71,14 +71,14 @@ describe('#get$()', () => { describe('#add()', () => { it('returns toast objects with auto assigned id', () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); const toast = toasts.add({ title: 'foo' }); expect(toast).toHaveProperty('id'); expect(toast).toHaveProperty('title', 'foo'); }); it('adds the toast to toasts list', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); const toast = toasts.add({}); const currentToasts = await getCurrentToasts(toasts); @@ -87,27 +87,27 @@ describe('#add()', () => { }); it('increments the toast ID for each additional toast', () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); expect(toasts.add({})).toHaveProperty('id', '0'); expect(toasts.add({})).toHaveProperty('id', '1'); expect(toasts.add({})).toHaveProperty('id', '2'); }); it('accepts a string, uses it as the title', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); expect(toasts.add('foo')).toHaveProperty('title', 'foo'); }); }); describe('#remove()', () => { it('removes a toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); toasts.remove(toasts.add('Test')); expect(await getCurrentToasts(toasts)).toHaveLength(0); }); it('ignores unknown toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); toasts.add('Test'); toasts.remove({ id: 'foo' }); @@ -118,12 +118,12 @@ describe('#remove()', () => { describe('#addSuccess()', () => { it('adds a success toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); expect(toasts.addSuccess({})).toHaveProperty('color', 'success'); }); it('returns the created toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); const toast = toasts.addSuccess({}); const currentToasts = await getCurrentToasts(toasts); expect(currentToasts[0]).toBe(toast); @@ -132,12 +132,12 @@ describe('#addSuccess()', () => { describe('#addWarning()', () => { it('adds a warning toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); expect(toasts.addWarning({})).toHaveProperty('color', 'warning'); }); it('returns the created toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); const toast = toasts.addWarning({}); const currentToasts = await getCurrentToasts(toasts); expect(currentToasts[0]).toBe(toast); @@ -146,12 +146,12 @@ describe('#addWarning()', () => { describe('#addDanger()', () => { it('adds a danger toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); expect(toasts.addDanger({})).toHaveProperty('color', 'danger'); }); it('returns the created toast', async () => { - const toasts = new ToastsSetup(); + const toasts = new ToastsApi(); const toast = toasts.addDanger({}); const currentToasts = await getCurrentToasts(toasts); expect(currentToasts[0]).toBe(toast); diff --git a/src/core/public/notifications/toasts/toasts_start.tsx b/src/core/public/notifications/toasts/toasts_api.tsx similarity index 98% rename from src/core/public/notifications/toasts/toasts_start.tsx rename to src/core/public/notifications/toasts/toasts_api.tsx index 28e77943724e9..d833c8586def5 100644 --- a/src/core/public/notifications/toasts/toasts_start.tsx +++ b/src/core/public/notifications/toasts/toasts_api.tsx @@ -34,7 +34,7 @@ const normalizeToast = (toastOrTitle: ToastInput) => { }; /** @public */ -export class ToastsSetup { +export class ToastsApi { private toasts$ = new Rx.BehaviorSubject([]); private idCounter = 0; diff --git a/src/core/public/notifications/toasts/toasts_service.mock.ts b/src/core/public/notifications/toasts/toasts_service.mock.ts index 6853eecc08480..e6a7d225f9243 100644 --- a/src/core/public/notifications/toasts/toasts_service.mock.ts +++ b/src/core/public/notifications/toasts/toasts_service.mock.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { ToastsSetup } from './toasts_start'; +import { ToastsApi } from './toasts_api'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked> = { +const createToastsApiMock = () => { + const api: jest.Mocked> = { get$: jest.fn(), add: jest.fn(), remove: jest.fn(), @@ -27,9 +27,14 @@ const createSetupContractMock = () => { addWarning: jest.fn(), addDanger: jest.fn(), }; - return setupContract; + return api; }; +const createSetupContractMock = createToastsApiMock; + +const createStartContractMock = createToastsApiMock; + export const toastsServiceMock = { createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/notifications/toasts/toasts_service.test.tsx b/src/core/public/notifications/toasts/toasts_service.test.tsx index 36f99ec8a69c4..bc1683aa2c490 100644 --- a/src/core/public/notifications/toasts/toasts_service.test.tsx +++ b/src/core/public/notifications/toasts/toasts_service.test.tsx @@ -19,9 +19,8 @@ import { mockReactDomRender, mockReactDomUnmount } from './toasts_service.test.mocks'; -import { of } from 'rxjs'; import { ToastsService } from './toasts_service'; -import { ToastsSetup } from './toasts_start'; +import { ToastsApi } from './toasts_api'; const mockI18n: any = { Context: function I18nContext() { @@ -30,22 +29,31 @@ const mockI18n: any = { }; describe('#setup()', () => { + it('returns a ToastsApi', () => { + const toasts = new ToastsService(); + + expect(toasts.setup()).toBeInstanceOf(ToastsApi); + }); +}); + +describe('#start()', () => { it('renders the GlobalToastList into the targetDomElement param', async () => { const targetDomElement = document.createElement('div'); targetDomElement.setAttribute('test', 'target-dom-element'); - const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) }); + const toasts = new ToastsService(); expect(mockReactDomRender).not.toHaveBeenCalled(); - toasts.setup({ i18n: mockI18n }); + toasts.setup(); + toasts.start({ i18n: mockI18n, targetDomElement }); expect(mockReactDomRender.mock.calls).toMatchSnapshot(); }); - it('returns a ToastsSetup', () => { - const toasts = new ToastsService({ - targetDomElement$: of(document.createElement('div')), - }); + it('returns a ToastsApi', () => { + const targetDomElement = document.createElement('div'); + const toasts = new ToastsService(); - expect(toasts.setup({ i18n: mockI18n })).toBeInstanceOf(ToastsSetup); + toasts.setup(); + expect(toasts.start({ i18n: mockI18n, targetDomElement })).toBeInstanceOf(ToastsApi); }); }); @@ -53,9 +61,10 @@ describe('#stop()', () => { it('unmounts the GlobalToastList from the targetDomElement', () => { const targetDomElement = document.createElement('div'); targetDomElement.setAttribute('test', 'target-dom-element'); - const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) }); + const toasts = new ToastsService(); - toasts.setup({ i18n: mockI18n }); + toasts.setup(); + toasts.start({ i18n: mockI18n, targetDomElement }); expect(mockReactDomUnmount).not.toHaveBeenCalled(); toasts.stop(); @@ -63,9 +72,7 @@ describe('#stop()', () => { }); it('does not fail if setup() was never called', () => { - const targetDomElement = document.createElement('div'); - targetDomElement.setAttribute('test', 'target-dom-element'); - const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) }); + const toasts = new ToastsService(); expect(() => { toasts.stop(); }).not.toThrowError(); @@ -73,9 +80,10 @@ describe('#stop()', () => { it('empties the content of the targetDomElement', () => { const targetDomElement = document.createElement('div'); - const toasts = new ToastsService({ targetDomElement$: of(targetDomElement) }); + const toasts = new ToastsService(); - toasts.setup({ i18n: mockI18n }); + toasts.setup(); + toasts.start({ i18n: mockI18n, targetDomElement }); toasts.stop(); expect(targetDomElement.childNodes).toHaveLength(0); }); diff --git a/src/core/public/notifications/toasts/toasts_service.tsx b/src/core/public/notifications/toasts/toasts_service.tsx index 912d01689ac67..9d7f12ec67c38 100644 --- a/src/core/public/notifications/toasts/toasts_service.tsx +++ b/src/core/public/notifications/toasts/toasts_service.tsx @@ -19,59 +19,43 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Observable, Subscription } from 'rxjs'; import { Toast } from '@elastic/eui'; import { I18nSetup } from '../../i18n'; import { GlobalToastList } from './global_toast_list'; -import { ToastsSetup } from './toasts_start'; +import { ToastsApi } from './toasts_api'; -interface Params { - targetDomElement$: Observable; -} - -interface Deps { +interface StartDeps { i18n: I18nSetup; + targetDomElement: HTMLElement; } export class ToastsService { - private domElemSubscription?: Subscription; + private api?: ToastsApi; private targetDomElement?: HTMLElement; - constructor(private readonly params: Params) {} - - public setup({ i18n }: Deps) { - const toasts = new ToastsSetup(); + public setup() { + this.api = new ToastsApi(); + return this.api!; + } - this.domElemSubscription = this.params.targetDomElement$.subscribe({ - next: targetDomElement => { - this.cleanupTargetDomElement(); - this.targetDomElement = targetDomElement; + public start({ i18n, targetDomElement }: StartDeps) { + this.targetDomElement = targetDomElement; - render( - - toasts.remove(toast)} - toasts$={toasts.get$()} - /> - , - targetDomElement - ); - }, - }); + render( + + this.api!.remove(toast)} + toasts$={this.api!.get$()} + /> + , + targetDomElement + ); - return toasts; + return this.api!; } public stop() { - this.cleanupTargetDomElement(); - - if (this.domElemSubscription) { - this.domElemSubscription.unsubscribe(); - } - } - - private cleanupTargetDomElement() { if (this.targetDomElement) { unmountComponentAtNode(this.targetDomElement); this.targetDomElement.textContent = ''; diff --git a/src/core/public/overlays/index.ts b/src/core/public/overlays/index.ts index 117868379570c..246cc7d4621f1 100644 --- a/src/core/public/overlays/index.ts +++ b/src/core/public/overlays/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { OverlayService, OverlaySetup } from './overlay_service'; +export { OverlayService, OverlayStart } from './overlay_service'; export { FlyoutRef } from './flyout'; diff --git a/src/core/public/overlays/overlay_service.mock.ts b/src/core/public/overlays/overlay_service.mock.ts index 3b21bfea1aff0..186e222326c7b 100644 --- a/src/core/public/overlays/overlay_service.mock.ts +++ b/src/core/public/overlays/overlay_service.mock.ts @@ -16,24 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -import { OverlayService, OverlaySetup } from './overlay_service'; +import { OverlayService, OverlayStart } from './overlay_service'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked> = { +const createStartContractMock = () => { + const startContract: jest.Mocked> = { openFlyout: jest.fn(), }; - return setupContract; + return startContract; }; const createMock = () => { const mocked: jest.Mocked> = { - setup: jest.fn(), + start: jest.fn(), }; - mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const overlayServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts index 9c03c96228c52..dad96d1dc4f49 100644 --- a/src/core/public/overlays/overlay_service.ts +++ b/src/core/public/overlays/overlay_service.ts @@ -20,10 +20,10 @@ import { FlyoutService } from './flyout'; import { FlyoutRef } from '..'; -import { I18nSetup } from '../i18n'; +import { I18nStart } from '../i18n'; -interface Deps { - i18n: I18nSetup; +interface StartDeps { + i18n: I18nStart; } /** @internal */ @@ -34,7 +34,7 @@ export class OverlayService { this.flyoutService = new FlyoutService(targetDomElement); } - public setup({ i18n }: Deps): OverlaySetup { + public start({ i18n }: StartDeps): OverlayStart { return { openFlyout: this.flyoutService.openFlyout.bind(this.flyoutService, i18n), }; @@ -42,7 +42,7 @@ export class OverlayService { } /** @public */ -export interface OverlaySetup { +export interface OverlayStart { openFlyout: ( flyoutChildren: React.ReactNode, flyoutProps?: { diff --git a/src/core/public/plugins/plugin.test.mocks.ts b/src/core/public/plugins/plugin.test.mocks.ts index b254f458a3759..b877847aaa90e 100644 --- a/src/core/public/plugins/plugin.test.mocks.ts +++ b/src/core/public/plugins/plugin.test.mocks.ts @@ -19,6 +19,7 @@ export const mockPlugin = { setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), }; export const mockInitializer = jest.fn(() => mockPlugin); diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index 0707cdd10cc7d..bbe2baf006a85 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -41,6 +41,7 @@ const addBasePath = (path: string) => path; beforeEach(() => { mockPluginLoader.mockClear(); mockPlugin.setup.mockClear(); + mockPlugin.start.mockClear(); mockPlugin.stop.mockClear(); plugin = new PluginWrapper(createManifest('plugin-a'), initializerContext); }); @@ -58,13 +59,21 @@ describe('PluginWrapper', () => { }); test('`setup` fails if plugin.setup is not a function', async () => { - mockInitializer.mockReturnValueOnce({ stop: jest.fn() } as any); + mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); await plugin.load(addBasePath); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); }); + test('`setup` fails if plugin.start is not a function', async () => { + mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); + await plugin.load(addBasePath); + await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` + ); + }); + test('`setup` calls initializer with initializer context', async () => { await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); @@ -79,6 +88,22 @@ describe('PluginWrapper', () => { expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps); }); + test('`start` fails if setup is not called first', async () => { + await plugin.load(addBasePath); + await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` + ); + }); + + test('`start` calls plugin.start with context and dependencies', async () => { + await plugin.load(addBasePath); + await plugin.setup({} as any, {} as any); + const context = { any: 'thing' } as any; + const deps = { otherDep: 'value' }; + await plugin.start(context, deps); + expect(mockPlugin.start).toHaveBeenCalledWith(context, deps); + }); + test('`stop` fails if plugin is not setup up', async () => { expect(() => plugin.stop()).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be stopped since it isn't set up."` @@ -93,7 +118,7 @@ describe('PluginWrapper', () => { }); test('`stop` does not fail if plugin.stop does not exist', async () => { - mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); + mockInitializer.mockReturnValueOnce({ setup: jest.fn(), start: jest.fn() } as any); await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); expect(() => plugin.stop()).not.toThrow(); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index 9b9a3f19dc006..a57d74bb26a9b 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -18,7 +18,7 @@ */ import { DiscoveredPlugin, PluginName } from '../../server'; -import { PluginInitializerContext, PluginSetupContext } from './plugin_context'; +import { PluginInitializerContext, PluginSetupContext, PluginStartContext } from './plugin_context'; import { loadPluginBundle } from './plugin_loader'; /** @@ -26,8 +26,14 @@ import { loadPluginBundle } from './plugin_loader'; * * @public */ -export interface Plugin = {}> { +export interface Plugin< + TSetup, + TStart, + TPluginsSetup extends Record = {}, + TPluginsStart extends Record = {} +> { setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; + start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; stop?: () => void; } @@ -37,9 +43,12 @@ export interface Plugin = * * @public */ -export type PluginInitializer = {}> = ( - core: PluginInitializerContext -) => Plugin; +export type PluginInitializer< + TSetup, + TStart, + TPluginsSetup extends Record = {}, + TPluginsStart extends Record = {} +> = (core: PluginInitializerContext) => Plugin; /** * Lightweight wrapper around discovered plugin that is responsible for instantiating @@ -49,14 +58,16 @@ export type PluginInitializer = Record + TStart = unknown, + TPluginsSetup extends Record = Record, + TPluginsStart extends Record = Record > { public readonly name: DiscoveredPlugin['id']; public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private initializer?: PluginInitializer; - private instance?: Plugin; + private initializer?: PluginInitializer; + private instance?: Plugin; constructor( readonly discoveredPlugin: DiscoveredPlugin, @@ -74,7 +85,10 @@ export class PluginWrapper< * @param addBasePath Function that adds the base path to a string for plugin bundle path. */ public async load(addBasePath: (path: string) => string) { - this.initializer = await loadPluginBundle(addBasePath, this.name); + this.initializer = await loadPluginBundle( + addBasePath, + this.name + ); } /** @@ -90,6 +104,21 @@ export class PluginWrapper< return await this.instance.setup(setupContext, plugins); } + /** + * Calls `setup` function exposed by the initialized plugin. + * @param startContext Context that consists of various core services tailored specifically + * for the `start` lifecycle event. + * @param plugins The dictionary where the key is the dependency name and the value + * is the contract returned by the dependency's `start` function. + */ + public async start(startContext: PluginStartContext, plugins: TPluginsStart) { + if (this.instance === undefined) { + throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); + } + + return await this.instance.start(startContext, plugins); + } + /** * Calls optional `stop` function exposed by the plugin initializer. */ @@ -114,6 +143,8 @@ export class PluginWrapper< if (typeof instance.setup !== 'function') { throw new Error(`Instance of plugin "${this.name}" does not define "setup" function.`); + } else if (typeof instance.start !== 'function') { + throw new Error(`Instance of plugin "${this.name}" does not define "start" function.`); } return instance; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 6d75d450dd7d0..8b178728fdd8f 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -22,11 +22,13 @@ import { BasePathSetup } from '../base_path'; import { ChromeSetup } from '../chrome'; import { CoreContext } from '../core_system'; import { FatalErrorsSetup } from '../fatal_errors'; -import { I18nSetup } from '../i18n'; -import { NotificationsSetup } from '../notifications'; +import { I18nSetup, I18nStart } from '../i18n'; +import { NotificationsSetup, NotificationsStart } from '../notifications'; import { UiSettingsSetup } from '../ui_settings'; import { PluginWrapper } from './plugin'; -import { PluginsServiceSetupDeps } from './plugins_service'; +import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; +import { CapabilitiesStart } from '../capabilities'; +import { OverlayStart } from '../overlays'; /** * The available core services passed to a `PluginInitializer` @@ -50,6 +52,18 @@ export interface PluginSetupContext { uiSettings: UiSettingsSetup; } +/** + * The available core services passed to a plugin's `Plugin#start` method. + * + * @public + */ +export interface PluginStartContext { + capabilities: CapabilitiesStart; + i18n: I18nStart; + notifications: NotificationsStart; + overlays: OverlayStart; +} + /** * Provides a plugin-specific context passed to the plugin's construtor. This is currently * empty but should provide static services in the future, such as config and logging. @@ -75,10 +89,10 @@ export function createPluginInitializerContext( * @param plugin * @internal */ -export function createPluginSetupContext( +export function createPluginSetupContext( coreContext: CoreContext, deps: PluginsServiceSetupDeps, - plugin: PluginWrapper + plugin: PluginWrapper ): PluginSetupContext { return { basePath: deps.basePath, @@ -89,3 +103,26 @@ export function createPluginSetupContext( uiSettings: deps.uiSettings, }; } + +/** + * Provides a plugin-specific context passed to the plugin's `start` lifecycle event. Currently + * this returns a shallow copy the service start contracts, but in the future could provide + * plugin-scoped versions of the service. + * + * @param coreContext + * @param deps + * @param plugin + * @internal + */ +export function createPluginStartContext( + coreContext: CoreContext, + deps: PluginsServiceStartDeps, + plugin: PluginWrapper +): PluginStartContext { + return { + capabilities: deps.capabilities, + i18n: deps.i18n, + notifications: deps.notifications, + overlays: deps.overlays, + }; +} diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts index 30a4a72dad2de..9ec24adaabbe7 100644 --- a/src/core/public/plugins/plugin_loader.ts +++ b/src/core/public/plugins/plugin_loader.ts @@ -61,65 +61,74 @@ export const LOAD_TIMEOUT = 120 * 1000; // 2 minutes */ export const loadPluginBundle: LoadPluginBundle = < TSetup, - TDependencies extends Record + TStart, + TPluginsSetup extends Record, + TPluginsStart extends Record >( addBasePath: (path: string) => string, pluginName: PluginName, - { timeoutMs = LOAD_TIMEOUT } = {} + { timeoutMs = LOAD_TIMEOUT }: { timeoutMs?: number } = {} ) => - new Promise>((resolve, reject) => { - const script = document.createElement('script'); - const coreWindow = (window as unknown) as CoreWindow; + new Promise>( + (resolve, reject) => { + const script = document.createElement('script'); + const coreWindow = (window as unknown) as CoreWindow; - // Assumes that all plugin bundles get put into the bundles/plugins subdirectory - const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`); - script.setAttribute('src', bundlePath); - script.setAttribute('id', `kbn-plugin-${pluginName}`); - script.setAttribute('async', ''); + // Assumes that all plugin bundles get put into the bundles/plugins subdirectory + const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`); + script.setAttribute('src', bundlePath); + script.setAttribute('id', `kbn-plugin-${pluginName}`); + script.setAttribute('async', ''); - // Add kbnNonce for CSP - script.setAttribute('nonce', coreWindow.__kbnNonce__); + // Add kbnNonce for CSP + script.setAttribute('nonce', coreWindow.__kbnNonce__); - const cleanupTag = () => { - clearTimeout(timeout); - // Set to null for IE memory leak issue. Webpack does the same thing. - // @ts-ignore - script.onload = script.onerror = null; - }; + const cleanupTag = () => { + clearTimeout(timeout); + // Set to null for IE memory leak issue. Webpack does the same thing. + // @ts-ignore + script.onload = script.onerror = null; + }; - // Wire up resolve and reject - script.onload = () => { - cleanupTag(); + // Wire up resolve and reject + script.onload = () => { + cleanupTag(); - const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`]; - if (!initializer || typeof initializer !== 'function') { - reject( - new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) - ); - } else { - resolve(initializer as PluginInitializer); - } - }; + const initializer = coreWindow.__kbnBundles__[`plugin/${pluginName}`]; + if (!initializer || typeof initializer !== 'function') { + reject( + new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) + ); + } else { + resolve(initializer as PluginInitializer); + } + }; - script.onerror = () => { - cleanupTag(); - reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`)); - }; + script.onerror = () => { + cleanupTag(); + reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`)); + }; - const timeout = setTimeout(() => { - cleanupTag(); - reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`)); - }, timeoutMs); + const timeout = setTimeout(() => { + cleanupTag(); + reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`)); + }, timeoutMs); - // Add the script tag to the end of the body to start downloading - document.body.appendChild(script); - }); + // Add the script tag to the end of the body to start downloading + document.body.appendChild(script); + } + ); /** * @internal */ -export type LoadPluginBundle = >( +export type LoadPluginBundle = < + TSetup, + TStart, + TPluginsSetup extends Record, + TPluginsStart extends Record +>( addBasePath: (path: string) => string, pluginName: PluginName, options?: { timeoutMs?: number } -) => Promise>; +) => Promise>; diff --git a/src/core/public/plugins/plugins_service.mock.ts b/src/core/public/plugins/plugins_service.mock.ts index d4c6ffb12aaef..4df57b05fda30 100644 --- a/src/core/public/plugins/plugins_service.mock.ts +++ b/src/core/public/plugins/plugins_service.mock.ts @@ -20,25 +20,36 @@ import { PluginsService, PluginsServiceSetup } from './plugins_service'; const createSetupContractMock = () => { - const setupContract: jest.Mocked> = { - pluginSetups: new Map(), + const setupContract: jest.Mocked = { + contracts: new Map(), }; // we have to suppress type errors until decide how to mock es6 class - return (setupContract as unknown) as PluginsServiceSetup; + return setupContract as PluginsServiceSetup; +}; + +const createStartContractMock = () => { + const startContract: jest.Mocked = { + contracts: new Map(), + }; + // we have to suppress type errors until decide how to mock es6 class + return startContract as PluginsServiceSetup; }; type PluginsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), }; mocked.setup.mockResolvedValue(createSetupContractMock()); + mocked.start.mockResolvedValue(createStartContractMock()); return mocked; }; export const pluginsServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index c0ea57e927cfb..0208974ba90ca 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -27,7 +27,21 @@ import { import { PluginName } from 'src/core/server'; import { CoreContext } from '../core_system'; -import { PluginsService } from './plugins_service'; +import { + PluginsService, + PluginsServiceStartDeps, + PluginsServiceSetupDeps, +} from './plugins_service'; +import { notificationServiceMock } from '../notifications/notifications_service.mock'; +import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; +import { i18nServiceMock } from '../i18n/i18n_service.mock'; +import { overlayServiceMock } from '../overlays/overlay_service.mock'; +import { PluginStartContext, PluginSetupContext } from './plugin_context'; +import { chromeServiceMock } from '../chrome/chrome_service.mock'; +import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; +import { basePathServiceMock } from '../base_path/base_path_service.mock'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; export let mockPluginInitializers: Map; @@ -35,41 +49,56 @@ mockPluginInitializerProvider.mockImplementation( pluginName => mockPluginInitializers.get(pluginName)! ); +type DeeplyMocked = { [P in keyof T]: jest.Mocked }; + const mockCoreContext: CoreContext = {}; -let mockDeps: any; -let mockInitContext: any; +let mockSetupDeps: DeeplyMocked; +let mockSetupContext: DeeplyMocked; +let mockStartDeps: DeeplyMocked; +let mockStartContext: DeeplyMocked; beforeEach(() => { - mockDeps = { - injectedMetadata: { - getPlugins: jest.fn(() => [ + mockSetupDeps = { + injectedMetadata: (function() { + const metadata = injectedMetadataServiceMock.createSetupContract(); + metadata.getPlugins.mockReturnValue([ { id: 'pluginA', plugin: createManifest('pluginA') }, { id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) }, { id: 'pluginC', plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }), }, - ]), - }, - basePath: { - addToPath(path: string) { - return path; - }, - }, - chrome: {}, - fatalErrors: {}, - i18n: {}, - notifications: {}, - uiSettings: {}, + ]); + return metadata; + })(), + basePath: (function() { + const basePath = basePathServiceMock.createSetupContract(); + basePath.addToPath.mockImplementation(path => path); + return basePath; + })(), + chrome: chromeServiceMock.createSetupContract(), + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + i18n: i18nServiceMock.createSetupContract(), + notifications: notificationServiceMock.createSetupContract(), + uiSettings: uiSettingsServiceMock.createSetupContract(), } as any; - mockInitContext = omit(mockDeps, 'injectedMetadata'); + mockSetupContext = omit(mockSetupDeps, 'injectedMetadata'); + mockStartDeps = { + capabilities: capabilitiesServiceMock.createStartContract(), + i18n: i18nServiceMock.createStartContract(), + injectedMetadata: injectedMetadataServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + overlays: overlayServiceMock.createStartContract(), + }; + mockStartContext = omit(mockStartDeps, 'injectedMetadata'); // Reset these for each test. mockPluginInitializers = new Map(([ [ 'pluginA', jest.fn(() => ({ - setup: jest.fn(() => ({ exportedValue: 1 })), + setup: jest.fn(() => ({ setupValue: 1 })), + start: jest.fn(() => ({ startValue: 2 })), stop: jest.fn(), })), ], @@ -77,7 +106,10 @@ beforeEach(() => { 'pluginB', jest.fn(() => ({ setup: jest.fn((core, deps: any) => ({ - pluginAPlusB: deps.pluginA.exportedValue + 1, + pluginAPlusB: deps.pluginA.setupValue + 1, + })), + start: jest.fn((core, deps: any) => ({ + pluginAPlusB: deps.pluginA.startValue + 1, })), stop: jest.fn(), })), @@ -86,6 +118,7 @@ beforeEach(() => { 'pluginC', jest.fn(() => ({ setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), })), ], @@ -113,7 +146,7 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () => mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); const pluginsService = new PluginsService(mockCoreContext); - await expect(pluginsService.setup(mockDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Could not load bundle"` ); }); @@ -121,24 +154,24 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () => test('`PluginsService.setup` fails if any plugin instance does not have a setup function', async () => { mockPluginInitializers.set('pluginA', (() => ({})) as any); const pluginsService = new PluginsService(mockCoreContext); - await expect(pluginsService.setup(mockDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."` ); }); test('`PluginsService.setup` calls loadPluginBundles with basePath and plugins', async () => { const pluginsService = new PluginsService(mockCoreContext); - await pluginsService.setup(mockDeps); + await pluginsService.setup(mockSetupDeps); expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginA'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginB'); - expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockDeps.basePath.addToPath, 'pluginC'); + expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginA'); + expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginB'); + expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.basePath.addToPath, 'pluginC'); }); test('`PluginsService.setup` initalizes plugins with CoreContext', async () => { const pluginsService = new PluginsService(mockCoreContext); - await pluginsService.setup(mockDeps); + await pluginsService.setup(mockSetupDeps); expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(mockCoreContext); expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(mockCoreContext); @@ -147,50 +180,102 @@ test('`PluginsService.setup` initalizes plugins with CoreContext', async () => { test('`PluginsService.setup` exposes dependent setup contracts to plugins', async () => { const pluginsService = new PluginsService(mockCoreContext); - await pluginsService.setup(mockDeps); + await pluginsService.setup(mockSetupDeps); const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; - expect(pluginAInstance.setup).toHaveBeenCalledWith(mockInitContext, {}); - expect(pluginBInstance.setup).toHaveBeenCalledWith(mockInitContext, { - pluginA: { exportedValue: 1 }, + expect(pluginAInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); + expect(pluginBInstance.setup).toHaveBeenCalledWith(mockSetupContext, { + pluginA: { setupValue: 1 }, }); // Does not supply value for `nonexist` optional dep - expect(pluginCInstance.setup).toHaveBeenCalledWith(mockInitContext, { - pluginA: { exportedValue: 1 }, + expect(pluginCInstance.setup).toHaveBeenCalledWith(mockSetupContext, { + pluginA: { setupValue: 1 }, }); }); test('`PluginsService.setup` does not set missing dependent setup contracts', async () => { - mockDeps.injectedMetadata.getPlugins.mockReturnValue([ + mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([ { id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) }, ]); - mockPluginInitializers.set('pluginD', jest.fn(() => ({ setup: jest.fn() })) as any); + mockPluginInitializers.set('pluginD', jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any); const pluginsService = new PluginsService(mockCoreContext); - await pluginsService.setup(mockDeps); + await pluginsService.setup(mockSetupDeps); // If a dependency is missing it should not be in the deps at all, not even as undefined. const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; - expect(pluginDInstance.setup).toHaveBeenCalledWith(mockInitContext, {}); + expect(pluginDInstance.setup).toHaveBeenCalledWith(mockSetupContext, {}); const pluginDDeps = pluginDInstance.setup.mock.calls[0][1]; expect(pluginDDeps).not.toHaveProperty('missing'); }); test('`PluginsService.setup` returns plugin setup contracts', async () => { const pluginsService = new PluginsService(mockCoreContext); - const { contracts } = await pluginsService.setup(mockDeps); + const { contracts } = await pluginsService.setup(mockSetupDeps); // Verify that plugin contracts were available - expect((contracts.get('pluginA')! as any).exportedValue).toEqual(1); + expect((contracts.get('pluginA')! as any).setupValue).toEqual(1); expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2); }); +test('`PluginsService.start` exposes dependent start contracts to plugins', async () => { + const pluginsService = new PluginsService(mockCoreContext); + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + + const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; + const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; + const pluginCInstance = mockPluginInitializers.get('pluginC')!.mock.results[0].value; + + expect(pluginAInstance.start).toHaveBeenCalledWith(mockStartContext, {}); + expect(pluginBInstance.start).toHaveBeenCalledWith(mockStartContext, { + pluginA: { startValue: 2 }, + }); + // Does not supply value for `nonexist` optional dep + expect(pluginCInstance.start).toHaveBeenCalledWith(mockStartContext, { + pluginA: { startValue: 2 }, + }); +}); + +test('`PluginsService.start` does not set missing dependent start contracts', async () => { + mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([ + { id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) }, + ]); + mockPluginInitializers.set('pluginD', jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any); + + const pluginsService = new PluginsService(mockCoreContext); + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + + // If a dependency is missing it should not be in the deps at all, not even as undefined. + const pluginDInstance = mockPluginInitializers.get('pluginD')!.mock.results[0].value; + expect(pluginDInstance.start).toHaveBeenCalledWith(mockStartContext, {}); + const pluginDDeps = pluginDInstance.start.mock.calls[0][1]; + expect(pluginDDeps).not.toHaveProperty('missing'); +}); + +test('`PluginsService.start` returns plugin start contracts', async () => { + const pluginsService = new PluginsService(mockCoreContext); + await pluginsService.setup(mockSetupDeps); + const { contracts } = await pluginsService.start(mockStartDeps); + + // Verify that plugin contracts were available + expect((contracts.get('pluginA')! as any).startValue).toEqual(2); + expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3); +}); + test('`PluginService.stop` calls the stop function on each plugin', async () => { const pluginsService = new PluginsService(mockCoreContext); - await pluginsService.setup(mockDeps); + await pluginsService.setup(mockSetupDeps); const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; const pluginBInstance = mockPluginInitializers.get('pluginB')!.mock.results[0].value; diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 0a157067aef55..5b1ce13252d11 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -17,15 +17,21 @@ * under the License. */ -import { CoreSetup } from '..'; +import { CoreSetup, CoreStart } from '..'; import { PluginName } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; import { PluginWrapper } from './plugin'; -import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context'; +import { + createPluginInitializerContext, + createPluginSetupContext, + createPluginStartContext, +} from './plugin_context'; /** @internal */ export type PluginsServiceSetupDeps = CoreSetup; +/** @internal */ +export type PluginsServiceStartDeps = CoreStart; /** @internal */ export interface PluginsServiceSetup { @@ -98,6 +104,41 @@ export class PluginsService implements CoreService { return { contracts }; } + public async start(deps: PluginsServiceStartDeps) { + // Setup each plugin with required and optional plugin contracts + const contracts = new Map(); + for (const [pluginName, plugin] of this.plugins.entries()) { + const pluginDeps = new Set([ + ...plugin.requiredPlugins, + ...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)), + ]); + + const pluginDepContracts = [...pluginDeps.keys()].reduce( + (depContracts, dependencyName) => { + // Only set if present. Could be absent if plugin does not have client-side code or is a + // missing optional plugin. + if (contracts.has(dependencyName)) { + depContracts[dependencyName] = contracts.get(dependencyName); + } + + return depContracts; + }, + {} as Record + ); + + contracts.set( + pluginName, + await plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ) + ); + } + + // Expose start contracts + return { contracts }; + } + public async stop() { // Stop plugins in reverse topological order. for (const pluginName of this.satupPlugins.reverse()) { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b4d06d150bc76..f50c078a9f351 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -6,7 +6,6 @@ import * as CSS from 'csstype'; import { default } from 'react'; -import { Observable } from 'rxjs'; import * as PropTypes from 'prop-types'; import * as Rx from 'rxjs'; import { Toast } from '@elastic/eui'; @@ -29,7 +28,7 @@ export interface Capabilities { } // @public -export interface CapabilitiesSetup { +export interface CapabilitiesStart { getCapabilities: () => Capabilities; } @@ -66,8 +65,6 @@ export interface CoreSetup { // (undocumented) basePath: BasePathSetup; // (undocumented) - capabilities: CapabilitiesSetup; - // (undocumented) chrome: ChromeSetup; // (undocumented) fatalErrors: FatalErrorsSetup; @@ -80,11 +77,23 @@ export interface CoreSetup { // (undocumented) notifications: NotificationsSetup; // (undocumented) - overlays: OverlaySetup; - // (undocumented) uiSettings: UiSettingsSetup; } +// @public (undocumented) +export interface CoreStart { + // (undocumented) + capabilities: CapabilitiesStart; + // (undocumented) + i18n: I18nStart; + // (undocumented) + injectedMetadata: InjectedMetadataStart; + // (undocumented) + notifications: NotificationsStart; + // (undocumented) + overlays: OverlayStart; +} + // @internal export class CoreSystem { constructor(params: Params); @@ -93,6 +102,8 @@ export class CoreSystem { fatalErrors: import(".").FatalErrorsSetup; } | undefined>; // (undocumented) + start(): Promise; + // (undocumented) stop(): void; } @@ -119,6 +130,9 @@ export interface I18nSetup { }) => JSX.Element; } +// @public (undocumented) +export type I18nStart = I18nSetup; + // @internal (undocumented) export interface InjectedMetadataParams { // (undocumented) @@ -197,10 +211,19 @@ export interface InjectedMetadataSetup { } // @public (undocumented) -export type NotificationsSetup = ReturnType; +export type InjectedMetadataStart = InjectedMetadataSetup; + +// @public (undocumented) +export interface NotificationsSetup { + // (undocumented) + toasts: ToastsApi; +} + +// @public (undocumented) +export type NotificationsStart = NotificationsSetup; // @public (undocumented) -export interface OverlaySetup { +export interface OverlayStart { // (undocumented) openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: { closeButtonAriaLabel?: string; @@ -209,15 +232,17 @@ export interface OverlaySetup { } // @public -export interface Plugin = {}> { +export interface Plugin = {}, TPluginsStart extends Record = {}> { // (undocumented) setup: (core: PluginSetupContext, plugins: TPluginsSetup) => TSetup | Promise; // (undocumented) + start: (core: PluginStartContext, plugins: TPluginsStart) => TStart | Promise; + // (undocumented) stop?: () => void; } // @public -export type PluginInitializer = {}> = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = {}, TPluginsStart extends Record = {}> = (core: PluginInitializerContext) => Plugin; // @public export interface PluginInitializerContext { @@ -245,7 +270,7 @@ export { Toast } export type ToastInput = string | Pick>; // @public (undocumented) -export class ToastsSetup { +export class ToastsApi { // (undocumented) add(toastOrTitle: ToastInput): Toast; // (undocumented) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js index 7179f810db084..1abe8581c54c6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js @@ -18,11 +18,11 @@ */ import { uiModules } from 'ui/modules'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; uiModules.get('kibana') .provider('dashboardConfig', () => { - let hideWriteControls = !uiCapabilities.dashboard.showWriteControls; + let hideWriteControls = !capabilities.get().dashboard.showWriteControls; return { /** diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js index 9984153fd30af..06b6d1d3695f8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js @@ -21,7 +21,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { toastNotifications } from 'ui/notify'; import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; @@ -88,7 +88,7 @@ export class DashboardAddPanel extends React.Component { )} /> - { uiCapabilities.visualize.save ? ( + { capabilities.get().visualize.save ? ( diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js index 8d98fc8cfe808..96147b3852020 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js @@ -27,11 +27,13 @@ import { jest.mock('ui/capabilities', () => ({ - uiCapabilities: { - visualize: { - show: true, - save: true - } + capabilities: { + get: () => ({ + visualize: { + show: true, + save: true + } + }) } }), { virtual: true }); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js index a27f63d89cb46..622133d681861 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js @@ -24,7 +24,7 @@ import 'ui/directives/css_truncate'; import 'ui/directives/field_name'; import './string_progress_bar'; import detailsHtml from './lib/detail_views/string.html'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { uiModules } from 'ui/modules'; const app = uiModules.get('apps/discover'); @@ -77,7 +77,7 @@ app.directive('discoverField', function ($compile, i18n) { }; - $scope.canVisualize = uiCapabilities.visualize.show; + $scope.canVisualize = capabilities.get().visualize.show; $scope.toggleDisplay = function (field) { if (field.display) { diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index fbfa930fe145e..1f041fc09dfc3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -18,7 +18,7 @@ */ import '../doc_table'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { i18n } from '@kbn/i18n'; import { EmbeddableFactory } from 'ui/embeddable'; import { @@ -63,7 +63,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory { onEmbeddableStateChanged: OnEmbeddableStateChanged ) { const editUrl = this.getEditPath(id); - const editable = uiCapabilities.discover.save as boolean; + const editable = capabilities.get().discover.save as boolean; // can't change this to be async / awayt, because an Anglular promise is expected to be returned. return this.searchLoader.get(id).then(savedObject => { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js index 601997290e345..7bae825e8366d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js @@ -20,7 +20,7 @@ import { management } from 'ui/management'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { I18nContext } from 'ui/i18n'; import indexTemplate from './index.html'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; @@ -45,7 +45,7 @@ function updateAdvancedSettings($scope, config, query) { , node, diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index b56085b4a92bd..6b8b3e43de4e6 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -24,7 +24,7 @@ import 'ui/vis/editors/default/sidebar'; import 'ui/visualize'; import 'ui/collapsible_sidebar'; import 'ui/query_bar'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import 'ui/search_bar'; import 'ui/apply_filters'; import 'ui/listen'; @@ -150,7 +150,7 @@ function VisEditor( dirty: !savedVis.id }; - $scope.topNavMenu = [...(uiCapabilities.visualize.save ? [{ + $scope.topNavMenu = [...(capabilities.get().visualize.save ? [{ key: i18n('kbn.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save' }), description: i18n('kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel', { defaultMessage: 'Save Visualization', @@ -215,7 +215,7 @@ function VisEditor( showShareContextMenu({ anchorElement, allowEmbed: true, - allowShortUrl: uiCapabilities.visualize.createShortUrl, + allowShortUrl: capabilities.get().visualize.createShortUrl, getUnhashableStates, objectId: savedVis.id, objectType: 'visualization', diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts index f77d44b8fb1b7..e10cfb3cdb6c7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts @@ -23,7 +23,7 @@ import { EmbeddableFactory } from 'ui/embeddable'; import { getVisualizeLoader } from 'ui/visualize/loader'; import { Legacy } from 'kibana'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { EmbeddableInstanceConfiguration, OnEmbeddableStateChanged, @@ -90,7 +90,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory { ${bundle.getRequires().join('\n ')} } -}).setup() +}) + +coreSystem + .setup() + .then(() => { + return coreSystem.start(); + }); `; diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index a04622ae19ec4..3953a3decfab2 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -19,7 +19,7 @@ import _ from 'lodash'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { DocTitleProvider } from 'ui/doc_title'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { notify, fatalError, toastNotifications } from 'ui/notify'; @@ -269,7 +269,7 @@ app.controller('timelion', function ( testId: 'timelionDocsButton', }; - if (uiCapabilities.timelion.save) { + if (capabilities.get().timelion.save) { return [newSheetAction, addSheetAction, saveSheetAction, deleteSheetAction, openSheetAction, optionsAction, helpAction]; } return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction]; diff --git a/src/legacy/ui/public/capabilities/index.ts b/src/legacy/ui/public/capabilities/index.ts index ca798d2d70ea2..c71deb6fe5891 100644 --- a/src/legacy/ui/public/capabilities/index.ts +++ b/src/legacy/ui/public/capabilities/index.ts @@ -17,15 +17,28 @@ * under the License. */ -import { Capabilities as UICapabilities, CapabilitiesSetup } from '../../../../core/public'; +import { Capabilities as UICapabilities, CapabilitiesStart } from '../../../../core/public'; -export { Capabilities as UICapabilities } from '../../../../core/public'; -export let uiCapabilities: UICapabilities = null!; +export { UICapabilities }; -export function __newPlatformInit__(capabililitiesService: CapabilitiesSetup) { +let uiCapabilities: UICapabilities = null!; + +export function __newPlatformStart__(capabililitiesService: CapabilitiesStart) { if (uiCapabilities) { throw new Error('ui/capabilities already initialized with new platform apis'); } uiCapabilities = capabililitiesService.getCapabilities(); } + +export const capabilities = { + get() { + if (!uiCapabilities) { + throw new Error( + `UI Capabilities are only available in the legacy platform once Angular has booted.` + ); + } + + return uiCapabilities; + }, +}; diff --git a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx index f89112fd82429..dccea2f61d567 100644 --- a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx +++ b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx @@ -18,11 +18,13 @@ */ jest.mock('ui/capabilities', () => ({ - uiCapabilities: { - uiCapability1: true, - uiCapability2: { - nestedProp: 'nestedValue', - }, + capabilities: { + get: () => ({ + uiCapability1: true, + uiCapability2: { + nestedProp: 'nestedValue', + }, + }), }, })); diff --git a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx index 1ee8ba72c6ccb..21f96cf918afd 100644 --- a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx +++ b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx @@ -18,11 +18,13 @@ */ jest.mock('ui/capabilities', () => ({ - uiCapabilities: { - uiCapability1: true, - uiCapability2: { - nestedProp: 'nestedValue', - }, + capabilities: { + get: () => ({ + uiCapability1: true, + uiCapability2: { + nestedProp: 'nestedValue', + }, + }), }, })); diff --git a/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx b/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx index 856665acd65b8..2e1f6add20d09 100644 --- a/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx +++ b/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import React, { ReactNode } from 'react'; -import { uiCapabilities, UICapabilities } from '../..'; +import { capabilities, UICapabilities } from '../..'; interface Props { children: ReactNode; @@ -38,7 +38,7 @@ export class UICapabilitiesProvider extends React.Component { public getChildContext(): ProviderContext { return { - uiCapabilities, + uiCapabilities: capabilities.get(), }; } diff --git a/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx index 42c10004b766b..e9b126daabb4d 100644 --- a/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx +++ b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx @@ -18,7 +18,7 @@ */ import React, { ReactNode } from 'react'; -import { uiCapabilities } from '..'; +import { capabilities } from '..'; import { UICapabilitiesContext } from './ui_capabilities_context'; interface Props { @@ -30,7 +30,7 @@ export class UICapabilitiesProvider extends React.Component { public render() { return ( - + {this.props.children} ); diff --git a/src/legacy/ui/public/chrome/api/base_path.test.ts b/src/legacy/ui/public/chrome/api/base_path.test.ts index 01c4f651f07f1..a69d1d3581164 100644 --- a/src/legacy/ui/public/chrome/api/base_path.test.ts +++ b/src/legacy/ui/public/chrome/api/base_path.test.ts @@ -18,7 +18,7 @@ */ import { basePathServiceMock } from '../../../../../core/public/mocks'; -import { __newPlatformInit__, initChromeBasePathApi } from './base_path'; +import { __newPlatformSetup__, initChromeBasePathApi } from './base_path'; function initChrome() { const chrome: any = {}; @@ -27,7 +27,7 @@ function initChrome() { } const newPlatformBasePath = basePathServiceMock.createSetupContract(); -__newPlatformInit__(newPlatformBasePath); +__newPlatformSetup__(newPlatformBasePath); beforeEach(() => { jest.clearAllMocks(); diff --git a/src/legacy/ui/public/chrome/api/base_path.ts b/src/legacy/ui/public/chrome/api/base_path.ts index f129d7beb30b5..87d637513f55f 100644 --- a/src/legacy/ui/public/chrome/api/base_path.ts +++ b/src/legacy/ui/public/chrome/api/base_path.ts @@ -20,7 +20,7 @@ import { BasePathSetup } from '../../../../../core/public'; let newPlatformBasePath: BasePathSetup; -export function __newPlatformInit__(instance: BasePathSetup) { +export function __newPlatformSetup__(instance: BasePathSetup) { if (newPlatformBasePath) { throw new Error('ui/chrome/api/base_path is already initialized'); } diff --git a/src/legacy/ui/public/chrome/api/breadcrumbs.ts b/src/legacy/ui/public/chrome/api/breadcrumbs.ts index 87201d0e758c1..9c58f265f4131 100644 --- a/src/legacy/ui/public/chrome/api/breadcrumbs.ts +++ b/src/legacy/ui/public/chrome/api/breadcrumbs.ts @@ -23,7 +23,7 @@ export type Breadcrumb = ChromeBreadcrumb; export type BreadcrumbsApi = ReturnType['breadcrumbs']; let newPlatformChrome: ChromeSetup; -export function __newPlatformInit__(instance: ChromeSetup) { +export function __newPlatformSetup__(instance: ChromeSetup) { if (newPlatformChrome) { throw new Error('ui/chrome/api/breadcrumbs is already initialized'); } diff --git a/src/legacy/ui/public/chrome/api/controls.test.ts b/src/legacy/ui/public/chrome/api/controls.test.ts index 0b1fe27cfbaf6..4dcbe59f99e33 100644 --- a/src/legacy/ui/public/chrome/api/controls.test.ts +++ b/src/legacy/ui/public/chrome/api/controls.test.ts @@ -20,11 +20,11 @@ import * as Rx from 'rxjs'; import { chromeServiceMock } from '../../../../../core/public/mocks'; -import { __newPlatformInit__, initChromeControlsApi } from './controls'; +import { __newPlatformSetup__, initChromeControlsApi } from './controls'; const newPlatformChrome = chromeServiceMock.createSetupContract(); -__newPlatformInit__(newPlatformChrome); +__newPlatformSetup__(newPlatformChrome); function setup() { const isVisible$ = new Rx.BehaviorSubject(true); diff --git a/src/legacy/ui/public/chrome/api/controls.ts b/src/legacy/ui/public/chrome/api/controls.ts index 9750f4c5faf47..e36625b03a1ad 100644 --- a/src/legacy/ui/public/chrome/api/controls.ts +++ b/src/legacy/ui/public/chrome/api/controls.ts @@ -22,7 +22,7 @@ import { ChromeSetup } from '../../../../../core/public'; let newPlatformChrome: ChromeSetup; -export function __newPlatformInit__(instance: ChromeSetup) { +export function __newPlatformSetup__(instance: ChromeSetup) { if (newPlatformChrome) { throw new Error('ui/chrome/api/controls is already initialized'); } diff --git a/src/legacy/ui/public/chrome/api/help_extension.ts b/src/legacy/ui/public/chrome/api/help_extension.ts index 2fafae89f2c9e..5bfd1d2d37170 100644 --- a/src/legacy/ui/public/chrome/api/help_extension.ts +++ b/src/legacy/ui/public/chrome/api/help_extension.ts @@ -20,7 +20,7 @@ import { ChromeHelpExtension, ChromeSetup } from '../../../../../core/public'; let newPlatformChrome: ChromeSetup; -export function __newPlatformInit__(instance: ChromeSetup) { +export function __newPlatformSetup__(instance: ChromeSetup) { if (newPlatformChrome) { throw new Error('ui/chrome/api/help_extension is already initialized'); } diff --git a/src/legacy/ui/public/chrome/api/injected_vars.test.ts b/src/legacy/ui/public/chrome/api/injected_vars.test.ts index 70b63176acc52..9fab84e6bc983 100644 --- a/src/legacy/ui/public/chrome/api/injected_vars.test.ts +++ b/src/legacy/ui/public/chrome/api/injected_vars.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { __newPlatformInit__, initChromeInjectedVarsApi } from './injected_vars'; +import { __newPlatformSetup__, initChromeInjectedVarsApi } from './injected_vars'; function initChrome() { const chrome: any = {}; @@ -29,7 +29,7 @@ const newPlatformInjectedMetadata: any = { getInjectedVars: jest.fn(), getInjectedVar: jest.fn(), }; -__newPlatformInit__(newPlatformInjectedMetadata); +__newPlatformSetup__(newPlatformInjectedMetadata); beforeEach(() => { jest.resetAllMocks(); diff --git a/src/legacy/ui/public/chrome/api/injected_vars.ts b/src/legacy/ui/public/chrome/api/injected_vars.ts index 112f8d3d6e3d2..25d3982a0968d 100644 --- a/src/legacy/ui/public/chrome/api/injected_vars.ts +++ b/src/legacy/ui/public/chrome/api/injected_vars.ts @@ -22,7 +22,7 @@ import { InjectedMetadataSetup } from '../../../../../core/public'; let newPlatformInjectedVars: InjectedMetadataSetup; -export function __newPlatformInit__(instance: InjectedMetadataSetup) { +export function __newPlatformSetup__(instance: InjectedMetadataSetup) { if (newPlatformInjectedVars) { throw new Error('ui/chrome/api/injected_vars is already initialized'); } diff --git a/src/legacy/ui/public/chrome/api/loading_count.js b/src/legacy/ui/public/chrome/api/loading_count.js index 979993c76d5a5..0aba5010771e0 100644 --- a/src/legacy/ui/public/chrome/api/loading_count.js +++ b/src/legacy/ui/public/chrome/api/loading_count.js @@ -21,7 +21,7 @@ import * as Rx from 'rxjs'; let newPlatformHttp; -export function __newPlatformInit__(instance) { +export function __newPlatformSetup__(instance) { if (newPlatformHttp) { throw new Error('ui/chrome/api/loading_count already initialized with new platform apis'); } diff --git a/src/legacy/ui/public/chrome/api/theme.test.ts b/src/legacy/ui/public/chrome/api/theme.test.ts index 98ccf26bb6433..7bee075809b80 100644 --- a/src/legacy/ui/public/chrome/api/theme.test.ts +++ b/src/legacy/ui/public/chrome/api/theme.test.ts @@ -20,11 +20,11 @@ import * as Rx from 'rxjs'; import { chromeServiceMock } from '../../../../../core/public/mocks'; -import { __newPlatformInit__, initChromeThemeApi } from './theme'; +import { __newPlatformSetup__, initChromeThemeApi } from './theme'; const newPlatformChrome = chromeServiceMock.createSetupContract(); -__newPlatformInit__(newPlatformChrome); +__newPlatformSetup__(newPlatformChrome); function setup() { const brand$ = new Rx.BehaviorSubject({ logo: 'foo', smallLogo: 'foo' }); diff --git a/src/legacy/ui/public/chrome/api/theme.ts b/src/legacy/ui/public/chrome/api/theme.ts index f89c983b4554d..dbc12636caf81 100644 --- a/src/legacy/ui/public/chrome/api/theme.ts +++ b/src/legacy/ui/public/chrome/api/theme.ts @@ -23,7 +23,7 @@ import { ChromeBrand, ChromeSetup } from '../../../../../core/public'; let newPlatformChrome: ChromeSetup; -export function __newPlatformInit__(instance: ChromeSetup) { +export function __newPlatformSetup__(instance: ChromeSetup) { if (newPlatformChrome) { throw new Error('ui/chrome/api/theme is already initialized'); } diff --git a/src/legacy/ui/public/chrome/api/ui_settings.js b/src/legacy/ui/public/chrome/api/ui_settings.js index 2ca945f0b025f..bce073a643daf 100644 --- a/src/legacy/ui/public/chrome/api/ui_settings.js +++ b/src/legacy/ui/public/chrome/api/ui_settings.js @@ -19,7 +19,7 @@ let newPlatformUiSettingsClient; -export function __newPlatformInit__(instance) { +export function __newPlatformSetup__(instance) { if (newPlatformUiSettingsClient) { throw new Error('ui/chrome/api/ui_settings already initialized'); } diff --git a/src/legacy/ui/public/chrome/services/global_nav_state.js b/src/legacy/ui/public/chrome/services/global_nav_state.js index c168e495edf28..a3ee7492cbf1e 100644 --- a/src/legacy/ui/public/chrome/services/global_nav_state.js +++ b/src/legacy/ui/public/chrome/services/global_nav_state.js @@ -21,7 +21,7 @@ import { distinctUntilChanged } from 'rxjs/operators'; import { uiModules } from '../../modules'; let newPlatformChrome; -export function __newPlatformInit__(instance) { +export function __newPlatformSetup__(instance) { if (newPlatformChrome) { throw new Error('ui/chrome/global_nav_state is already initialized'); } diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx index 198940bc55a9c..284f9cf4784ed 100644 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ b/src/legacy/ui/public/i18n/index.test.tsx @@ -21,11 +21,11 @@ import { render } from 'enzyme'; import PropTypes from 'prop-types'; import React from 'react'; -import { __newPlatformInit__, wrapInI18nContext } from '.'; +import { __newPlatformSetup__, wrapInI18nContext } from '.'; describe('ui/i18n', () => { test('renders children and forwards properties', () => { - __newPlatformInit__(({ children }) =>
Context: {children}
); + __newPlatformSetup__(({ children }) =>
Context: {children}
); const mockPropTypes = { stringProp: PropTypes.string.isRequired, diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx index e3177ea524a6c..afe062a4d76f8 100644 --- a/src/legacy/ui/public/i18n/index.tsx +++ b/src/legacy/ui/public/i18n/index.tsx @@ -25,7 +25,7 @@ import { uiModules } from 'ui/modules'; import { I18nSetup } from '../../../../core/public'; export let I18nContext: I18nSetup['Context'] = null!; -export function __newPlatformInit__(context: typeof I18nContext) { +export function __newPlatformSetup__(context: typeof I18nContext) { if (I18nContext) { throw new Error('ui/i18n already initialized with new platform apis'); } diff --git a/src/legacy/ui/public/inspector/inspector.tsx b/src/legacy/ui/public/inspector/inspector.tsx index 06163edb6e736..6f1b7d46dff1f 100644 --- a/src/legacy/ui/public/inspector/inspector.tsx +++ b/src/legacy/ui/public/inspector/inspector.tsx @@ -73,7 +73,7 @@ function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSess if an inspector can be shown.`); } - return getNewPlatform().setup.core.overlays.openFlyout( + return getNewPlatform().start.core.overlays.openFlyout( , { 'data-test-subj': 'inspectorPanel', diff --git a/src/legacy/ui/public/management/section.js b/src/legacy/ui/public/management/section.js index 76fe4f024f395..d5e70c03d4259 100644 --- a/src/legacy/ui/public/management/section.js +++ b/src/legacy/ui/public/management/section.js @@ -19,7 +19,7 @@ import { assign } from 'lodash'; import { IndexedArray } from '../indexed_array'; -import { uiCapabilities } from '../capabilities'; +import { capabilities } from '../capabilities'; const listeners = []; @@ -56,7 +56,7 @@ export class ManagementSection { get visibleItems() { return this.items.inOrder.filter(item => { - const capabilityManagementSection = uiCapabilities.management[this.id]; + const capabilityManagementSection = capabilities.get().management[this.id]; const itemCapability = capabilityManagementSection ? capabilityManagementSection[item.id] : null; return item.visible && itemCapability !== false; diff --git a/src/legacy/ui/public/management/section.test.js b/src/legacy/ui/public/management/section.test.js index eff92fb307894..a45fca426e2b4 100644 --- a/src/legacy/ui/public/management/section.test.js +++ b/src/legacy/ui/public/management/section.test.js @@ -17,14 +17,16 @@ * under the License. */ jest.mock('ui/capabilities', () => ({ - uiCapabilities: { - navLinks: {}, - management: { - kibana: { - sampleFeature1: true, - sampleFeature2: false, + capabilities: { + get: () => ({ + navLinks: {}, + management: { + kibana: { + sampleFeature1: true, + sampleFeature2: false, + } } - } + }) } })); diff --git a/src/legacy/ui/public/metadata.js b/src/legacy/ui/public/metadata.js index 6701475d947d0..3d04e02f79cb7 100644 --- a/src/legacy/ui/public/metadata.js +++ b/src/legacy/ui/public/metadata.js @@ -19,7 +19,7 @@ export let metadata = null; -export function __newPlatformInit__(legacyMetadata) { +export function __newPlatformSetup__(legacyMetadata) { if (metadata === null) { metadata = legacyMetadata; } else { diff --git a/src/legacy/ui/public/new_platform/index.ts b/src/legacy/ui/public/new_platform/index.ts index 7e2e13f7c2a2a..7e62bfa7eefc6 100644 --- a/src/legacy/ui/public/new_platform/index.ts +++ b/src/legacy/ui/public/new_platform/index.ts @@ -16,4 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -export { __newPlatformInit__, getNewPlatform } from './new_platform'; +export { __newPlatformSetup__, __newPlatformStart__, getNewPlatform } from './new_platform'; diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 400ce8a4371a1..1706d567c616a 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -16,26 +16,39 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup } from '../../../../core/public'; +import { CoreSetup, CoreStart } from '../../../../core/public'; const runtimeContext = { setup: { core: (null as unknown) as CoreSetup, plugins: {}, }, + start: { + core: (null as unknown) as CoreStart, + plugins: {}, + }, }; -export function __newPlatformInit__(core: CoreSetup) { +export function __newPlatformSetup__(core: CoreSetup) { if (runtimeContext.setup.core) { - throw new Error('New platform core api was already initialized'); + throw new Error('New platform core api was already set up'); } runtimeContext.setup.core = core; } +export function __newPlatformStart__(core: CoreStart) { + if (runtimeContext.start.core) { + throw new Error('New platform core api was already started'); + } + + runtimeContext.start.core = core; +} + export function getNewPlatform() { - if (runtimeContext.setup.core === null) { + if (runtimeContext.setup.core === null || runtimeContext.start.core === null) { throw new Error('runtimeContext is not initialized yet'); } + return runtimeContext; } diff --git a/src/legacy/ui/public/notify/fatal_error.ts b/src/legacy/ui/public/notify/fatal_error.ts index ed4fa31ee4639..a837e33b16be0 100644 --- a/src/legacy/ui/public/notify/fatal_error.ts +++ b/src/legacy/ui/public/notify/fatal_error.ts @@ -26,7 +26,7 @@ import { let newPlatformFatalErrors: FatalErrorsSetup; -export function __newPlatformInit__(instance: FatalErrorsSetup) { +export function __newPlatformSetup__(instance: FatalErrorsSetup) { if (newPlatformFatalErrors) { throw new Error('ui/notify/fatal_error already initialized with new platform apis'); } diff --git a/src/legacy/ui/public/notify/toasts/index.ts b/src/legacy/ui/public/notify/toasts/index.ts index 9ba1a1667b61b..c9054cba86d74 100644 --- a/src/legacy/ui/public/notify/toasts/index.ts +++ b/src/legacy/ui/public/notify/toasts/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { toastNotifications, __newPlatformInit__ } from './toasts'; +export { toastNotifications, __newPlatformSetup__ } from './toasts'; export { Toast, ToastInput } from './toast_notifications'; diff --git a/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts b/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts index 2d5f8561d2e9d..0d49e08d9c616 100644 --- a/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts +++ b/src/legacy/ui/public/notify/toasts/toast_notifications.test.ts @@ -18,14 +18,14 @@ */ import sinon from 'sinon'; -import { ToastsSetup } from '../../../../../core/public'; +import { ToastsApi } from '../../../../../core/public'; import { ToastNotifications } from './toast_notifications'; describe('ToastNotifications', () => { describe('interface', () => { function setup() { - return { toastNotifications: new ToastNotifications(new ToastsSetup()) }; + return { toastNotifications: new ToastNotifications(new ToastsApi()) }; } describe('add method', () => { diff --git a/src/legacy/ui/public/notify/toasts/toast_notifications.ts b/src/legacy/ui/public/notify/toasts/toast_notifications.ts index a894f056b1777..6171cf2c0e985 100644 --- a/src/legacy/ui/public/notify/toasts/toast_notifications.ts +++ b/src/legacy/ui/public/notify/toasts/toast_notifications.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Toast, ToastInput, ToastsSetup } from '../../../../../core/public'; +import { Toast, ToastInput, ToastsApi } from '../../../../../core/public'; export { Toast, ToastInput }; @@ -26,7 +26,7 @@ export class ToastNotifications { private onChangeCallback?: () => void; - constructor(private readonly toasts: ToastsSetup) { + constructor(private readonly toasts: ToastsApi) { toasts.get$().subscribe(list => { this.list = list; diff --git a/src/legacy/ui/public/notify/toasts/toasts.ts b/src/legacy/ui/public/notify/toasts/toasts.ts index 27d23c38a92ef..0574bf228cf28 100644 --- a/src/legacy/ui/public/notify/toasts/toasts.ts +++ b/src/legacy/ui/public/notify/toasts/toasts.ts @@ -17,12 +17,12 @@ * under the License. */ -import { ToastsSetup } from '../../../../../core/public'; +import { ToastsApi } from '../../../../../core/public'; import { ToastNotifications } from './toast_notifications'; export let toastNotifications: ToastNotifications; -export function __newPlatformInit__(toasts: ToastsSetup) { +export function __newPlatformSetup__(toasts: ToastsApi) { if (toastNotifications) { throw new Error('ui/notify/toasts already initialized with new platform apis'); } diff --git a/src/legacy/ui/public/registry/feature_catalogue.js b/src/legacy/ui/public/registry/feature_catalogue.js index e9b8742f94b01..0eb8a4a334521 100644 --- a/src/legacy/ui/public/registry/feature_catalogue.js +++ b/src/legacy/ui/public/registry/feature_catalogue.js @@ -18,7 +18,7 @@ */ import { uiRegistry } from './_registry'; -import { uiCapabilities } from '../capabilities'; +import { capabilities } from '../capabilities'; export const FeatureCatalogueRegistryProvider = uiRegistry({ name: 'featureCatalogue', @@ -26,7 +26,7 @@ export const FeatureCatalogueRegistryProvider = uiRegistry({ group: ['category'], order: ['title'], filter: featureCatalogItem => { - const isDisabledViaCapabilities = uiCapabilities.catalogue[featureCatalogItem.id] === false; + const isDisabledViaCapabilities = capabilities.get().catalogue[featureCatalogItem.id] === false; return !isDisabledViaCapabilities && Object.keys(featureCatalogItem).length > 0; } }); diff --git a/src/legacy/ui/public/registry/feature_catalogue.test.js b/src/legacy/ui/public/registry/feature_catalogue.test.js index bf97fbd93c15b..89829799a2461 100644 --- a/src/legacy/ui/public/registry/feature_catalogue.test.js +++ b/src/legacy/ui/public/registry/feature_catalogue.test.js @@ -17,14 +17,16 @@ * under the License. */ jest.mock('ui/capabilities', () => ({ - uiCapabilities: { - navLinks: {}, - management: {}, - catalogue: { - item1: true, - item2: false, - item3: true, - }, + capabilities: { + get: () => ({ + navLinks: {}, + management: {}, + catalogue: { + item1: true, + item2: false, + item3: true, + }, + }), } })); import { FeatureCatalogueCategory, FeatureCatalogueRegistryProvider } from './feature_catalogue'; diff --git a/src/legacy/ui/ui_bundles/app_entry_template.js b/src/legacy/ui/ui_bundles/app_entry_template.js index 6ae72bf284948..4d8ef81f02646 100644 --- a/src/legacy/ui/ui_bundles/app_entry_template.js +++ b/src/legacy/ui/ui_bundles/app_entry_template.js @@ -62,6 +62,8 @@ i18n.load(injectedMetadata.i18n.translationsUrl) if (i18nError) { coreSetup.fatalErrors.add(i18nError); } + + return coreSystem.start(); }); }); `; diff --git a/src/plugins/testbed/public/index.ts b/src/plugins/testbed/public/index.ts index 2c93b3aaa0859..44eea308a31d9 100644 --- a/src/plugins/testbed/public/index.ts +++ b/src/plugins/testbed/public/index.ts @@ -18,6 +18,7 @@ */ import { PluginInitializer } from 'kibana/public'; -import { TestbedPlugin, TestbedPluginSetup } from './plugin'; +import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin'; -export const plugin: PluginInitializer = () => new TestbedPlugin(); +export const plugin: PluginInitializer = () => + new TestbedPlugin(); diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts index d682acceb4b32..ad187f4d3b92e 100644 --- a/src/plugins/testbed/public/plugin.ts +++ b/src/plugins/testbed/public/plugin.ts @@ -19,11 +19,17 @@ import { Plugin, PluginSetupContext } from 'kibana/public'; -export class TestbedPlugin implements Plugin { +export class TestbedPlugin implements Plugin { public setup(core: PluginSetupContext, deps: {}) { // eslint-disable-next-line no-console - console.log(`Testbed plugin loaded`); + console.log(`Testbed plugin set up`); + } + + public start() { + // eslint-disable-next-line no-console + console.log(`Testbed plugin started`); } } export type TestbedPluginSetup = ReturnType; +export type TestbedPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_plugin_a/public/index.ts b/test/plugin_functional/plugins/core_plugin_a/public/index.ts index 065464c1797eb..ec46f8c6d3ef9 100644 --- a/test/plugin_functional/plugins/core_plugin_a/public/index.ts +++ b/test/plugin_functional/plugins/core_plugin_a/public/index.ts @@ -18,6 +18,7 @@ */ import { PluginInitializer } from 'kibana/public'; -import { CorePluginAPlugin, CorePluginAPluginSetup } from './plugin'; +import { CorePluginAPlugin, CorePluginAPluginSetup, CorePluginAPluginStart } from './plugin'; -export const plugin: PluginInitializer = () => new CorePluginAPlugin(); +export const plugin: PluginInitializer = () => + new CorePluginAPlugin(); diff --git a/test/plugin_functional/plugins/core_plugin_a/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_a/public/plugin.tsx index 23b34e156a614..9a133b7d2b2dc 100644 --- a/test/plugin_functional/plugins/core_plugin_a/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_a/public/plugin.tsx @@ -19,7 +19,7 @@ import { Plugin, PluginSetupContext } from 'kibana/public'; -export class CorePluginAPlugin implements Plugin { +export class CorePluginAPlugin implements Plugin { public setup(core: PluginSetupContext, deps: {}) { return { getGreeting() { @@ -27,6 +27,9 @@ export class CorePluginAPlugin implements Plugin { }, }; } + + public start() {} } export type CorePluginAPluginSetup = ReturnType; +export type CorePluginAPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_plugin_b/public/index.ts b/test/plugin_functional/plugins/core_plugin_b/public/index.ts index 73d930054fa59..db3b5c42a964f 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/index.ts +++ b/test/plugin_functional/plugins/core_plugin_b/public/index.ts @@ -18,7 +18,15 @@ */ import { PluginInitializer } from 'kibana/public'; -import { CorePluginBDeps, CorePluginBPlugin, CorePluginBPluginSetup } from './plugin'; +import { + CorePluginBDeps, + CorePluginBPlugin, + CorePluginBPluginSetup, + CorePluginBPluginStart, +} from './plugin'; -export const plugin: PluginInitializer = () => - new CorePluginBPlugin(); +export const plugin: PluginInitializer< + CorePluginBPluginSetup, + CorePluginBPluginStart, + CorePluginBDeps +> = () => new CorePluginBPlugin(); diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index c1d1094a32569..616d8a8a07943 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -30,10 +30,14 @@ export interface CorePluginBDeps { core_plugin_a: CorePluginAPluginSetup; } -export class CorePluginBPlugin implements Plugin { +export class CorePluginBPlugin + implements Plugin { public setup(core: PluginSetupContext, deps: CorePluginBDeps) { window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; } + + public start() {} } export type CorePluginBPluginSetup = ReturnType; +export type CorePluginBPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx index 997c84ebbe051..3f04adc11a418 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx @@ -38,7 +38,7 @@ class SamplePanelAction extends ContextMenuAction { if (!embeddable) { return; } - getNewPlatform().setup.core.overlays.openFlyout( + getNewPlatform().start.core.overlays.openFlyout( diff --git a/x-pack/plugins/__mocks__/ui/capabilities.ts b/x-pack/plugins/__mocks__/ui/capabilities.ts index 5de2c6144fc6c..2725ee428778b 100644 --- a/x-pack/plugins/__mocks__/ui/capabilities.ts +++ b/x-pack/plugins/__mocks__/ui/capabilities.ts @@ -15,14 +15,17 @@ let internals: UICapabilities = { }, }; -export const uiCapabilities = new Proxy( - {}, - { - get: (target, property) => { - return internals[String(property)] as any; - }, - } -); +export const capabilities = { + get: () => + new Proxy( + {}, + { + get: (target, property) => { + return internals[String(property)] as any; + }, + } + ), +}; export function setMockCapabilities(mockCapabilities: UICapabilities) { internals = mockCapabilities; diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index bb814f8c30d99..0c03762cc56d3 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = path => { @@ -13,7 +13,7 @@ export const getInitialState = path => { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - canUserWrite: uiCapabilities.canvas.save, + canUserWrite: capabilities.get().canvas.save, elementStats: { total: 0, ready: 0, diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index efd29820bac6c..3711fe2fff250 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -47,7 +47,7 @@ import { import { getOutlinkEncoders, } from './services/outlink_encoders'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; const app = uiModules.get('app/graph'); @@ -804,7 +804,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private // if saving is disabled using uiCapabilities, we don't want to render the save // button so it's consistent with all of the other applications - if (uiCapabilities.graph.save) { + if (capabilities.get().graph.save) { // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality if (!$scope.allSavingDisabled) { $scope.topNavMenu.push({ @@ -855,7 +855,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private }); // if deleting is disabled using uiCapabilities, we don't want to render the delete // button so it's consistent with all of the other applications - if (uiCapabilities.graph.delete) { + if (capabilities.get().graph.delete) { // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality if (!$scope.allSavingDisabled) { diff --git a/x-pack/plugins/maps/public/angular/map_controller.js b/x-pack/plugins/maps/public/angular/map_controller.js index 00b5a1005f6fc..7603ecdc44aa5 100644 --- a/x-pack/plugins/maps/public/angular/map_controller.js +++ b/x-pack/plugins/maps/public/angular/map_controller.js @@ -10,7 +10,7 @@ import 'ui/listen'; import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { render, unmountComponentAtNode } from 'react-dom'; import { uiModules } from 'ui/modules'; import { timefilter } from 'ui/timefilter'; @@ -127,7 +127,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage // clear old UI state store.dispatch(setSelectedLayer(null)); store.dispatch(updateFlyout(FLYOUT_STATE.NONE)); - store.dispatch(setReadOnly(!uiCapabilities.maps.save)); + store.dispatch(setReadOnly(!capabilities.get().maps.save)); handleStoreChanges(store); unsubscribe = store.subscribe(() => { @@ -297,7 +297,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage const inspectorAdapters = getInspectorAdapters(store.getState()); Inspector.open(inspectorAdapters, {}); } - }, ...(uiCapabilities.maps.save ? [{ + }, ...(capabilities.get().maps.save ? [{ key: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', { defaultMessage: `save` }), diff --git a/x-pack/plugins/maps/public/index.js b/x-pack/plugins/maps/public/index.js index 532bbd09dbd8d..ede38cfc28938 100644 --- a/x-pack/plugins/maps/public/index.js +++ b/x-pack/plugins/maps/public/index.js @@ -14,7 +14,7 @@ import 'uiExports/search'; import 'uiExports/embeddableFactories'; import 'ui/agg_types'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import chrome from 'ui/chrome'; import routes from 'ui/routes'; import 'ui/kbn_top_nav'; @@ -53,7 +53,7 @@ routes $scope.delete = (ids) => { return gisMapSavedObjectLoader.delete(ids); }; - $scope.readOnly = !uiCapabilities.maps.save; + $scope.readOnly = !capabilities.get().maps.save; }, resolve: { hasMaps: function (kbnUrl) { diff --git a/x-pack/plugins/security/public/views/management/edit_role/index.js b/x-pack/plugins/security/public/views/management/edit_role/index.js index 4f965ef718697..449ad975cd6b1 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/plugins/security/public/views/management/edit_role/index.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import routes from 'ui/routes'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { kfetch } from 'ui/kfetch'; import { fatalError } from 'ui/notify'; import template from 'plugins/security/views/management/edit_role/edit_role.html'; @@ -146,7 +146,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, { allowFieldLevelSecurity={allowFieldLevelSecurity} spaces={spaces} spacesEnabled={enableSpaceAwarePrivileges} - uiCapabilities={uiCapabilities} + uiCapabilities={capabilities.get()} features={features} privileges={privileges} /> diff --git a/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx b/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx index 3fe340cfba98f..5f05bc74ec86e 100644 --- a/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx @@ -7,7 +7,7 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, CSSProperties } from 'react'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { MANAGE_SPACES_URL } from '../lib/constants'; interface Props { @@ -20,7 +20,7 @@ interface Props { export class ManageSpacesButton extends Component { public render() { - if (!uiCapabilities.spaces.manage) { + if (!capabilities.get().spaces.manage) { return null; } diff --git a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx index c87e081bc6676..20e0ba587ac32 100644 --- a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx @@ -7,10 +7,10 @@ import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; export const SecureSpaceMessage = ({}) => { - if (uiCapabilities.spaces.manage) { + if (capabilities.get().spaces.manage) { return ( diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx index 2f0c7bbb339f4..9e368ec60316e 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx @@ -18,7 +18,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import { SpacesNavState } from 'plugins/spaces/views/nav_control'; import React, { Component, Fragment } from 'react'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; import { Breadcrumb } from 'ui/chrome'; // @ts-ignore import { toastNotifications } from 'ui/notify'; @@ -132,7 +132,7 @@ class ManageSpacePageUI extends Component { }; public getForm = () => { - if (!uiCapabilities.spaces.manage) { + if (!capabilities.get().spaces.manage) { return ; } @@ -166,7 +166,7 @@ class ManageSpacePageUI extends Component { diff --git a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx index 1cc498a041e56..079da733963ef 100644 --- a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx @@ -20,7 +20,7 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { uiCapabilities } from 'ui/capabilities'; +import { capabilities } from 'ui/capabilities'; // @ts-ignore import { toastNotifications } from 'ui/notify'; import { Feature } from '../../../../../xpack_main/types'; @@ -80,7 +80,7 @@ class SpacesGridPageUI extends Component { public getPageContent() { const { intl } = this.props; - if (!uiCapabilities.spaces.manage) { + if (!capabilities.get().spaces.manage) { return ; }