From bf4e072facb6eb2532da52a1a5edb45d76d78c72 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 30 Apr 2019 15:27:39 -0500 Subject: [PATCH 1/7] Move chrome navlinks to core --- ...bana-plugin-public.chromenavlink.active.md | 15 ++ ...bana-plugin-public.chromenavlink.appurl.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 ++ .../kibana-plugin-public.chromenavlink.md | 29 +++ ...ibana-plugin-public.chromenavlink.order.md | 13 ++ ...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 + ...-plugin-public.legacynavlink.lastsuburl.md | 11 ++ .../kibana-plugin-public.legacynavlink.md | 1 + .../core/public/kibana-plugin-public.md | 2 + .../application/application_service.tsx | 2 - src/core/public/chrome/chrome_service.mock.ts | 20 ++ src/core/public/chrome/chrome_service.ts | 18 +- src/core/public/chrome/index.ts | 2 + .../public/chrome/nav_links/index.ts} | 8 +- src/core/public/chrome/nav_links/nav_link.ts | 114 +++++++++++ .../nav_links/nav_links_service.test.ts | 181 ++++++++++++++++++ .../chrome/nav_links/nav_links_service.ts | 144 ++++++++++++++ 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/plugins/plugin_context.ts | 4 +- .../public/plugins/plugins_service.test.ts | 1 + src/core/public/public.api.md | 26 ++- .../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 | 177 +++++++++-------- 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 | 163 ++++++++++++++++ .../ui/public/chrome/api/sub_url_hooks.js | 3 +- .../header_global_nav/components/header.tsx | 73 ++++--- .../header_global_nav/header_global_nav.js | 8 +- 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 | 13 +- .../public/hacks/toggle_app_link_in_nav.js | 6 +- .../public/hacks/job_completion_notifier.js | 9 +- 57 files changed, 1136 insertions(+), 411 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.appurl.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.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.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 create mode 100644 docs/development/core/public/kibana-plugin-public.legacynavlink.lastsuburl.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.appurl.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.appurl.md new file mode 100644 index 00000000000000..d995838193c034 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.appurl.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [appUrl](./kibana-plugin-public.chromenavlink.appurl.md) + +## ChromeNavLink.appUrl property + +The base URL used to open the root of an application. + +Signature: + +```typescript +readonly appUrl: 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.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md new file mode 100644 index 00000000000000..2669d253f6be3e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -0,0 +1,29 @@ + + +[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. | +| [appUrl](./kibana-plugin-public.chromenavlink.appurl.md) | string | The base URL 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. | +| [order](./kibana-plugin-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [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.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.legacynavlink.lastsuburl.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.lastsuburl.md new file mode 100644 index 00000000000000..4cd55196efc833 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.lastsuburl.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [lastSubUrl](./kibana-plugin-public.legacynavlink.lastsuburl.md) + +## LegacyNavLink.lastSubUrl property + +Signature: + +```typescript +lastSubUrl?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.md index 8cc6aaf06334a0..3aaa77dc3f2930 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.md @@ -18,6 +18,7 @@ export interface LegacyNavLink | [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) | string | | | [icon](./kibana-plugin-public.legacynavlink.icon.md) | string | | | [id](./kibana-plugin-public.legacynavlink.id.md) | string | | +| [lastSubUrl](./kibana-plugin-public.legacynavlink.lastsuburl.md) | string | | | [order](./kibana-plugin-public.legacynavlink.order.md) | number | | | [title](./kibana-plugin-public.legacynavlink.title.md) | string | | | [url](./kibana-plugin-public.legacynavlink.url.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 27df06fbb56305..7046bc30af69ee 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -23,6 +23,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..bf69981d8163be 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -76,8 +76,6 @@ export interface App extends BaseApp { /** @internal */ export interface LegacyApp extends BaseApp { appUrl: string; - - url?: string; } /** @internal */ diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index bef316968555de..89a7cf57a2b6ff 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,36 @@ const createSetupContractMock = () => { return setupContract; }; +const createStartContractMock = (): jest.Mocked => { + return { + navLinks: { + getNavLinks$: jest.fn(), + clear: jest.fn(), + exists: 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..8223594ffd6108 100644 --- a/src/core/public/chrome/chrome_service.ts +++ b/src/core/public/chrome/chrome_service.ts @@ -25,6 +25,8 @@ 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'; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; @@ -65,10 +67,15 @@ interface SetupDeps { notifications: NotificationsSetup; } +interface StartDeps { + application: ApplicationStart; +} + /** @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 +190,6 @@ export class ChromeService { map(set => [...set]), takeUntil(this.stop$) ), - /** * Get an observable of the current badge */ @@ -222,10 +228,20 @@ export class ChromeService { }; } + public start({ application }: StartDeps) { + return { + navLinks: this.navLinks.start({ application }), + }; + } + 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..72fc5baeb85f4d --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -0,0 +1,114 @@ +/* + * 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. + */ + +/** + * @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 URL used to open the root of an application. + */ + readonly appUrl: 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. + */ + readonly url?: 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; +} + +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) { + 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..4cc89366b13a31 --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -0,0 +1,181 @@ +/* + * 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; + +describe('NavLinksService', () => { + describe('#getNavLinks$()', () => { + it('sorts navlinks by `order` property', async () => { + const start = new NavLinksService().start({ application: mockAppService }); + 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 service = new NavLinksService(); + const start = service.start({ application: mockAppService }); + 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 service = new NavLinksService(); + const start = service.start({ application: mockAppService }); + const last$ = start + .getNavLinks$() + .pipe(takeLast(1)) + .toPromise(); + service.stop(); + await expect(last$).resolves.toBeInstanceOf(Array); + }); + }); + + describe('#get()', () => { + it('returns link if exists', () => { + const start = new NavLinksService().start({ application: mockAppService }); + expect(start.get('app1')!.title).toEqual('App 1'); + }); + + it('returns undefined if it does not exist', () => { + const start = new NavLinksService().start({ application: mockAppService }); + expect(start.get('phony')).toBeUndefined(); + }); + }); + + describe('#getAll()', () => { + it('returns a sorted array of navlinks', () => { + const start = new NavLinksService().start({ application: mockAppService }); + expect(start.getAll().map(l => l.id)).toEqual(['app2', 'app1', 'legacyApp']); + }); + }); + + describe('#exists()', () => { + it('returns true if exists', () => { + const start = new NavLinksService().start({ application: mockAppService }); + expect(start.exists('app1')).toBe(true); + }); + + it('returns false if it does not exist', () => { + const start = new NavLinksService().start({ application: mockAppService }); + expect(start.exists('phony')).toBe(false); + }); + }); + + describe('#showOnly()', () => { + it('does nothing if link does not exist', async () => { + const start = new NavLinksService().start({ application: mockAppService }); + 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 () => { + const start = new NavLinksService().start({ application: mockAppService }); + 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 () => { + const start = new NavLinksService().start({ application: mockAppService }); + expect(start.update('app1', { hidden: true })).toMatchInlineSnapshot(` +Object { + "appUrl": "/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', () => { + const start = new NavLinksService().start({ application: mockAppService }); + expect(start.update('fake', { hidden: true })).toBeUndefined(); + }); + }); + + describe('#enableForcedAppSwitcherNavigation()', () => { + it('flips #getForceAppSwitcherNavigation$()', async () => { + const start = new NavLinksService().start({ application: mockAppService }); + 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..9daa0d61ecc4f4 --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -0,0 +1,144 @@ +/* + * 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'; + +interface StartDeps { + application: ApplicationStart; +} + +export class NavLinksService { + private readonly stop$ = new ReplaySubject(1); + + public start({ application }: StartDeps) { + const navLinks$ = new BehaviorSubject>( + application.availableApps.map( + app => + new NavLinkWrapper({ + ...app, + // Either rootRoute or appUrl must be defined. + appUrl: (app.rootRoute || app.appUrl)!, + }) + ) + ); + 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.find(l => l.id === id); + return link ? link.properties : undefined; + }, + + /** + * Get the current state of all navlinks. + */ + getAll() { + return sortNavLinks(navLinks$.value); + }, + + /** + * Check whether or not a navlink exists. + * @param id + */ + exists(id: string) { + return this.get(id) !== undefined; + }, + + /** + * Remove all navlinks except the one matching the given id. + * NOTE: this is not reversible. + * @param id + */ + showOnly(id: string) { + if (!this.exists(id)) { + return; + } + + navLinks$.next(navLinks$.value.filter(link => link.id === 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.exists(id)) { + return; + } + + navLinks$.next( + navLinks$.value.map(link => { + return link.id === id ? link.update(values) : link; + }) + ); + + 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: ReadonlyArray) { + return sortBy(navLinks.map(link => link.properties), 'order'); +} diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 925c6186ea31f3..6ec62a4a14ff4d 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -160,6 +160,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 }); const notificationsTargetDomElement = document.createElement('div'); const legacyPlatformTargetDomElement = document.createElement('div'); @@ -180,6 +181,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 a0fda91695cb1a..51a0c3aa73a30f 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'; @@ -83,6 +85,8 @@ export interface CoreStart { application: ApplicationStart; /** {@link BasePathStart} */ basePath: BasePathStart; + /** {@link ChromeStart} */ + chrome: ChromeStart; /** {@link I18nStart} */ i18n: I18nStart; /** {@link InjectedMetadataStart} */ @@ -102,10 +106,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/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 a03bb50c8e2a3c..0c207f8c265bec 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -87,11 +87,29 @@ export interface ChromeBreadcrumb { // @public (undocumented) export type ChromeHelpExtension = (element: HTMLDivElement) => (() => void); +// @public (undocumented) +export interface ChromeNavLink { + readonly active?: boolean; + readonly appUrl: string; + readonly disabled?: boolean; + readonly euiIconType?: string; + readonly hidden?: boolean; + readonly icon?: string; + readonly id: string; + readonly order: number; + 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 +145,8 @@ export interface CoreStart { // (undocumented) basePath: BasePathStart; // (undocumented) + chrome: ChromeStart; + // (undocumented) i18n: I18nStart; // (undocumented) injectedMetadata: InjectedMetadataStart; @@ -270,6 +290,8 @@ export interface LegacyNavLink { // (undocumented) id: string; // (undocumented) + lastSubUrl?: string; + // (undocumented) order: number; // (undocumented) title: string; @@ -401,8 +423,8 @@ export interface UiSettingsState { // Warnings were encountered during analysis: // -// src/core/public/injected_metadata/injected_metadata_service.ts:48:7 - (ae-forgotten-export) The symbol "PluginName" needs to be exported by the entry point index.d.ts -// src/core/public/injected_metadata/injected_metadata_service.ts:49:7 - (ae-forgotten-export) The symbol "DiscoveredPlugin" needs to be exported by the entry point index.d.ts +// src/core/public/injected_metadata/injected_metadata_service.ts:49:7 - (ae-forgotten-export) The symbol "PluginName" needs to be exported by the entry point index.d.ts +// src/core/public/injected_metadata/injected_metadata_service.ts:50:7 - (ae-forgotten-export) The symbol "DiscoveredPlugin" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) 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..bf39f3f9634f47 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'; const basePath = '/someBasePath'; @@ -38,38 +40,28 @@ 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 = []; + + 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 () { @@ -79,94 +71,119 @@ describe('chrome nav apis', function () { 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 - } - ]; + fakedLinks = [{ + id: appId, + title: 'Discover', + url: `${appUrl}?id=${deletedId}`, + appUrl + }]; + const { chrome - } = init({ appUrlStore, nav }); + } = init({ + appUrlStore, + nav: [{ + id: appId, + linkToLastSubUrl: true, + }] + }); 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 - } - ]; + fakedLinks = [{ + id: appId, + title: 'Discover', + url: lastUrl, + appUrl + }]; + const { chrome - } = init({ appUrlStore, nav }); + } = init({ + appUrlStore, + nav: [{ + id: appId, + linkToLastSubUrl: true + }] + }); 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', + appUrl: 'https://localhost:9200/app/kibana#discover', }, { - url: 'https://localhost:9200/app/kibana#visualize', - subUrlBase: 'https://localhost:9200/app/kibana#visualize' + id: 'kibana:visualize', + appUrl: 'https://localhost:9200/app/kibana#visualize', }, { - url: 'https://localhost:9200/app/kibana#dashboards', - subUrlBase: 'https://localhost:9200/app/kibana#dashboard' + id: 'kibana:dashboard', + appUrl: 'https://localhost:9200/app/kibana#dashboards', }, - ].map(l => { - l.lastSubUrl = l.url; - return l; - }); + ]; const { internals - } = init({ appUrlStore, nav }); + } = init({ + appUrlStore, + nav: [ + { + id: 'kibana:discover', + subUrlBase: '/app/kibana#discover' + }, + { + id: 'kibana:visualize', + subUrlBase: '/app/kibana#visualize' + }, + { + id: 'kibana:dashboard', + subUrlBase: '/app/kibana#dashboard' + }, + ] + }); 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(internals.nav[1].lastSubUrl).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate'); - expect(internals.nav[1].active).to.be(false); + expect(fakedLinks[0].url).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate'); + expect(fakedLinks[0].active).to.be(false); + + expect(fakedLinks[1].url).to.be('https://localhost:9200/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('https://localhost:9200/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 = [ - { + fakedLinks = [{ + id: 'kibana:visualize', + appUrl: 'https://localhost:9200/app/kibana#visualize', + url: 'https://localhost:9200/app/kibana#visualize', + }]; + + const { chrome } = init({ + appUrlStore, + 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 }); + subUrlBase: '/app/kibana#visualize' + }] + }); const basePath = '/xyz'; const appId = 'kibana'; @@ -177,7 +194,7 @@ describe('chrome nav apis', function () { const kibanaParsedUrl = new KibanaParsedUrl({ basePath, appId, appPath, hostname, port, protocol }); 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: 'https://localhost:9200/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..4d24f7044e98ff --- /dev/null +++ b/src/legacy/ui/public/chrome/api/nav.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 { pick } from 'lodash'; +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'; + +export interface ChromeNavLinks { + untrackNavLinksForDeletedSavedObjects(deletedIds: string[]): void; + trackSubUrlForApp(linkId: string, parsedKibanaUrl: KibanaParsedUrl): void; +} + +interface LegacyNavLinkProperties { + readonly subUrlBase: string; + readonly linkToLastSubUrl: boolean; +} + +type LegacyNavLink = ChromeNavLink & LegacyNavLinkProperties; + +interface NavInternals { + nav: LegacyNavLink[]; + appUrlStore: Storage; + trackPossibleSubUrl(url: string): void; +} + +export function initChromeNavApi(chrome: any, internals: NavInternals) { + let coreNavLinks: ChromeStart['navLinks']; + onStart(({ core }) => (coreNavLinks = core.chrome.navLinks)); + + // These are legacy link properties that are not supported in the new platform. + // Index by link id for lookups when tracking sub urls. + const legacyNavProps = new Map( + internals.nav.map( + link => + [link.id, pick(link, ['subUrlBase', 'linkToLastSubUrl'])] as [ + string, + LegacyNavLinkProperties + ] + ) + ); + + /** + * 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); + }); + if (includedId === undefined) { + return false; + } + return true; + } + + coreNavLinks.getAll().forEach(link => { + const legacyProps = legacyNavProps.get(link.id)!; + if (legacyProps.linkToLastSubUrl && urlContainsDeletedId(link.url!)) { + setLastUrl(link, link.appUrl); + } + }); + }; + + /** + * 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()); + + for (let link of coreNavLinks.getAll()) { + const subUrlBase = legacyNavProps.get(link.id)!.subUrlBase; + const active = kibanaParsedUrl.getAppRootPath().startsWith(subUrlBase); + link = coreNavLinks.update(link.id, { active })!; + + if (active) { + setLastUrl(link, url); + continue; + } + + link = refreshLastUrl(link); + + const newGlobalState = kibanaParsedUrl.getGlobalState(); + if (newGlobalState) { + injectNewGlobalState(link, kibanaParsedUrl.appId, newGlobalState); + } + } + }; + + function lastSubUrlKey(link: ChromeNavLink) { + return `lastSubUrl:${link.appUrl}`; + } + + function getLastUrl(link: ChromeNavLink) { + return internals.appUrlStore.getItem(lastSubUrlKey(link)); + } + + function setLastUrl(link: ChromeNavLink, url: string) { + 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.appUrl, + })!; + } + + function injectNewGlobalState( + link: ChromeNavLink, + fromAppId: string, + newGlobalState: string | string[] + ) { + const kibanaParsedUrl = absoluteToParsedUrl(link.url!, 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(() => 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 0d43688bb75865..5ab17e296cf90a 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -21,6 +21,7 @@ import { getUnhashableStatesProvider, unhashUrl, } from '../../state_management/state_hashing'; +import { onStart } from '../../new_platform'; export function registerSubUrlHooks(angularModule, internals) { angularModule.run(($rootScope, Private) => { @@ -30,7 +31,7 @@ export function registerSubUrlHooks(angularModule, internals) { function updateSubUrls() { const urlWithHashes = window.location.href; const urlWithStates = unhashUrl(urlWithHashes, getUnhashableStates()); - internals.trackPossibleSubUrl(urlWithStates); + onStart(() => internals.trackPossibleSubUrl(urlWithStates)); } function onRouteChange($event) { 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..da51afe584d210 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,12 +52,10 @@ 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'; -import { relativeToAbsolute } from 'ui/url/relative_to_absolute'; import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; @@ -65,7 +63,12 @@ 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, + BasePathStart, +} from '../../../../../../../core/public'; interface Props { appTitle?: string; @@ -73,13 +76,13 @@ interface Props { breadcrumbs$: Rx.Observable; homeHref: string; isVisible: boolean; - navLinks$: Rx.Observable; + navLinks$: Rx.Observable; + addBasePath: BasePathStart['addToPath']; 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 +91,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 href = chrome.addBasePath(recentlyAccessed.link); + const navLink = navLinks.find(nl => href.startsWith(nl.appUrl)); let titleAndAriaLabel = recentlyAccessed.label; if (navLink) { @@ -114,10 +117,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.appUrl, }; } @@ -218,13 +221,13 @@ class HeaderUI extends Component { public render() { const { appTitle, + addBasePath, badge$, breadcrumbs$, isVisible, navControls, helpExtension$, intl, - uiCapabilities, } = this.props; const { navLinks, recentlyAccessed } = this.state; @@ -235,31 +238,27 @@ 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: addBasePath(navLink.href), + iconType: navLink.euiIconType, + icon: + !navLink.euiIconType && navLink.icon ? ( + + ) : ( + undefined + ), + isActive: navLink.active, + '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..a777f6c17849f9 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,10 @@ 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$(), + addBasePath: newPlatformStart.basePath.addToPath, 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..06e2a8ab4e119a 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,20 @@ 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'); + const navLinkUpdates = {}; // hide by default, only show once the xpackInfo is initialized - navLink.hidden = true; + navLinkUpdates.hidden = true; const showAppLink = xpackInfo.get('features.ml.showLinks', false); - navLink.hidden = !showAppLink; + navLinkUpdates.hidden = !showAppLink; if (showAppLink) { - navLink.disabled = !xpackInfo.get('features.ml.isAvailable', false); + navLinkUpdates.disabled = !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..690baa39f68968 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.exists('kibana:management')) { + const managementUrl = core.chrome.navLinks.get('kibana:management').url; const reportingSectionUrl = `${managementUrl}/kibana/reporting`; seeReportLink = (

From f200c84cdc9c7c79d36f47f201c3d6429f6e5e9c Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 8 May 2019 09:17:33 -0500 Subject: [PATCH 2/7] Update generated API docs --- .../kibana-plugin-public.legacynavlink.lastsuburl.md | 11 ----------- .../core/public/kibana-plugin-public.legacynavlink.md | 1 - src/core/public/public.api.md | 6 ++---- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-public.legacynavlink.lastsuburl.md diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.lastsuburl.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.lastsuburl.md deleted file mode 100644 index 4cd55196efc833..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.lastsuburl.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [lastSubUrl](./kibana-plugin-public.legacynavlink.lastsuburl.md) - -## LegacyNavLink.lastSubUrl property - -Signature: - -```typescript -lastSubUrl?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.md index 3aaa77dc3f2930..8cc6aaf06334a0 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.md @@ -18,7 +18,6 @@ export interface LegacyNavLink | [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) | string | | | [icon](./kibana-plugin-public.legacynavlink.icon.md) | string | | | [id](./kibana-plugin-public.legacynavlink.id.md) | string | | -| [lastSubUrl](./kibana-plugin-public.legacynavlink.lastsuburl.md) | string | | | [order](./kibana-plugin-public.legacynavlink.order.md) | number | | | [title](./kibana-plugin-public.legacynavlink.title.md) | string | | | [url](./kibana-plugin-public.legacynavlink.url.md) | string | | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0c207f8c265bec..01df16eb11719b 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -290,8 +290,6 @@ export interface LegacyNavLink { // (undocumented) id: string; // (undocumented) - lastSubUrl?: string; - // (undocumented) order: number; // (undocumented) title: string; @@ -423,8 +421,8 @@ export interface UiSettingsState { // Warnings were encountered during analysis: // -// src/core/public/injected_metadata/injected_metadata_service.ts:49:7 - (ae-forgotten-export) The symbol "PluginName" needs to be exported by the entry point index.d.ts -// src/core/public/injected_metadata/injected_metadata_service.ts:50:7 - (ae-forgotten-export) The symbol "DiscoveredPlugin" needs to be exported by the entry point index.d.ts +// src/core/public/injected_metadata/injected_metadata_service.ts:48:7 - (ae-forgotten-export) The symbol "PluginName" needs to be exported by the entry point index.d.ts +// src/core/public/injected_metadata/injected_metadata_service.ts:49:7 - (ae-forgotten-export) The symbol "DiscoveredPlugin" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) From a0dfbcc174df4c24153bf078b16b5e06401a09b0 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 8 May 2019 10:54:26 -0500 Subject: [PATCH 3/7] Review comments --- src/core/public/chrome/chrome_service.mock.ts | 28 +++++------ src/core/public/chrome/nav_links/nav_link.ts | 4 ++ .../nav_links/nav_links_service.test.ts | 6 +-- .../chrome/nav_links/nav_links_service.ts | 50 +++++++++++-------- .../ui/public/chrome/api/__tests__/nav.js | 14 +++--- src/legacy/ui/public/chrome/api/nav.ts | 5 +- .../header_global_nav/components/header.tsx | 19 +++---- .../ml/public/hacks/toggle_app_link_in_nav.js | 13 +++-- .../public/hacks/job_completion_notifier.js | 2 +- 9 files changed, 73 insertions(+), 68 deletions(-) diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 89a7cf57a2b6ff..a56c2b63ca493c 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -54,21 +54,19 @@ const createSetupContractMock = () => { return setupContract; }; -const createStartContractMock = (): jest.Mocked => { - return { - navLinks: { - getNavLinks$: jest.fn(), - clear: jest.fn(), - exists: jest.fn(), - get: jest.fn(), - getAll: jest.fn(), - showOnly: jest.fn(), - update: jest.fn(), - enableForcedAppSwitcherNavigation: jest.fn(), - getForceAppSwitcherNavigation$: jest.fn(), - }, - }; -}; +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 = () => { diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 72fc5baeb85f4d..be6d5e44131955 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -17,6 +17,8 @@ * under the License. */ +import { pick } from '../../../utils'; + /** * @public */ @@ -109,6 +111,8 @@ export class NavLinkWrapper { } public update(newProps: NavLinkUpdateableFields) { + // Enforce limited properties at runtime for JS code + newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url']); 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 index 4cc89366b13a31..0e3e6872074d7b 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -86,15 +86,15 @@ describe('NavLinksService', () => { }); }); - describe('#exists()', () => { + describe('#has()', () => { it('returns true if exists', () => { const start = new NavLinksService().start({ application: mockAppService }); - expect(start.exists('app1')).toBe(true); + expect(start.has('app1')).toBe(true); }); it('returns false if it does not exist', () => { const start = new NavLinksService().start({ application: mockAppService }); - expect(start.exists('phony')).toBe(false); + expect(start.has('phony')).toBe(false); }); }); diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index 9daa0d61ecc4f4..f6bf12c399750e 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -31,14 +31,19 @@ export class NavLinksService { private readonly stop$ = new ReplaySubject(1); public start({ application }: StartDeps) { - const navLinks$ = new BehaviorSubject>( - application.availableApps.map( - app => - new NavLinkWrapper({ - ...app, - // Either rootRoute or appUrl must be defined. - appUrl: (app.rootRoute || app.appUrl)!, - }) + const navLinks$ = new BehaviorSubject>( + new Map( + application.availableApps.map( + app => + [ + app.id, + new NavLinkWrapper({ + ...app, + // Either rootRoute or appUrl must be defined. + appUrl: (app.rootRoute || app.appUrl)!, + }), + ] as [string, NavLinkWrapper] + ) ) ); const forceAppSwitcherNavigation$ = new BehaviorSubject(false); @@ -59,8 +64,8 @@ export class NavLinksService { * @param id */ get(id: string) { - const link = navLinks$.value.find(l => l.id === id); - return link ? link.properties : undefined; + const link = navLinks$.value.get(id); + return link && link.properties; }, /** @@ -74,8 +79,8 @@ export class NavLinksService { * Check whether or not a navlink exists. * @param id */ - exists(id: string) { - return this.get(id) !== undefined; + has(id: string) { + return navLinks$.value.has(id); }, /** @@ -84,11 +89,11 @@ export class NavLinksService { * @param id */ showOnly(id: string) { - if (!this.exists(id)) { + if (!this.has(id)) { return; } - navLinks$.next(navLinks$.value.filter(link => link.id === id)); + navLinks$.next(new Map([...navLinks$.value.entries()].filter(([linkId]) => linkId === id))); }, /** @@ -98,14 +103,19 @@ export class NavLinksService { * @param values */ update(id: string, values: NavLinkUpdateableFields) { - if (!this.exists(id)) { + if (!this.has(id)) { return; } navLinks$.next( - navLinks$.value.map(link => { - return link.id === id ? link.update(values) : link; - }) + new Map( + [...navLinks$.value.entries()].map(([linkId, link]) => { + return [linkId, link.id === id ? link.update(values) : link] as [ + string, + NavLinkWrapper + ]; + }) + ) ); return this.get(id); @@ -139,6 +149,6 @@ export class NavLinksService { } } -function sortNavLinks(navLinks: ReadonlyArray) { - return sortBy(navLinks.map(link => link.properties), 'order'); +function sortNavLinks(navLinks: ReadonlyMap) { + return sortBy([...navLinks.values()].map(link => link.properties), 'order'); } diff --git a/src/legacy/ui/public/chrome/api/__tests__/nav.js b/src/legacy/ui/public/chrome/api/__tests__/nav.js index bf39f3f9634f47..c7a8f58a8ef551 100644 --- a/src/legacy/ui/public/chrome/api/__tests__/nav.js +++ b/src/legacy/ui/public/chrome/api/__tests__/nav.js @@ -75,7 +75,7 @@ describe('chrome nav apis', function () { id: appId, title: 'Discover', url: `${appUrl}?id=${deletedId}`, - appUrl + appUrl, }]; const { @@ -85,7 +85,7 @@ describe('chrome nav apis', function () { nav: [{ id: appId, linkToLastSubUrl: true, - }] + }], }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); @@ -99,7 +99,7 @@ describe('chrome nav apis', function () { id: appId, title: 'Discover', url: lastUrl, - appUrl + appUrl, }]; const { @@ -109,7 +109,7 @@ describe('chrome nav apis', function () { nav: [{ id: appId, linkToLastSubUrl: true - }] + }], }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); @@ -152,7 +152,7 @@ describe('chrome nav apis', function () { id: 'kibana:dashboard', subUrlBase: '/app/kibana#dashboard' }, - ] + ], }); internals.trackPossibleSubUrl('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); @@ -181,8 +181,8 @@ describe('chrome nav apis', function () { appUrlStore, nav: [{ id: 'kibana:visualize', - subUrlBase: '/app/kibana#visualize' - }] + subUrlBase: '/app/kibana#visualize', + }], }); const basePath = '/xyz'; diff --git a/src/legacy/ui/public/chrome/api/nav.ts b/src/legacy/ui/public/chrome/api/nav.ts index 4d24f7044e98ff..21cfdf1c9b6422 100644 --- a/src/legacy/ui/public/chrome/api/nav.ts +++ b/src/legacy/ui/public/chrome/api/nav.ts @@ -65,10 +65,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { const includedId = deletedIds.find(deletedId => { return url.includes(deletedId); }); - if (includedId === undefined) { - return false; - } - return true; + return includedId !== undefined; } coreNavLinks.getAll().forEach(link => { 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 da51afe584d210..05c86bda782845 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 @@ -245,17 +245,14 @@ class HeaderUI extends Component { label: navLink.title, href: addBasePath(navLink.href), iconType: navLink.euiIconType, - icon: - !navLink.euiIconType && navLink.icon ? ( - - ) : ( - undefined - ), + icon: !navLink.euiIconType && navLink.icon && ( + + ), isActive: navLink.active, 'data-test-subj': 'navDrawerAppsMenuLink', })); 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 06e2a8ab4e119a..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 @@ -13,14 +13,13 @@ import { onStart } from 'ui/new_platform'; uiModules.get('xpack/ml').run((Private) => { const xpackInfo = Private(XPackInfoProvider); - const navLinkUpdates = {}; - // hide by default, only show once the xpackInfo is initialized - navLinkUpdates.hidden = true; const showAppLink = xpackInfo.get('features.ml.showLinks', false); - navLinkUpdates.hidden = !showAppLink; - if (showAppLink) { - navLinkUpdates.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/reporting/public/hacks/job_completion_notifier.js b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js index 690baa39f68968..42b63b2d420641 100644 --- a/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js +++ b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js @@ -64,7 +64,7 @@ uiModules.get('kibana') // 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. // NOTE: this should be looking at configuration rather than the existence of a navLink - if (core.chrome.navLinks.exists('kibana:management')) { + if (core.chrome.navLinks.has('kibana:management')) { const managementUrl = core.chrome.navLinks.get('kibana:management').url; const reportingSectionUrl = `${managementUrl}/kibana/reporting`; seeReportLink = ( From 69cee4f81cf2ffd0f19c17bbae05dad0d77c834f Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 8 May 2019 14:13:49 -0500 Subject: [PATCH 4/7] Small tweaks --- .../header_global_nav/components/header.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) 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 05c86bda782845..17d338b985a891 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 @@ -244,16 +244,20 @@ class HeaderUI extends Component { key: navLink.id, label: navLink.title, href: addBasePath(navLink.href), - iconType: navLink.euiIconType, - icon: !navLink.euiIconType && navLink.icon && ( - - ), + isDisabled: navLink.disabled, isActive: navLink.active, + iconType: navLink.euiIconType, + icon: + !navLink.euiIconType && navLink.icon ? ( + + ) : ( + undefined + ), 'data-test-subj': 'navDrawerAppsMenuLink', })); From f06b76780ed6fcf27455935920b93f12e6b64aee Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 9 May 2019 14:24:17 -0500 Subject: [PATCH 5/7] Use absolute URLs to handle multiple Kibana instances on a domain --- src/core/public/chrome/chrome_service.ts | 6 ++- src/core/public/chrome/nav_links/nav_link.ts | 4 +- .../nav_links/nav_links_service.test.ts | 29 ++++++------ .../chrome/nav_links/nav_links_service.ts | 13 +++++- src/core/public/core_system.ts | 2 +- .../ui/public/chrome/api/__tests__/nav.js | 45 ++++++++++--------- src/legacy/ui/public/chrome/api/nav.ts | 27 ++++++++--- .../ui/public/chrome/api/sub_url_hooks.js | 4 +- .../header_global_nav/components/header.tsx | 8 ++-- .../header_global_nav/header_global_nav.js | 1 - 10 files changed, 79 insertions(+), 60 deletions(-) diff --git a/src/core/public/chrome/chrome_service.ts b/src/core/public/chrome/chrome_service.ts index 8223594ffd6108..37146b05f92067 100644 --- a/src/core/public/chrome/chrome_service.ts +++ b/src/core/public/chrome/chrome_service.ts @@ -27,6 +27,7 @@ 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'; @@ -69,6 +70,7 @@ interface SetupDeps { interface StartDeps { application: ApplicationStart; + basePath: BasePathStart; } /** @internal */ @@ -228,9 +230,9 @@ export class ChromeService { }; } - public start({ application }: StartDeps) { + public start({ application, basePath }: StartDeps) { return { - navLinks: this.navLinks.start({ application }), + navLinks: this.navLinks.start({ application, basePath }), }; } diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index be6d5e44131955..487a0eefd92439 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -67,9 +67,9 @@ export interface ChromeNavLink { readonly tooltip?: string; /** - * The base URL used to open the root of an application. + * The base route used to open the root of an application. */ - readonly appUrl: string; + readonly baseUrl: string; /** * A url that legacy apps can set to deep link into their applications. diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index 0e3e6872074d7b..9acc1bc132da8f 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -28,10 +28,21 @@ const mockAppService = { ], } 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 () => { - const start = new NavLinksService().start({ application: mockAppService }); expect( await start .getNavLinks$() @@ -44,8 +55,6 @@ describe('NavLinksService', () => { }); it('emits multiple values', async () => { - const service = new NavLinksService(); - const start = service.start({ application: mockAppService }); const navLinkIds$ = start.getNavLinks$().pipe(map(links => links.map(l => l.id))); const emittedLinks: string[][] = []; navLinkIds$.subscribe(r => emittedLinks.push(r)); @@ -56,8 +65,6 @@ describe('NavLinksService', () => { }); it('completes when service is stopped', async () => { - const service = new NavLinksService(); - const start = service.start({ application: mockAppService }); const last$ = start .getNavLinks$() .pipe(takeLast(1)) @@ -69,38 +76,32 @@ describe('NavLinksService', () => { describe('#get()', () => { it('returns link if exists', () => { - const start = new NavLinksService().start({ application: mockAppService }); expect(start.get('app1')!.title).toEqual('App 1'); }); it('returns undefined if it does not exist', () => { - const start = new NavLinksService().start({ application: mockAppService }); expect(start.get('phony')).toBeUndefined(); }); }); describe('#getAll()', () => { it('returns a sorted array of navlinks', () => { - const start = new NavLinksService().start({ application: mockAppService }); expect(start.getAll().map(l => l.id)).toEqual(['app2', 'app1', 'legacyApp']); }); }); describe('#has()', () => { it('returns true if exists', () => { - const start = new NavLinksService().start({ application: mockAppService }); expect(start.has('app1')).toBe(true); }); it('returns false if it does not exist', () => { - const start = new NavLinksService().start({ application: mockAppService }); expect(start.has('phony')).toBe(false); }); }); describe('#showOnly()', () => { it('does nothing if link does not exist', async () => { - const start = new NavLinksService().start({ application: mockAppService }); start.showOnly('fake'); expect( await start @@ -114,7 +115,6 @@ describe('NavLinksService', () => { }); it('removes all other links', async () => { - const start = new NavLinksService().start({ application: mockAppService }); start.showOnly('app1'); expect( await start @@ -130,10 +130,9 @@ describe('NavLinksService', () => { describe('#update()', () => { it('updates the navlinks and returns the updated link', async () => { - const start = new NavLinksService().start({ application: mockAppService }); expect(start.update('app1', { hidden: true })).toMatchInlineSnapshot(` Object { - "appUrl": "/app1", + "baseUrl": "http://localhost/wow/app1", "hidden": true, "icon": "app1", "id": "app1", @@ -153,14 +152,12 @@ Object { }); it('returns undefined if link does not exist', () => { - const start = new NavLinksService().start({ application: mockAppService }); expect(start.update('fake', { hidden: true })).toBeUndefined(); }); }); describe('#enableForcedAppSwitcherNavigation()', () => { it('flips #getForceAppSwitcherNavigation$()', async () => { - const start = new NavLinksService().start({ application: mockAppService }); await expect( start .getForceAppSwitcherNavigation$() diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index f6bf12c399750e..156d8e2d01573e 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -22,15 +22,17 @@ 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 }: StartDeps) { + public start({ application, basePath }: StartDeps) { const navLinks$ = new BehaviorSubject>( new Map( application.availableApps.map( @@ -40,7 +42,7 @@ export class NavLinksService { new NavLinkWrapper({ ...app, // Either rootRoute or appUrl must be defined. - appUrl: (app.rootRoute || app.appUrl)!, + baseUrl: relativeToAbsolute(basePath.addToPath((app.rootRoute || app.appUrl)!)), }), ] as [string, NavLinkWrapper] ) @@ -152,3 +154,10 @@ export class NavLinksService { 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 2bf077f3a37757..4002eafba7b038 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -169,7 +169,7 @@ export class CoreSystem { const http = await this.http.start(); const i18n = await this.i18n.start(); const application = await this.application.start({ basePath, injectedMetadata }); - const chrome = await this.chrome.start({ application }); + const chrome = await this.chrome.start({ application, basePath }); const notificationsTargetDomElement = document.createElement('div'); const overlayTargetDomElement = document.createElement('div'); diff --git a/src/legacy/ui/public/chrome/api/__tests__/nav.js b/src/legacy/ui/public/chrome/api/__tests__/nav.js index c7a8f58a8ef551..bc39230fb87acf 100644 --- a/src/legacy/ui/public/chrome/api/__tests__/nav.js +++ b/src/legacy/ui/public/chrome/api/__tests__/nav.js @@ -22,8 +22,8 @@ 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'; @@ -45,6 +45,12 @@ 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) => { @@ -66,7 +72,7 @@ describe('chrome nav apis', function () { 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 () { @@ -75,7 +81,7 @@ describe('chrome nav apis', function () { id: appId, title: 'Discover', url: `${appUrl}?id=${deletedId}`, - appUrl, + baseUrl: appUrl, }]; const { @@ -99,7 +105,7 @@ describe('chrome nav apis', function () { id: appId, title: 'Discover', url: lastUrl, - appUrl, + baseUrl: appUrl, }]; const { @@ -123,15 +129,15 @@ describe('chrome nav apis', function () { fakedLinks = [ { id: 'kibana:discover', - appUrl: 'https://localhost:9200/app/kibana#discover', + baseUrl: `${baseUrl}/app/kibana#discover`, }, { id: 'kibana:visualize', - appUrl: 'https://localhost:9200/app/kibana#visualize', + baseUrl: `${baseUrl}/app/kibana#visualize`, }, { id: 'kibana:dashboard', - appUrl: 'https://localhost:9200/app/kibana#dashboards', + baseUrl: `${baseUrl}/app/kibana#dashboards`, }, ]; @@ -155,15 +161,15 @@ describe('chrome nav apis', function () { ], }); - internals.trackPossibleSubUrl('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); + internals.trackPossibleSubUrl(`${baseUrl}/app/kibana#dashboard?_g=globalstate`); - expect(fakedLinks[0].url).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate'); + expect(fakedLinks[0].url).to.be(`${baseUrl}/app/kibana#discover?_g=globalstate`); expect(fakedLinks[0].active).to.be(false); - expect(fakedLinks[1].url).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate'); + expect(fakedLinks[1].url).to.be(`${baseUrl}/app/kibana#visualize?_g=globalstate`); expect(fakedLinks[1].active).to.be(false); - expect(fakedLinks[2].url).to.be('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); + expect(fakedLinks[2].url).to.be(`${baseUrl}/app/kibana#dashboard?_g=globalstate`); expect(fakedLinks[2].active).to.be(true); }); }); @@ -173,8 +179,8 @@ describe('chrome nav apis', function () { const appUrlStore = new StubBrowserStorage(); fakedLinks = [{ id: 'kibana:visualize', - appUrl: 'https://localhost:9200/app/kibana#visualize', - url: 'https://localhost:9200/app/kibana#visualize', + baseUrl: `${baseUrl}/app/kibana#visualize`, + url: `${baseUrl}/app/kibana#visualize`, }]; const { chrome } = init({ @@ -185,16 +191,11 @@ describe('chrome nav apis', function () { }], }); - 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 }); + const kibanaParsedUrl = absoluteToParsedUrl(`${baseUrl}/xyz/app/kibana#visualize/1234?_g=globalstate`, '/xyz'); chrome.trackSubUrlForApp('kibana:visualize', kibanaParsedUrl); - expect(coreNavLinks.update.calledWith('kibana:visualize', { url: 'https://localhost:9200/xyz/app/kibana#visualize/1234?_g=globalstate' })).to.be(true); + 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/nav.ts b/src/legacy/ui/public/chrome/api/nav.ts index 21cfdf1c9b6422..25304477535cef 100644 --- a/src/legacy/ui/public/chrome/api/nav.ts +++ b/src/legacy/ui/public/chrome/api/nav.ts @@ -22,6 +22,7 @@ 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; @@ -29,7 +30,7 @@ export interface ChromeNavLinks { } interface LegacyNavLinkProperties { - readonly subUrlBase: string; + subUrlBase: string; readonly linkToLastSubUrl: boolean; } @@ -71,7 +72,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { coreNavLinks.getAll().forEach(link => { const legacyProps = legacyNavProps.get(link.id)!; if (legacyProps.linkToLastSubUrl && urlContainsDeletedId(link.url!)) { - setLastUrl(link, link.appUrl); + setLastUrl(link, link.baseUrl); } }); }; @@ -99,7 +100,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { for (let link of coreNavLinks.getAll()) { const subUrlBase = legacyNavProps.get(link.id)!.subUrlBase; - const active = kibanaParsedUrl.getAppRootPath().startsWith(subUrlBase); + const active = url.startsWith(subUrlBase); link = coreNavLinks.update(link.id, { active })!; if (active) { @@ -117,7 +118,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { }; function lastSubUrlKey(link: ChromeNavLink) { - return `lastSubUrl:${link.appUrl}`; + return `lastSubUrl:${link.baseUrl}`; } function getLastUrl(link: ChromeNavLink) { @@ -125,6 +126,10 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { } function setLastUrl(link: ChromeNavLink, url: string) { + if (legacyNavProps.get(link.id)!.linkToLastSubUrl === false) { + return; + } + internals.appUrlStore.setItem(lastSubUrlKey(link), url); refreshLastUrl(link); } @@ -133,7 +138,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { const lastSubUrl = getLastUrl(link); return coreNavLinks.update(link.id, { - url: lastSubUrl || link.url || link.appUrl, + url: lastSubUrl || link.url || link.baseUrl, })!; } @@ -142,7 +147,10 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { fromAppId: string, newGlobalState: string | string[] ) { - const kibanaParsedUrl = absoluteToParsedUrl(link.url!, chrome.getBasePath()); + 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; @@ -156,5 +164,10 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { // simulate a possible change in url to initialize the // link.active and link.lastUrl properties - onStart(() => internals.trackPossibleSubUrl(document.location.href)); + onStart(() => { + [...legacyNavProps.values()].forEach( + link => (link.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 856240d70d1033..5ed3af9b59ff68 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -33,7 +33,7 @@ export function registerSubUrlHooks(angularModule, internals) { function updateSubUrls() { const urlWithHashes = window.location.href; const urlWithStates = unhashUrl(urlWithHashes, getUnhashableStates()); - onStart(() => internals.trackPossibleSubUrl(urlWithStates)); + internals.trackPossibleSubUrl(urlWithStates); } function onRouteChange($event) { @@ -61,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 17d338b985a891..879e8b7b27d201 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 @@ -77,7 +77,6 @@ interface Props { homeHref: string; isVisible: boolean; navLinks$: Rx.Observable; - addBasePath: BasePathStart['addToPath']; recentlyAccessed$: Rx.Observable; forceAppSwitcherNavigation$: Rx.Observable; helpExtension$: Rx.Observable; @@ -95,7 +94,7 @@ function extendRecentlyAccessedHistoryItem( recentlyAccessed: RecentlyAccessedHistoryItem ) { const href = chrome.addBasePath(recentlyAccessed.link); - const navLink = navLinks.find(nl => href.startsWith(nl.appUrl)); + const navLink = navLinks.find(nl => href.startsWith(nl.baseUrl)); let titleAndAriaLabel = recentlyAccessed.label; if (navLink) { @@ -120,7 +119,7 @@ function extendRecentlyAccessedHistoryItem( function extendNavLink(navLink: ChromeNavLink) { return { ...navLink, - href: navLink.url && !navLink.active ? navLink.url : navLink.appUrl, + href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl, }; } @@ -221,7 +220,6 @@ class HeaderUI extends Component { public render() { const { appTitle, - addBasePath, badge$, breadcrumbs$, isVisible, @@ -243,7 +241,7 @@ class HeaderUI extends Component { .map(navLink => ({ key: navLink.id, label: navLink.title, - href: addBasePath(navLink.href), + href: navLink.href, isDisabled: navLink.disabled, isActive: navLink.active, iconType: navLink.euiIconType, 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 a777f6c17849f9..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 @@ -45,7 +45,6 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili breadcrumbs$: chrome.breadcrumbs.get$(), helpExtension$: chrome.helpExtension.get$(), navLinks$: newPlatformStart.chrome.navLinks.getNavLinks$(), - addBasePath: newPlatformStart.basePath.addToPath, recentlyAccessed$: recentlyAccessed.get$(), forceAppSwitcherNavigation$: newPlatformStart.chrome.navLinks.getForceAppSwitcherNavigation$(), navControls, From f1af62573a70aade5647103fdef9a1ae387a3c67 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 9 May 2019 15:26:02 -0500 Subject: [PATCH 6/7] Fix recently accessed + lint --- ...d => kibana-plugin-public.chromenavlink.baseurl.md} | 8 ++++---- .../core/public/kibana-plugin-public.chromenavlink.md | 2 +- src/core/public/public.api.md | 2 +- .../directives/header_global_nav/components/header.tsx | 10 +++------- 4 files changed, 9 insertions(+), 13 deletions(-) rename docs/development/core/public/{kibana-plugin-public.chromenavlink.appurl.md => kibana-plugin-public.chromenavlink.baseurl.md} (55%) diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.appurl.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md similarity index 55% rename from docs/development/core/public/kibana-plugin-public.chromenavlink.appurl.md rename to docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md index d995838193c034..5d50e45c9fe552 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.appurl.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.baseurl.md @@ -1,13 +1,13 @@ -[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [appUrl](./kibana-plugin-public.chromenavlink.appurl.md) +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) -## ChromeNavLink.appUrl property +## ChromeNavLink.baseUrl property -The base URL used to open the root of an application. +The base route used to open the root of an application. Signature: ```typescript -readonly appUrl: string; +readonly baseUrl: string; ``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md index 2669d253f6be3e..f25551c873578c 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -16,7 +16,7 @@ export interface ChromeNavLink | 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. | -| [appUrl](./kibana-plugin-public.chromenavlink.appurl.md) | string | The base URL used to open the root of an application. | +| [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. | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aad81b12d603db..90e5bc6c578868 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -90,7 +90,7 @@ export type ChromeHelpExtension = (element: HTMLDivElement) => (() => void); // @public (undocumented) export interface ChromeNavLink { readonly active?: boolean; - readonly appUrl: string; + readonly baseUrl: string; readonly disabled?: boolean; readonly euiIconType?: string; readonly hidden?: boolean; 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 879e8b7b27d201..8839a7420f9f03 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 @@ -56,6 +56,7 @@ 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'; +import { relativeToAbsolute } from 'ui/url/relative_to_absolute'; import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; @@ -63,12 +64,7 @@ import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; import { NavControlSide } from '../'; -import { - ChromeBadge, - ChromeBreadcrumb, - ChromeNavLink, - BasePathStart, -} from '../../../../../../../core/public'; +import { ChromeBadge, ChromeBreadcrumb, ChromeNavLink } from '../../../../../../../core/public'; interface Props { appTitle?: string; @@ -93,7 +89,7 @@ function extendRecentlyAccessedHistoryItem( navLinks: ChromeNavLink[], recentlyAccessed: RecentlyAccessedHistoryItem ) { - const href = chrome.addBasePath(recentlyAccessed.link); + const href = relativeToAbsolute(chrome.addBasePath(recentlyAccessed.link)); const navLink = navLinks.find(nl => href.startsWith(nl.baseUrl)); let titleAndAriaLabel = recentlyAccessed.label; From 661362f5b06485c5820fc173cb3fa1382b1d7b06 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 9 May 2019 17:01:25 -0500 Subject: [PATCH 7/7] Fix icon mapping for recentlyAccessed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring subUrlBase into new platform ☹️ --- ...n-public.chromenavlink.linktolastsuburl.md | 15 ++++ .../kibana-plugin-public.chromenavlink.md | 2 + ...-plugin-public.chromenavlink.suburlbase.md | 15 ++++ .../application/application_service.tsx | 2 + src/core/public/chrome/nav_links/nav_link.ts | 38 +++++++--- src/core/public/legacy/legacy_service.ts | 2 + src/core/public/public.api.md | 2 + .../ui/public/chrome/api/__tests__/nav.js | 58 +++----------- src/legacy/ui/public/chrome/api/nav.ts | 76 ++++++++----------- .../header_global_nav/components/header.tsx | 2 +- 10 files changed, 107 insertions(+), 105 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md 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 index f25551c873578c..e13efce19c0946 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -22,7 +22,9 @@ export interface ChromeNavLink | [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.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/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index bf69981d8163be..d195984552e660 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -76,6 +76,8 @@ export interface App extends BaseApp { /** @internal */ export interface LegacyApp extends BaseApp { appUrl: string; + subUrlBase?: string; + linkToLastSubUrl?: boolean; } /** @internal */ diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 487a0eefd92439..a2f75b220b3193 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -71,15 +71,6 @@ export interface ChromeNavLink { */ readonly baseUrl: 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. - */ - readonly url?: string; - /** * A EUI iconType that will be used for the app's icon. This icon * takes precendence over the `icon` property. @@ -91,10 +82,35 @@ export interface ChromeNavLink { * 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 + Pick >; export class NavLinkWrapper { @@ -112,7 +128,7 @@ export class NavLinkWrapper { public update(newProps: NavLinkUpdateableFields) { // Enforce limited properties at runtime for JS code - newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url']); + newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase']); return new NavLinkWrapper({ ...this.properties, ...newProps }); } } diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 6e6f2564974264..c9e90376e9766f 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -91,6 +91,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/public.api.md b/src/core/public/public.api.md index 90e5bc6c578868..d1d88a64558b33 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -96,7 +96,9 @@ export interface ChromeNavLink { 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; diff --git a/src/legacy/ui/public/chrome/api/__tests__/nav.js b/src/legacy/ui/public/chrome/api/__tests__/nav.js index bc39230fb87acf..ff20f69586180e 100644 --- a/src/legacy/ui/public/chrome/api/__tests__/nav.js +++ b/src/legacy/ui/public/chrome/api/__tests__/nav.js @@ -82,18 +82,10 @@ describe('chrome nav apis', function () { title: 'Discover', url: `${appUrl}?id=${deletedId}`, baseUrl: appUrl, + linkToLastSubUrl: true, }]; - const { - chrome - } = init({ - appUrlStore, - nav: [{ - id: appId, - linkToLastSubUrl: true, - }], - }); - + const { chrome } = init({ appUrlStore }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); expect(coreNavLinks.update.calledWith(appId, { url: appUrl })).to.be(true); }); @@ -106,18 +98,10 @@ describe('chrome nav apis', function () { title: 'Discover', url: lastUrl, baseUrl: appUrl, + linkToLastSubUrl: true }]; - const { - chrome - } = init({ - appUrlStore, - nav: [{ - id: appId, - linkToLastSubUrl: true - }], - }); - + const { chrome } = init({ appUrlStore }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); expect(coreNavLinks.update.calledWith(appId, { url: appUrl })).to.be(false); }); @@ -130,37 +114,21 @@ describe('chrome nav apis', function () { { id: 'kibana:discover', baseUrl: `${baseUrl}/app/kibana#discover`, + subUrlBase: '/app/kibana#discover' }, { id: 'kibana:visualize', baseUrl: `${baseUrl}/app/kibana#visualize`, + subUrlBase: '/app/kibana#visualize' }, { id: 'kibana:dashboard', baseUrl: `${baseUrl}/app/kibana#dashboards`, + subUrlBase: '/app/kibana#dashboard' }, ]; - const { - internals - } = init({ - appUrlStore, - nav: [ - { - id: 'kibana:discover', - subUrlBase: '/app/kibana#discover' - }, - { - id: 'kibana:visualize', - subUrlBase: '/app/kibana#visualize' - }, - { - id: 'kibana:dashboard', - subUrlBase: '/app/kibana#dashboard' - }, - ], - }); - + const { internals } = init({ appUrlStore }); internals.trackPossibleSubUrl(`${baseUrl}/app/kibana#dashboard?_g=globalstate`); expect(fakedLinks[0].url).to.be(`${baseUrl}/app/kibana#discover?_g=globalstate`); @@ -181,16 +149,10 @@ describe('chrome nav apis', function () { id: 'kibana:visualize', baseUrl: `${baseUrl}/app/kibana#visualize`, url: `${baseUrl}/app/kibana#visualize`, + subUrlBase: '/app/kibana#visualize', }]; - const { chrome } = init({ - appUrlStore, - nav: [{ - id: '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( diff --git a/src/legacy/ui/public/chrome/api/nav.ts b/src/legacy/ui/public/chrome/api/nav.ts index 25304477535cef..8a4504125d82bc 100644 --- a/src/legacy/ui/public/chrome/api/nav.ts +++ b/src/legacy/ui/public/chrome/api/nav.ts @@ -17,7 +17,6 @@ * under the License. */ -import { pick } from 'lodash'; import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; import { absoluteToParsedUrl } from '../../url/absolute_to_parsed_url'; import { onStart } from '../../new_platform'; @@ -29,15 +28,7 @@ export interface ChromeNavLinks { trackSubUrlForApp(linkId: string, parsedKibanaUrl: KibanaParsedUrl): void; } -interface LegacyNavLinkProperties { - subUrlBase: string; - readonly linkToLastSubUrl: boolean; -} - -type LegacyNavLink = ChromeNavLink & LegacyNavLinkProperties; - interface NavInternals { - nav: LegacyNavLink[]; appUrlStore: Storage; trackPossibleSubUrl(url: string): void; } @@ -46,18 +37,6 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { let coreNavLinks: ChromeStart['navLinks']; onStart(({ core }) => (coreNavLinks = core.chrome.navLinks)); - // These are legacy link properties that are not supported in the new platform. - // Index by link id for lookups when tracking sub urls. - const legacyNavProps = new Map( - internals.nav.map( - link => - [link.id, pick(link, ['subUrlBase', 'linkToLastSubUrl'])] as [ - string, - LegacyNavLinkProperties - ] - ) - ); - /** * Clear last url for deleted saved objects to avoid loading pages with "Could not locate..." */ @@ -70,8 +49,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { } coreNavLinks.getAll().forEach(link => { - const legacyProps = legacyNavProps.get(link.id)!; - if (legacyProps.linkToLastSubUrl && urlContainsDeletedId(link.url!)) { + if (link.linkToLastSubUrl && urlContainsDeletedId(link.url!)) { setLastUrl(link, link.baseUrl); } }); @@ -98,23 +76,26 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { internals.trackPossibleSubUrl = async function(url: string) { const kibanaParsedUrl = absoluteToParsedUrl(url, chrome.getBasePath()); - for (let link of coreNavLinks.getAll()) { - const subUrlBase = legacyNavProps.get(link.id)!.subUrlBase; - const active = url.startsWith(subUrlBase); - link = coreNavLinks.update(link.id, { active })!; - - if (active) { - setLastUrl(link, url); - continue; - } - - link = refreshLastUrl(link); - - const newGlobalState = kibanaParsedUrl.getGlobalState(); - if (newGlobalState) { - injectNewGlobalState(link, kibanaParsedUrl.appId, newGlobalState); - } - } + 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) { @@ -126,7 +107,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { } function setLastUrl(link: ChromeNavLink, url: string) { - if (legacyNavProps.get(link.id)!.linkToLastSubUrl === false) { + if (link.linkToLastSubUrl === false) { return; } @@ -164,10 +145,15 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { // simulate a possible change in url to initialize the // link.active and link.lastUrl properties - onStart(() => { - [...legacyNavProps.values()].forEach( - link => (link.subUrlBase = relativeToAbsolute(chrome.addBasePath(link.subUrlBase))) - ); + 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/directives/header_global_nav/components/header.tsx b/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx index 8839a7420f9f03..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 @@ -90,7 +90,7 @@ function extendRecentlyAccessedHistoryItem( recentlyAccessed: RecentlyAccessedHistoryItem ) { const href = relativeToAbsolute(chrome.addBasePath(recentlyAccessed.link)); - const navLink = navLinks.find(nl => href.startsWith(nl.baseUrl)); + const navLink = navLinks.find(nl => href.startsWith(nl.subUrlBase || nl.baseUrl)); let titleAndAriaLabel = recentlyAccessed.label; if (navLink) {