From 2e0e13c1e6803907f742a54e2270876693168584 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 30 Apr 2019 15:27:39 -0500 Subject: [PATCH] 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 + src/core/public/chrome/chrome_service.mock.ts | 19 ++ src/core/public/chrome/chrome_service.ts | 40 ++-- src/core/public/chrome/index.ts | 2 + .../public/chrome/nav_links/index.ts} | 8 +- src/core/public/chrome/nav_links/nav_link.ts | 116 +++++++++++ .../chrome/nav_links/nav_links_service.ts | 87 ++++++++ src/core/public/core_system.ts | 3 + src/core/public/index.ts | 6 + .../injected_metadata_service.ts | 1 + src/core/public/legacy/legacy_service.test.ts | 2 + src/core/public/legacy/legacy_service.ts | 4 + 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 +- .../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 | 189 ++++++++++++++++++ .../ui/public/chrome/api/sub_url_hooks.js | 3 +- .../header_global_nav/components/header.tsx | 63 +++--- .../header_global_nav/header_global_nav.js | 5 +- 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 +- 56 files changed, 923 insertions(+), 420 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.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 000000000000000..51005323139860e --- /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 000000000000000..d995838193c0346 --- /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 000000000000000..87f290573b4968f --- /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 000000000000000..37d196ae4558a28 --- /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 000000000000000..cde90415a2df2bb --- /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 000000000000000..05e182e756d7e05 --- /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 000000000000000..179ca9200178ce4 --- /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 000000000000000..2669d253f6be3eb --- /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 000000000000000..19c86371e334b66 --- /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 000000000000000..7c4ff8612f23170 --- /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 000000000000000..c33ca742fae29a9 --- /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 000000000000000..bba9d83ab434cbe --- /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 000000000000000..28ea29dab9b5082 --- /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 000000000000000..9a574edf6b3e5b9 --- /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 800973808741816..5ceedb7416f03a0 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 000000000000000..4cd55196efc8338 --- /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 8cc6aaf06334a0c..3aaa77dc3f2930a 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 27df06fbb563059..7046bc30af69ee2 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/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index bef316968555ded..2eb67ced0b1b184 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,35 @@ const createSetupContractMock = () => { return setupContract; }; +const createStartContractMock = (): jest.Mocked => { + return { + navLinks: { + getNavLinks$: jest.fn(), + add: jest.fn(), + clear: jest.fn(), + exists: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + showOnly: jest.fn(), + update: 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 1bd8a8b1ef362e8..c361d3aa621029e 100644 --- a/src/core/public/chrome/chrome_service.ts +++ b/src/core/public/chrome/chrome_service.ts @@ -23,8 +23,11 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { IconType } from '@elastic/eui'; +import { BasePathSetup, BasePathStart } from '../base_path'; 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'; @@ -61,20 +64,27 @@ interface ConstructorParams { } interface SetupDeps { + basePath: BasePathSetup; injectedMetadata: InjectedMetadataSetup; notifications: NotificationsSetup; } +interface StartDeps { + application: ApplicationStart; + basePath: BasePathStart; +} + /** @internal */ export class ChromeService { private readonly stop$ = new Rx.ReplaySubject(1); private readonly browserSupportsCsp: boolean; + private readonly navLinks = new NavLinksService(); public constructor({ browserSupportsCsp }: ConstructorParams) { this.browserSupportsCsp = browserSupportsCsp; } - public setup({ injectedMetadata, notifications }: SetupDeps) { + public setup({ basePath, injectedMetadata, notifications }: SetupDeps) { const FORCE_HIDDEN = isEmbedParamInHash(); const brand$ = new Rx.BehaviorSubject({}); @@ -109,14 +119,8 @@ export class ChromeService { * */ setBrand: (brand: ChromeBrand) => { - brand$.next( - Object.freeze({ - logo: brand.logo, - smallLogo: brand.smallLogo, - }) - ); + brand$.next(Object.freeze({ logo: brand.logo, smallLogo: brand.smallLogo })); }, - /** * Get an observable of the current brand information. */ @@ -130,7 +134,6 @@ export class ChromeService { setIsVisible: (visibility: boolean) => { isVisible$.next(visibility); }, - /** * Get an observable of the current visibility state of the chrome. */ @@ -139,7 +142,6 @@ export class ChromeService { map(visibility => (FORCE_HIDDEN ? false : visibility)), takeUntil(this.stop$) ), - /** * Set the collapsed state of the chrome navigation. */ @@ -151,12 +153,10 @@ export class ChromeService { localStorage.removeItem(IS_COLLAPSED_KEY); } }, - /** * Get an observable of the current collapsed state of the chrome. */ getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)), - /** * Add a className that should be set on the application container. */ @@ -165,7 +165,6 @@ export class ChromeService { update.add(className); applicationClasses$.next(update); }, - /** * Remove a className added with `addApplicationClass()`. If className is unknown it is ignored. */ @@ -174,7 +173,6 @@ export class ChromeService { update.delete(className); applicationClasses$.next(update); }, - /** * Get the current set of classNames that will be set on the application container. */ @@ -183,7 +181,6 @@ export class ChromeService { map(set => [...set]), takeUntil(this.stop$) ), - /** * Get an observable of the current badge */ @@ -200,19 +197,16 @@ export class ChromeService { * Get an observable of the current list of breadcrumbs */ getBreadcrumbs$: () => breadcrumbs$.pipe(takeUntil(this.stop$)), - /** * Override the current set of breadcrumbs */ setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { breadcrumbs$.next(newBreadcrumbs); }, - /** * Get an observable of the current custom help conttent */ getHelpExtension$: () => helpExtension$.pipe(takeUntil(this.stop$)), - /** * Override the current set of breadcrumbs */ @@ -222,10 +216,20 @@ export class ChromeService { }; } + public start({ application, basePath }: StartDeps) { + return { + navLinks: this.navLinks.start(application, basePath), + }; + } + public stop() { + this.navLinks.stop(); this.stop$.next(); } } /** @public */ export type ChromeSetup = ReturnType; + +/** @public */ +export type ChromeStart = ReturnType; diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index fc816b8dd136d61..77008f5bc534956 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 e3b68ddf0228252..8060d5cab23ebff 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 000000000000000..ed404878958c750 --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -0,0 +1,116 @@ +/* + * 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 { BasePathSetup } from '../../base_path'; + +/** + * @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, private readonly basePath: BasePathSetup) { + 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 }, this.basePath); + } +} 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 000000000000000..31443b88e0ad9eb --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -0,0 +1,87 @@ +/* + * 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 { BasePathStart } from '../../base_path'; +import { NavLinkWrapper, NavLinkUpdateableFields } from './nav_link'; +import { ApplicationStart } from '../../application'; + +export class NavLinksService { + private readonly stop$ = new ReplaySubject(1); + + public start(application: ApplicationStart, basePath: BasePathStart) { + const navLinks$ = new BehaviorSubject>( + application.availableApps.map(app => new NavLinkWrapper(app as any, basePath)) + ); + + return { + getNavLinks$: () => { + return navLinks$.pipe( + map(sortNavLinks), + takeUntil(this.stop$) + ); + }, + + clear() { + navLinks$.next([]); + }, + + get(id: string) { + const link = navLinks$.value.find(l => l.id === id); + return link ? link.properties : undefined; + }, + + getAll() { + return sortNavLinks(navLinks$.value); + }, + + exists(id: string) { + return this.get(id) !== undefined; + }, + + showOnly(id: string) { + navLinks$.next(navLinks$.value.filter(link => link.id === id)); + }, + + 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); + }, + }; + } + + 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 925c6186ea31f30..290b8046795ae7c 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -130,6 +130,7 @@ export class CoreSystem { const chrome = this.chrome.setup({ injectedMetadata, notifications, + basePath, }); const core: CoreSetup = { @@ -160,6 +161,7 @@ export class CoreSystem { const basePath = await this.basePath.start({ injectedMetadata }); const i18n = await this.i18n.start(); const application = await this.application.start({ basePath, injectedMetadata }); + const chrome = await this.chrome.start({ application, basePath }); const notificationsTargetDomElement = document.createElement('div'); const legacyPlatformTargetDomElement = document.createElement('div'); @@ -180,6 +182,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 a0fda91695cb1a6..51a0c3aa73a30f0 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/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 625f29d695274bd..2e755337e6ba481 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -28,6 +28,7 @@ export interface LegacyNavLink { title: string; order: number; url: string; + lastSubUrl?: string; icon?: string; euiIconType?: string; } diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 78c159304eda89a..59130ece12b07f1 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -193,6 +193,7 @@ const defaultSetupDeps = { const applicationStart = applicationServiceMock.createStartContract(); const basePathStart = basePathServiceMock.createStartContract(); +const chromeStart = chromeServiceMock.createStartContract(); const i18nStart = i18nServiceMock.createStartContract(); const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); @@ -202,6 +203,7 @@ const defaultStartDeps = { core: { application: applicationStart, basePath: basePathStart, + chrome: chromeStart, i18n: i18nStart, injectedMetadata: injectedMetadataStart, notifications: notificationsStart, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index bc1aff696ed6366..d3c57c3cf2b4a79 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -85,11 +85,15 @@ export class LegacyPlatformService { injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) => application.registerLegacyApp({ id: navLink.id, + // active: navLink.active, + // disabled: navLink.disabled, + // hidden: navLink.hidden, order: navLink.order, title: navLink.title, euiIconType: navLink.euiIconType, icon: navLink.icon, appUrl: navLink.url, + url: navLink.lastSubUrl, }) ); diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index e5d3989d4a27799..3ab021aa7d99e97 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 a03bb50c8e2a3c0..0c207f8c265bec5 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 d1908be4bea7d89..8cce2c4e73e363c 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 bf69fa09823f25a..03c86767067bf29 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 3426932ea5e72b5..c4b07f95f26e249 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 ec531600bd21eb6..4d188030b731278 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 af7f9c8d34a61a8..fe5df3a8de54de1 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/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 79ed7c4e98b5582..73dc777b317d935 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 000000000000000..8aa8940c5bbd543 --- /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 faf43058259e844..bf39f3f9634f475 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 d01ed1a355614e1..e6457fec9363308 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 f7c639bc5733c24..000000000000000 --- 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 1feb0a8f22e0a08..000000000000000 --- 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 000000000000000..25c80834054742f --- /dev/null +++ b/src/legacy/ui/public/chrome/api/nav.ts @@ -0,0 +1,189 @@ +/* + * 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 { 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; + enableForcedAppSwitcherNavigation(): this; + getForcedAppSwitcherNavigation$(): Rx.Observable; +} + +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)); + const forceAppSwitcherNavigation$ = new Rx.BehaviorSubject(false); + + // 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 + ] + ) + ); + + /** + * 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 + */ + chrome.enableForcedAppSwitcherNavigation = () => { + forceAppSwitcherNavigation$.next(true); + return chrome; + }; + + /** used only by directive */ + chrome.getForceAppSwitcherNavigation$ = () => { + return forceAppSwitcherNavigation$.asObservable(); + }; + + /** + * Clear last url for deleted saved objects to avoid loading pages with "Could not locate.." + * + * Really should be part of the subUrl service. + */ + 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 relativeUrl = absoluteToParsedUrl(url).getRootRelativePath(); + for (let link of coreNavLinks.getAll()) { + const subUrlBase = legacyNavProps.get(link.id)!.subUrlBase; + const active = relativeUrl.startsWith(subUrlBase); + link = coreNavLinks.update(link.id, { active })!; + + if (active) { + setLastUrl(link, url); + continue; + } + + link = refreshLastUrl(link); + + const kibanaParsedUrl = absoluteToParsedUrl(url, chrome.getBasePath()); + 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 0d43688bb75865e..5ab17e296cf90a8 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 af593384ae16ec3..43169783aca78c2 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 @@ -53,11 +53,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 +64,7 @@ import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; import { NavControlSide } from '../'; -import { ChromeBadge, ChromeBreadcrumb } from '../../../../../../../core/public'; +import { ChromeBadge, ChromeBreadcrumb, ChromeNavLink } from '../../../../../../../core/public'; interface Props { appTitle?: string; @@ -73,7 +72,7 @@ interface Props { breadcrumbs$: Rx.Observable; homeHref: string; isVisible: boolean; - navLinks$: Rx.Observable; + navLinks$: Rx.Observable; recentlyAccessed$: Rx.Observable; forceAppSwitcherNavigation$: Rx.Observable; helpExtension$: Rx.Observable; @@ -88,11 +87,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 +113,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, }; } @@ -224,7 +223,6 @@ class HeaderUI extends Component { navControls, helpExtension$, intl, - uiCapabilities, } = this.props; const { navLinks, recentlyAccessed } = this.state; @@ -235,29 +233,28 @@ class HeaderUI extends Component { const leftNavControls = navControls.bySide[NavControlSide.Left]; const rightNavControls = navControls.bySide[NavControlSide.Right]; - let navLinksArray = navLinks.map(navLink => - navLink.hidden || !uiCapabilities.navLinks[navLink.id] - ? null - : { - key: navLink.id, - label: navLink.title, - href: navLink.href, - iconType: navLink.euiIconType, - icon: - !navLink.euiIconType && navLink.icon ? ( - - ) : ( - undefined - ), - isActive: navLink.active, - 'data-test-subj': 'navDrawerAppsMenuLink', - } - ); + let navLinksArray = navLinks + .filter(navLink => !navLink.hidden) + .map(navLink => ({ + 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); 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 f4b5022089b4d39..7508c0d9264ba56 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,7 +44,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili badge$: chrome.badge.get$(), breadcrumbs$: chrome.breadcrumbs.get$(), helpExtension$: chrome.helpExtension.get$(), - navLinks$: chrome.getNavLinks$(), + navLinks$: newPlatformStart.chrome.navLinks.getNavLinks$(), recentlyAccessed$: recentlyAccessed.get$(), forceAppSwitcherNavigation$: chrome.getForceAppSwitcherNavigation$(), navControls, diff --git a/src/legacy/ui/public/chrome/index.d.ts b/src/legacy/ui/public/chrome/index.d.ts index 71532fbbebccca8..7f61c7a2ca8c427 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 7f1653a9f8d8643..d431c775d1d2e64 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 90a8f31364f549f..a4304876c7ec25c 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 a653804d634b209..8b04e8ee82c8626 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 837e40815fb8bb4..17c8760ab2e28a1 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 70162e321f716e3..5d6e406e9e85608 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 bed0fe46c323533..06e2a8ab4e119a2 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 451793b83dd6591..c68d0d37b77c5e5 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 505ef204f4d0f66..690baa39f68968f 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 = (