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