From c3af1fd2220dc9b2f2e73cec65c00b21d4e27d8e Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 10 May 2019 09:03:10 -0500 Subject: [PATCH] Move Nav APIs to new platform (#34490) This moves the core Nav APIs from `ui/chrome` into the `ChromeService` in the new platform. - `ChromeStart` now exposes a sub-service for reading and making limited updates to navlinks. These are powered by apps registered with the `ApplicationService` and filtered by UI Capabilities before being exposed by the `ChromeService`. - The `header-global-nav` directive now consumes navlinks from the new platform. - The `lastSubUrl` feature utilized by legacy apps has been refactored and will remain in `ui/chrome`. This feature utilizes the limited fields that `ChromeService` exposes to updates by outside code. This change is the main blocker to moving the Chrome UI to the new platform. This will be necessary to enable the new platform to control top-level routing. --- ...bana-plugin-public.chromenavlink.active.md | 15 ++ ...ana-plugin-public.chromenavlink.baseurl.md | 13 ++ ...na-plugin-public.chromenavlink.disabled.md | 15 ++ ...plugin-public.chromenavlink.euiicontype.md | 13 ++ ...bana-plugin-public.chromenavlink.hidden.md | 15 ++ ...kibana-plugin-public.chromenavlink.icon.md | 13 ++ .../kibana-plugin-public.chromenavlink.id.md | 13 ++ ...n-public.chromenavlink.linktolastsuburl.md | 15 ++ .../kibana-plugin-public.chromenavlink.md | 31 +++ ...ibana-plugin-public.chromenavlink.order.md | 13 ++ ...-plugin-public.chromenavlink.suburlbase.md | 15 ++ ...ibana-plugin-public.chromenavlink.title.md | 13 ++ ...ana-plugin-public.chromenavlink.tooltip.md | 13 ++ .../kibana-plugin-public.chromenavlink.url.md | 15 ++ .../kibana-plugin-public.chromestart.md | 12 ++ .../kibana-plugin-public.corestart.chrome.md | 13 ++ .../public/kibana-plugin-public.corestart.md | 1 + .../core/public/kibana-plugin-public.md | 2 + .../application/application_service.tsx | 4 +- src/core/public/chrome/chrome_service.mock.ts | 18 ++ src/core/public/chrome/chrome_service.ts | 20 +- src/core/public/chrome/index.ts | 2 + .../public/chrome/nav_links/index.ts} | 8 +- src/core/public/chrome/nav_links/nav_link.ts | 134 +++++++++++++ .../nav_links/nav_links_service.test.ts | 178 +++++++++++++++++ .../chrome/nav_links/nav_links_service.ts | 163 ++++++++++++++++ src/core/public/core_system.ts | 2 + src/core/public/index.ts | 6 + src/core/public/legacy/legacy_service.test.ts | 2 + src/core/public/legacy/legacy_service.ts | 2 + src/core/public/plugins/plugin_context.ts | 4 +- .../public/plugins/plugins_service.test.ts | 1 + src/core/public/public.api.md | 22 +++ .../kibana/public/context/index.js | 3 +- .../hacks/__tests__/hide_empty_tools.js | 12 +- .../dev_tools/hacks/hide_empty_tools.js | 7 +- .../components/fetch_error/fetch_error.js | 4 +- .../kibana/public/visualize/editor/editor.js | 3 +- .../status_page/public/status_page.js | 6 +- .../tests_bundle/tests_entry_template.js | 3 +- .../public/hacks/toggle_app_link_in_nav.ts | 27 +++ .../ui/public/chrome/api/__tests__/nav.js | 182 ++++++++---------- src/legacy/ui/public/chrome/api/angular.js | 4 +- src/legacy/ui/public/chrome/api/nav.d.ts | 48 ----- src/legacy/ui/public/chrome/api/nav.js | 177 ----------------- src/legacy/ui/public/chrome/api/nav.ts | 159 +++++++++++++++ .../ui/public/chrome/api/sub_url_hooks.js | 3 +- .../header_global_nav/components/header.tsx | 64 +++--- .../header_global_nav/header_global_nav.js | 7 +- src/legacy/ui/public/chrome/index.d.ts | 1 - src/legacy/ui/public/url/kibana_parsed_url.ts | 2 +- ...nk_in_nav.js => toggle_app_link_in_nav.ts} | 12 +- .../dashboard_mode/public/dashboard_viewer.js | 3 +- x-pack/plugins/graph/public/app.js | 3 +- .../public/hacks/toggle_app_link_in_nav.js | 30 +-- .../ml/public/hacks/toggle_app_link_in_nav.js | 18 +- .../public/hacks/toggle_app_link_in_nav.js | 6 +- .../public/hacks/job_completion_notifier.js | 9 +- 58 files changed, 1168 insertions(+), 431 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.active.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.id.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.order.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.title.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.url.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromestart.md create mode 100644 docs/development/core/public/kibana-plugin-public.corestart.chrome.md rename src/{legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.js => core/public/chrome/nav_links/index.ts} (78%) create mode 100644 src/core/public/chrome/nav_links/nav_link.ts create mode 100644 src/core/public/chrome/nav_links/nav_links_service.test.ts create mode 100644 src/core/public/chrome/nav_links/nav_links_service.ts create mode 100644 src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts delete mode 100644 src/legacy/ui/public/chrome/api/nav.d.ts delete mode 100644 src/legacy/ui/public/chrome/api/nav.js create mode 100644 src/legacy/ui/public/chrome/api/nav.ts rename x-pack/plugins/apm/public/hacks/{toggle_app_link_in_nav.js => toggle_app_link_in_nav.ts} (50%) diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md new file mode 100644 index 00000000000000..51005323139860 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [active](./kibana-plugin-public.chromenavlink.active.md) + +## ChromeNavLink.active property + +Indicates whether or not this app is currently on the screen. + +NOTE: remove this when ApplicationService is implemented and managing apps. + +Signature: + +```typescript +readonly active?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md new file mode 100644 index 00000000000000..5d50e45c9fe552 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) + +## ChromeNavLink.baseUrl property + +The base route used to open the root of an application. + +Signature: + +```typescript +readonly baseUrl: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md new file mode 100644 index 00000000000000..87f290573b4968 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [disabled](./kibana-plugin-public.chromenavlink.disabled.md) + +## ChromeNavLink.disabled property + +Disables a link from being clickable. + +NOTE: this is only used by the ML and Graph plugins currently. They use this field to disable the nav link when the license is expired. + +Signature: + +```typescript +readonly disabled?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md new file mode 100644 index 00000000000000..37d196ae4558a2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.euiicontype.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) + +## ChromeNavLink.euiIconType property + +A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. + +Signature: + +```typescript +readonly euiIconType?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md new file mode 100644 index 00000000000000..cde90415a2df2b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [hidden](./kibana-plugin-public.chromenavlink.hidden.md) + +## ChromeNavLink.hidden property + +Hides a link from the navigation. + +NOTE: remove this when ApplicationService is implemented. Instead, plugins should only register an Application if needed. + +Signature: + +```typescript +readonly hidden?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md new file mode 100644 index 00000000000000..05e182e756d7e0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.icon.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [icon](./kibana-plugin-public.chromenavlink.icon.md) + +## ChromeNavLink.icon property + +A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided. + +Signature: + +```typescript +readonly icon?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.id.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.id.md new file mode 100644 index 00000000000000..179ca9200178ce --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.id.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [id](./kibana-plugin-public.chromenavlink.id.md) + +## ChromeNavLink.id property + +A unique identifier for looking up links. + +Signature: + +```typescript +readonly id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md new file mode 100644 index 00000000000000..9a7f438f289a65 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) + +## ChromeNavLink.linkToLastSubUrl property + +Whether or not the subUrl feature should be enabled. + +NOTE: only read by legacy platform. + +Signature: + +```typescript +readonly linkToLastSubUrl?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md new file mode 100644 index 00000000000000..e13efce19c0946 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -0,0 +1,31 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) + +## ChromeNavLink interface + + +Signature: + +```typescript +export interface ChromeNavLink +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [active](./kibana-plugin-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen.NOTE: remove this when ApplicationService is implemented and managing apps. | +| [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | +| [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable.NOTE: this is only used by the ML and Graph plugins currently. They use this field to disable the nav link when the license is expired. | +| [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation.NOTE: remove this when ApplicationService is implemented. Instead, plugins should only register an Application if needed. | +| [icon](./kibana-plugin-public.chromenavlink.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.chromenavlink.id.md) | string | A unique identifier for looking up links. | +| [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled.NOTE: only read by legacy platform. | +| [order](./kibana-plugin-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an applcation.NOTE: this should be removed once legacy apps are gone. | +| [title](./kibana-plugin-public.chromenavlink.title.md) | string | The title of the application. | +| [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | +| [url](./kibana-plugin-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications.NOTE: Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should be removed once the ApplicationService is implemented and mounting apps. At that time, each app can handle opening to the previous location when they are mounted. | + diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md new file mode 100644 index 00000000000000..19c86371e334b6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [order](./kibana-plugin-public.chromenavlink.order.md) + +## ChromeNavLink.order property + +An ordinal used to sort nav links relative to one another for display. + +Signature: + +```typescript +readonly order: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md new file mode 100644 index 00000000000000..a00984396cc0c7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) + +## ChromeNavLink.subUrlBase property + +A url base that legacy apps can set to match deep URLs to an applcation. + +NOTE: this should be removed once legacy apps are gone. + +Signature: + +```typescript +readonly subUrlBase?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.title.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.title.md new file mode 100644 index 00000000000000..7c4ff8612f2317 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.title.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [title](./kibana-plugin-public.chromenavlink.title.md) + +## ChromeNavLink.title property + +The title of the application. + +Signature: + +```typescript +readonly title: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md new file mode 100644 index 00000000000000..c33ca742fae29a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.tooltip.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) + +## ChromeNavLink.tooltip property + +A tooltip shown when hovering over an app link. + +Signature: + +```typescript +readonly tooltip?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md new file mode 100644 index 00000000000000..bba9d83ab434cb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [url](./kibana-plugin-public.chromenavlink.url.md) + +## ChromeNavLink.url property + +A url that legacy apps can set to deep link into their applications. + +NOTE: Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should be removed once the ApplicationService is implemented and mounting apps. At that time, each app can handle opening to the previous location when they are mounted. + +Signature: + +```typescript +readonly url?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromestart.md b/docs/development/core/public/kibana-plugin-public.chromestart.md new file mode 100644 index 00000000000000..28ea29dab9b508 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromestart.md @@ -0,0 +1,12 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeStart](./kibana-plugin-public.chromestart.md) + +## ChromeStart type + + +Signature: + +```typescript +export declare type ChromeStart = ReturnType; +``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.chrome.md b/docs/development/core/public/kibana-plugin-public.corestart.chrome.md new file mode 100644 index 00000000000000..9a574edf6b3e5b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.corestart.chrome.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [chrome](./kibana-plugin-public.corestart.chrome.md) + +## CoreStart.chrome property + +[ChromeStart](./kibana-plugin-public.chromestart.md) + +Signature: + +```typescript +chrome: ChromeStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index 80097380874181..5ceedb7416f03a 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -16,6 +16,7 @@ export interface CoreStart | --- | --- | --- | | [application](./kibana-plugin-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [basePath](./kibana-plugin-public.corestart.basepath.md) | BasePathStart | [BasePathStart](./kibana-plugin-public.basepathstart.md) | +| [chrome](./kibana-plugin-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-public.chromestart.md) | | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | | [injectedMetadata](./kibana-plugin-public.corestart.injectedmetadata.md) | InjectedMetadataStart | [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) | | [notifications](./kibana-plugin-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index a7c9a99f1b9d08..a4b0dab9070966 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -22,6 +22,7 @@ | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | +| [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) | | | [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle | | [CoreStart](./kibana-plugin-public.corestart.md) | | | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | @@ -43,6 +44,7 @@ | [BasePathStart](./kibana-plugin-public.basepathstart.md) | Provides access to the 'server.basePath' configuration option in kibana.yml | | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeSetup](./kibana-plugin-public.chromesetup.md) | | +| [ChromeStart](./kibana-plugin-public.chromestart.md) | | | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | | | [InjectedMetadataStart](./kibana-plugin-public.injectedmetadatastart.md) | | diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 7a8fa20e138593..d195984552e660 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -76,8 +76,8 @@ export interface App extends BaseApp { /** @internal */ export interface LegacyApp extends BaseApp { appUrl: string; - - url?: string; + subUrlBase?: string; + linkToLastSubUrl?: boolean; } /** @internal */ diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index bef316968555de..a56c2b63ca493c 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -23,6 +23,7 @@ import { ChromeBreadcrumb, ChromeService, ChromeSetup, + ChromeStart, } from './chrome_service'; const createSetupContractMock = () => { @@ -53,17 +54,34 @@ const createSetupContractMock = () => { return setupContract; }; +const createStartContractMock = (): jest.Mocked => ({ + navLinks: { + getNavLinks$: jest.fn(), + clear: jest.fn(), + has: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + showOnly: jest.fn(), + update: jest.fn(), + enableForcedAppSwitcherNavigation: jest.fn(), + getForceAppSwitcherNavigation$: jest.fn(), + }, +}); + type ChromeServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), }; mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const chromeServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/chrome/chrome_service.ts b/src/core/public/chrome/chrome_service.ts index 1bd8a8b1ef362e..37146b05f92067 100644 --- a/src/core/public/chrome/chrome_service.ts +++ b/src/core/public/chrome/chrome_service.ts @@ -25,6 +25,9 @@ import { map, takeUntil } from 'rxjs/operators'; import { IconType } from '@elastic/eui'; import { InjectedMetadataSetup } from '../injected_metadata'; import { NotificationsSetup } from '../notifications'; +import { NavLinksService } from './nav_links/nav_links_service'; +import { ApplicationStart } from '../application'; +import { BasePathStart } from '../base_path'; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; @@ -65,10 +68,16 @@ interface SetupDeps { notifications: NotificationsSetup; } +interface StartDeps { + application: ApplicationStart; + basePath: BasePathStart; +} + /** @internal */ export class ChromeService { private readonly stop$ = new Rx.ReplaySubject(1); private readonly browserSupportsCsp: boolean; + private readonly navLinks = new NavLinksService(); public constructor({ browserSupportsCsp }: ConstructorParams) { this.browserSupportsCsp = browserSupportsCsp; @@ -183,7 +192,6 @@ export class ChromeService { map(set => [...set]), takeUntil(this.stop$) ), - /** * Get an observable of the current badge */ @@ -222,10 +230,20 @@ export class ChromeService { }; } + public start({ application, basePath }: StartDeps) { + return { + navLinks: this.navLinks.start({ application, basePath }), + }; + } + public stop() { + this.navLinks.stop(); this.stop$.next(); } } /** @public */ export type ChromeSetup = ReturnType; + +/** @public */ +export type ChromeStart = ReturnType; diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index fc816b8dd136d6..77008f5bc53495 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -22,6 +22,8 @@ export { ChromeBreadcrumb, ChromeService, ChromeSetup, + ChromeStart, ChromeBrand, ChromeHelpExtension, } from './chrome_service'; +export { ChromeNavLink } from './nav_links'; diff --git a/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.js b/src/core/public/chrome/nav_links/index.ts similarity index 78% rename from src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.js rename to src/core/public/chrome/nav_links/index.ts index e3b68ddf022825..8060d5cab23ebf 100644 --- a/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.js +++ b/src/core/public/chrome/nav_links/index.ts @@ -17,9 +17,5 @@ * under the License. */ -import chrome from 'ui/chrome'; - -const timelionUiEnabled = chrome.getInjected('timelionUiEnabled'); -if (timelionUiEnabled === false && chrome.navLinkExists('timelion')) { - chrome.getNavLinkById('timelion').hidden = true; -} +export { ChromeNavLink } from './nav_link'; +export { NavLinksService } from './nav_links_service'; diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts new file mode 100644 index 00000000000000..a2f75b220b3193 --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -0,0 +1,134 @@ +/* + * 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 { pick } from '../../../utils'; + +/** + * @public + */ +export interface ChromeNavLink { + /** + * A unique identifier for looking up links. + */ + readonly id: string; + + /** + * Indicates whether or not this app is currently on the screen. + * + * NOTE: remove this when ApplicationService is implemented and managing apps. + */ + readonly active?: boolean; + + /** + * Disables a link from being clickable. + * + * NOTE: this is only used by the ML and Graph plugins currently. They use this field + * to disable the nav link when the license is expired. + */ + readonly disabled?: boolean; + + /** + * Hides a link from the navigation. + * + * NOTE: remove this when ApplicationService is implemented. Instead, plugins should only + * register an Application if needed. + */ + readonly hidden?: boolean; + + /** + * An ordinal used to sort nav links relative to one another for display. + */ + readonly order: number; + + /** + * The title of the application. + */ + readonly title: string; + + /** + * A tooltip shown when hovering over an app link. + */ + readonly tooltip?: string; + + /** + * The base route used to open the root of an application. + */ + readonly baseUrl: string; + + /** + * A EUI iconType that will be used for the app's icon. This icon + * takes precendence over the `icon` property. + */ + readonly euiIconType?: string; + + /** + * A URL to an image file used as an icon. Used as a fallback + * if `euiIconType` is not provided. + */ + readonly icon?: string; + + /** LEGACY FIELDS */ + + /** + * A url base that legacy apps can set to match deep URLs to an applcation. + * + * NOTE: this should be removed once legacy apps are gone. + */ + readonly subUrlBase?: string; + + /** + * Whether or not the subUrl feature should be enabled. + * + * NOTE: only read by legacy platform. + */ + readonly linkToLastSubUrl?: boolean; + + /** + * A url that legacy apps can set to deep link into their applications. + * + * NOTE: Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should + * be removed once the ApplicationService is implemented and mounting apps. At that + * time, each app can handle opening to the previous location when they are mounted. + */ + readonly url?: string; +} + +export type NavLinkUpdateableFields = Partial< + Pick +>; + +export class NavLinkWrapper { + public readonly id: string; + public readonly properties: Readonly; + + constructor(properties: ChromeNavLink) { + if (!properties || !properties.id) { + throw new Error('`id` is required.'); + } + + this.id = properties.id; + this.properties = Object.freeze(properties); + } + + public update(newProps: NavLinkUpdateableFields) { + // Enforce limited properties at runtime for JS code + newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase']); + return new NavLinkWrapper({ ...this.properties, ...newProps }); + } +} 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 new file mode 100644 index 00000000000000..9acc1bc132da8f --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -0,0 +1,178 @@ +/* + * 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 { NavLinksService } from './nav_links_service'; +import { take, map, takeLast } from 'rxjs/operators'; + +const mockAppService = { + availableApps: [ + { id: 'app1', order: 0, title: 'App 1', icon: 'app1', rootRoute: '/app1' }, + { id: 'app2', order: -10, title: 'App 2', euiIconType: 'canvasApp', rootRoute: '/app2' }, + { id: 'legacyApp', order: 20, title: 'Legacy App', appUrl: '/legacy-app' }, + ], +} as any; + +const mockBasePath = { + addToPath: (url: string) => `wow${url}`, +} as any; + +describe('NavLinksService', () => { + let service: NavLinksService; + let start: ReturnType; + + beforeEach(() => { + service = new NavLinksService(); + start = service.start({ application: mockAppService, basePath: mockBasePath }); + }); + + describe('#getNavLinks$()', () => { + it('sorts navlinks by `order` property', async () => { + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.map(l => l.id)) + ) + .toPromise() + ).toEqual(['app2', 'app1', 'legacyApp']); + }); + + it('emits multiple values', async () => { + const navLinkIds$ = start.getNavLinks$().pipe(map(links => links.map(l => l.id))); + const emittedLinks: string[][] = []; + navLinkIds$.subscribe(r => emittedLinks.push(r)); + start.update('app1', { active: true }); + + service.stop(); + expect(emittedLinks).toEqual([['app2', 'app1', 'legacyApp'], ['app2', 'app1', 'legacyApp']]); + }); + + it('completes when service is stopped', async () => { + const last$ = start + .getNavLinks$() + .pipe(takeLast(1)) + .toPromise(); + service.stop(); + await expect(last$).resolves.toBeInstanceOf(Array); + }); + }); + + describe('#get()', () => { + it('returns link if exists', () => { + expect(start.get('app1')!.title).toEqual('App 1'); + }); + + it('returns undefined if it does not exist', () => { + expect(start.get('phony')).toBeUndefined(); + }); + }); + + describe('#getAll()', () => { + it('returns a sorted array of navlinks', () => { + expect(start.getAll().map(l => l.id)).toEqual(['app2', 'app1', 'legacyApp']); + }); + }); + + describe('#has()', () => { + it('returns true if exists', () => { + expect(start.has('app1')).toBe(true); + }); + + it('returns false if it does not exist', () => { + expect(start.has('phony')).toBe(false); + }); + }); + + describe('#showOnly()', () => { + it('does nothing if link does not exist', async () => { + start.showOnly('fake'); + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.map(l => l.id)) + ) + .toPromise() + ).toEqual(['app2', 'app1', 'legacyApp']); + }); + + it('removes all other links', async () => { + start.showOnly('app1'); + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.map(l => l.id)) + ) + .toPromise() + ).toEqual(['app1']); + }); + }); + + describe('#update()', () => { + it('updates the navlinks and returns the updated link', async () => { + expect(start.update('app1', { hidden: true })).toMatchInlineSnapshot(` +Object { + "baseUrl": "http://localhost/wow/app1", + "hidden": true, + "icon": "app1", + "id": "app1", + "order": 0, + "rootRoute": "/app1", + "title": "App 1", +} +`); + const hiddenLinkIds = await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.filter(l => l.hidden).map(l => l.id)) + ) + .toPromise(); + expect(hiddenLinkIds).toEqual(['app1']); + }); + + it('returns undefined if link does not exist', () => { + expect(start.update('fake', { hidden: true })).toBeUndefined(); + }); + }); + + describe('#enableForcedAppSwitcherNavigation()', () => { + it('flips #getForceAppSwitcherNavigation$()', async () => { + await expect( + start + .getForceAppSwitcherNavigation$() + .pipe(take(1)) + .toPromise() + ).resolves.toBe(false); + + start.enableForcedAppSwitcherNavigation(); + + await expect( + start + .getForceAppSwitcherNavigation$() + .pipe(take(1)) + .toPromise() + ).resolves.toBe(true); + }); + }); +}); diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts new file mode 100644 index 00000000000000..156d8e2d01573e --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -0,0 +1,163 @@ +/* + * 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 { sortBy } from 'lodash'; +import { BehaviorSubject, ReplaySubject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { NavLinkWrapper, NavLinkUpdateableFields } from './nav_link'; +import { ApplicationStart } from '../../application'; +import { BasePathStart } from '../../base_path'; + +interface StartDeps { + application: ApplicationStart; + basePath: BasePathStart; +} + +export class NavLinksService { + private readonly stop$ = new ReplaySubject(1); + + public start({ application, basePath }: StartDeps) { + const navLinks$ = new BehaviorSubject>( + new Map( + application.availableApps.map( + app => + [ + app.id, + new NavLinkWrapper({ + ...app, + // Either rootRoute or appUrl must be defined. + baseUrl: relativeToAbsolute(basePath.addToPath((app.rootRoute || app.appUrl)!)), + }), + ] as [string, NavLinkWrapper] + ) + ) + ); + const forceAppSwitcherNavigation$ = new BehaviorSubject(false); + + return { + /** + * Get an observable for a sorted list of navlinks. + */ + getNavLinks$: () => { + return navLinks$.pipe( + map(sortNavLinks), + takeUntil(this.stop$) + ); + }, + + /** + * Get the state of a navlink at this point in time. + * @param id + */ + get(id: string) { + const link = navLinks$.value.get(id); + return link && link.properties; + }, + + /** + * Get the current state of all navlinks. + */ + getAll() { + return sortNavLinks(navLinks$.value); + }, + + /** + * Check whether or not a navlink exists. + * @param id + */ + has(id: string) { + return navLinks$.value.has(id); + }, + + /** + * Remove all navlinks except the one matching the given id. + * NOTE: this is not reversible. + * @param id + */ + showOnly(id: string) { + if (!this.has(id)) { + return; + } + + navLinks$.next(new Map([...navLinks$.value.entries()].filter(([linkId]) => linkId === id))); + }, + + /** + * Update the navlink for the given id with the updated attributes. + * Returns the updated navlink or `undefined` if it does not exist. + * @param id + * @param values + */ + update(id: string, values: NavLinkUpdateableFields) { + if (!this.has(id)) { + return; + } + + navLinks$.next( + new Map( + [...navLinks$.value.entries()].map(([linkId, link]) => { + return [linkId, link.id === id ? link.update(values) : link] as [ + string, + NavLinkWrapper + ]; + }) + ) + ); + + return this.get(id); + }, + + /** + * Enable forced navigation mode, which will trigger a page refresh + * when a nav link is clicked and only the hash is updated. This is only + * necessary when rendering the status page in place of another app, as + * links to that app will set the current URL and change the hash, but + * the routes for the correct are not loaded so nothing will happen. + * https://github.com/elastic/kibana/pull/29770 + * + * Used only by status_page plugin + */ + enableForcedAppSwitcherNavigation() { + forceAppSwitcherNavigation$.next(true); + }, + + /** + * An observable of the forced app switcher state. + */ + getForceAppSwitcherNavigation$() { + return forceAppSwitcherNavigation$.asObservable(); + }, + }; + } + + public stop() { + this.stop$.next(); + } +} + +function sortNavLinks(navLinks: ReadonlyMap) { + return sortBy([...navLinks.values()].map(link => link.properties), 'order'); +} + +function relativeToAbsolute(url: string) { + // convert all link urls to absolute urls + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 8fd329fb10c52a..57a76908ea8c0b 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -165,6 +165,7 @@ export class CoreSystem { const basePath = await this.basePath.start({ injectedMetadata }); const i18n = await this.i18n.start(); const application = await this.application.start({ basePath, injectedMetadata }); + const chrome = await this.chrome.start({ application, basePath }); const notificationsTargetDomElement = document.createElement('div'); const overlayTargetDomElement = document.createElement('div'); @@ -186,6 +187,7 @@ export class CoreSystem { const core: CoreStart = { application, basePath, + chrome, i18n, injectedMetadata, notifications, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 5cdbf35e5a5190..ce4119aa365a34 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -23,7 +23,9 @@ import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, + ChromeNavLink, ChromeSetup, + ChromeStart, } from './chrome'; import { FatalErrorsSetup } from './fatal_errors'; import { HttpSetup } from './http'; @@ -84,6 +86,8 @@ export interface CoreStart { application: ApplicationStart; /** {@link BasePathStart} */ basePath: BasePathStart; + /** {@link ChromeStart} */ + chrome: ChromeStart; /** {@link I18nStart} */ i18n: I18nStart; /** {@link InjectedMetadataStart} */ @@ -103,10 +107,12 @@ export { FatalErrorsSetup, Capabilities, ChromeSetup, + ChromeStart, ChromeBadge, ChromeBreadcrumb, ChromeBrand, ChromeHelpExtension, + ChromeNavLink, I18nSetup, I18nStart, InjectedMetadataSetup, diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 78c159304eda89..59130ece12b07f 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -193,6 +193,7 @@ const defaultSetupDeps = { const applicationStart = applicationServiceMock.createStartContract(); const basePathStart = basePathServiceMock.createStartContract(); +const chromeStart = chromeServiceMock.createStartContract(); const i18nStart = i18nServiceMock.createStartContract(); const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); @@ -202,6 +203,7 @@ const defaultStartDeps = { core: { application: applicationStart, basePath: basePathStart, + chrome: chromeStart, i18n: i18nStart, injectedMetadata: injectedMetadataStart, notifications: notificationsStart, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index bc1aff696ed636..76b39ffafc3dde 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -90,6 +90,8 @@ export class LegacyPlatformService { euiIconType: navLink.euiIconType, icon: navLink.icon, appUrl: navLink.url, + subUrlBase: navLink.subUrlBase, + linkToLastSubUrl: navLink.linkToLastSubUrl, }) ); diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index e5d3989d4a2779..3ab021aa7d99e9 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -19,7 +19,7 @@ import { DiscoveredPlugin } from '../../server'; import { BasePathSetup, BasePathStart } from '../base_path'; -import { ChromeSetup } from '../chrome'; +import { ChromeSetup, ChromeStart } from '../chrome'; import { CoreContext } from '../core_system'; import { FatalErrorsSetup } from '../fatal_errors'; import { I18nSetup, I18nStart } from '../i18n'; @@ -61,6 +61,7 @@ export interface PluginSetupContext { */ export interface PluginStartContext { application: Pick; + chrome: ChromeStart; basePath: BasePathStart; i18n: I18nStart; notifications: NotificationsStart; @@ -127,6 +128,7 @@ export function createPluginStartContext { mockStartDeps = { application: applicationServiceMock.createStartContract(), basePath: basePathServiceMock.createStartContract(), + chrome: chromeServiceMock.createStartContract(), i18n: i18nServiceMock.createStartContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index ffe9284cb0e5c8..e011c9909ca47c 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -87,11 +87,31 @@ export interface ChromeBreadcrumb { // @public (undocumented) export type ChromeHelpExtension = (element: HTMLDivElement) => (() => void); +// @public (undocumented) +export interface ChromeNavLink { + readonly active?: boolean; + readonly baseUrl: string; + readonly disabled?: boolean; + readonly euiIconType?: string; + readonly hidden?: boolean; + readonly icon?: string; + readonly id: string; + readonly linkToLastSubUrl?: boolean; + readonly order: number; + readonly subUrlBase?: string; + readonly title: string; + readonly tooltip?: string; + readonly url?: string; +} + // Warning: (ae-forgotten-export) The symbol "ChromeService" needs to be exported by the entry point index.d.ts // // @public (undocumented) export type ChromeSetup = ReturnType; +// @public (undocumented) +export type ChromeStart = ReturnType; + // @internal (undocumented) export interface CoreContext { } @@ -127,6 +147,8 @@ export interface CoreStart { // (undocumented) basePath: BasePathStart; // (undocumented) + chrome: ChromeStart; + // (undocumented) i18n: I18nStart; // (undocumented) injectedMetadata: InjectedMetadataStart; diff --git a/src/legacy/core_plugins/kibana/public/context/index.js b/src/legacy/core_plugins/kibana/public/context/index.js index d1908be4bea7d8..8cce2c4e73e363 100644 --- a/src/legacy/core_plugins/kibana/public/context/index.js +++ b/src/legacy/core_plugins/kibana/public/context/index.js @@ -27,6 +27,7 @@ import { i18n } from '@kbn/i18n'; import './app'; import contextAppRouteTemplate from './index.html'; import { getRootBreadcrumbs } from '../discover/breadcrumbs'; +import { getNewPlatform } from 'ui/new_platform'; uiRoutes .when('/context/:indexPatternId/:type/:id*', { @@ -85,7 +86,7 @@ function ContextAppRouteController( this.anchorType = $routeParams.type; this.anchorId = $routeParams.id; this.indexPattern = indexPattern; - this.discoverUrl = chrome.getNavLinkById('kibana:discover').lastSubUrl; + this.discoverUrl = getNewPlatform().start.core.chrome.navLinks.get('kibana:discover').url; this.filters = _.cloneDeep(queryFilter.getFilters()); } diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js index bf69fa09823f25..03c86767067bf2 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js +++ b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js @@ -20,11 +20,11 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import chrome from 'ui/chrome'; import { hideEmptyDevTools } from '../hide_empty_tools'; +import { getNewPlatform } from 'ui/new_platform'; describe('hide dev tools', function () { - let navlinks; + let updateNavLink; function PrivateWithoutTools() { return []; @@ -35,12 +35,12 @@ describe('hide dev tools', function () { } function isHidden() { - return !!chrome.getNavLinkById('kibana:dev_tools').hidden; + return updateNavLink.calledWith('kibana:dev_tools', { hidden: true }); } beforeEach(function () { - navlinks = {}; - sinon.stub(chrome, 'getNavLinkById').returns(navlinks); + const coreNavLinks = getNewPlatform().start.core.chrome.navLinks; + updateNavLink = sinon.spy(coreNavLinks, 'update'); }); it('should hide the app if there are no dev tools', function () { @@ -54,6 +54,6 @@ describe('hide dev tools', function () { }); afterEach(function () { - chrome.getNavLinkById.restore(); + updateNavLink.restore(); }); }); diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js index 3426932ea5e72b..c4b07f95f26e24 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js +++ b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js @@ -18,14 +18,15 @@ */ import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; +import { getNewPlatform } from 'ui/new_platform'; export function hideEmptyDevTools(Private) { const hasTools = !!Private(DevToolsRegistryProvider).length; if (!hasTools) { - const navLink = chrome.getNavLinkById('kibana:dev_tools'); - navLink.hidden = true; + getNewPlatform().start.core.chrome.navLinks.update('kibana:dev_tools', { + hidden: true + }); } } diff --git a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js index ec531600bd21eb..4d188030b73127 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js @@ -20,9 +20,9 @@ import 'ngreact'; import React, { Fragment } from 'react'; import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; import { wrapInI18nContext } from 'ui/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getNewPlatform } from 'ui/new_platform'; import { EuiFlexGroup, @@ -40,7 +40,7 @@ const DiscoverFetchError = ({ fetchError }) => { let body; if (fetchError.lang === 'painless') { - const managementUrl = chrome.getNavLinkById('kibana:management').url; + const managementUrl = getNewPlatform().start.core.chrome.navLinks.get('kibana:management').url; const url = `${managementUrl}/kibana/index_patterns`; body = ( diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index af7f9c8d34a61a..fe5df3a8de54de 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -53,6 +53,7 @@ import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; +import { getNewPlatform } from 'ui/new_platform'; import { data } from 'plugins/data'; data.search.loadLegacyDirectives(); @@ -505,7 +506,7 @@ function VisEditor( // url, not the unsaved one. chrome.trackSubUrlForApp('kibana:visualize', savedVisualizationParsedUrl); - const lastDashboardAbsoluteUrl = chrome.getNavLinkById('kibana:dashboard').lastSubUrl; + const lastDashboardAbsoluteUrl = getNewPlatform().start.core.chrome.navLinks.get('kibana:dashboard').url; const dashboardParsedUrl = absoluteToParsedUrl(lastDashboardAbsoluteUrl, chrome.getBasePath()); dashboardParsedUrl.addQueryParameter(DashboardConstants.NEW_VISUALIZATION_ID_PARAM, savedVis.id); kbnUrl.change(dashboardParsedUrl.appPath); diff --git a/src/legacy/core_plugins/status_page/public/status_page.js b/src/legacy/core_plugins/status_page/public/status_page.js index 7aa770b076cf32..140550256303a3 100644 --- a/src/legacy/core_plugins/status_page/public/status_page.js +++ b/src/legacy/core_plugins/status_page/public/status_page.js @@ -20,10 +20,14 @@ import 'ui/autoload/styles'; import 'ui/i18n'; import chrome from 'ui/chrome'; +import { onStart } from 'ui/new_platform'; import { destroyStatusPage, renderStatusPage } from './components/render'; +onStart(({ core }) => { + core.chrome.navLinks.enableForcedAppSwitcherNavigation(); +}); + chrome - .enableForcedAppSwitcherNavigation() .setRootTemplate(require('plugins/status_page/status_page.html')) .setRootController('ui', function ($scope, buildNum, buildSha) { $scope.$$postDigest(() => { 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 79ed7c4e98b558..73dc777b317d93 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -94,7 +94,8 @@ const coreSystem = new CoreSystem({ uiSettings: { defaults: ${JSON.stringify(defaultUiSettings, null, 2).split('\n').join('\n ')}, user: {} - } + }, + nav: [] }, csp: { warnLegacyBrowsers: false, diff --git a/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts b/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts new file mode 100644 index 00000000000000..8aa8940c5bbd54 --- /dev/null +++ b/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts @@ -0,0 +1,27 @@ +/* + * 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 { onStart } from 'ui/new_platform'; + +onStart(({ core }) => { + const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled'); + if (timelionUiEnabled === false) { + core.chrome.navLinks.update('timelion', { hidden: true }); + } +}); diff --git a/src/legacy/ui/public/chrome/api/__tests__/nav.js b/src/legacy/ui/public/chrome/api/__tests__/nav.js index faf43058259e84..ff20f69586180e 100644 --- a/src/legacy/ui/public/chrome/api/__tests__/nav.js +++ b/src/legacy/ui/public/chrome/api/__tests__/nav.js @@ -18,10 +18,12 @@ */ import expect from '@kbn/expect'; +import sinon from 'sinon'; import { initChromeNavApi } from '../nav'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { KibanaParsedUrl } from '../../../url/kibana_parsed_url'; +import { getNewPlatform } from 'ui/new_platform'; +import { absoluteToParsedUrl } from '../../../url/absolute_to_parsed_url'; const basePath = '/someBasePath'; @@ -38,146 +40,124 @@ function init(customInternals = { basePath }) { return { chrome, internals }; } -describe('chrome nav apis', function () { - describe('#getNavLinkById', () => { - it('retrieves the correct nav link, given its ID', () => { - const appUrlStore = new StubBrowserStorage(); - const nav = [ - { id: 'kibana:discover', title: 'Discover' } - ]; - const { - chrome - } = init({ appUrlStore, nav }); - const navLink = chrome.getNavLinkById('kibana:discover'); - expect(navLink).to.eql(nav[0]); - }); - - it('throws an error if the nav link with the given ID is not found', () => { - const appUrlStore = new StubBrowserStorage(); - const nav = [ - { id: 'kibana:discover', title: 'Discover' } - ]; - const { - chrome - } = init({ appUrlStore, nav }); - - let errorThrown = false; - try { - chrome.getNavLinkById('nonexistent'); - } catch (e) { - errorThrown = true; +describe('chrome nav apis', function () { + let coreNavLinks; + let fakedLinks = []; + + const baseUrl = (function () { + const a = document.createElement('a'); + a.setAttribute('href', '/'); + return a.href.slice(0, a.href.length - 1); + }()); + + beforeEach(() => { + coreNavLinks = getNewPlatform().start.core.chrome.navLinks; + sinon.stub(coreNavLinks, 'update').callsFake((linkId, updateAttrs) => { + const link = fakedLinks.find(({ id }) => id === linkId); + for (const key of Object.keys(updateAttrs)) { + link[key] = updateAttrs[key]; } - expect(errorThrown).to.be(true); + return link; }); + sinon.stub(coreNavLinks, 'getAll').callsFake(() => fakedLinks); + sinon.stub(coreNavLinks, 'get').callsFake((linkId) => fakedLinks.find(({ id }) => id === linkId)); + }); + + afterEach(() => { + coreNavLinks.update.restore(); + coreNavLinks.getAll.restore(); + coreNavLinks.get.restore(); }); describe('#untrackNavLinksForDeletedSavedObjects', function () { const appId = 'appId'; - const appUrl = 'https://localhost:9200/app/kibana#test'; + const appUrl = `${baseUrl}/app/kibana#test`; const deletedId = 'IAMDELETED'; it('should clear last url when last url contains link to deleted saved object', function () { const appUrlStore = new StubBrowserStorage(); - const nav = [ - { - id: appId, - title: 'Discover', - linkToLastSubUrl: true, - lastSubUrl: `${appUrl}?id=${deletedId}`, - url: appUrl - } - ]; - const { - chrome - } = init({ appUrlStore, nav }); - + fakedLinks = [{ + id: appId, + title: 'Discover', + url: `${appUrl}?id=${deletedId}`, + baseUrl: appUrl, + linkToLastSubUrl: true, + }]; + + const { chrome } = init({ appUrlStore }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); - expect(chrome.getNavLinkById('appId').lastSubUrl).to.be(appUrl); + expect(coreNavLinks.update.calledWith(appId, { url: appUrl })).to.be(true); }); it('should not clear last url when last url does not contains link to deleted saved object', function () { const lastUrl = `${appUrl}?id=anotherSavedObjectId`; const appUrlStore = new StubBrowserStorage(); - const nav = [ - { - id: appId, - title: 'Discover', - linkToLastSubUrl: true, - lastSubUrl: lastUrl, - url: appUrl - } - ]; - const { - chrome - } = init({ appUrlStore, nav }); - + fakedLinks = [{ + id: appId, + title: 'Discover', + url: lastUrl, + baseUrl: appUrl, + linkToLastSubUrl: true + }]; + + const { chrome } = init({ appUrlStore }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); - expect(chrome.getNavLinkById(appId).lastSubUrl).to.be(lastUrl); + expect(coreNavLinks.update.calledWith(appId, { url: appUrl })).to.be(false); }); }); describe('internals.trackPossibleSubUrl()', function () { it('injects the globalState of the current url to all links for the same app', function () { const appUrlStore = new StubBrowserStorage(); - const nav = [ + fakedLinks = [ { - url: 'https://localhost:9200/app/kibana#discover', - subUrlBase: 'https://localhost:9200/app/kibana#discover' + id: 'kibana:discover', + baseUrl: `${baseUrl}/app/kibana#discover`, + subUrlBase: '/app/kibana#discover' }, { - url: 'https://localhost:9200/app/kibana#visualize', - subUrlBase: 'https://localhost:9200/app/kibana#visualize' + id: 'kibana:visualize', + baseUrl: `${baseUrl}/app/kibana#visualize`, + subUrlBase: '/app/kibana#visualize' }, { - url: 'https://localhost:9200/app/kibana#dashboards', - subUrlBase: 'https://localhost:9200/app/kibana#dashboard' + id: 'kibana:dashboard', + baseUrl: `${baseUrl}/app/kibana#dashboards`, + subUrlBase: '/app/kibana#dashboard' }, - ].map(l => { - l.lastSubUrl = l.url; - return l; - }); + ]; - const { - internals - } = init({ appUrlStore, nav }); + const { internals } = init({ appUrlStore }); + internals.trackPossibleSubUrl(`${baseUrl}/app/kibana#dashboard?_g=globalstate`); - internals.trackPossibleSubUrl('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); - expect(internals.nav[0].lastSubUrl).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate'); - expect(internals.nav[0].active).to.be(false); + expect(fakedLinks[0].url).to.be(`${baseUrl}/app/kibana#discover?_g=globalstate`); + expect(fakedLinks[0].active).to.be(false); - expect(internals.nav[1].lastSubUrl).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate'); - expect(internals.nav[1].active).to.be(false); + expect(fakedLinks[1].url).to.be(`${baseUrl}/app/kibana#visualize?_g=globalstate`); + expect(fakedLinks[1].active).to.be(false); - expect(internals.nav[2].lastSubUrl).to.be('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); - expect(internals.nav[2].active).to.be(true); + expect(fakedLinks[2].url).to.be(`${baseUrl}/app/kibana#dashboard?_g=globalstate`); + expect(fakedLinks[2].active).to.be(true); }); }); - describe('internals.trackSubUrlForApp()', function () { + describe('chrome.trackSubUrlForApp()', function () { it('injects a manual app url', function () { const appUrlStore = new StubBrowserStorage(); - const nav = [ - { - id: 'kibana:visualize', - url: 'https://localhost:9200/app/kibana#visualize', - lastSubUrl: 'https://localhost:9200/app/kibana#visualize', - subUrlBase: 'https://localhost:9200/app/kibana#visualize' - } - ]; - - const { chrome, internals } = init({ appUrlStore, nav }); - - const basePath = '/xyz'; - const appId = 'kibana'; - const appPath = 'visualize/1234?_g=globalstate'; - const hostname = 'localhost'; - const port = '9200'; - const protocol = 'https'; - - const kibanaParsedUrl = new KibanaParsedUrl({ basePath, appId, appPath, hostname, port, protocol }); + fakedLinks = [{ + id: 'kibana:visualize', + baseUrl: `${baseUrl}/app/kibana#visualize`, + url: `${baseUrl}/app/kibana#visualize`, + subUrlBase: '/app/kibana#visualize', + }]; + + const { chrome } = init({ appUrlStore }); + const kibanaParsedUrl = absoluteToParsedUrl(`${baseUrl}/xyz/app/kibana#visualize/1234?_g=globalstate`, '/xyz'); chrome.trackSubUrlForApp('kibana:visualize', kibanaParsedUrl); - expect(internals.nav[0].lastSubUrl).to.be('https://localhost:9200/xyz/app/kibana#visualize/1234?_g=globalstate'); + expect( + coreNavLinks.update.calledWith('kibana:visualize', { url: `${baseUrl}/xyz/app/kibana#visualize/1234?_g=globalstate` }) + ).to.be(true); }); }); }); diff --git a/src/legacy/ui/public/chrome/api/angular.js b/src/legacy/ui/public/chrome/api/angular.js index d01ed1a355614e..e6457fec936330 100644 --- a/src/legacy/ui/public/chrome/api/angular.js +++ b/src/legacy/ui/public/chrome/api/angular.js @@ -29,9 +29,7 @@ export function initAngularApi(chrome, internals) { configureAppAngularModule(kibana); - kibana - .value('chrome', chrome) - .run(internals.$initNavLinksDeepWatch); + kibana.value('chrome', chrome); registerSubUrlHooks(kibana, internals); directivesProvider(chrome, internals); diff --git a/src/legacy/ui/public/chrome/api/nav.d.ts b/src/legacy/ui/public/chrome/api/nav.d.ts deleted file mode 100644 index f7c639bc5733c2..00000000000000 --- a/src/legacy/ui/public/chrome/api/nav.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { IconType } from '@elastic/eui'; -import * as Rx from 'rxjs'; - -import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; - -export interface NavLink { - title: string; - url: string; - subUrlBase: string; - id: string; - euiIconType: IconType; - icon?: string; - active: boolean; - lastSubUrl?: string; - hidden?: boolean; - disabled?: boolean; -} - -export interface ChromeNavLinks { - getNavLinks$(): Rx.Observable; - getNavLinks(): NavLink[]; - navLinkExists(id: string): boolean; - getNavLinkById(id: string): NavLink; - showOnlyById(id: string): void; - untrackNavLinksForDeletedSavedObjects(deletedIds: string[]): void; - trackSubUrlForApp(linkId: string, parsedKibanaUrl: KibanaParsedUrl): void; - enableForcedAppSwitcherNavigation(): this; - getForcedAppSwitcherNavigation$(): Rx.Observable; -} diff --git a/src/legacy/ui/public/chrome/api/nav.js b/src/legacy/ui/public/chrome/api/nav.js deleted file mode 100644 index 1feb0a8f22e0a0..00000000000000 --- a/src/legacy/ui/public/chrome/api/nav.js +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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 * as Rx from 'rxjs'; -import { mapTo } from 'rxjs/operators'; -import { remove } from 'lodash'; -import { relativeToAbsolute } from '../../url/relative_to_absolute'; -import { absoluteToParsedUrl } from '../../url/absolute_to_parsed_url'; - -export function initChromeNavApi(chrome, internals) { - const navUpdate$ = new Rx.BehaviorSubject(undefined); - - chrome.getNavLinks = function () { - return internals.nav; - }; - - chrome.getNavLinks$ = function () { - return navUpdate$.pipe(mapTo(internals.nav)); - }; - - // track navLinks with $rootScope.$watch like the old nav used to, necessary - // as long as random parts of the app are directly mutating the navLinks - internals.$initNavLinksDeepWatch = function ($rootScope) { - $rootScope.$watch( - () => internals.nav, - () => navUpdate$.next(), - true - ); - }; - - - const forceAppSwitcherNavigation$ = new Rx.BehaviorSubject(false); - /** - * Enable forced navigation mode, which will trigger a page refresh - * when a nav link is clicked and only the hash is updated. This is only - * necessary when rendering the status page in place of another app, as - * links to that app will set the current URL and change the hash, but - * the routes for the correct are not loaded so nothing will happen. - * https://github.com/elastic/kibana/pull/29770 - */ - chrome.enableForcedAppSwitcherNavigation = () => { - forceAppSwitcherNavigation$.next(true); - return chrome; - }; - chrome.getForceAppSwitcherNavigation$ = () => { - return forceAppSwitcherNavigation$.asObservable(); - }; - - chrome.navLinkExists = (id) => { - return !!internals.nav.find(link => link.id === id); - }; - - chrome.getNavLinkById = (id) => { - const navLink = internals.nav.find(link => link.id === id); - if (!navLink) { - throw new Error(`Nav link for id = ${id} not found`); - } - return navLink; - }; - - chrome.showOnlyById = (id) => { - remove(internals.nav, app => app.id !== id); - }; - - function lastSubUrlKey(link) { - return `lastSubUrl:${link.url}`; - } - - function setLastUrl(link, url) { - if (link.linkToLastSubUrl === false) { - return; - } - - link.lastSubUrl = url; - internals.appUrlStore.setItem(lastSubUrlKey(link), url); - } - - function refreshLastUrl(link) { - link.lastSubUrl = internals.appUrlStore.getItem(lastSubUrlKey(link)) || link.lastSubUrl || link.url; - } - - function injectNewGlobalState(link, fromAppId, newGlobalState) { - const kibanaParsedUrl = absoluteToParsedUrl(link.lastSubUrl, chrome.getBasePath()); - - // don't copy global state if links are for different apps - if (fromAppId !== kibanaParsedUrl.appId) return; - - kibanaParsedUrl.setGlobalState(newGlobalState); - - link.lastSubUrl = kibanaParsedUrl.getAbsoluteUrl(); - } - - /** - * Clear last url for deleted saved objects to avoid loading pages with "Could not locate.." - */ - chrome.untrackNavLinksForDeletedSavedObjects = (deletedIds) => { - function urlContainsDeletedId(url) { - const includedId = deletedIds.find(deletedId => { - return url.includes(deletedId); - }); - if (includedId === undefined) { - return false; - } - return true; - } - - internals.nav.forEach(link => { - if (link.linkToLastSubUrl && urlContainsDeletedId(link.lastSubUrl)) { - setLastUrl(link, link.url); - } - }); - }; - - /** - * Manually sets the last url for the given app. The last url for a given app is updated automatically during - * normal page navigation, so this should only need to be called to insert a last url that was not actually - * navigated to. For instance, when saving an object and redirecting to another page, the last url of the app - * should be the saved instance, but because of the redirect to a different page (e.g. `Save and Add to Dashboard` - * on visualize tab), it won't be tracked automatically and will need to be inserted manually. See - * https://github.com/elastic/kibana/pull/11932 for more background on why this was added. - * @param linkId {String} - an id that represents the navigation link. - * @param kibanaParsedUrl {KibanaParsedUrl} the url to track - */ - chrome.trackSubUrlForApp = (linkId, kibanaParsedUrl) => { - for (const link of internals.nav) { - if (link.id === linkId) { - const absoluteUrl = kibanaParsedUrl.getAbsoluteUrl(); - setLastUrl(link, absoluteUrl); - return; - } - } - }; - - internals.trackPossibleSubUrl = function (url) { - const kibanaParsedUrl = absoluteToParsedUrl(url, chrome.getBasePath()); - - for (const link of internals.nav) { - link.active = url.startsWith(link.subUrlBase); - if (link.active) { - setLastUrl(link, url); - continue; - } - - refreshLastUrl(link); - - const newGlobalState = kibanaParsedUrl.getGlobalState(); - if (newGlobalState) { - injectNewGlobalState(link, kibanaParsedUrl.appId, newGlobalState); - } - } - }; - - internals.nav.forEach(link => { - link.url = relativeToAbsolute(chrome.addBasePath(link.url)); - link.subUrlBase = relativeToAbsolute(chrome.addBasePath(link.subUrlBase)); - }); - - // simulate a possible change in url to initialize the - // link.active and link.lastUrl properties - internals.trackPossibleSubUrl(document.location.href); -} diff --git a/src/legacy/ui/public/chrome/api/nav.ts b/src/legacy/ui/public/chrome/api/nav.ts new file mode 100644 index 00000000000000..8a4504125d82bc --- /dev/null +++ b/src/legacy/ui/public/chrome/api/nav.ts @@ -0,0 +1,159 @@ +/* + * 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 { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; +import { absoluteToParsedUrl } from '../../url/absolute_to_parsed_url'; +import { onStart } from '../../new_platform'; +import { ChromeStart, ChromeNavLink } from '../../../../../core/public'; +import { relativeToAbsolute } from '../../url/relative_to_absolute'; + +export interface ChromeNavLinks { + untrackNavLinksForDeletedSavedObjects(deletedIds: string[]): void; + trackSubUrlForApp(linkId: string, parsedKibanaUrl: KibanaParsedUrl): void; +} + +interface NavInternals { + appUrlStore: Storage; + trackPossibleSubUrl(url: string): void; +} + +export function initChromeNavApi(chrome: any, internals: NavInternals) { + let coreNavLinks: ChromeStart['navLinks']; + onStart(({ core }) => (coreNavLinks = core.chrome.navLinks)); + + /** + * Clear last url for deleted saved objects to avoid loading pages with "Could not locate..." + */ + chrome.untrackNavLinksForDeletedSavedObjects = (deletedIds: string[]) => { + function urlContainsDeletedId(url: string) { + const includedId = deletedIds.find(deletedId => { + return url.includes(deletedId); + }); + return includedId !== undefined; + } + + coreNavLinks.getAll().forEach(link => { + if (link.linkToLastSubUrl && urlContainsDeletedId(link.url!)) { + setLastUrl(link, link.baseUrl); + } + }); + }; + + /** + * Manually sets the last url for the given app. The last url for a given app is updated automatically during + * normal page navigation, so this should only need to be called to insert a last url that was not actually + * navigated to. For instance, when saving an object and redirecting to another page, the last url of the app + * should be the saved instance, but because of the redirect to a different page (e.g. `Save and Add to Dashboard` + * on visualize tab), it won't be tracked automatically and will need to be inserted manually. See + * https://github.com/elastic/kibana/pull/11932 for more background on why this was added. + * + * @param id {String} - an id that represents the navigation link. + * @param kibanaParsedUrl {KibanaParsedUrl} the url to track + */ + chrome.trackSubUrlForApp = (id: string, kibanaParsedUrl: KibanaParsedUrl) => { + const navLink = coreNavLinks.get(id); + if (navLink) { + setLastUrl(navLink, kibanaParsedUrl.getAbsoluteUrl()); + } + }; + + internals.trackPossibleSubUrl = async function(url: string) { + const kibanaParsedUrl = absoluteToParsedUrl(url, chrome.getBasePath()); + + coreNavLinks + .getAll() + // Filter only legacy links + .filter(link => link.subUrlBase) + .forEach(link => { + const active = url.startsWith(link.subUrlBase!); + link = coreNavLinks.update(link.id, { active })!; + + if (active) { + setLastUrl(link, url); + return; + } + + link = refreshLastUrl(link); + + const newGlobalState = kibanaParsedUrl.getGlobalState(); + if (newGlobalState) { + injectNewGlobalState(link, kibanaParsedUrl.appId, newGlobalState); + } + }); + }; + + function lastSubUrlKey(link: ChromeNavLink) { + return `lastSubUrl:${link.baseUrl}`; + } + + function getLastUrl(link: ChromeNavLink) { + return internals.appUrlStore.getItem(lastSubUrlKey(link)); + } + + function setLastUrl(link: ChromeNavLink, url: string) { + if (link.linkToLastSubUrl === false) { + return; + } + + internals.appUrlStore.setItem(lastSubUrlKey(link), url); + refreshLastUrl(link); + } + + function refreshLastUrl(link: ChromeNavLink) { + const lastSubUrl = getLastUrl(link); + + return coreNavLinks.update(link.id, { + url: lastSubUrl || link.url || link.baseUrl, + })!; + } + + function injectNewGlobalState( + link: ChromeNavLink, + fromAppId: string, + newGlobalState: string | string[] + ) { + const kibanaParsedUrl = absoluteToParsedUrl( + getLastUrl(link) || link.url || link.baseUrl, + chrome.getBasePath() + ); + + // don't copy global state if links are for different apps + if (fromAppId !== kibanaParsedUrl.appId) return; + + kibanaParsedUrl.setGlobalState(newGlobalState); + + coreNavLinks.update(link.id, { + url: kibanaParsedUrl.getAbsoluteUrl(), + }); + } + + // simulate a possible change in url to initialize the + // link.active and link.lastUrl properties + onStart(({ core }) => { + core.chrome.navLinks + .getAll() + .filter(link => link.subUrlBase) + .forEach(link => { + core.chrome.navLinks.update(link.id, { + subUrlBase: relativeToAbsolute(chrome.addBasePath(link.subUrlBase)), + }); + }); + internals.trackPossibleSubUrl(document.location.href); + }); +} diff --git a/src/legacy/ui/public/chrome/api/sub_url_hooks.js b/src/legacy/ui/public/chrome/api/sub_url_hooks.js index beb762a1235e6e..5ed3af9b59ff68 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -23,6 +23,7 @@ import { getUnhashableStatesProvider, unhashUrl, } from '../../state_management/state_hashing'; +import { onStart } from '../../new_platform'; export function registerSubUrlHooks(angularModule, internals) { angularModule.run(($rootScope, Private, $location) => { @@ -60,7 +61,7 @@ export function registerSubUrlHooks(angularModule, internals) { $rootScope.$on('$routeChangeSuccess', onRouteChange); $rootScope.$on('$routeUpdate', onRouteChange); - updateSubUrls(); // initialize sub urls + onStart(updateSubUrls); // initialize sub urls }); } diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx b/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx index af593384ae16ec..4c9c4001a9a4e0 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx @@ -52,8 +52,7 @@ import { import { i18n } from '@kbn/i18n'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { UICapabilities } from 'ui/capabilities'; -import chrome, { NavLink } from 'ui/chrome'; +import chrome from 'ui/chrome'; import { HelpExtension } from 'ui/chrome'; import { RecentlyAccessedHistoryItem } from 'ui/persisted_log'; import { ChromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls'; @@ -65,7 +64,7 @@ import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; import { NavControlSide } from '../'; -import { ChromeBadge, ChromeBreadcrumb } from '../../../../../../../core/public'; +import { ChromeBadge, ChromeBreadcrumb, ChromeNavLink } from '../../../../../../../core/public'; interface Props { appTitle?: string; @@ -73,13 +72,12 @@ interface Props { breadcrumbs$: Rx.Observable; homeHref: string; isVisible: boolean; - navLinks$: Rx.Observable; + navLinks$: Rx.Observable; recentlyAccessed$: Rx.Observable; forceAppSwitcherNavigation$: Rx.Observable; helpExtension$: Rx.Observable; navControls: ChromeHeaderNavControlsRegistry; intl: InjectedIntl; - uiCapabilities: UICapabilities; } // Providing a buffer between the limit and the cut off index @@ -88,11 +86,11 @@ const TRUNCATE_LIMIT: number = 64; const TRUNCATE_AT: number = 58; function extendRecentlyAccessedHistoryItem( - navLinks: NavLink[], + navLinks: ChromeNavLink[], recentlyAccessed: RecentlyAccessedHistoryItem ) { const href = relativeToAbsolute(chrome.addBasePath(recentlyAccessed.link)); - const navLink = navLinks.find(nl => href.startsWith(nl.subUrlBase)); + const navLink = navLinks.find(nl => href.startsWith(nl.subUrlBase || nl.baseUrl)); let titleAndAriaLabel = recentlyAccessed.label; if (navLink) { @@ -114,10 +112,10 @@ function extendRecentlyAccessedHistoryItem( }; } -function extendNavLink(navLink: NavLink) { +function extendNavLink(navLink: ChromeNavLink) { return { ...navLink, - href: navLink.lastSubUrl && !navLink.active ? navLink.lastSubUrl : navLink.url, + href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl, }; } @@ -224,7 +222,6 @@ class HeaderUI extends Component { navControls, helpExtension$, intl, - uiCapabilities, } = this.props; const { navLinks, recentlyAccessed } = this.state; @@ -235,31 +232,28 @@ class HeaderUI extends Component { const leftNavControls = navControls.bySide[NavControlSide.Left]; const rightNavControls = navControls.bySide[NavControlSide.Right]; - let navLinksArray = navLinks.map(navLink => - navLink.hidden || !uiCapabilities.navLinks[navLink.id] - ? null - : { - key: navLink.id, - label: navLink.title, - href: navLink.href, - iconType: navLink.euiIconType, - icon: - !navLink.euiIconType && navLink.icon ? ( - - ) : ( - undefined - ), - isActive: navLink.active, - 'data-test-subj': 'navDrawerAppsMenuLink', - } - ); - // filter out the null items - navLinksArray = navLinksArray.filter(item => item !== null); + const navLinksArray = navLinks + .filter(navLink => !navLink.hidden) + .map(navLink => ({ + key: navLink.id, + label: navLink.title, + href: navLink.href, + isDisabled: navLink.disabled, + isActive: navLink.active, + iconType: navLink.euiIconType, + icon: + !navLink.euiIconType && navLink.icon ? ( + + ) : ( + undefined + ), + 'data-test-subj': 'navDrawerAppsMenuLink', + })); const recentLinksArray = [ { diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js index f4b5022089b4d3..d2e16cf7dad444 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js @@ -22,6 +22,7 @@ import { uiModules } from '../../../modules'; import { Header } from './components/header'; import { wrapInI18nContext } from 'ui/i18n'; import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls'; +import { getNewPlatform } from '../../../new_platform'; const module = uiModules.get('kibana'); @@ -29,6 +30,8 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili const { recentlyAccessed } = require('ui/persisted_log'); const navControls = Private(chromeHeaderNavControlsRegistry); const homeHref = chrome.addBasePath('/app/kibana#/home'); + const newPlatform = getNewPlatform(); + const newPlatformStart = newPlatform.start.core; return reactDirective(wrapInI18nContext(Header), [ // scope accepted by directive, passed in as React props @@ -41,9 +44,9 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili badge$: chrome.badge.get$(), breadcrumbs$: chrome.breadcrumbs.get$(), helpExtension$: chrome.helpExtension.get$(), - navLinks$: chrome.getNavLinks$(), + navLinks$: newPlatformStart.chrome.navLinks.getNavLinks$(), recentlyAccessed$: recentlyAccessed.get$(), - forceAppSwitcherNavigation$: chrome.getForceAppSwitcherNavigation$(), + forceAppSwitcherNavigation$: newPlatformStart.chrome.navLinks.getForceAppSwitcherNavigation$(), navControls, homeHref, uiCapabilities, diff --git a/src/legacy/ui/public/chrome/index.d.ts b/src/legacy/ui/public/chrome/index.d.ts index 71532fbbebccca..7f61c7a2ca8c42 100644 --- a/src/legacy/ui/public/chrome/index.d.ts +++ b/src/legacy/ui/public/chrome/index.d.ts @@ -55,5 +55,4 @@ declare const chrome: Chrome; export default chrome; export { Chrome }; export { Breadcrumb } from './api/breadcrumbs'; -export { NavLink } from './api/nav'; export { HelpExtension } from './api/help_extension'; diff --git a/src/legacy/ui/public/url/kibana_parsed_url.ts b/src/legacy/ui/public/url/kibana_parsed_url.ts index 7f1653a9f8d864..d431c775d1d2e6 100644 --- a/src/legacy/ui/public/url/kibana_parsed_url.ts +++ b/src/legacy/ui/public/url/kibana_parsed_url.ts @@ -106,7 +106,7 @@ export class KibanaParsedUrl { return query._g || ''; } - public setGlobalState(newGlobalState: string) { + public setGlobalState(newGlobalState: string | string[]) { if (!this.appPath) { return; } diff --git a/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.ts similarity index 50% rename from x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.js rename to x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.ts index 90a8f31364f549..a4304876c7ec25 100644 --- a/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { onStart } from 'ui/new_platform'; -const apmUiEnabled = chrome.getInjected('apmUiEnabled'); -if (apmUiEnabled === false && chrome.navLinkExists('apm')) { - chrome.getNavLinkById('apm').hidden = true; -} +onStart(({ core }) => { + const apmUiEnabled = core.injectedMetadata.getInjectedVar('apmUiEnabled'); + if (apmUiEnabled === false) { + core.chrome.navLinks.update('apm', { hidden: true }); + } +}); diff --git a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js index a653804d634b20..8b04e8ee82c862 100644 --- a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js @@ -37,6 +37,7 @@ import 'ui/agg_response'; import 'ui/agg_types'; import 'ui/timepicker'; import 'leaflet'; +import { getNewPlatform } from 'ui/new_platform'; import { showAppRedirectNotification } from 'ui/notify'; import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashboard/dashboard_constants'; @@ -49,7 +50,7 @@ routes.otherwise({ redirectTo: defaultUrl() }); chrome .setRootController('kibana', function () { - chrome.showOnlyById('kibana:dashboard'); + getNewPlatform().start.core.chrome.navLinks.showOnly('kibana:dashboard'); }); uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 837e40815fb8bb..17c8760ab2e28a 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -26,6 +26,7 @@ import { notify, addAppRedirectMessageToUrl, fatalError, toastNotifications } fr import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; +import { getNewPlatform } from 'ui/new_platform'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; @@ -758,7 +759,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private .on('zoom', redraw)); - const managementUrl = chrome.getNavLinkById('kibana:management').url; + const managementUrl = getNewPlatform().start.core.chrome.navLinks.get('kibana:management').url; const url = `${managementUrl}/kibana/index_patterns`; if ($scope.indices.length === 0) { diff --git a/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js index 70162e321f716e..5d6e406e9e8560 100644 --- a/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; +import { onStart } from 'ui/new_platform'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; -uiModules.get('xpack/graph').run((Private) => { - const xpackInfo = Private(XPackInfoProvider); - if (!chrome.navLinkExists('graph')) { - return; - } +uiModules.get('xpack/graph') + .run(Private => { + const xpackInfo = Private(XPackInfoProvider); - const navLink = chrome.getNavLinkById('graph'); - navLink.hidden = true; - const showAppLink = xpackInfo.get('features.graph.showAppLink', false); - navLink.hidden = !showAppLink; - if (showAppLink) { - navLink.disabled = !xpackInfo.get('features.graph.enableAppLink', false); - navLink.tooltip = xpackInfo.get('features.graph.message'); - } -}); + const navLinkUpdates = {}; + navLinkUpdates.hidden = true; + const showAppLink = xpackInfo.get('features.graph.showAppLink', false); + navLinkUpdates.hidden = !showAppLink; + if (showAppLink) { + navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); + navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); + } + + onStart(({ core }) => core.chrome.navLinks.update('graph', navLinkUpdates)); + }); diff --git a/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js index bed0fe46c32353..27c5e28b3300b8 100644 --- a/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js @@ -7,19 +7,19 @@ import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; -import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; +import { onStart } from 'ui/new_platform'; uiModules.get('xpack/ml').run((Private) => { const xpackInfo = Private(XPackInfoProvider); - if (!chrome.navLinkExists('ml')) return; - const navLink = chrome.getNavLinkById('ml'); - // hide by default, only show once the xpackInfo is initialized - navLink.hidden = true; const showAppLink = xpackInfo.get('features.ml.showLinks', false); - navLink.hidden = !showAppLink; - if (showAppLink) { - navLink.disabled = !xpackInfo.get('features.ml.isAvailable', false); - } + + const navLinkUpdates = { + // hide by default, only show once the xpackInfo is initialized + hidden: !showAppLink, + disabled: !showAppLink || (showAppLink && !xpackInfo.get('features.ml.isAvailable', false)) + }; + + onStart(({ core }) => core.chrome.navLinks.update('ml', navLinkUpdates)); }); diff --git a/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js index 451793b83dd659..c68d0d37b77c5e 100644 --- a/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; +import { onStart } from 'ui/new_platform'; uiModules.get('monitoring/hacks').run((monitoringUiEnabled) => { - if (monitoringUiEnabled || !chrome.navLinkExists('monitoring')) { + if (monitoringUiEnabled) { return; } - chrome.getNavLinkById('monitoring').hidden = true; + onStart(({ core }) => core.chrome.navLinks.update('monitoring', { hidden: true })); }); diff --git a/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js index 505ef204f4d0f6..42b63b2d420641 100644 --- a/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js +++ b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js @@ -7,7 +7,6 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { toastNotifications } from 'ui/notify'; -import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; import { get } from 'lodash'; import { jobQueueClient } from 'plugins/reporting/lib/job_queue_client'; @@ -20,6 +19,7 @@ import { EuiButton, } from '@elastic/eui'; import { downloadReport } from '../lib/download_report'; +import { getNewPlatform } from 'ui/new_platform'; /** * Poll for changes to reports. Inform the user of changes when the license is active. @@ -59,10 +59,13 @@ uiModules.get('kibana') let seeReportLink; + const core = getNewPlatform().start.core; + // In-case the license expired/changed between the time they queued the job and the time that // the job completes, that way we don't give the user a toast to download their report if they can't. - if (chrome.navLinkExists('kibana:management')) { - const managementUrl = chrome.getNavLinkById('kibana:management').url; + // NOTE: this should be looking at configuration rather than the existence of a navLink + if (core.chrome.navLinks.has('kibana:management')) { + const managementUrl = core.chrome.navLinks.get('kibana:management').url; const reportingSectionUrl = `${managementUrl}/kibana/reporting`; seeReportLink = (