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 = (