From 1774abb1f5b88ea63fc929d0675293e261002c2d Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 26 Jul 2019 14:20:49 -0500 Subject: [PATCH 01/14] Add core-only bundle --- .../ui/ui_bundles/ui_bundles_controller.js | 7 +++++ src/legacy/ui/ui_render/ui_render_mixin.js | 30 +++++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/legacy/ui/ui_bundles/ui_bundles_controller.js b/src/legacy/ui/ui_bundles/ui_bundles_controller.js index a4521268ea121c..7041d54d8804c9 100644 --- a/src/legacy/ui/ui_bundles/ui_bundles_controller.js +++ b/src/legacy/ui/ui_bundles/ui_bundles_controller.js @@ -81,6 +81,13 @@ export class UiBundlesController { this._postLoaders = []; this._bundles = []; + // create a bundle for core-only with no modules + this.add({ + id: 'core', + modules: [], + template: appEntryTemplate + }); + // create a bundle for each uiApp for (const uiApp of uiApps) { this.add({ diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 47d13184bfd0a1..998bcbc8f4f179 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -102,9 +102,7 @@ export function uiRenderMixin(kbnServer, server, config) { async handler(request, h) { const { id } = request.params; const app = server.getUiAppById(id) || server.getHiddenUiAppById(id); - if (!app) { - throw Boom.notFound(`Unknown app: ${id}`); - } + const isCore = !app; const uiSettings = request.getUiSettingsService(); const darkMode = !authEnabled || request.auth.isAuthenticated @@ -130,7 +128,9 @@ export function uiRenderMixin(kbnServer, server, config) { ), `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, `${regularBundlePath}/commons.style.css`, - `${regularBundlePath}/${app.getId()}.style.css`, + ...( + !isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : [] + ), ...kbnServer.uiExports.styleSheetPaths .filter(path => ( path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light') @@ -145,7 +145,7 @@ export function uiRenderMixin(kbnServer, server, config) { const bootstrap = new AppBootstrap({ templateData: { - appId: app.getId(), + appId: isCore ? 'core' : app.getId(), regularBundlePath, dllBundlePath, styleSheetPaths, @@ -169,7 +169,6 @@ export function uiRenderMixin(kbnServer, server, config) { async handler(req, h) { const id = req.params.id; const app = server.getUiAppById(id); - if (!app) throw Boom.notFound('Unknown app ' + id); try { if (kbnServer.status.isGreen()) { @@ -183,9 +182,15 @@ export function uiRenderMixin(kbnServer, server, config) { } }); - async function getLegacyKibanaPayload({ app, translations, request, includeUserProvidedConfig }) { + async function getUiSettings({ request, includeUserProvidedConfig }) { const uiSettings = request.getUiSettingsService(); + return props({ + defaults: uiSettings.getDefaults(), + user: includeUserProvidedConfig && uiSettings.getUserProvided() + }); + } + async function getLegacyKibanaPayload({ app, translations, request, includeUserProvidedConfig }) { return { app, translations, @@ -198,16 +203,15 @@ export function uiRenderMixin(kbnServer, server, config) { basePath: request.getBasePath(), serverName: config.get('server.name'), devMode: config.get('env.dev'), - uiSettings: await props({ - defaults: uiSettings.getDefaults(), - user: includeUserProvidedConfig && uiSettings.getUserProvided() - }) + uiSettings: await getUiSettings({ request, includeUserProvidedConfig }), }; } async function renderApp({ app, h, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) { const request = h.request; const basePath = request.getBasePath(); + const uiSettings = await getUiSettings({ request, includeUserProvidedConfig }); + app = app || { getId: () => 'core' }; const legacyMetadata = await getLegacyKibanaPayload({ app, @@ -228,7 +232,7 @@ export function uiRenderMixin(kbnServer, server, config) { bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`, i18n: (id, options) => i18n.translate(id, options), locale: i18n.getLocale(), - darkMode: get(legacyMetadata.uiSettings.user, ['theme:darkMode', 'userValue'], false), + darkMode: get(uiSettings.user, ['theme:darkMode', 'userValue'], false), injectedMetadata: { version: kbnServer.version, @@ -245,7 +249,7 @@ export function uiRenderMixin(kbnServer, server, config) { request, mergeVariables( injectedVarsOverrides, - await server.getInjectedUiAppVars(app.getId()), + app ? await server.getInjectedUiAppVars(app.getId()) : {}, defaultInjectedVars, ), ), From 5ec6416ea587baedb355dff34d2b4ed94d26a205 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 29 Jul 2019 13:35:36 -0500 Subject: [PATCH 02/14] Add ApplicationService mounting --- .../core/public/kibana-plugin-public.app.md | 20 ++ .../public/kibana-plugin-public.app.mount.md | 13 + ...bana-plugin-public.appbase.capabilities.md | 13 + ...ibana-plugin-public.appbase.euiicontype.md | 13 + .../kibana-plugin-public.appbase.icon.md | 13 + .../public/kibana-plugin-public.appbase.id.md | 11 + .../public/kibana-plugin-public.appbase.md | 25 ++ .../kibana-plugin-public.appbase.order.md | 13 + .../kibana-plugin-public.appbase.title.md | 13 + .../kibana-plugin-public.appbase.tooltip$.md | 13 + .../kibana-plugin-public.applicationsetup.md | 3 +- ...lugin-public.applicationsetup.register.md} | 6 +- ...c.applicationsetup.registermountcontext.md | 25 ++ ...n-public.applicationstart.availableapps.md | 13 - .../kibana-plugin-public.applicationstart.md | 8 +- ...n-public.applicationstart.navigatetoapp.md | 28 ++ ...c.applicationstart.registermountcontext.md | 25 ++ ...bana-plugin-public.appmountcontext.core.md | 22 ++ .../kibana-plugin-public.appmountcontext.md | 20 ++ ...n-public.appmountparameters.appbasepath.md | 53 ++++ ...lugin-public.appmountparameters.element.md | 13 + ...kibana-plugin-public.appmountparameters.md | 20 ++ .../public/kibana-plugin-public.appunmount.md | 13 + ...ibana-plugin-public.chromenavlink.order.md | 2 +- ...ana-plugin-public.coresetup.application.md | 13 + .../public/kibana-plugin-public.coresetup.md | 1 + ...ana-plugin-public.corestart.application.md | 2 +- .../public/kibana-plugin-public.corestart.md | 2 +- .../core/public/kibana-plugin-public.md | 5 + package.json | 2 + renovate.json5 | 16 +- .../text/0004_application_service_mounting.md | 67 ++-- .../application_service.test.tsx.snap | 42 +++ .../application/application_service.mock.ts | 41 ++- .../application_service.test.mocks.ts | 8 + .../application/application_service.test.tsx | 212 +++++++++++-- .../application/application_service.tsx | 230 +++++++------- .../capabilities/capabilities_service.mock.ts | 6 +- .../capabilities/capabilities_service.test.ts | 20 +- .../capabilities/capabilities_service.tsx | 27 +- src/core/public/application/index.ts | 14 +- src/core/public/application/types.ts | 293 ++++++++++++++++++ .../public/application/ui/app_container.tsx | 115 +++++++ .../application/ui/app_not_found_screen.tsx | 51 +++ src/core/public/application/ui/app_router.tsx | 53 ++++ src/core/public/application/ui/index.ts | 20 ++ .../application/ui/ui_integration.test.tsx | 131 ++++++++ src/core/public/chrome/chrome_service.test.ts | 2 +- src/core/public/chrome/chrome_service.tsx | 14 +- src/core/public/chrome/nav_links/nav_link.ts | 10 +- .../nav_links/nav_links_service.test.ts | 31 +- .../chrome/nav_links/nav_links_service.ts | 24 +- src/core/public/chrome/ui/header/header.tsx | 29 +- src/core/public/core_system.test.ts | 4 +- src/core/public/core_system.ts | 35 ++- src/core/public/http/http_service.mock.ts | 19 +- src/core/public/index.ts | 15 +- .../injected_metadata_service.mock.ts | 2 + .../injected_metadata_service.ts | 7 + src/core/public/legacy/legacy_service.test.ts | 19 ++ src/core/public/legacy/legacy_service.ts | 40 ++- src/core/public/mocks.ts | 1 + src/core/public/plugins/plugin_context.ts | 8 + .../public/plugins/plugins_service.test.ts | 11 +- src/core/public/public.api.md | 75 ++++- .../rendering/rendering_service.test.tsx | 10 +- .../public/rendering/rendering_service.tsx | 26 +- src/core/utils/context.mock.ts | 4 +- src/core/utils/pick.ts | 5 +- .../tests_bundle/tests_entry_template.js | 1 + src/legacy/ui/public/chrome/chrome.js | 1 - .../ui/public/chrome/directives/kbn_chrome.js | 24 +- src/legacy/ui/ui_render/ui_render_mixin.js | 3 +- yarn.lock | 4 +- 74 files changed, 1868 insertions(+), 325 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.app.md create mode 100644 docs/development/core/public/kibana-plugin-public.app.mount.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.capabilities.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.euiicontype.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.icon.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.id.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.order.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.title.md create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.tooltip$.md rename docs/development/core/public/{kibana-plugin-public.applicationsetup.registerapp.md => kibana-plugin-public.applicationsetup.register.md} (72%) create mode 100644 docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md delete mode 100644 docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md create mode 100644 docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md create mode 100644 docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md create mode 100644 docs/development/core/public/kibana-plugin-public.appmountcontext.core.md create mode 100644 docs/development/core/public/kibana-plugin-public.appmountcontext.md create mode 100644 docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md create mode 100644 docs/development/core/public/kibana-plugin-public.appmountparameters.element.md create mode 100644 docs/development/core/public/kibana-plugin-public.appmountparameters.md create mode 100644 docs/development/core/public/kibana-plugin-public.appunmount.md create mode 100644 docs/development/core/public/kibana-plugin-public.coresetup.application.md create mode 100644 src/core/public/application/__snapshots__/application_service.test.tsx.snap create mode 100644 src/core/public/application/types.ts create mode 100644 src/core/public/application/ui/app_container.tsx create mode 100644 src/core/public/application/ui/app_not_found_screen.tsx create mode 100644 src/core/public/application/ui/app_router.tsx create mode 100644 src/core/public/application/ui/index.ts create mode 100644 src/core/public/application/ui/ui_integration.test.tsx diff --git a/docs/development/core/public/kibana-plugin-public.app.md b/docs/development/core/public/kibana-plugin-public.app.md new file mode 100644 index 00000000000000..317d70e8045290 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.app.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [App](./kibana-plugin-public.app.md) + +## App interface + +Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. + +Signature: + +```typescript +export interface App extends AppBase +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [mount](./kibana-plugin-public.app.mount.md) | (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount> | A mount function called when the user navigates to this app's rootRoute. | + diff --git a/docs/development/core/public/kibana-plugin-public.app.mount.md b/docs/development/core/public/kibana-plugin-public.app.mount.md new file mode 100644 index 00000000000000..fc2debd50fff87 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.app.mount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [App](./kibana-plugin-public.app.md) > [mount](./kibana-plugin-public.app.mount.md) + +## App.mount property + +A mount function called when the user navigates to this app's `rootRoute`. + +Signature: + +```typescript +mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.capabilities.md b/docs/development/core/public/kibana-plugin-public.appbase.capabilities.md new file mode 100644 index 00000000000000..450972e41bb299 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.capabilities.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [capabilities](./kibana-plugin-public.appbase.capabilities.md) + +## AppBase.capabilities property + +Custom capabilities defined by the app. + +Signature: + +```typescript +capabilities?: Partial; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.euiicontype.md b/docs/development/core/public/kibana-plugin-public.appbase.euiicontype.md new file mode 100644 index 00000000000000..99c7e852ff9052 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.euiicontype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) + +## AppBase.euiIconType property + +A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. + +Signature: + +```typescript +euiIconType?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.icon.md b/docs/development/core/public/kibana-plugin-public.appbase.icon.md new file mode 100644 index 00000000000000..d94d0897bc5b75 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.icon.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [icon](./kibana-plugin-public.appbase.icon.md) + +## AppBase.icon property + +A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided. + +Signature: + +```typescript +icon?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.id.md b/docs/development/core/public/kibana-plugin-public.appbase.id.md new file mode 100644 index 00000000000000..57daa0c94bdf6b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [id](./kibana-plugin-public.appbase.id.md) + +## AppBase.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.md b/docs/development/core/public/kibana-plugin-public.appbase.md new file mode 100644 index 00000000000000..338d30e780aaf0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) + +## AppBase interface + + +Signature: + +```typescript +export interface AppBase +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [capabilities](./kibana-plugin-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | +| [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [icon](./kibana-plugin-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | +| [id](./kibana-plugin-public.appbase.id.md) | string | | +| [order](./kibana-plugin-public.appbase.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [title](./kibana-plugin-public.appbase.title.md) | string | The title of the application. | +| [tooltip$](./kibana-plugin-public.appbase.tooltip$.md) | Observable<string> | An observable for a tooltip shown when hovering over app link. | + diff --git a/docs/development/core/public/kibana-plugin-public.appbase.order.md b/docs/development/core/public/kibana-plugin-public.appbase.order.md new file mode 100644 index 00000000000000..dc0ea14a7b860b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.order.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [order](./kibana-plugin-public.appbase.order.md) + +## AppBase.order property + +An ordinal used to sort nav links relative to one another for display. + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.title.md b/docs/development/core/public/kibana-plugin-public.appbase.title.md new file mode 100644 index 00000000000000..4d0fb0c18e8143 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.title.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [title](./kibana-plugin-public.appbase.title.md) + +## AppBase.title property + +The title of the application. + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.tooltip$.md b/docs/development/core/public/kibana-plugin-public.appbase.tooltip$.md new file mode 100644 index 00000000000000..1b8ca490825f92 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.tooltip$.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [tooltip$](./kibana-plugin-public.appbase.tooltip$.md) + +## AppBase.tooltip$ property + +An observable for a tooltip shown when hovering over app link. + +Signature: + +```typescript +tooltip$?: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.md index a3ab77e43446c9..d7ed7bad20ceaa 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.md @@ -15,5 +15,6 @@ export interface ApplicationSetup | Method | Description | | --- | --- | -| [registerApp(app)](./kibana-plugin-public.applicationsetup.registerapp.md) | Register an mountable application to the system. Apps will be mounted based on their rootRoute. | +| [register(app)](./kibana-plugin-public.applicationsetup.register.md) | Register an mountable application to the system. Apps will be mounted based on their rootRoute. | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md similarity index 72% rename from docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md rename to docs/development/core/public/kibana-plugin-public.applicationsetup.register.md index f2532ae71ca2f6..a24eb19c2ea6bb 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md @@ -1,15 +1,15 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerApp](./kibana-plugin-public.applicationsetup.registerapp.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [register](./kibana-plugin-public.applicationsetup.register.md) -## ApplicationSetup.registerApp() method +## ApplicationSetup.register() method Register an mountable application to the system. Apps will be mounted based on their `rootRoute`. Signature: ```typescript -registerApp(app: App): void; +register(app: App): void; ``` ## Parameters diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md new file mode 100644 index 00000000000000..cda400df6d0294 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerMountContext](./kibana-plugin-public.applicationsetup.registermountcontext.md) + +## ApplicationSetup.registerMountContext() method + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. + +Signature: + +```typescript +registerMountContext(contextName: T, provider: IContextProvider): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | +| provider | IContextProvider<AppMountContext, keyof AppMountContext> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md b/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md deleted file mode 100644 index 8bbd1dfcd31fad..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) - -## ApplicationStart.availableApps property - -Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions. - -Signature: - -```typescript -availableApps: readonly App[]; -``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 5854a7c65714eb..2d1450dce940e7 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -15,6 +15,12 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | -| [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) | readonly App[] | Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions. | | [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | +## Methods + +| Method | Description | +| --- | --- | +| [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigiate to a given app | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md new file mode 100644 index 00000000000000..eef31fe661f54f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [navigateToApp](./kibana-plugin-public.applicationstart.navigatetoapp.md) + +## ApplicationStart.navigateToApp() method + +Navigiate to a given app + +Signature: + +```typescript +navigateToApp(appId: string, options?: { + path?: string; + state?: any; + }): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | | +| options | {
path?: string;
state?: any;
} | | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md new file mode 100644 index 00000000000000..fc86aaf658b681 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [registerMountContext](./kibana-plugin-public.applicationstart.registermountcontext.md) + +## ApplicationStart.registerMountContext() method + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. + +Signature: + +```typescript +registerMountContext(contextName: T, provider: IContextProvider): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | +| provider | IContextProvider<AppMountContext, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md new file mode 100644 index 00000000000000..63b3ead814f003 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountContext](./kibana-plugin-public.appmountcontext.md) > [core](./kibana-plugin-public.appmountcontext.core.md) + +## AppMountContext.core property + +Core service APIs available to mounted applications. + +Signature: + +```typescript +core: { + application: Pick; + chrome: ChromeStart; + docLinks: DocLinksStart; + http: HttpStart; + i18n: I18nStart; + notifications: NotificationsStart; + overlays: OverlayStart; + uiSettings: UiSettingsClientContract; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.md new file mode 100644 index 00000000000000..c6541e3eca392f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountContext](./kibana-plugin-public.appmountcontext.md) + +## AppMountContext interface + +The context object received when applications are mounted to the DOM. + +Signature: + +```typescript +export interface AppMountContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [core](./kibana-plugin-public.appmountcontext.core.md) | {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
uiSettings: UiSettingsClientContract;
} | Core service APIs available to mounted applications. | + diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md new file mode 100644 index 00000000000000..c8ddf12e7c318d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md @@ -0,0 +1,53 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md) + +## AppMountParameters.appBasePath property + +The base path for configuring the application's router. + +Signature: + +```typescript +appBasePath: string; +``` + +## Example + +How to configure react-router with a base path: + +```ts +// inside your plugin's setup function +export class MyPlugin implements Plugin { + setup({ application }) { + application.register({ + id: 'my-app', + async mount(context, params) { + const { renderApp } = await import('./applcation'); + return renderApp(context, params); + }, + }); +} + +``` + +```ts +// application.tsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, Route } from 'react-router-dom'; + +export renderApp = (context, { appBasePath, element }) => { + ReactDOM.render( + // pass `appBasePath` to `basename` + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +} + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.element.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.element.md new file mode 100644 index 00000000000000..dbe496c01c2150 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.element.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [element](./kibana-plugin-public.appmountparameters.element.md) + +## AppMountParameters.element property + +The container element to render the application into. + +Signature: + +```typescript +element: HTMLElement; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.md new file mode 100644 index 00000000000000..8733f9cd4915d3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) + +## AppMountParameters interface + + +Signature: + +```typescript +export interface AppMountParameters +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md) | string | The base path for configuring the application's router. | +| [element](./kibana-plugin-public.appmountparameters.element.md) | HTMLElement | The container element to render the application into. | + diff --git a/docs/development/core/public/kibana-plugin-public.appunmount.md b/docs/development/core/public/kibana-plugin-public.appunmount.md new file mode 100644 index 00000000000000..61782d19ca8c58 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appunmount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppUnmount](./kibana-plugin-public.appunmount.md) + +## AppUnmount type + +A function called when an application should be unmounted from the page. This function should be synchronous. + +Signature: + +```typescript +export declare type AppUnmount = () => void; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md index e4e2ad2c7a3a7b..1fef9fc1dc359d 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md @@ -9,5 +9,5 @@ An ordinal used to sort nav links relative to one another for display. Signature: ```typescript -readonly order: number; +readonly order?: number; ``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.application.md b/docs/development/core/public/kibana-plugin-public.coresetup.application.md new file mode 100644 index 00000000000000..4b39b2c76802b1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.coresetup.application.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [application](./kibana-plugin-public.coresetup.application.md) + +## CoreSetup.application property + +[ApplicationSetup](./kibana-plugin-public.applicationsetup.md) + +Signature: + +```typescript +application: ApplicationSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index a4b5b88df36dc4..9b94e2db528319 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -16,6 +16,7 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | +| [application](./kibana-plugin-public.coresetup.application.md) | ApplicationSetup | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | [context](./kibana-plugin-public.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-public.contextsetup.md) | | [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | | [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-public.httpsetup.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.application.md b/docs/development/core/public/kibana-plugin-public.corestart.application.md index 1dd05ff947aeba..c3bf2953b5175d 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.application.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.application.md @@ -9,5 +9,5 @@ Signature: ```typescript -application: Pick; +application: Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index 446e4587352142..74b578f4511584 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -16,7 +16,7 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [application](./kibana-plugin-public.corestart.application.md) | Pick<ApplicationStart, 'capabilities'> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | +| [application](./kibana-plugin-public.corestart.application.md) | Pick<ApplicationStart, 'capabilities' | 'navigateToApp' | 'registerMountContext'> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [chrome](./kibana-plugin-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-public.chromestart.md) | | [docLinks](./kibana-plugin-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | | [http](./kibana-plugin-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-public.httpstart.md) | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 5fda9f91593061..69c9aebbc8c825 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -23,8 +23,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | Interface | Description | | --- | --- | +| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. | +| [AppBase](./kibana-plugin-public.appbase.md) | | | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | +| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. | +| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | | | [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | @@ -80,6 +84,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | +| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | | [HttpBody](./kibana-plugin-public.httpbody.md) | | diff --git a/package.json b/package.json index b3ca135eea9c3b..3b1718c9e9459f 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "@kbn/pm": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", + "@types/history": "^4.7.2", "@types/json-stable-stringify": "^1.0.32", "@types/lodash.clonedeep": "^4.5.4", "@types/react-grid-layout": "^0.16.7", @@ -164,6 +165,7 @@ "handlebars": "4.1.2", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", + "history": "^4.9.0", "hjson": "3.1.2", "hoek": "^5.0.4", "http-proxy-agent": "^2.1.0", diff --git a/renovate.json5 b/renovate.json5 index 71d0a1ef9fee55..ee9a230539e050 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -240,6 +240,14 @@ '(\\b|_)storybook(\\b|_)', ], }, + { + groupSlug: 'history', + groupName: 'history related packages', + packageNames: [ + 'history', + '@types/history', + ], + }, { groupSlug: 'json-stable-stringify', groupName: 'json-stable-stringify related packages', @@ -616,14 +624,6 @@ '@types/git-url-parse', ], }, - { - groupSlug: 'history', - groupName: 'history related packages', - packageNames: [ - 'history', - '@types/history', - ], - }, { groupSlug: 'jsdom', groupName: 'jsdom related packages', diff --git a/rfcs/text/0004_application_service_mounting.md b/rfcs/text/0004_application_service_mounting.md index 30e8d9a05b8b4e..7dc577abc48e3d 100644 --- a/rfcs/text/0004_application_service_mounting.md +++ b/rfcs/text/0004_application_service_mounting.md @@ -18,14 +18,14 @@ import ReactDOM from 'react-dom'; import { MyApp } from './componnets'; -export function renderApp(context, targetDomElement) { +export function renderApp(context, { element }) { ReactDOM.render( , - targetDomElement + element ); return () => { - ReactDOM.unmountComponentAtNode(targetDomElement); + ReactDOM.unmountComponentAtNode(element); }; } ``` @@ -38,9 +38,9 @@ class MyPlugin { application.register({ id: 'my-app', title: 'My Application', - async mount(context, targetDomElement) { + async mount(context, params) { const { renderApp } = await import('./applcation'); - return renderApp(context, targetDomElement); + return renderApp(context, params); } }); } @@ -63,9 +63,7 @@ lock-in. ```ts /** A context type that implements the Handler Context pattern from RFC-0003 */ -export interface MountContext { - /** This is the base path for setting up your router. */ - basename: string; +export interface AppMountContext { /** These services serve as an example, but are subject to change. */ core: { http: { @@ -93,6 +91,13 @@ export interface MountContext { [contextName: string]: unknown; } +export interface AppMountParams { + /** The base path the application is mounted on. Used to configure routers. */ + appBasePath: string; + /** The element the application should render into */ + element: HTMLElement; +} + export type Unmount = () => Promise | void; export interface AppSpec { @@ -109,11 +114,11 @@ export interface AppSpec { /** * A mount function called when the user navigates to this app's route. - * @param context the `MountContext generated for this app - * @param targetDomElement An HTMLElement to mount the application onto. + * @param context the `AppMountContext` generated for this app + * @param params the `AppMountParams` * @returns An unmounting function that will be called to unmount the application. */ - mount(context: MountContext, targetDomElement: HTMLElement): Unmount | Promise; + mount(context: MountContext, params: AppMountParams): Unmount | Promise; /** * A EUI iconType that will be used for the app's icon. This icon @@ -158,19 +163,21 @@ When an app is registered via `register`, it must provide a `mount` function that will be invoked whenever the window's location has changed from another app to this app. -This function is called with a `MountContext` and an `HTMLElement` for the -application to render itself to. The mount function must also return a function -that can be called by the ApplicationService to unmount the application at the -given DOM node. The mount function may return a Promise of an unmount function -in order to import UI code dynamically. +This function is called with a `AppMountContext` and an +`AppMountParams` which contains a `HTMLElement` for the application to +render itself to. The mount function must also return a function that can be +called by the ApplicationService to unmount the application at the given DOM +Element. The mount function may return a Promise of an unmount function in order +to import UI code dynamically. The ApplicationService's `register` method will only be available during the *setup* lifecycle event. This allows the system to know when all applications have been registered. -The `mount` function will also get access to the `MountContext` that has many of -the same core services available during the `start` lifecycle. Plugins can also -register additional context attributes via the `registerMountContext` function. +The `mount` function will also get access to the `AppMountContext` that +has many of the same core services available during the `start` lifecycle. +Plugins can also register additional context attributes via the +`registerMountContext` function. ## Routing @@ -190,7 +197,7 @@ An example: "overview" page: mykibana.com/app/my-app/overview When setting up a router, your application should only handle the part of the -URL following the `context.basename` provided when you application is mounted. +URL following the `params.appBasePath` provided when you application is mounted. ### Legacy Applications @@ -211,7 +218,7 @@ a full-featured router and code-splitting. Note that using React or any other 3rd party tools featured here is not required to build a Kibana Application. ```tsx -// my_plugin/public/application.ts +// my_plugin/public/application.tsx import React from 'react'; import ReactDOM from 'react-dom'; @@ -239,16 +246,16 @@ const MyApp = ({ basename }) => ( , ); -export function renderApp(context, targetDomElement) { +export function renderApp(context, params) { ReactDOM.render( - // `context.basename` would be `/app/my-app` in this example. - // This exact string is not guaranteed to be stable, always reference - // `context.basename`. - , - targetDomElem + // `params.appBasePath` would be `/app/my-app` in this example. + // This exact string is not guaranteed to be stable, always reference the + // provided value at `params.appBasePath`. + , + params.element ); - return () => ReactDOM.unmountComponentAtNode(targetDomElem); + return () => ReactDOM.unmountComponentAtNode(params.element); } ``` @@ -259,9 +266,9 @@ export class MyPlugin { setup({ application }) { application.register({ id: 'my-app', - async mount(context, targetDomElem) { + async mount(context, params) { const { renderApp } = await import('./applcation'); - return renderApp(context, targetDomElement); + return renderApp(context, params); } }); } diff --git a/src/core/public/application/__snapshots__/application_service.test.tsx.snap b/src/core/public/application/__snapshots__/application_service.test.tsx.snap new file mode 100644 index 00000000000000..f2756df74cda51 --- /dev/null +++ b/src/core/public/application/__snapshots__/application_service.test.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#start() returns renderable JSX tree 1`] = ` + +`; diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 85d997f3dc9aad..a0a8b5514882b5 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -17,23 +17,49 @@ * under the License. */ +import { Subject } from 'rxjs'; + import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; -import { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service'; +import { ApplicationService } from './application_service'; +import { + ApplicationSetup, + InternalApplicationStart, + ApplicationStart, + InternalApplicationSetup, +} from './types'; type ApplicationServiceContract = PublicMethodsOf; const createSetupContractMock = (): jest.Mocked => ({ - registerApp: jest.fn(), + register: jest.fn(), + registerMountContext: jest.fn(), +}); + +const createInternalSetupContractMock = (): jest.Mocked => ({ + register: jest.fn(), registerLegacyApp: jest.fn(), + registerMountContext: jest.fn(), }); -const createStartContractMock = (): jest.Mocked => ({ - ...capabilitiesServiceMock.createStartContract(), +const createStartContractMock = (legacyMode = false): jest.Mocked => ({ + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + registerMountContext: jest.fn(), +}); + +const createInternalStartContractMock = (): jest.Mocked => ({ + availableApps: new Map(), + availableLegacyApps: new Map(), + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + registerMountContext: jest.fn(), + currentAppId$: new Subject(), + getComponent: jest.fn(), }); const createMock = (): jest.Mocked => ({ - setup: jest.fn().mockReturnValue(createSetupContractMock()), - start: jest.fn().mockReturnValue(createStartContractMock()), + setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), + start: jest.fn().mockReturnValue(createInternalStartContractMock()), stop: jest.fn(), }); @@ -41,4 +67,7 @@ export const applicationServiceMock = { create: createMock, createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, + + createInternalSetupContract: createInternalSetupContractMock, + createInternalStartContract: createInternalStartContractMock, }; diff --git a/src/core/public/application/application_service.test.mocks.ts b/src/core/public/application/application_service.test.mocks.ts index c28d0a203068a8..d829cf18e56be2 100644 --- a/src/core/public/application/application_service.test.mocks.ts +++ b/src/core/public/application/application_service.test.mocks.ts @@ -26,3 +26,11 @@ export const CapabilitiesServiceConstructor = jest jest.doMock('./capabilities', () => ({ CapabilitiesService: CapabilitiesServiceConstructor, })); + +export const MockHistory = { + push: jest.fn(), +}; +export const createBrowserHistoryMock = jest.fn().mockReturnValue(MockHistory); +jest.doMock('history', () => ({ + createBrowserHistory: createBrowserHistoryMock, +})); diff --git a/src/core/public/application/application_service.test.tsx b/src/core/public/application/application_service.test.tsx index d2266671367a28..cffac8cb27df22 100644 --- a/src/core/public/application/application_service.test.tsx +++ b/src/core/public/application/application_service.test.tsx @@ -17,57 +17,219 @@ * under the License. */ +import { shallow } from 'enzyme'; +import React from 'react'; + import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; -import { MockCapabilitiesService } from './application_service.test.mocks'; +import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks'; import { ApplicationService } from './application_service'; +import { contextServiceMock } from '../context/context_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; + +describe('#setup()', () => { + describe('register', () => { + it('throws an error if two apps with the same id are registered', () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1' } as any); + expect(() => + setup.register(Symbol(), { id: 'app1' } as any) + ).toThrowErrorMatchingInlineSnapshot( + `"An application is already registered with the id \\"app1\\""` + ); + }); + + it('throws error if additional apps are registered after setup', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + await service.start({ http, injectedMetadata }); + expect(() => + setup.register(Symbol(), { id: 'app1' } as any) + ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); + }); + }); + + describe('registerLegacyApp', () => { + it('throws an error if two apps with the same id are registered', () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.registerLegacyApp({ id: 'app2' } as any); + expect(() => + setup.registerLegacyApp({ id: 'app2' } as any) + ).toThrowErrorMatchingInlineSnapshot( + `"A legacy application is already registered with the id \\"app2\\""` + ); + }); + + it('throws error if additional apps are registered after setup', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + await service.start({ http, injectedMetadata }); + expect(() => + setup.registerLegacyApp({ id: 'app2' } as any) + ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); + }); + }); + + it("`registerMountContext` calls context container's registerContext", () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + const container = context.createContextContainer.mock.results[0].value; + const pluginId = Symbol(); + const noop = () => {}; + setup.registerMountContext(pluginId, 'test' as any, noop as any); + expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', noop); + }); +}); describe('#start()', () => { + beforeEach(() => { + MockHistory.push.mockReset(); + }); + it('exposes available apps from capabilities', async () => { const service = new ApplicationService(); - const setup = service.setup(); - setup.registerApp({ id: 'app1' } as any); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1' } as any); setup.registerLegacyApp({ id: 'app2' } as any); + + const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - const startContract = await service.start({ injectedMetadata }); + const startContract = await service.start({ http, injectedMetadata }); + expect(startContract.availableApps).toMatchInlineSnapshot(` -Array [ - Object { - "id": "app1", - }, -] -`); + Map { + "app1" => Object { + "id": "app1", + }, + } + `); expect(startContract.availableLegacyApps).toMatchInlineSnapshot(` -Array [ - Object { - "id": "app2", - }, -] -`); + Map { + "app2" => Object { + "id": "app2", + }, + } + `); }); it('passes registered applications to capabilities', async () => { const service = new ApplicationService(); - const setup = service.setup(); - setup.registerApp({ id: 'app1' } as any); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1' } as any); + + const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - await service.start({ injectedMetadata }); + await service.start({ http, injectedMetadata }); + expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ - apps: [{ id: 'app1' }], - legacyApps: [], + apps: new Map([['app1', { id: 'app1' }]]), + legacyApps: new Map(), injectedMetadata, }); }); it('passes registered legacy applications to capabilities', async () => { const service = new ApplicationService(); - const setup = service.setup(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); setup.registerLegacyApp({ id: 'legacyApp1' } as any); + + const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - await service.start({ injectedMetadata }); + await service.start({ http, injectedMetadata }); + expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ - apps: [], - legacyApps: [{ id: 'legacyApp1' }], + apps: new Map(), + legacyApps: new Map([['legacyApp1', { id: 'legacyApp1' }]]), injectedMetadata, }); }); + + it('returns renderable JSX tree', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + expect(shallow(React.createElement(() => start.getComponent()))).toMatchSnapshot(); + }); + + describe('navigateToApp', () => { + it('changes the browser history to /app/:appId', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + start.navigateToApp('myTestApp'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined); + start.navigateToApp('myOtherApp'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined); + }); + + it('appends a path if specified', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + start.navigateToApp('myTestApp', { path: 'deep/link/to/location/2' }); + expect(MockHistory.push).toHaveBeenCalledWith( + '/app/myTestApp/deep/link/to/location/2', + undefined + ); + }); + + it('includes state if specified', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + start.navigateToApp('myTestApp', { state: 'my-state' }); + expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', 'my-state'); + }); + + it('redirects when in legacyMode', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(true); + const redirectTo = jest.fn(); + const start = await service.start({ http, injectedMetadata, redirectTo }); + start.navigateToApp('myTestApp'); + expect(redirectTo).toHaveBeenCalledWith('/app/myTestApp'); + }); + }); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 528b81ad40be7c..62178eebf0d705 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -17,108 +17,44 @@ * under the License. */ -import { Observable, BehaviorSubject } from 'rxjs'; -import { CapabilitiesService, Capabilities } from './capabilities'; -import { InjectedMetadataStart } from '../injected_metadata'; -import { RecursiveReadonly } from '../../utils'; - -interface BaseApp { - id: string; - - /** - * An ordinal used to sort nav links relative to one another for display. - */ - order: number; - - /** - * The title of the application. - */ - title: string; - - /** - * An observable for a tooltip shown when hovering over app link. - */ - tooltip$?: Observable; - - /** - * A EUI iconType that will be used for the app's icon. This icon - * takes precendence over the `icon` property. - */ - euiIconType?: string; - - /** - * A URL to an image file used as an icon. Used as a fallback - * if `euiIconType` is not provided. - */ - icon?: string; - - /** - * Custom capabilities defined by the app. - */ - capabilities?: Partial; -} - -/** @public */ -export interface App extends BaseApp { - /** - * A mount function called when the user navigates to this app's `rootRoute`. - * @param targetDomElement An HTMLElement to mount the application onto. - * @returns An unmounting function that will be called to unmount the application. - */ - mount(targetDomElement: HTMLElement): () => void; -} - -/** @internal */ -export interface LegacyApp extends BaseApp { - appUrl: string; - subUrlBase?: string; - linkToLastSubUrl?: boolean; -} - -/** @internal */ -export type MixedApp = Partial & Partial & BaseApp; +import { createBrowserHistory } from 'history'; +import { BehaviorSubject } from 'rxjs'; +import React from 'react'; -/** @public */ -export interface ApplicationSetup { - /** - * Register an mountable application to the system. Apps will be mounted based on their `rootRoute`. - * @param app - */ - registerApp(app: App): void; - - /** - * Register metadata about legacy applications. Legacy apps will not be mounted when navigated to. - * @param app - * @internal - */ - registerLegacyApp(app: LegacyApp): void; +import { InjectedMetadataStart } from '../injected_metadata'; +import { CapabilitiesService } from './capabilities'; +import { AppRouter } from './ui'; +import { HttpStart } from '../http'; +import { IContextContainer } from '../context'; +import { ContextSetup } from '../context/context_service'; +import { + AppMountContext, + App, + LegacyApp, + AppMounter, + AppUnmount, + AppMountParameters, + InternalApplicationSetup, + InternalApplicationStart, +} from './types'; + +interface SetupDeps { + context: ContextSetup; } -/** - * @public - */ -export interface ApplicationStart { - /** - * Gets the read-only capabilities. - */ - capabilities: RecursiveReadonly; - - /** - * Apps available based on the current capabilities. Should be used - * to show navigation links and make routing decisions. - */ - availableApps: readonly App[]; - +interface StartDeps { + http: HttpStart; + injectedMetadata: InjectedMetadataStart; /** - * Apps available based on the current capabilities. Should be used - * to show navigation links and make routing decisions. - * @internal + * Only necessary for redirecting to legacy apps + * @deprecated */ - availableLegacyApps: readonly LegacyApp[]; + redirectTo?: (path: string) => void; } -interface StartDeps { - injectedMetadata: InjectedMetadataStart; +interface AppBox { + app: App; + mount: AppMounter; } /** @@ -126,30 +62,116 @@ interface StartDeps { * @internal */ export class ApplicationService { - private readonly apps$ = new BehaviorSubject([]); - private readonly legacyApps$ = new BehaviorSubject([]); + private readonly apps$ = new BehaviorSubject>(new Map()); + private readonly legacyApps$ = new BehaviorSubject>(new Map()); private readonly capabilities = new CapabilitiesService(); + private mountContext?: IContextContainer< + AppMountContext, + AppUnmount | Promise, + [AppMountParameters] + >; + + public setup({ context }: SetupDeps): InternalApplicationSetup { + this.mountContext = context.createContextContainer(); - public setup(): ApplicationSetup { return { - registerApp: (app: App) => { - this.apps$.next([...this.apps$.value, app]); + register: (plugin: symbol, app: App) => { + if (this.apps$.value.has(app.id)) { + throw new Error(`An application is already registered with the id "${app.id}"`); + } + if (this.apps$.isStopped) { + throw new Error(`Applications cannot be registered after "setup"`); + } + + const appBox: AppBox = { + app, + mount: this.mountContext!.createHandler(plugin, app.mount), + }; + this.apps$.next(new Map([...this.apps$.value.entries(), [app.id, appBox]])); }, registerLegacyApp: (app: LegacyApp) => { - this.legacyApps$.next([...this.legacyApps$.value, app]); + if (this.legacyApps$.value.has(app.id)) { + throw new Error(`A legacy application is already registered with the id "${app.id}"`); + } + if (this.apps$.isStopped) { + throw new Error(`Applications cannot be registered after "setup"`); + } + + this.legacyApps$.next(new Map([...this.legacyApps$.value.entries(), [app.id, app]])); }, + registerMountContext: this.mountContext.registerContext, }; } - public async start({ injectedMetadata }: StartDeps): Promise { + public async start({ + http, + injectedMetadata, + redirectTo = (path: string) => (window.location.href = path), + }: StartDeps): Promise { + if (!this.mountContext) { + throw new Error(`ApplicationService#setup() must be invoked before start.`); + } + + // Disable registration of new applications this.apps$.complete(); this.legacyApps$.complete(); - return this.capabilities.start({ - apps: this.apps$.value, + const legacyMode = injectedMetadata.getLegacyMode(); + const currentAppId$ = new BehaviorSubject(undefined); + const { availableApps, availableLegacyApps, capabilities } = await this.capabilities.start({ + apps: new Map([...this.apps$.value].map(([id, { app }]) => [id, app])), legacyApps: this.legacyApps$.value, injectedMetadata, }); + + // Only setup history if we're not in legacy mode + const history = legacyMode ? null : createBrowserHistory({ basename: http.basePath.get() }); + + return { + availableApps, + availableLegacyApps, + capabilities, + registerMountContext: this.mountContext.registerContext, + currentAppId$, + + navigateToApp: (appId, { path, state }: { path?: string; state?: any } = {}) => { + let appPath: string; + if (path) { + const pathWithoutPrecedingSlash = path.replace(/^\//, ''); + appPath = `/app/${appId}/${pathWithoutPrecedingSlash}`; + } else { + appPath = `/app/${appId}`; + } + + if (legacyMode) { + // If we're in legacy mode, do a full page refresh to load the NP app. + redirectTo(http.basePath.prepend(appPath)); + } else { + // basePath not needed here because `history` is configured with basename + history!.push(appPath, state); + } + }, + + getComponent: () => { + // Filter only available apps and map to just the mount function. + const appMounters = new Map( + [...this.apps$.value] + .filter(([id]) => availableApps.has(id)) + .map(([id, { mount }]) => [id, mount]) + ); + + return legacyMode ? null : ( + + ); + }, + }; } public stop() {} diff --git a/src/core/public/application/capabilities/capabilities_service.mock.ts b/src/core/public/application/capabilities/capabilities_service.mock.ts index 71b069fd80434e..29c3275f0e3b27 100644 --- a/src/core/public/application/capabilities/capabilities_service.mock.ts +++ b/src/core/public/application/capabilities/capabilities_service.mock.ts @@ -18,11 +18,11 @@ */ import { CapabilitiesService, CapabilitiesStart } from './capabilities_service'; import { deepFreeze } from '../../../utils/'; -import { App, LegacyApp } from '../application_service'; +import { App, LegacyApp } from '../types'; const createStartContractMock = ( - apps: readonly App[] = [], - legacyApps: readonly LegacyApp[] = [] + apps: ReadonlyMap = new Map(), + legacyApps: ReadonlyMap = new Map() ): jest.Mocked => ({ availableApps: apps, availableLegacyApps: legacyApps, diff --git a/src/core/public/application/capabilities/capabilities_service.test.ts b/src/core/public/application/capabilities/capabilities_service.test.ts index 1c60c1eeb195aa..e80e9a7af321a8 100644 --- a/src/core/public/application/capabilities/capabilities_service.test.ts +++ b/src/core/public/application/capabilities/capabilities_service.test.ts @@ -19,6 +19,7 @@ import { InjectedMetadataService } from '../../injected_metadata'; import { CapabilitiesService } from './capabilities_service'; +import { LegacyApp, App } from '../types'; describe('#start', () => { const injectedMetadata = new InjectedMetadataService({ @@ -39,17 +40,22 @@ describe('#start', () => { } as any, }).start(); - const apps = [{ id: 'app1' }, { id: 'app2', capabilities: { app2: { feature: true } } }] as any; - const legacyApps = [ - { id: 'legacyApp1' }, - { id: 'legacyApp2', capabilities: { app2: { feature: true } } }, - ] as any; + const apps = new Map([ + ['app1', { id: 'app1' }], + ['app2', { id: 'app2', capabilities: { app2: { feature: true } } }], + ] as Array<[string, App]>); + const legacyApps = new Map([ + ['legacyApp1', { id: 'legacyApp1' }], + ['legacyApp2', { id: 'legacyApp2', capabilities: { app2: { feature: true } } }], + ] as Array<[string, LegacyApp]>); it('filters available apps based on returned navLinks', async () => { const service = new CapabilitiesService(); const startContract = await service.start({ apps, legacyApps, injectedMetadata }); - expect(startContract.availableApps).toEqual([{ id: 'app1' }]); - expect(startContract.availableLegacyApps).toEqual([{ id: 'legacyApp1' }]); + expect(startContract.availableApps).toEqual(new Map([['app1', { id: 'app1' }]])); + expect(startContract.availableLegacyApps).toEqual( + new Map([['legacyApp1', { id: 'legacyApp1' }]]) + ); }); it('does not allow Capabilities to be modified', async () => { diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index 51c5a218e70bd3..b5be7b9304c89d 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -18,12 +18,12 @@ */ import { deepFreeze, RecursiveReadonly } from '../../../utils'; -import { LegacyApp, App } from '../application_service'; +import { LegacyApp, App } from '../types'; import { InjectedMetadataStart } from '../../injected_metadata'; interface StartDeps { - apps: readonly App[]; - legacyApps: readonly LegacyApp[]; + apps: ReadonlyMap; + legacyApps: ReadonlyMap; injectedMetadata: InjectedMetadataStart; } @@ -53,8 +53,8 @@ export interface Capabilities { /** @internal */ export interface CapabilitiesStart { capabilities: RecursiveReadonly; - availableApps: readonly App[]; - availableLegacyApps: readonly LegacyApp[]; + availableApps: ReadonlyMap; + availableLegacyApps: ReadonlyMap; } /** @@ -68,10 +68,23 @@ export class CapabilitiesService { injectedMetadata, }: StartDeps): Promise { const capabilities = deepFreeze(injectedMetadata.getCapabilities()); + const availableApps = new Map( + [...apps.entries()].filter( + ([appId]) => + capabilities.navLinks[appId] === undefined || capabilities.navLinks[appId] === true + ) + ); + + const availableLegacyApps = new Map( + [...legacyApps.entries()].filter( + ([appId]) => + capabilities.navLinks[appId] === undefined || capabilities.navLinks[appId] === true + ) + ); return { - availableApps: apps.filter(app => capabilities.navLinks[app.id]), - availableLegacyApps: legacyApps.filter(app => capabilities.navLinks[app.id]), + availableApps, + availableLegacyApps, capabilities, }; } diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 137b46e6573e6d..ae25b54cf07a84 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -17,5 +17,17 @@ * under the License. */ -export { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service'; +export { ApplicationService } from './application_service'; export { Capabilities } from './capabilities'; +export { + App, + AppBase, + AppUnmount, + AppMountContext, + AppMountParameters, + ApplicationSetup, + ApplicationStart, + // Internal types + InternalApplicationStart, + LegacyApp, +} from './types'; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts new file mode 100644 index 00000000000000..88346eded86f27 --- /dev/null +++ b/src/core/public/application/types.ts @@ -0,0 +1,293 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, Subject } from 'rxjs'; + +import { Capabilities } from './capabilities'; +import { ChromeStart } from '../chrome'; +import { IContextProvider } from '../context'; +import { DocLinksStart } from '../doc_links'; +import { HttpStart } from '../http'; +import { I18nStart } from '../i18n'; +import { NotificationsStart } from '../notifications'; +import { OverlayStart } from '../overlays'; +import { PluginOpaqueId } from '../plugins'; +import { UiSettingsClientContract } from '../ui_settings'; +import { RecursiveReadonly } from '../../utils'; + +/** @public */ +export interface AppBase { + id: string; + + /** + * The title of the application. + */ + title: string; + + /** + * An ordinal used to sort nav links relative to one another for display. + */ + order?: number; + + /** + * An observable for a tooltip shown when hovering over app link. + */ + tooltip$?: Observable; + + /** + * A EUI iconType that will be used for the app's icon. This icon + * takes precendence over the `icon` property. + */ + euiIconType?: string; + + /** + * A URL to an image file used as an icon. Used as a fallback + * if `euiIconType` is not provided. + */ + icon?: string; + + /** + * Custom capabilities defined by the app. + */ + capabilities?: Partial; +} + +/** + * Extension of {@link AppBase | common app properties} with the mount function. + * @public + */ +export interface App extends AppBase { + /** + * A mount function called when the user navigates to this app's `rootRoute`. + * @param context The mount context for this app. + * @param targetDomElement An HTMLElement to mount the application onto. + * @returns An unmounting function that will be called to unmount the application. + */ + mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +} + +/** @internal */ +export interface LegacyApp extends AppBase { + appUrl: string; + subUrlBase?: string; + linkToLastSubUrl?: boolean; +} + +/** + * The context object received when applications are mounted to the DOM. + * @public + */ +export interface AppMountContext { + /** + * Core service APIs available to mounted applications. + */ + core: { + /** {@link ApplicationStart} */ + application: Pick; + /** {@link ChromeStart} */ + chrome: ChromeStart; + /** {@link DocLinksStart} */ + docLinks: DocLinksStart; + /** {@link HttpStart} */ + http: HttpStart; + /** {@link I18nStart} */ + i18n: I18nStart; + /** {@link NotificationsStart} */ + notifications: NotificationsStart; + /** {@link OverlayStart} */ + overlays: OverlayStart; + /** {@link UiSettingsClient} */ + uiSettings: UiSettingsClientContract; + }; +} + +/** @public */ +export interface AppMountParameters { + /** + * The container element to render the application into. + */ + element: HTMLElement; + + /** + * The base path for configuring the application's router. + * + * @example + * + * How to configure react-router with a base path: + * + * ```ts + * // inside your plugin's setup function + * export class MyPlugin implements Plugin { + * setup({ application }) { + * application.register({ + * id: 'my-app', + * async mount(context, params) { + * const { renderApp } = await import('./applcation'); + * return renderApp(context, params); + * }, + * }); + * } + * ``` + * + * ```ts + * // application.tsx + * import React from 'react'; + * import ReactDOM from 'react-dom'; + * import { BrowserRouter, Route } from 'react-router-dom'; + * + * export renderApp = (context, { appBasePath, element }) => { + * ReactDOM.render( + * // pass `appBasePath` to `basename` + * + * + * , + * element + * ); + * + * return () => ReactDOM.unmountComponentAtNode(element); + * } + * ``` + */ + appBasePath: string; +} + +/** + * A function called when an application should be unmounted from the page. This function should be synchronous. + * @public + */ +export type AppUnmount = () => void; + +/** @internal */ +export type AppMounter = (params: AppMountParameters) => Promise; + +/** @public */ +export interface ApplicationSetup { + /** + * Register an mountable application to the system. Apps will be mounted based on their `rootRoute`. + * @param app + */ + register(app: App): void; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + contextName: T, + provider: IContextProvider + ): void; +} + +/** @internal */ +export interface InternalApplicationSetup { + /** + * Register an mountable application to the system. Apps will be mounted based on their `rootRoute`. + * @param plugin - opaque ID of the plugin that registers this application + * @param app + */ + register(plugin: symbol, app: App): void; + + /** + * Register metadata about legacy applications. Legacy apps will not be mounted when navigated to. + * @param app + * @internal + */ + registerLegacyApp(app: LegacyApp): void; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + pluginOpaqueId: symbol, + contextName: T, + provider: IContextProvider + ): void; +} + +/** @public */ +export interface ApplicationStart { + /** + * Gets the read-only capabilities. + */ + capabilities: RecursiveReadonly; + + /** + * Navigiate to a given app + * + * @param appId + * @param options.path - optional path inside application to deep link to + * @param options.state - optional state to forward to the application + */ + navigateToApp(appId: string, options?: { path?: string; state?: any }): void; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + contextName: T, + provider: IContextProvider + ): void; +} + +/** @internal */ +export interface InternalApplicationStart + extends Pick { + /** + * Apps available based on the current capabilities. Should be used + * to show navigation links and make routing decisions. + */ + availableApps: ReadonlyMap; + /** + * Apps available based on the current capabilities. Should be used + * to show navigation links and make routing decisions. + * @internal + */ + availableLegacyApps: ReadonlyMap; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + pluginOpaqueId: PluginOpaqueId, + contextName: T, + provider: IContextProvider + ): void; + + // Internal APIs + currentAppId$: Subject; + getComponent(): JSX.Element | null; +} diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx new file mode 100644 index 00000000000000..1c54f9aa54b6a9 --- /dev/null +++ b/src/core/public/application/ui/app_container.tsx @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { Subject } from 'rxjs'; + +import { LegacyApp, AppMounter, AppUnmount } from '../types'; +import { HttpStart } from '../../http'; +import { AppNotFound } from './app_not_found_screen'; + +interface Props extends RouteComponentProps<{ appId: string }> { + apps: ReadonlyMap; + legacyApps: ReadonlyMap; + basePath: HttpStart['basePath']; + currentAppId$: Subject; + /** + * Only necessary for redirecting to legacy apps + * @deprecated + */ + redirectTo: (path: string) => void; +} + +interface State { + appNotFound: boolean; +} + +export class AppContainer extends React.Component { + private readonly containerDiv = React.createRef(); + private unmountFunc?: AppUnmount; + + constructor(props: Props) { + super(props); + this.state = { appNotFound: false }; + } + + componentDidMount() { + this.mountApp(); + } + + componentWillUnmount() { + this.unmountApp(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.match.params.appId !== this.props.match.params.appId) { + this.unmountApp(); + this.mountApp(); + } + } + + async mountApp() { + const { apps, legacyApps, match, basePath, currentAppId$, redirectTo } = this.props; + const appId = match.params.appId; + + const mount = apps.get(appId); + if (mount) { + this.unmountFunc = await mount({ + appBasePath: basePath.prepend(`/app/${appId}`), + element: this.containerDiv.current!, + }); + currentAppId$.next(appId); + this.setState({ appNotFound: false }); + return; + } + + const legacyApp = findLegacyApp(appId, legacyApps); + if (legacyApp) { + // Give the current app a chance to shutdown + await this.unmountApp(); + redirectTo(basePath.prepend(`/app/${appId}`)); + this.setState({ appNotFound: false }); + return; + } + + this.setState({ appNotFound: true }); + } + + async unmountApp() { + if (this.unmountFunc) { + await this.unmountFunc(); + this.unmountFunc = undefined; + } + } + + render() { + return ( + + {this.state.appNotFound && } +
+ + ); + } +} + +function findLegacyApp(appId: string, apps: ReadonlyMap) { + const matchingApps = [...apps.entries()].filter(([id]) => id.split(':')[0] === appId); + return matchingApps.length ? matchingApps[0][1] : null; +} diff --git a/src/core/public/application/ui/app_not_found_screen.tsx b/src/core/public/application/ui/app_not_found_screen.tsx new file mode 100644 index 00000000000000..73a999c5dbf162 --- /dev/null +++ b/src/core/public/application/ui/app_not_found_screen.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const AppNotFound = () => ( + + + + + + + } + body={ +

+ +

+ } + /> +
+
+
+); diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx new file mode 100644 index 00000000000000..9d8acf19785567 --- /dev/null +++ b/src/core/public/application/ui/app_router.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { History } from 'history'; +import React from 'react'; +import { Router, Route } from 'react-router-dom'; +import { Subject } from 'rxjs'; + +import { LegacyApp, AppMounter } from '../types'; +import { AppContainer } from './app_container'; +import { HttpStart } from '../../http'; + +interface Props { + apps: ReadonlyMap; + legacyApps: ReadonlyMap; + basePath: HttpStart['basePath']; + currentAppId$: Subject; + history: History; + /** + * Only necessary for redirecting to legacy apps + * @deprecated + */ + redirectTo?: (path: string) => void; +} + +export const AppRouter: React.StatelessComponent = ({ + history, + redirectTo = (path: string) => (window.location.href = path), + ...otherProps +}) => ( + + } + /> + +); diff --git a/src/core/public/application/ui/index.ts b/src/core/public/application/ui/index.ts new file mode 100644 index 00000000000000..65d04bab5fa03d --- /dev/null +++ b/src/core/public/application/ui/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { AppRouter } from './app_router'; diff --git a/src/core/public/application/ui/ui_integration.test.tsx b/src/core/public/application/ui/ui_integration.test.tsx new file mode 100644 index 00000000000000..d8cd9c616c8f1f --- /dev/null +++ b/src/core/public/application/ui/ui_integration.test.tsx @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { mount, ReactWrapper } from 'enzyme'; +import { createMemoryHistory, History } from 'history'; +import { BehaviorSubject } from 'rxjs'; + +import { I18nProvider } from '@kbn/i18n/react'; + +import { AppMounter, LegacyApp, AppMountParameters } from '../types'; +import { httpServiceMock } from '../../http/http_service.mock'; +import { AppRouter } from './app_router'; +import { AppNotFound } from './app_not_found_screen'; + +const createMountHandler = (htmlString: string) => + jest.fn(async ({ appBasePath: basename, element: el }: AppMountParameters) => { + ReactDOM.render( +
, + el + ); + return jest.fn(() => ReactDOM.unmountComponentAtNode(el)); + }); + +describe('AppContainer', () => { + let apps: Map, Parameters>>; + let legacyApps: Map; + let history: History; + let router: ReactWrapper; + let redirectTo: jest.Mock; + let currentAppId$: BehaviorSubject; + + const navigate = async (path: string) => { + history.push(path); + router.update(); + // flushes any pending promises + return new Promise(resolve => setImmediate(resolve)); + }; + + beforeEach(() => { + redirectTo = jest.fn(); + apps = new Map([ + ['app1', createMountHandler('App 1')], + ['app2', createMountHandler('
App 2
')], + ]); + legacyApps = new Map([ + ['legacyApp1', { id: 'legacyApp1' }], + ['baseApp:legacyApp2', { id: 'baseApp:legacyApp2' }], + ]) as Map; + history = createMemoryHistory(); + currentAppId$ = new BehaviorSubject(undefined); + // Use 'asdf' as the basepath + const http = httpServiceMock.createStartContract('/asdf'); + router = mount( + + + + ); + }); + + it('calls mountHandler and returned unmount function when navigating between apps', async () => { + await navigate('/app/app1'); + expect(apps.get('app1')!).toHaveBeenCalled(); + expect(router.html()).toMatchInlineSnapshot(` + "
+ basename: /asdf/app/app1 + html: App 1 +
" + `); + + const app1Unmount = await apps.get('app1')!.mock.results[0].value; + await navigate('/app/app2'); + expect(app1Unmount).toHaveBeenCalled(); + + expect(apps.get('app2')!).toHaveBeenCalled(); + expect(router.html()).toMatchInlineSnapshot(` + "
+ basename: /asdf/app/app2 + html:
App 2
+
" + `); + }); + + it('updates currentApp$ after mounting', async () => { + await navigate('/app/app1'); + expect(currentAppId$.value).toEqual('app1'); + await navigate('/app/app2'); + expect(currentAppId$.value).toEqual('app2'); + }); + + it('sets window.location.href when navigating to legacy apps', async () => { + await navigate('/app/legacyApp1'); + expect(redirectTo).toHaveBeenCalledWith('/asdf/app/legacyApp1'); + }); + + it('handles legacy apps with subapps', async () => { + await navigate('/app/baseApp'); + expect(redirectTo).toHaveBeenCalledWith('/asdf/app/baseApp'); + }); + + it('displays error page if no app is found', async () => { + await navigate('/app/unknown'); + expect(router.exists(AppNotFound)).toBe(true); + }); +}); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 392846f8433ba7..9a7d87b449b19c 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -38,7 +38,7 @@ const store = new Map(); function defaultStartDeps() { return { - application: applicationServiceMock.createStartContract(), + application: applicationServiceMock.createInternalStartContract(), docLinks: docLinksServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 668bce522bf4eb..0fd68619b73cf1 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -27,7 +27,7 @@ import { IconType } from '@elastic/eui'; import { InjectedMetadataStart } from '../injected_metadata'; import { NotificationsStart } from '../notifications'; -import { ApplicationStart } from '../application'; +import { InternalApplicationStart } from '../application'; import { HttpStart } from '../http'; import { ChromeNavLinks, NavLinksService } from './nav_links'; @@ -73,7 +73,7 @@ interface ConstructorParams { } interface StartDeps { - application: ApplicationStart; + application: InternalApplicationStart; docLinks: DocLinksStart; http: HttpStart; injectedMetadata: InjectedMetadataStart; @@ -83,14 +83,11 @@ interface StartDeps { /** @internal */ export class ChromeService { private readonly stop$ = new ReplaySubject(1); - private readonly browserSupportsCsp: boolean; private readonly navControls = new NavControlsService(); private readonly navLinks = new NavLinksService(); private readonly recentlyAccessed = new RecentlyAccessedService(); - constructor({ browserSupportsCsp }: ConstructorParams) { - this.browserSupportsCsp = browserSupportsCsp; - } + constructor(private readonly params: ConstructorParams) {} public async start({ application, @@ -114,7 +111,7 @@ export class ChromeService { const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); - if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( i18n.translate('core.chrome.legacyBrowserWarning', { defaultMessage: 'Your browser does not meet the security requirements for Kibana.', @@ -137,6 +134,7 @@ export class ChromeService { badge$={badge$.pipe(takeUntil(this.stop$))} basePath={http.basePath} breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))} + currentAppId$={application.currentAppId$} kibanaDocLink={docLinks.links.kibana} forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()} helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))} @@ -146,7 +144,9 @@ export class ChromeService { takeUntil(this.stop$) )} kibanaVersion={injectedMetadata.getKibanaVersion()} + legacyMode={injectedMetadata.getLegacyMode()} navLinks$={navLinks.getNavLinks$()} + navigateToApp={application.navigateToApp} recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index b323bf5318b230..d87d171e028e17 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -28,11 +28,6 @@ export interface ChromeNavLink { */ readonly id: string; - /** - * An ordinal used to sort nav links relative to one another for display. - */ - readonly order: number; - /** * The title of the application. */ @@ -43,6 +38,11 @@ export interface ChromeNavLink { */ readonly baseUrl: string; + /** + * An ordinal used to sort nav links relative to one another for display. + */ + readonly order?: number; + /** * A tooltip shown when hovering over an app link. */ diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index dfef8dc7989f6f..8c135b3c4c49f0 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -19,20 +19,27 @@ import { NavLinksService } from './nav_links_service'; import { take, map, takeLast } from 'rxjs/operators'; +import { LegacyApp } from '../../application'; const mockAppService = { - availableApps: [], - availableLegacyApps: [ - { id: 'legacyApp1', order: 0, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' }, - { - id: 'legacyApp2', - order: -10, - title: 'Legacy App 2', - euiIconType: 'canvasApp', - appUrl: '/app2', - }, - { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3' }, - ], + availableApps: new Map(), + availableLegacyApps: new Map([ + [ + 'legacyApp1', + { id: 'legacyApp1', order: 0, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' }, + ], + [ + 'legacyApp2', + { + id: 'legacyApp2', + order: -10, + title: 'Legacy App 2', + euiIconType: 'canvasApp', + appUrl: '/app2', + }, + ], + ['legacyApp3', { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3' }], + ]), } as any; const mockHttp = { diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index 2250ec40f0f441..5254012665eaf9 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -21,11 +21,11 @@ import { sortBy } from 'lodash'; import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { NavLinkWrapper, ChromeNavLinkUpdateableFields, ChromeNavLink } from './nav_link'; -import { ApplicationStart } from '../../application'; +import { InternalApplicationStart } from '../../application'; import { HttpStart } from '../../http'; interface StartDeps { - application: ApplicationStart; + application: InternalApplicationStart; http: HttpStart; } @@ -99,10 +99,22 @@ export class NavLinksService { private readonly stop$ = new ReplaySubject(1); public start({ application, http }: StartDeps): ChromeNavLinks { - const legacyAppLinks = application.availableLegacyApps.map( - app => + const appLinks = [...application.availableApps.entries()].map( + ([appId, app]) => [ - app.id, + appId, + new NavLinkWrapper({ + ...app, + legacy: false, + baseUrl: relativeToAbsolute(http.basePath.prepend(`/app/${appId}`)), + }), + ] as [string, NavLinkWrapper] + ); + + const legacyAppLinks = [...application.availableLegacyApps.entries()].map( + ([appId, app]) => + [ + appId, new NavLinkWrapper({ ...app, legacy: true, @@ -112,7 +124,7 @@ export class NavLinksService { ); const navLinks$ = new BehaviorSubject>( - new Map(legacyAppLinks) + new Map([...legacyAppLinks, ...appLinks]) ); const forceAppSwitcherNavigation$ = new BehaviorSubject(false); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 04c1a11824870a..79387aa80c1d6a 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -152,6 +152,7 @@ interface Props { appTitle$: Rx.Observable; badge$: Rx.Observable; breadcrumbs$: Rx.Observable; + currentAppId$: Rx.Observable; homeHref: string; isVisible$: Rx.Observable; kibanaDocLink: string; @@ -159,14 +160,17 @@ interface Props { recentlyAccessed$: Rx.Observable; forceAppSwitcherNavigation$: Rx.Observable; helpExtension$: Rx.Observable; + legacyMode: boolean; navControlsLeft$: Rx.Observable; navControlsRight$: Rx.Observable; + navigateToApp: (appId: string) => void; intl: InjectedIntl; basePath: HttpStart['basePath']; } interface State { appTitle: string; + currentAppId?: string; isVisible: boolean; navLinks: ReadonlyArray>; recentlyAccessed: ReadonlyArray>; @@ -201,7 +205,11 @@ class HeaderUI extends Component { this.props.navLinks$, this.props.recentlyAccessed$, // Types for combineLatest only handle up to 6 inferred types so we combine these two separately. - Rx.combineLatest(this.props.navControlsLeft$, this.props.navControlsRight$) + Rx.combineLatest( + this.props.navControlsLeft$, + this.props.navControlsRight$, + this.props.currentAppId$ + ) ).subscribe({ next: ([ appTitle, @@ -209,7 +217,7 @@ class HeaderUI extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight], + [navControlsLeft, navControlsRight, currentAppId], ]) => { this.setState({ appTitle, @@ -221,6 +229,7 @@ class HeaderUI extends Component { ), navControlsLeft, navControlsRight, + currentAppId, }); }, }); @@ -268,9 +277,12 @@ class HeaderUI extends Component { intl, kibanaDocLink, kibanaVersion, + legacyMode, + navigateToApp, } = this.props; const { appTitle, + currentAppId, isVisible, navControlsLeft, navControlsRight, @@ -287,9 +299,18 @@ class HeaderUI extends Component { .map(navLink => ({ key: navLink.id, label: navLink.title, - href: navLink.href, + + // Legacy apps use href, NP apps use onClick + // This needs to work with both href and onClick to support "open in new tab" correctly, however EUI + // does not current support this. + // https://github.com/elastic/eui/pull/1933 + href: legacyMode || navLink.legacy ? navLink.href : undefined, + onClick: !legacyMode && !navLink.legacy ? () => navigateToApp(navLink.id) : undefined, + + // Legacy apps use `active` property, NP apps should match the current app + isActive: navLink.active || currentAppId === navLink.id, isDisabled: navLink.disabled, - isActive: navLink.active, + iconType: navLink.euiIconType, icon: !navLink.euiIconType && navLink.icon ? ( diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 7310a8f33eba4e..895fc785b11b1c 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -272,7 +272,9 @@ describe('#start()', () => { await startCore(); expect(MockRenderingService.start).toHaveBeenCalledTimes(1); expect(MockRenderingService.start).toHaveBeenCalledWith({ + application: expect.any(Object), chrome: expect.any(Object), + injectedMetadata: expect.any(Object), targetDomElement: expect.any(HTMLElement), }); }); @@ -364,7 +366,7 @@ describe('LegacyPlatformService targetDomElement', () => { it('only mounts the element when start, after setting up the legacyPlatformService', async () => { const core = createCoreSystem(); - let targetDomElementInStart: HTMLElement | null; + let targetDomElementInStart: HTMLElement | undefined; MockLegacyPlatformService.start.mockImplementation(({ targetDomElement }) => { targetDomElementInStart = targetDomElement; }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 7782c93c7bbb1b..20621a927d6782 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -32,7 +32,7 @@ import { OverlayService } from './overlays'; import { PluginsService } from './plugins'; import { UiSettingsService } from './ui_settings'; import { ApplicationService } from './application'; -import { mapToObject } from '../utils/'; +import { mapToObject, pick } from '../utils/'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects/saved_objects_service'; @@ -77,6 +77,7 @@ export class CoreSystem { private readonly context: ContextService; private readonly rootDomElement: HTMLElement; + private readonly coreContext: CoreContext; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -106,14 +107,14 @@ export class CoreSystem { this.savedObjects = new SavedObjectsService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); - this.application = new ApplicationService(); this.chrome = new ChromeService({ browserSupportsCsp }); this.docLinks = new DocLinksService(); this.rendering = new RenderingService(); + this.application = new ApplicationService(); - const core: CoreContext = { coreId: Symbol('core') }; - this.context = new ContextService(core); - this.plugins = new PluginsService(core, injectedMetadata.uiPlugins); + this.coreContext = { coreId: Symbol('core') }; + this.context = new ContextService(this.coreContext); + this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.legacyPlatform = new LegacyPlatformService({ requireLegacyFiles, @@ -133,10 +134,10 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); - const application = this.application.setup(); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ pluginDependencies }); + const application = this.application.setup({ context }); const core: InternalCoreSetup = { application, @@ -150,7 +151,11 @@ export class CoreSystem { // Services that do not expose contracts at setup const plugins = await this.plugins.setup(core); - await this.legacyPlatform.setup({ core, plugins: mapToObject(plugins.contracts) }); + + await this.legacyPlatform.setup({ + core, + plugins: mapToObject(plugins.contracts), + }); return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { @@ -171,7 +176,7 @@ export class CoreSystem { const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); - const application = await this.application.start({ injectedMetadata }); + const application = await this.application.start({ http, injectedMetadata }); const coreUiTargetDomElement = document.createElement('div'); coreUiTargetDomElement.id = 'kibana-body'; @@ -200,6 +205,17 @@ export class CoreSystem { }); const uiSettings = await this.uiSettings.start(); + application.registerMountContext(this.coreContext.coreId, 'core', () => ({ + application: pick(application, ['capabilities', 'navigateToApp']), + chrome, + docLinks, + http, + i18n, + notifications, + overlays, + uiSettings, + })); + const core: InternalCoreStart = { application, chrome, @@ -215,9 +231,12 @@ export class CoreSystem { const plugins = await this.plugins.start(core); const rendering = this.rendering.start({ + application, chrome, + injectedMetadata, targetDomElement: coreUiTargetDomElement, }); + await this.legacyPlatform.start({ core, plugins: mapToObject(plugins.contracts), diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 4ce84f8ab38d13..628e84267ccb40 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -25,7 +25,7 @@ type ServiceSetupMockType = jest.Mocked & { basePath: jest.Mocked; }; -const createServiceMock = (): ServiceSetupMockType => ({ +const createServiceMock = (basePath = ''): ServiceSetupMockType => ({ fetch: jest.fn(), get: jest.fn(), head: jest.fn(), @@ -35,8 +35,8 @@ const createServiceMock = (): ServiceSetupMockType => ({ delete: jest.fn(), options: jest.fn(), basePath: { - get: jest.fn(), - prepend: jest.fn(), + get: jest.fn(() => basePath), + prepend: jest.fn(path => `${basePath}${path}`), remove: jest.fn(), }, addLoadingCount: jest.fn(), @@ -46,22 +46,19 @@ const createServiceMock = (): ServiceSetupMockType => ({ removeAllInterceptors: jest.fn(), }); -const createSetupContractMock = createServiceMock; -const createStartContractMock = createServiceMock; - -const createMock = () => { +const createMock = (basePath = '') => { const mocked: jest.Mocked> = { setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }; - mocked.setup.mockReturnValue(createSetupContractMock()); - mocked.start.mockReturnValue(createSetupContractMock()); + mocked.setup.mockReturnValue(createServiceMock(basePath)); + mocked.start.mockReturnValue(createServiceMock(basePath)); return mocked; }; export const httpServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, - createStartContract: createStartContractMock, + createSetupContract: createServiceMock, + createStartContract: createServiceMock, }; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index abc922ff97c1d5..e62f9795722403 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -68,9 +68,12 @@ import { ApplicationSetup, Capabilities, ApplicationStart } from './application' import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } from './context'; +import { InternalApplicationSetup, InternalApplicationStart } from './application/types'; export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; + +export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application'; export { SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, @@ -114,6 +117,8 @@ export { * https://github.com/Microsoft/web-build-tools/issues/1237 */ export interface CoreSetup { + /** {@link ApplicationSetup} */ + application: ApplicationSetup; /** {@link ContextSetup} */ context: ContextSetup; /** {@link FatalErrorsSetup} */ @@ -137,7 +142,7 @@ export interface CoreSetup { */ export interface CoreStart { /** {@link ApplicationStart} */ - application: Pick; + application: Pick; /** {@link ChromeStart} */ chrome: ChromeStart; /** {@link DocLinksStart} */ @@ -157,14 +162,14 @@ export interface CoreStart { } /** @internal */ -export interface InternalCoreSetup extends CoreSetup { - application: ApplicationSetup; +export interface InternalCoreSetup extends Omit { + application: InternalApplicationSetup; injectedMetadata: InjectedMetadataSetup; } /** @internal */ -export interface InternalCoreStart extends CoreStart { - application: ApplicationStart; +export interface InternalCoreStart extends Omit { + application: InternalApplicationStart; injectedMetadata: InjectedMetadataStart; } 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 c4579bee3f1310..9e1d5aeec7ff49 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -25,6 +25,7 @@ const createSetupContractMock = () => { getKibanaBranch: jest.fn(), getCapabilities: jest.fn(), getCspConfig: jest.fn(), + getLegacyMode: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), getInjectedVar: jest.fn(), @@ -34,6 +35,7 @@ const createSetupContractMock = () => { setupContract.getCapabilities.mockReturnValue({} as any); setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); + setupContract.getLegacyMode.mockReturnValue(true); setupContract.getLegacyMetadata.mockReturnValue({ nav: [], uiSettings: { diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 9fbc9554855126..fa93d0f5288b4f 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -51,6 +51,7 @@ export interface InjectedMetadataParams { plugin: DiscoveredPlugin; }>; capabilities: Capabilities; + legacyMode: boolean; legacyMetadata: { app: unknown; translations: unknown; @@ -112,6 +113,10 @@ export class InjectedMetadataService { return this.state.uiPlugins; }, + getLegacyMode: () => { + return this.state.legacyMode; + }, + getLegacyMetadata: () => { return this.state.legacyMetadata; }, @@ -156,6 +161,8 @@ export interface InjectedMetadataSetup { id: string; plugin: DiscoveredPlugin; }>; + /** Indicates whether or not we are rendering a known legacy app. */ + getLegacyMode: () => boolean; getLegacyMetadata: () => { app: unknown; translations: unknown; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index eb5b3e90f1a525..060ab6f62ad07d 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -98,6 +98,7 @@ const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); const uiSettingsStart = uiSettingsServiceMock.createStartContract(); const savedObjectsStart = savedObjectsMock.createStartContract(); +const mockStorage = { getItem: jest.fn() } as any; const defaultStartDeps = { core: { @@ -112,6 +113,7 @@ const defaultStartDeps = { uiSettings: uiSettingsStart, savedObjects: savedObjectsStart, }, + lastSubUrlStorage: mockStorage, targetDomElement: document.createElement('div'), plugins: {}, }; @@ -138,6 +140,23 @@ describe('#setup()', () => { }); describe('#start()', () => { + it('fetches and sets legacy lastSubUrls', () => { + chromeStart.navLinks.getAll.mockReturnValue([ + { id: 'link1', baseUrl: 'http://wowza.com/app1', legacy: true } as any, + ]); + mockStorage.getItem.mockReturnValue('http://wowza.com/app1/subUrl'); + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start({ ...defaultStartDeps, lastSubUrlStorage: mockStorage }); + + expect(chromeStart.navLinks.update).toHaveBeenCalledWith('link1', { + url: 'http://wowza.com/app1/subUrl', + }); + }); + it('initializes ui/new_platform with core APIs', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 7d852773ad03fa..73f4682debe50e 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -34,7 +34,8 @@ interface SetupDeps { interface StartDeps { core: InternalCoreStart; plugins: Record; - targetDomElement: HTMLElement; + lastSubUrlStorage?: Storage; + targetDomElement?: HTMLElement; } interface BootstrapModule { @@ -55,10 +56,7 @@ export class LegacyPlatformService { constructor(private readonly params: LegacyPlatformParams) {} public setup({ core, plugins }: SetupDeps) { - // 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').__setup__(core, plugins); - + // Always register legacy apps, even if not in legacy mode. core.injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) => core.application.registerLegacyApp({ id: navLink.id, @@ -71,9 +69,36 @@ export class LegacyPlatformService { linkToLastSubUrl: navLink.linkToLastSubUrl, }) ); + + // Inject parts of the new platform into parts of the legacy platform + // so that legacy APIs/modules can mimic their new platform counterparts + if (core.injectedMetadata.getLegacyMode()) { + require('ui/new_platform').__setup__(core, plugins); + } } - public start({ core, targetDomElement, plugins }: StartDeps) { + public start({ + core, + targetDomElement, + plugins, + lastSubUrlStorage = window.sessionStorage, + }: StartDeps) { + // Initialize legacy sub urls + core.chrome.navLinks + .getAll() + .filter(link => link.legacy) + .forEach(navLink => { + const lastSubUrl = lastSubUrlStorage.getItem(`lastSubUrl:${navLink.baseUrl}`); + core.chrome.navLinks.update(navLink.id, { + url: lastSubUrl || navLink.url || navLink.baseUrl, + }); + }); + + // Only import and bootstrap legacy platform if we're in legacy mode. + if (!core.injectedMetadata.getLegacyMode()) { + return; + } + // 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').__start__(core, plugins); @@ -91,7 +116,8 @@ export class LegacyPlatformService { this.targetDomElement = targetDomElement; - this.bootstrapModule.bootstrap(this.targetDomElement); + // `targetDomElement` is always defined when in legacy mode + this.bootstrapModule.bootstrap(this.targetDomElement!); } public stop() { diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 0f3a01c793ae3e..7c99f69d6fd7ad 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -42,6 +42,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; function createCoreSetupMock() { const mock: MockedKeys = { + application: applicationServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 66cb7c4a1171e3..fc39537513b6c3 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -76,6 +76,11 @@ export function createPluginSetupContext< plugin: PluginWrapper ): CoreSetup { return { + application: { + register: app => deps.application.register(plugin.opaqueId, app), + registerMountContext: (contextName, provider) => + deps.application.registerMountContext(plugin.opaqueId, contextName, provider), + }, context: omit(deps.context, 'setCurrentPlugin'), fatalErrors: deps.fatalErrors, http: deps.http, @@ -107,6 +112,9 @@ export function createPluginStartContext< return { application: { capabilities: deps.application.capabilities, + navigateToApp: deps.application.navigateToApp, + registerMountContext: (contextName, provider) => + deps.application.registerMountContext(plugin.opaqueId, contextName, provider), }, docLinks: deps.docLinks, http: deps.http, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 2b689e45b4f1ad..d6411554e5f85b 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -72,7 +72,7 @@ beforeEach(() => { }, ]; mockSetupDeps = { - application: applicationServiceMock.createSetupContract(), + application: applicationServiceMock.createInternalSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), @@ -81,10 +81,11 @@ beforeEach(() => { uiSettings: uiSettingsServiceMock.createSetupContract(), }; mockSetupContext = { - ...omit(mockSetupDeps, 'application', 'injectedMetadata'), + ...omit(mockSetupDeps, 'injectedMetadata'), + application: expect.any(Object), }; mockStartDeps = { - application: applicationServiceMock.createStartContract(), + application: applicationServiceMock.createInternalStartContract(), docLinks: docLinksServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), chrome: chromeServiceMock.createStartContract(), @@ -97,9 +98,7 @@ beforeEach(() => { }; mockStartContext = { ...omit(mockStartDeps, 'injectedMetadata'), - application: { - capabilities: mockStartDeps.application.capabilities, - }, + application: expect.any(Object), chrome: omit(mockStartDeps.chrome, 'getComponent'), }; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index a5c31e41e02672..b9541b576c04c0 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -8,26 +8,65 @@ import { IconType } from '@elastic/eui'; import { Observable } from 'rxjs'; import React from 'react'; import * as Rx from 'rxjs'; +import { Subject } from 'rxjs'; import { EuiGlobalToastListToast as Toast } from '@elastic/eui'; +// @public +export interface App extends AppBase { + mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +} + +// @public (undocumented) +export interface AppBase { + capabilities?: Partial; + euiIconType?: string; + icon?: string; + // (undocumented) + id: string; + order?: number; + title: string; + tooltip$?: Observable; +} + // @public (undocumented) export interface ApplicationSetup { - // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts - registerApp(app: App): void; - // Warning: (ae-forgotten-export) The symbol "LegacyApp" needs to be exported by the entry point index.d.ts - // - // @internal - registerLegacyApp(app: LegacyApp): void; + register(app: App): void; + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public (undocumented) export interface ApplicationStart { - availableApps: readonly App[]; - // @internal - availableLegacyApps: readonly LegacyApp[]; capabilities: RecursiveReadonly; + navigateToApp(appId: string, options?: { + path?: string; + state?: any; + }): void; + registerMountContext(contextName: T, provider: IContextProvider): void; } +// @public +export interface AppMountContext { + core: { + application: Pick; + chrome: ChromeStart; + docLinks: DocLinksStart; + http: HttpStart; + i18n: I18nStart; + notifications: NotificationsStart; + overlays: OverlayStart; + uiSettings: UiSettingsClientContract; + }; +} + +// @public (undocumented) +export interface AppMountParameters { + appBasePath: string; + element: HTMLElement; +} + +// @public +export type AppUnmount = () => void; + // @public export interface Capabilities { [key: string]: Record>; @@ -102,7 +141,7 @@ export interface ChromeNavLink { readonly legacy: boolean; // @deprecated readonly linkToLastSubUrl?: boolean; - readonly order: number; + readonly order?: number; // @deprecated readonly subUrlBase?: string; readonly title: string; @@ -182,6 +221,8 @@ export interface CoreContext { // @public export interface CoreSetup { + // (undocumented) + application: ApplicationSetup; // (undocumented) context: ContextSetup; // (undocumented) @@ -197,7 +238,7 @@ export interface CoreSetup { // @public export interface CoreStart { // (undocumented) - application: Pick; + application: Pick; // (undocumented) chrome: ChromeStart; // (undocumented) @@ -503,9 +544,11 @@ export type IContextHandler, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; // @internal (undocumented) -export interface InternalCoreSetup extends CoreSetup { +export interface InternalCoreSetup extends Omit { + // Warning: (ae-forgotten-export) The symbol "InternalApplicationSetup" needs to be exported by the entry point index.d.ts + // // (undocumented) - application: ApplicationSetup; + application: InternalApplicationSetup; // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -513,9 +556,11 @@ export interface InternalCoreSetup extends CoreSetup { } // @internal (undocumented) -export interface InternalCoreStart extends CoreStart { +export interface InternalCoreStart extends Omit { + // Warning: (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts + // // (undocumented) - application: ApplicationStart; + application: InternalApplicationStart; // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/src/core/public/rendering/rendering_service.test.tsx b/src/core/public/rendering/rendering_service.test.tsx index 5b4ab939966577..f74014abf13532 100644 --- a/src/core/public/rendering/rendering_service.test.tsx +++ b/src/core/public/rendering/rendering_service.test.tsx @@ -21,14 +21,20 @@ import React from 'react'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { RenderingService } from './rendering_service'; +import { InternalApplicationStart } from '../application'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; describe('RenderingService#start', () => { const getService = () => { const rendering = new RenderingService(); + const application = { + getComponent: () =>
Hello application!
, + } as InternalApplicationStart; const chrome = chromeServiceMock.createStartContract(); chrome.getComponent.mockReturnValue(
Hello chrome!
); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); const targetDomElement = document.createElement('div'); - const start = rendering.start({ chrome, targetDomElement }); + const start = rendering.start({ application, chrome, injectedMetadata, targetDomElement }); return { start, targetDomElement }; }; @@ -54,7 +60,7 @@ describe('RenderingService#start', () => { start: { legacyTargetDomElement }, targetDomElement, } = getService(); - legacyTargetDomElement.innerHTML = 'Hello legacy!'; + legacyTargetDomElement!.innerHTML = 'Hello legacy!'; expect(targetDomElement.querySelector('#legacy')).toMatchInlineSnapshot(` (); + const appUi = application.getComponent(); + + const legacyMode = injectedMetadata.getLegacyMode(); + const legacyRef = legacyMode ? React.createRef() : null; ReactDOM.render(
{chromeUi} -
+ {!legacyMode && ( +
+
+
{appUi}
+
+
+ )} + + {legacyMode &&
}
, targetDomElement ); return { - legacyTargetDomElement: legacyRef.current!, + // When in legacy mode, return legacy div, otherwise undefined. + legacyTargetDomElement: legacyRef ? legacyRef.current! : undefined, }; } } /** @internal */ export interface RenderingStart { - legacyTargetDomElement: HTMLDivElement; + legacyTargetDomElement?: HTMLDivElement; } diff --git a/src/core/utils/context.mock.ts b/src/core/utils/context.mock.ts index d59d0066c4e6ee..4d91c11542b2f8 100644 --- a/src/core/utils/context.mock.ts +++ b/src/core/utils/context.mock.ts @@ -24,7 +24,9 @@ export type ContextContainerMock = jest.Mocked> const createContextMock = () => { const contextMock: ContextContainerMock = { registerContext: jest.fn(), - createHandler: jest.fn(), + createHandler: jest.fn((id, handler) => (...args: any[]) => + Promise.resolve(handler({}, ...args)) + ), }; contextMock.createHandler.mockImplementation((pluginId, handler) => (...args) => handler({}, ...args) diff --git a/src/core/utils/pick.ts b/src/core/utils/pick.ts index d55c76a3ca77d6..77854f9af680b4 100644 --- a/src/core/utils/pick.ts +++ b/src/core/utils/pick.ts @@ -17,10 +17,7 @@ * under the License. */ -export function pick, K extends keyof T>( - obj: T, - keys: K[] -): Pick { +export function pick(obj: T, keys: K[]): Pick { return keys.reduce( (acc, key) => { if (obj.hasOwnProperty(key)) { diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 53b627d39595e4..5f2abd8c9e0832 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -85,6 +85,7 @@ const coreSystem = new CoreSystem({ injectedMetadata: { version: '1.2.3', buildNumber: 1234, + legacyMode: true, legacyMetadata: { nav: [], version: '1.2.3', diff --git a/src/legacy/ui/public/chrome/chrome.js b/src/legacy/ui/public/chrome/chrome.js index 8f58da9107673f..a5a0521013a6e1 100644 --- a/src/legacy/ui/public/chrome/chrome.js +++ b/src/legacy/ui/public/chrome/chrome.js @@ -95,7 +95,6 @@ const waitForBootstrap = new Promise(resolve => { document.body.setAttribute('id', `${internals.app.id}-app`); chrome.setupAngular(); - // targetDomElement.setAttribute('id', 'kibana-body'); targetDomElement.setAttribute('kbn-chrome', 'true'); targetDomElement.setAttribute('ng-class', '{ \'hidden-chrome\': !chrome.getVisible() }'); targetDomElement.className = 'app-wrapper'; diff --git a/src/legacy/ui/public/chrome/directives/kbn_chrome.js b/src/legacy/ui/public/chrome/directives/kbn_chrome.js index d81a1ceb5f288e..755cb8b42d3637 100644 --- a/src/legacy/ui/public/chrome/directives/kbn_chrome.js +++ b/src/legacy/ui/public/chrome/directives/kbn_chrome.js @@ -77,15 +77,21 @@ export function kbnChromeProvider(chrome, internals) { // Non-scope based code (e.g., React) // Banners - ReactDOM.render( - - - , - document.getElementById('globalBannerList') - ); + const bannerListContainer = document.getElementById('globalBannerList'); + // Banners not supported in New Platform yet + // https://github.com/elastic/kibana/issues/41986 + if (bannerListContainer) { + ReactDOM.render( + + + , + bannerListContainer + ); + } + return chrome; } diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 998bcbc8f4f179..1e91c4ca2c9049 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -164,7 +164,7 @@ export function uiRenderMixin(kbnServer, server, config) { }); server.route({ - path: '/app/{id}', + path: '/app/{id}/{path?}', method: 'GET', async handler(req, h) { const id = req.params.id; @@ -239,6 +239,7 @@ export function uiRenderMixin(kbnServer, server, config) { buildNumber: config.get('pkg.buildNum'), branch: config.get('pkg.branch'), basePath, + legacyMode: app.getId() !== 'core', i18n: { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, diff --git a/yarn.lock b/yarn.lock index 26ab312da1ad62..9bcbf5c8a7f38f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3417,7 +3417,7 @@ resolved "https://registry.yarnpkg.com/@types/has-ansi/-/has-ansi-3.0.0.tgz#636403dc4e0b2649421c4158e5c404416f3f0330" integrity sha512-H3vFOwfLlFEC0MOOrcSkus8PCnMCzz4N0EqUbdJZCdDhBTfkAu86aRYA+MTxjKW6jCpUvxcn4715US8g+28BMA== -"@types/history@*": +"@types/history@*", "@types/history@^4.7.2": version "4.7.2" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220" integrity sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q== @@ -14435,7 +14435,7 @@ history-extra@^5.0.1: resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-5.0.1.tgz#95a2e59dda526c4241d0ae1b124a77a5e4675ce8" integrity sha512-6XV1L1lHgporVWgppa/Kq+Fnz4lhBew7iMxYCTfzVmoEywsAKJnTjdw1zOd+EGLHGYp0/V8jSVMEgqx4QbHLTw== -history@4.9.0: +history@4.9.0, history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== From d5f769dcc8397ed781f31656a3a57c8e080f9c46 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 6 Aug 2019 13:27:03 -0500 Subject: [PATCH 03/14] Add LegacyCore{Setup,Start} --- ...ana-plugin-public.corestart.application.md | 2 +- .../public/kibana-plugin-public.corestart.md | 2 +- ...public.legacycoresetup.injectedmetadata.md | 15 ++++++++ .../kibana-plugin-public.legacycoresetup.md | 28 +++++++++++++++ ...public.legacycorestart.injectedmetadata.md | 15 ++++++++ .../kibana-plugin-public.legacycorestart.md | 28 +++++++++++++++ .../core/public/kibana-plugin-public.md | 2 ++ src/core/public/core_system.ts | 22 ++++++++++-- src/core/public/index.ts | 34 ++++++++++++++----- src/core/public/legacy/legacy_service.test.ts | 8 ++--- src/core/public/legacy/legacy_service.ts | 28 +++++++++++++-- src/core/public/plugins/plugins_service.ts | 2 +- src/core/public/public.api.md | 23 ++++--------- .../public/legacy_compat/angular_config.tsx | 20 +++++------ .../ui/public/new_platform/new_platform.ts | 16 ++++----- .../components/app/Main/UpdateBreadcrumbs.tsx | 4 +-- .../ServiceIntegrations/WatcherFlyout.tsx | 8 ++--- .../ServiceIntegrations/index.tsx | 4 +-- .../__test__/ServiceOverview.test.tsx | 4 +-- .../DiscoverLinks.integration.test.tsx | 4 +-- .../shared/Links/InfraLink.test.tsx | 4 +-- .../shared/Links/KibanaLink.test.tsx | 4 +-- .../MachineLearningLinks/MLJobLink.test.tsx | 4 +-- .../MachineLearningLinks/MLLink.test.tsx | 4 +-- .../__test__/TransactionActionMenu.test.tsx | 4 +-- .../apm/public/context/CoreContext.tsx | 6 ++-- .../apm/public/new-platform/plugin.tsx | 4 +-- 27 files changed, 218 insertions(+), 81 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.legacycoresetup.injectedmetadata.md create mode 100644 docs/development/core/public/kibana-plugin-public.legacycoresetup.md create mode 100644 docs/development/core/public/kibana-plugin-public.legacycorestart.injectedmetadata.md create mode 100644 docs/development/core/public/kibana-plugin-public.legacycorestart.md diff --git a/docs/development/core/public/kibana-plugin-public.corestart.application.md b/docs/development/core/public/kibana-plugin-public.corestart.application.md index c3bf2953b5175d..c26701ca80529a 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.application.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.application.md @@ -9,5 +9,5 @@ Signature: ```typescript -application: Pick; +application: ApplicationStart; ``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index 74b578f4511584..5c1626958c4df6 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -16,7 +16,7 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [application](./kibana-plugin-public.corestart.application.md) | Pick<ApplicationStart, 'capabilities' | 'navigateToApp' | 'registerMountContext'> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | +| [application](./kibana-plugin-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [chrome](./kibana-plugin-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-public.chromestart.md) | | [docLinks](./kibana-plugin-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | | [http](./kibana-plugin-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-public.httpstart.md) | diff --git a/docs/development/core/public/kibana-plugin-public.legacycoresetup.injectedmetadata.md b/docs/development/core/public/kibana-plugin-public.legacycoresetup.injectedmetadata.md new file mode 100644 index 00000000000000..f71277e64ff17a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycoresetup.injectedmetadata.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) > [injectedMetadata](./kibana-plugin-public.legacycoresetup.injectedmetadata.md) + +## LegacyCoreSetup.injectedMetadata property + +> Warning: This API is now obsolete. +> +> + +Signature: + +```typescript +injectedMetadata: InjectedMetadataSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacycoresetup.md b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md new file mode 100644 index 00000000000000..f704bc65d12a52 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) + +## LegacyCoreSetup interface + +> Warning: This API is now obsolete. +> +> + +Setup interface exposed to the legacy platform via the `ui/new_platform` module. + +Signature: + +```typescript +export interface LegacyCoreSetup extends CoreSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [injectedMetadata](./kibana-plugin-public.legacycoresetup.injectedmetadata.md) | InjectedMetadataSetup | | + +## Remarks + +Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreSetup](./kibana-plugin-public.coresetup.md), unsupported methods will throw exceptions when called. + diff --git a/docs/development/core/public/kibana-plugin-public.legacycorestart.injectedmetadata.md b/docs/development/core/public/kibana-plugin-public.legacycorestart.injectedmetadata.md new file mode 100644 index 00000000000000..cd818c3f5adc72 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycorestart.injectedmetadata.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) > [injectedMetadata](./kibana-plugin-public.legacycorestart.injectedmetadata.md) + +## LegacyCoreStart.injectedMetadata property + +> Warning: This API is now obsolete. +> +> + +Signature: + +```typescript +injectedMetadata: InjectedMetadataStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacycorestart.md b/docs/development/core/public/kibana-plugin-public.legacycorestart.md new file mode 100644 index 00000000000000..775c3fb1ffe3d1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycorestart.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) + +## LegacyCoreStart interface + +> Warning: This API is now obsolete. +> +> + +Start interface exposed to the legacy platform via the `ui/new_platform` module. + +Signature: + +```typescript +export interface LegacyCoreStart extends CoreStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [injectedMetadata](./kibana-plugin-public.legacycorestart.injectedmetadata.md) | InjectedMetadataStart | | + +## Remarks + +Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreStart](./kibana-plugin-public.corestart.md), unsupported methods will throw exceptions when called. + diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 69c9aebbc8c825..ccabdc62c5e7a2 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -58,6 +58,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | +| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | +| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 20621a927d6782..4eb16572d8fec1 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -20,12 +20,17 @@ import './core.css'; import { CoreId } from '../server'; -import { InternalCoreSetup, InternalCoreStart } from '.'; +import { CoreSetup, CoreStart } from '.'; import { ChromeService } from './chrome'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; import { HttpService } from './http'; import { I18nService } from './i18n'; -import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata'; +import { + InjectedMetadataParams, + InjectedMetadataService, + InjectedMetadataSetup, + InjectedMetadataStart, +} from './injected_metadata'; import { LegacyPlatformParams, LegacyPlatformService } from './legacy'; import { NotificationsService } from './notifications'; import { OverlayService } from './overlays'; @@ -37,6 +42,7 @@ import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects/saved_objects_service'; import { ContextService } from './context'; +import { InternalApplicationSetup, InternalApplicationStart } from './application/types'; interface Params { rootDomElement: HTMLElement; @@ -51,6 +57,18 @@ export interface CoreContext { coreId: CoreId; } +/** @internal */ +export interface InternalCoreSetup extends Omit { + application: InternalApplicationSetup; + injectedMetadata: InjectedMetadataSetup; +} + +/** @internal */ +export interface InternalCoreStart extends Omit { + application: InternalApplicationStart; + injectedMetadata: InjectedMetadataStart; +} + /** * The CoreSystem is the root of the new platform, and setups all parts * of Kibana in the UI, including the LegacyPlatform which is managed diff --git a/src/core/public/index.ts b/src/core/public/index.ts index e62f9795722403..2cbb7a50442613 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -68,7 +68,6 @@ import { ApplicationSetup, Capabilities, ApplicationStart } from './application' import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } from './context'; -import { InternalApplicationSetup, InternalApplicationStart } from './application/types'; export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; @@ -142,7 +141,7 @@ export interface CoreSetup { */ export interface CoreStart { /** {@link ApplicationStart} */ - application: Pick; + application: ApplicationStart; /** {@link ChromeStart} */ chrome: ChromeStart; /** {@link DocLinksStart} */ @@ -161,15 +160,34 @@ export interface CoreStart { uiSettings: UiSettingsClientContract; } -/** @internal */ -export interface InternalCoreSetup extends Omit { - application: InternalApplicationSetup; +/** + * Setup interface exposed to the legacy platform via the `ui/new_platform` module. + * + * @remarks + * Some methods are not supported in the legacy platform and while present to make this type compatibile with + * {@link CoreSetup}, unsupported methods will throw exceptions when called. + * + * @public + * @deprecated + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface LegacyCoreSetup extends CoreSetup { + /** @deprecated */ injectedMetadata: InjectedMetadataSetup; } -/** @internal */ -export interface InternalCoreStart extends Omit { - application: InternalApplicationStart; +/** + * Start interface exposed to the legacy platform via the `ui/new_platform` module. + * + * @remarks + * Some methods are not supported in the legacy platform and while present to make this type compatibile with + * {@link CoreStart}, unsupported methods will throw exceptions when called. + * + * @public + * @deprecated + */ +export interface LegacyCoreStart extends CoreStart { + /** @deprecated */ injectedMetadata: InjectedMetadataStart; } diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 060ab6f62ad07d..37e07af0a7da58 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -61,7 +61,7 @@ import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; -const applicationSetup = applicationServiceMock.createSetupContract(); +const applicationSetup = applicationServiceMock.createInternalSetupContract(); const contextSetup = contextServiceMock.createSetupContract(); const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract(); const httpSetup = httpServiceMock.createSetupContract(); @@ -88,7 +88,7 @@ const defaultSetupDeps = { plugins: {}, }; -const applicationStart = applicationServiceMock.createStartContract(); +const applicationStart = applicationServiceMock.createInternalStartContract(); const docLinksStart = docLinksServiceMock.createStartContract(); const httpStart = httpServiceMock.createStartContract(); const chromeStart = chromeServiceMock.createStartContract(); @@ -134,7 +134,7 @@ describe('#setup()', () => { legacyPlatform.setup(defaultSetupDeps); expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(defaultSetupDeps.core, {}); + expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(expect.any(Object), {}); }); }); }); @@ -166,7 +166,7 @@ describe('#start()', () => { legacyPlatform.start(defaultStartDeps); expect(mockUiNewPlatformStart).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformStart).toHaveBeenCalledWith(defaultStartDeps.core, {}); + expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); }); describe('useLegacyTestHarness = false', () => { diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 73f4682debe50e..6abc195a77b4bc 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -18,7 +18,8 @@ */ import angular from 'angular'; -import { InternalCoreSetup, InternalCoreStart } from '../'; +import { InternalCoreSetup, InternalCoreStart } from '../core_system'; +import { LegacyCoreSetup, LegacyCoreStart } from '../'; /** @internal */ export interface LegacyPlatformParams { @@ -70,10 +71,18 @@ export class LegacyPlatformService { }) ); + const legacyCore: LegacyCoreSetup = { + ...core, + application: { + register: notSupported(`core.application.register()`), + registerMountContext: notSupported(`core.application.registerMountContext()`), + }, + }; + // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts if (core.injectedMetadata.getLegacyMode()) { - require('ui/new_platform').__setup__(core, plugins); + require('ui/new_platform').__setup__(legacyCore, plugins); } } @@ -99,9 +108,18 @@ export class LegacyPlatformService { return; } + const legacyCore: LegacyCoreStart = { + ...core, + application: { + capabilities: core.application.capabilities, + navigateToApp: core.application.navigateToApp, + registerMountContext: notSupported(`core.application.registerMountContext()`), + }, + }; + // 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').__start__(core, plugins); + require('ui/new_platform').__start__(legacyCore, plugins); // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first @@ -155,3 +173,7 @@ export class LegacyPlatformService { return require('ui/chrome'); } } + +const notSupported = (methodName: string) => (...args: any[]) => { + throw new Error(`${methodName} is not supported in the legacy platform.`); +}; diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 13a52d78d72fc0..1ab9d7f2fa9b29 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -26,7 +26,7 @@ import { createPluginSetupContext, createPluginStartContext, } from './plugin_context'; -import { InternalCoreSetup, InternalCoreStart } from '..'; +import { InternalCoreSetup, InternalCoreStart } from '../core_system'; /** @internal */ export type PluginsServiceSetupDeps = InternalCoreSetup; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b9541b576c04c0..aa4763088b8202 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -8,7 +8,6 @@ import { IconType } from '@elastic/eui'; import { Observable } from 'rxjs'; import React from 'react'; import * as Rx from 'rxjs'; -import { Subject } from 'rxjs'; import { EuiGlobalToastListToast as Toast } from '@elastic/eui'; // @public @@ -238,7 +237,7 @@ export interface CoreSetup { // @public export interface CoreStart { // (undocumented) - application: Pick; + application: ApplicationStart; // (undocumented) chrome: ChromeStart; // (undocumented) @@ -543,27 +542,19 @@ export type IContextHandler, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; -// @internal (undocumented) -export interface InternalCoreSetup extends Omit { - // Warning: (ae-forgotten-export) The symbol "InternalApplicationSetup" needs to be exported by the entry point index.d.ts - // - // (undocumented) - application: InternalApplicationSetup; +// @public @deprecated +export interface LegacyCoreSetup extends CoreSetup { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @deprecated (undocumented) injectedMetadata: InjectedMetadataSetup; } -// @internal (undocumented) -export interface InternalCoreStart extends Omit { - // Warning: (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - application: InternalApplicationStart; +// @public @deprecated +export interface LegacyCoreStart extends CoreStart { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @deprecated (undocumented) injectedMetadata: InjectedMetadataStart; } diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 1e22003b328338..28d57e9f8e8c98 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -33,7 +33,7 @@ import * as Rx from 'rxjs'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { InternalCoreStart } from 'kibana/public'; +import { CoreStart, LegacyCoreStart } from 'kibana/public'; import { fatalError } from 'ui/notify'; import { capabilities } from 'ui/capabilities'; @@ -77,7 +77,7 @@ export const configureAppAngularModule = (angularModule: IModule) => { .run($setupUrlOverflowHandling(newPlatform)); }; -const getEsUrl = (newPlatform: InternalCoreStart) => { +const getEsUrl = (newPlatform: CoreStart) => { const a = document.createElement('a'); a.href = newPlatform.http.basePath.prepend('/elasticsearch'); const protocolPort = /https/.test(a.protocol) ? 443 : 80; @@ -90,7 +90,7 @@ const getEsUrl = (newPlatform: InternalCoreStart) => { }; }; -const setupCompileProvider = (newPlatform: InternalCoreStart) => ( +const setupCompileProvider = (newPlatform: LegacyCoreStart) => ( $compileProvider: ICompileProvider ) => { if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) { @@ -98,7 +98,7 @@ const setupCompileProvider = (newPlatform: InternalCoreStart) => ( } }; -const setupLocationProvider = (newPlatform: InternalCoreStart) => ( +const setupLocationProvider = (newPlatform: CoreStart) => ( $locationProvider: ILocationProvider ) => { $locationProvider.html5Mode({ @@ -110,7 +110,7 @@ const setupLocationProvider = (newPlatform: InternalCoreStart) => ( $locationProvider.hashPrefix(''); }; -export const $setupXsrfRequestInterceptor = (newPlatform: InternalCoreStart) => { +export const $setupXsrfRequestInterceptor = (newPlatform: LegacyCoreStart) => { const version = newPlatform.injectedMetadata.getLegacyMetadata().version; // Configure jQuery prefilter @@ -145,7 +145,7 @@ export const $setupXsrfRequestInterceptor = (newPlatform: InternalCoreStart) => * @param {HttpService} $http * @return {undefined} */ -const capture$httpLoadingCount = (newPlatform: InternalCoreStart) => ( +const capture$httpLoadingCount = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $http: IHttpService ) => { @@ -166,7 +166,7 @@ const capture$httpLoadingCount = (newPlatform: InternalCoreStart) => ( * lets us integrate with the angular router so that we can automatically clear * the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly */ -const $setupBreadcrumbsAutoClear = (newPlatform: InternalCoreStart) => ( +const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -213,7 +213,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: InternalCoreStart) => ( * lets us integrate with the angular router so that we can automatically clear * the badge if we switch to a Kibana app that does not use the badge correctly */ -const $setupBadgeAutoClear = (newPlatform: InternalCoreStart) => ( +const $setupBadgeAutoClear = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -253,7 +253,7 @@ const $setupBadgeAutoClear = (newPlatform: InternalCoreStart) => ( * the helpExtension if we switch to a Kibana app that does not set its own * helpExtension */ -const $setupHelpExtensionAutoClear = (newPlatform: InternalCoreStart) => ( +const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -285,7 +285,7 @@ const $setupHelpExtensionAutoClear = (newPlatform: InternalCoreStart) => ( }); }; -const $setupUrlOverflowHandling = (newPlatform: InternalCoreStart) => ( +const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( $location: ILocationService, $rootScope: IRootScopeService, Private: any, diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 5e0eb2feeb4501..1afc941de06416 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { InternalCoreSetup, InternalCoreStart } from '../../../../core/public'; +import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Setup as InspectorSetup, @@ -34,32 +34,32 @@ export interface PluginsStart { } export const npSetup = { - core: (null as unknown) as InternalCoreSetup, + core: (null as unknown) as LegacyCoreSetup, plugins: {} as PluginsSetup, }; export const npStart = { - core: (null as unknown) as InternalCoreStart, + core: (null as unknown) as LegacyCoreStart, plugins: {} as PluginsStart, }; /** * Only used by unit tests - * @internal + * @Legacy */ export function __reset__() { - npSetup.core = (null as unknown) as InternalCoreSetup; + npSetup.core = (null as unknown) as LegacyCoreSetup; npSetup.plugins = {} as any; - npStart.core = (null as unknown) as InternalCoreStart; + npStart.core = (null as unknown) as LegacyCoreStart; npStart.plugins = {} as any; } -export function __setup__(coreSetup: InternalCoreSetup, plugins: PluginsSetup) { +export function __setup__(coreSetup: LegacyCoreSetup, plugins: PluginsSetup) { npSetup.core = coreSetup; npSetup.plugins = plugins; } -export function __start__(coreStart: InternalCoreStart, plugins: PluginsStart) { +export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) { npStart.core = coreStart; npStart.plugins = plugins; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx index 5eb2626f448728..e5620ee7710439 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx @@ -7,7 +7,7 @@ import { Location } from 'history'; import { last } from 'lodash'; import React from 'react'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; import { useCore } from '../../../hooks/useCore'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs'; @@ -16,7 +16,7 @@ import { routes } from './route_config'; interface Props { location: Location; breadcrumbs: Breadcrumb[]; - core: InternalCoreStart; + core: LegacyCoreStart; } class UpdateBreadcrumbsComponent extends React.Component { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 134934ff8425e1..982f60a53d895c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -31,7 +31,7 @@ import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; import { toastNotifications } from 'ui/notify'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { KibanaLink } from '../../../shared/Links/KibanaLink'; import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch'; @@ -40,7 +40,7 @@ import { CoreContext } from '../../../../context/CoreContext'; type ScheduleKey = keyof Schedule; -const getUserTimezone = memoize((core: InternalCoreStart): string => { +const getUserTimezone = memoize((core: LegacyCoreStart): string => { return core.uiSettings.get('dateFormat:tz') === 'Browser' ? moment.tz.guess() : core.uiSettings.get('dateFormat:tz'); @@ -156,7 +156,7 @@ export class WatcherFlyout extends Component< }; public createWatch = () => { - const core: InternalCoreStart = this.context; + const core: LegacyCoreStart = this.context; const { serviceName } = this.props.urlParams; if (!serviceName) { @@ -278,7 +278,7 @@ export class WatcherFlyout extends Component< return null; } - const core: InternalCoreStart = this.context; + const core: LegacyCoreStart = this.context; const userTimezoneSetting = getUserTimezone(core); const dailyTime = this.state.daily; const inputTime = `${dailyTime}Z`; // Add tz to make into UTC diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx index 513c58d7a834a6..820bc6b2595cf9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx @@ -13,7 +13,7 @@ import { import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; import React, { Fragment } from 'react'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; import { idx } from '@kbn/elastic-idx'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { LicenseContext } from '../../../../context/LicenseContext'; @@ -67,7 +67,7 @@ export class ServiceIntegrations extends React.Component { }; public getWatcherPanelItems = () => { - const core: InternalCoreStart = this.context; + const core: LegacyCoreStart = this.context; return [ { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index b29428cc555eda..c20672745a99c0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -12,7 +12,7 @@ import * as callApmApi from '../../../../services/rest/callApmApi'; import { ServiceOverview } from '..'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; import * as coreHooks from '../../../../hooks/useCore'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; @@ -30,7 +30,7 @@ describe('Service Overview -> View', () => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; // mock urlParams spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 80cecd8ea7f4d8..23331f316084fa 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -15,7 +15,7 @@ import { DiscoverErrorLink } from '../DiscoverErrorLink'; import { DiscoverSpanLink } from '../DiscoverSpanLink'; import { DiscoverTransactionLink } from '../DiscoverTransactionLink'; import * as hooks from '../../../../../hooks/useCore'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -32,7 +32,7 @@ beforeAll(() => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx index 9925d87a159cad..5221831fa57e88 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { InfraLink } from './InfraLink'; import * as hooks from '../../../hooks/useCore'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; const coreMock = ({ http: { @@ -17,7 +17,7 @@ const coreMock = ({ prepend: (path: string) => `/basepath${path}` } } -} as unknown) as InternalCoreStart; +} as unknown) as LegacyCoreStart; jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx index 24637f971bf3c8..a5b27403510173 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { KibanaLink } from './KibanaLink'; import * as hooks from '../../../hooks/useCore'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; describe('KibanaLink', () => { beforeEach(() => { @@ -19,7 +19,7 @@ describe('KibanaLink', () => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index c577a38029d29a..2fbb700a8eca1c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLJobLink } from './MLJobLink'; import * as hooks from '../../../../hooks/useCore'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; describe('MLJobLink', () => { beforeEach(() => { @@ -19,7 +19,7 @@ describe('MLJobLink', () => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; spyOn(hooks, 'useCore').and.returnValue(coreMock); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index 5f66cf8563260c..8eb1c9a3ec1960 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -10,7 +10,7 @@ import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLLink } from './MLLink'; import * as savedObjects from '../../../../services/rest/savedObjects'; import * as hooks from '../../../../hooks/useCore'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -20,7 +20,7 @@ const coreMock = ({ prepend: (path: string) => `/basepath${path}` } } -} as unknown) as InternalCoreStart; +} as unknown) as LegacyCoreStart; jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 89adbd5c0d832a..4fa5751b3578b5 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -13,7 +13,7 @@ import * as Transactions from './mockData'; import * as apmIndexPatternHooks from '../../../../hooks/useAPMIndexPattern'; import * as coreHoooks from '../../../../hooks/useCore'; import { ISavedObject } from '../../../../services/rest/savedObjects'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -35,7 +35,7 @@ describe('TransactionActionMenu component', () => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; jest .spyOn(apmIndexPatternHooks, 'useAPMIndexPattern') diff --git a/x-pack/legacy/plugins/apm/public/context/CoreContext.tsx b/x-pack/legacy/plugins/apm/public/context/CoreContext.tsx index 0bf39e4d9dadb6..5e260f4a1c7b70 100644 --- a/x-pack/legacy/plugins/apm/public/context/CoreContext.tsx +++ b/x-pack/legacy/plugins/apm/public/context/CoreContext.tsx @@ -5,10 +5,10 @@ */ import React, { createContext } from 'react'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; -const CoreContext = createContext({} as InternalCoreStart); -const CoreProvider: React.SFC<{ core: InternalCoreStart }> = props => { +const CoreContext = createContext({} as LegacyCoreStart); +const CoreProvider: React.SFC<{ core: LegacyCoreStart }> = props => { const { core, ...restProps } = props; return ; }; diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index abd793245cbb69..b6f659e1749cf6 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -8,7 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; import { history } from '../utils/history'; import { CoreProvider } from '../context/CoreContext'; import { LocationProvider } from '../context/LocationContext'; @@ -54,7 +54,7 @@ const App = () => { }; export class Plugin { - public start(core: InternalCoreStart) { + public start(core: LegacyCoreStart) { const { i18n } = core; ReactDOM.render( From bef01aa67bc6233c86ccf0e53c3f3c3d48594ff7 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 12 Aug 2019 14:18:47 -0500 Subject: [PATCH 04/14] Fix PR comments --- ...plugin-public.applicationsetup.register.md | 2 +- ...c.applicationsetup.registermountcontext.md | 4 ++-- ...c.applicationstart.registermountcontext.md | 2 +- ...bana-plugin-public.appmountcontextnames.md | 12 +++++++++++ .../core/public/kibana-plugin-public.md | 1 + .../application/application_service.tsx | 11 ++++++---- src/core/public/application/index.ts | 1 + src/core/public/application/types.ts | 21 +++++++++++-------- .../public/application/ui/app_container.tsx | 3 +-- src/core/public/chrome/chrome_service.mock.ts | 2 +- src/core/public/chrome/chrome_service.test.ts | 2 +- src/core/public/chrome/chrome_service.tsx | 4 ++-- src/core/public/index.ts | 10 ++++++++- src/core/public/public.api.md | 7 +++++-- .../rendering/rendering_service.test.tsx | 2 +- .../public/rendering/rendering_service.tsx | 2 +- .../ui/public/new_platform/new_platform.ts | 2 +- 17 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.appmountcontextnames.md diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md index a24eb19c2ea6bb..50bcc28fd17135 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md @@ -16,7 +16,7 @@ register(app: App): void; | Parameter | Type | Description | | --- | --- | --- | -| app | App | | +| app | App | an [App](./kibana-plugin-public.app.md) | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md index cda400df6d0294..7c32e63efe7de6 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md @@ -9,7 +9,7 @@ Register a context provider for application mounting. Will only be available to Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters @@ -17,7 +17,7 @@ registerMountContext(contextName: T, provider: | Parameter | Type | Description | | --- | --- | --- | | contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<AppMountContext, keyof AppMountContext> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | +| provider | IContextProvider<AppMountContext, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md index fc86aaf658b681..9042bf2accd2b3 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md @@ -9,7 +9,7 @@ Register a context provider for application mounting. Will only be available to Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontextnames.md b/docs/development/core/public/kibana-plugin-public.appmountcontextnames.md new file mode 100644 index 00000000000000..fa83ebc86d6c16 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountcontextnames.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountContextNames](./kibana-plugin-public.appmountcontextnames.md) + +## AppMountContextNames type + + +Signature: + +```typescript +export declare type AppMountContextNames = keyof AppMountContext; +``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index ccabdc62c5e7a2..3adccbf10c432b 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -86,6 +86,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | +| [AppMountContextNames](./kibana-plugin-public.appmountcontextnames.md) | | | [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 62178eebf0d705..5f33a9253c5131 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -25,8 +25,7 @@ import { InjectedMetadataStart } from '../injected_metadata'; import { CapabilitiesService } from './capabilities'; import { AppRouter } from './ui'; import { HttpStart } from '../http'; -import { IContextContainer } from '../context'; -import { ContextSetup } from '../context/context_service'; +import { ContextSetup, IContextContainer } from '../context'; import { AppMountContext, App, @@ -93,7 +92,7 @@ export class ApplicationService { if (this.legacyApps$.value.has(app.id)) { throw new Error(`A legacy application is already registered with the id "${app.id}"`); } - if (this.apps$.isStopped) { + if (this.legacyApps$.isStopped) { throw new Error(`Applications cannot be registered after "setup"`); } @@ -153,6 +152,10 @@ export class ApplicationService { }, getComponent: () => { + if (legacyMode) { + return null; + } + // Filter only available apps and map to just the mount function. const appMounters = new Map( [...this.apps$.value] @@ -160,7 +163,7 @@ export class ApplicationService { .map(([id, { mount }]) => [id, mount]) ); - return legacyMode ? null : ( + return ( Promise; export interface ApplicationSetup { /** * Register an mountable application to the system. Apps will be mounted based on their `rootRoute`. - * @param app + * @param app - an {@link App} */ register(app: App): void; @@ -191,9 +194,9 @@ export interface ApplicationSetup { * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ - registerMountContext( + registerMountContext( contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -204,7 +207,7 @@ export interface InternalApplicationSetup { * @param plugin - opaque ID of the plugin that registers this application * @param app */ - register(plugin: symbol, app: App): void; + register(plugin: PluginOpaqueId, app: App): void; /** * Register metadata about legacy applications. Legacy apps will not be mounted when navigated to. @@ -221,10 +224,10 @@ export interface InternalApplicationSetup { * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ - registerMountContext( - pluginOpaqueId: symbol, + registerMountContext( + pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -252,7 +255,7 @@ export interface ApplicationStart { * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ - registerMountContext( + registerMountContext( contextName: T, provider: IContextProvider ): void; @@ -281,7 +284,7 @@ export interface InternalApplicationStart * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ - registerMountContext( + registerMountContext( pluginOpaqueId: PluginOpaqueId, contextName: T, provider: IContextProvider diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 1c54f9aa54b6a9..641a08f57aeca1 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -82,8 +82,7 @@ export class AppContainer extends React.Component { const legacyApp = findLegacyApp(appId, legacyApps); if (legacyApp) { - // Give the current app a chance to shutdown - await this.unmountApp(); + this.unmountApp(); redirectTo(basePath.prepend(`/app/${appId}`)); this.setState({ appNotFound: false }); return; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 74f2a09b895dee..3775989c5126b4 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -27,7 +27,7 @@ import { const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { - getComponent: jest.fn(), + getHeaderComponent: jest.fn(), navLinks: { getNavLinks$: jest.fn(), has: jest.fn(), diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 9a7d87b449b19c..45e94040eeb4a3 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -87,7 +87,7 @@ Array [ const start = await service.start(defaultStartDeps()); // Have to do some fanagling to get the type system and enzyme to accept this. // Don't capture the snapshot because it's 600+ lines long. - expect(shallow(React.createElement(() => start.getComponent()))).toBeDefined(); + expect(shallow(React.createElement(() => start.getHeaderComponent()))).toBeDefined(); }); }); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 0fd68619b73cf1..c8946bf2c98a46 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -124,7 +124,7 @@ export class ChromeService { navLinks, recentlyAccessed, - getComponent: () => ( + getHeaderComponent: () => ( @@ -375,5 +375,5 @@ export interface InternalChromeStart extends ChromeStart { * Used only by MountingService to render the header UI * @internal */ - getComponent(): JSX.Element; + getHeaderComponent(): JSX.Element; } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 2cbb7a50442613..ee97e337928aa6 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -72,7 +72,15 @@ import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } fr export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; -export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application'; +export { + App, + AppBase, + AppUnmount, + AppMountContext, + AppMountContextNames, + AppMountParameters, +} from './application'; + export { SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aa4763088b8202..0f01a91da94cb8 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -30,7 +30,7 @@ export interface AppBase { // @public (undocumented) export interface ApplicationSetup { register(app: App): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public (undocumented) @@ -40,7 +40,7 @@ export interface ApplicationStart { path?: string; state?: any; }): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public @@ -57,6 +57,9 @@ export interface AppMountContext { }; } +// @public (undocumented) +export type AppMountContextNames = keyof AppMountContext; + // @public (undocumented) export interface AppMountParameters { appBasePath: string; diff --git a/src/core/public/rendering/rendering_service.test.tsx b/src/core/public/rendering/rendering_service.test.tsx index f74014abf13532..4d6e7daa7b4ef0 100644 --- a/src/core/public/rendering/rendering_service.test.tsx +++ b/src/core/public/rendering/rendering_service.test.tsx @@ -31,7 +31,7 @@ describe('RenderingService#start', () => { getComponent: () =>
Hello application!
, } as InternalApplicationStart; const chrome = chromeServiceMock.createStartContract(); - chrome.getComponent.mockReturnValue(
Hello chrome!
); + chrome.getHeaderComponent.mockReturnValue(
Hello chrome!
); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); const targetDomElement = document.createElement('div'); const start = rendering.start({ application, chrome, injectedMetadata, targetDomElement }); diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 23a96dc056846f..2e066feca8bf35 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -44,7 +44,7 @@ interface StartDeps { */ export class RenderingService { start({ application, chrome, injectedMetadata, targetDomElement }: StartDeps): RenderingStart { - const chromeUi = chrome.getComponent(); + const chromeUi = chrome.getHeaderComponent(); const appUi = application.getComponent(); const legacyMode = injectedMetadata.getLegacyMode(); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 1afc941de06416..4f55349e3efe26 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -45,7 +45,7 @@ export const npStart = { /** * Only used by unit tests - * @Legacy + * @internal */ export function __reset__() { npSetup.core = (null as unknown) as LegacyCoreSetup; From 8f8c6a0276e4a3b6c74fcc042ab855f5c87ed23d Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 12 Aug 2019 15:33:17 -0500 Subject: [PATCH 05/14] Add functional tests --- test/functional/page_objects/common_page.js | 20 ++- .../core_plugin_a/public/application.tsx | 137 +++++++++++++++++ .../plugins/core_plugin_a/public/plugin.tsx | 10 ++ .../core_plugin_b/public/application.tsx | 144 ++++++++++++++++++ .../plugins/core_plugin_b/public/plugin.tsx | 10 ++ .../test_suites/core_plugins/applications.js | 82 ++++++++++ .../test_suites/core_plugins/index.js | 1 + 7 files changed, 399 insertions(+), 5 deletions(-) create mode 100644 test/plugin_functional/plugins/core_plugin_a/public/application.tsx create mode 100644 test/plugin_functional/plugins/core_plugin_b/public/application.tsx create mode 100644 test/plugin_functional/test_suites/core_plugins/applications.js diff --git a/test/functional/page_objects/common_page.js b/test/functional/page_objects/common_page.js index 651d82608961a4..bd483105373a88 100644 --- a/test/functional/page_objects/common_page.js +++ b/test/functional/page_objects/common_page.js @@ -151,11 +151,21 @@ export function CommonPageProvider({ getService, getPageObjects }) { navigateToApp(appName, { basePath = '', shouldLoginIfPrompted = true, shouldAcceptAlert = true, hash = '' } = {}) { const self = this; - const appConfig = config.get(['apps', appName]); - const appUrl = getUrl.noAuth(config.get('servers.kibana'), { - pathname: `${basePath}${appConfig.pathname}`, - hash: hash || appConfig.hash, - }); + + let appUrl; + if (config.has(['apps', appName])) { + // Legacy applications + const appConfig = config.get(['apps', appName]); + appUrl = getUrl.noAuth(config.get('servers.kibana'), { + pathname: `${basePath}${appConfig.pathname}`, + hash: hash || appConfig.hash, + }); + } else { + appUrl = getUrl.noAuth(config.get('servers.kibana'), { + pathname: `${basePath}/app/${appName}` + }); + } + log.debug('navigating to ' + appName + ' url: ' + appUrl); function navigateTo(url) { diff --git a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx new file mode 100644 index 00000000000000..de82037cc1a49a --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageSideBar, + EuiTitle, + EuiSideNav, +} from '@elastic/eui'; + +import { AppMountContext, AppMountParameters } from 'kibana/public'; + +const Home = () => ( + + + + +

Welcome to Foo!

+
+
+
+ + + + +

Bar home page section title

+
+
+
+ Wow what a home page this is! +
+
+); + +const PageA = () => ( + + + + +

Page A

+
+
+
+ + + + +

Page A section title

+
+
+
+ Page A's content goes here +
+
+); + +type NavProps = RouteComponentProps & { + navigateToApp: AppMountContext['core']['application']['navigateToApp']; +}; +const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( + history.push('/'), + 'data-test-subj': 'foo-nav-home', + }, + { + id: 'page-a', + name: 'Page A', + onClick: () => history.push('/page-a'), + 'data-test-subj': 'foo-nav-page-a', + }, + { + id: 'linktobar', + name: 'Open Bar / Page B', + onClick: () => navigateToApp('bar', { path: 'page-b?query=here', state: 'foo!!' }), + 'data-test-subj': 'foo-nav-bar-page-b', + }, + ], + }, + ]} + /> +)); + +const FooApp = ({ basename, context }: { basename: string; context: AppMountContext }) => ( + + + +