diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.disablesuburltracking.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.disablesuburltracking.md
new file mode 100644
index 0000000000000..0054adc693dc3
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.disablesuburltracking.md
@@ -0,0 +1,17 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [disableSubUrlTracking](./kibana-plugin-public.chromenavlink.disablesuburltracking.md)
+
+## ChromeNavLink.disableSubUrlTracking property
+
+> Warning: This API is now obsolete.
+>
+>
+
+A flag that tells legacy chrome to ignore the link when tracking sub-urls
+
+Signature:
+
+```typescript
+readonly disableSubUrlTracking?: boolean;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md
index 4cb9080222ac5..49a75ca8287ab 100644
--- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md
+++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md
@@ -18,6 +18,7 @@ export interface ChromeNavLink
| [active](./kibana-plugin-public.chromenavlink.active.md) | boolean
| Indicates whether or not this app is currently on the screen. |
| [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. |
+| [disableSubUrlTracking](./kibana-plugin-public.chromenavlink.disablesuburltracking.md) | boolean
| A flag that tells legacy chrome to ignore the link when tracking sub-urls |
| [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. |
| [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. |
diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts
index 3b16c030ddcc9..dbb0cff1b2602 100644
--- a/src/core/public/chrome/nav_links/nav_link.ts
+++ b/src/core/public/chrome/nav_links/nav_link.ts
@@ -72,6 +72,17 @@ export interface ChromeNavLink {
*/
readonly subUrlBase?: string;
+ /**
+ * A flag that tells legacy chrome to ignore the link when
+ * tracking sub-urls
+ *
+ * @internalRemarks
+ * This should be removed once legacy apps are gone.
+ *
+ * @deprecated
+ */
+ readonly disableSubUrlTracking?: boolean;
+
/**
* Whether or not the subUrl feature should be enabled.
*
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 610b08708c681..58166e5b5de0a 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -253,6 +253,8 @@ export interface ChromeNavLink {
readonly baseUrl: string;
// @deprecated
readonly disabled?: boolean;
+ // @deprecated
+ readonly disableSubUrlTracking?: boolean;
readonly euiIconType?: string;
readonly hidden?: boolean;
readonly icon?: string;
diff --git a/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap
new file mode 100644
index 0000000000000..822462192f59a
--- /dev/null
+++ b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` 1`] = `
+Array [
+ Object {
+ "disableSubUrlTracking": undefined,
+ "disabled": false,
+ "euiIconType": undefined,
+ "hidden": false,
+ "icon": undefined,
+ "id": "link-a",
+ "linkToLastSubUrl": true,
+ "order": 0,
+ "subUrlBase": "/some-custom-url",
+ "title": "AppA",
+ "tooltip": "",
+ "url": "/some-custom-url",
+ },
+ Object {
+ "disableSubUrlTracking": true,
+ "disabled": false,
+ "euiIconType": undefined,
+ "hidden": false,
+ "icon": undefined,
+ "id": "link-b",
+ "linkToLastSubUrl": true,
+ "order": 0,
+ "subUrlBase": "/url-b",
+ "title": "AppB",
+ "tooltip": "",
+ "url": "/url-b",
+ },
+ Object {
+ "euiIconType": undefined,
+ "icon": undefined,
+ "id": "app-a",
+ "linkToLastSubUrl": true,
+ "order": 0,
+ "title": "AppA",
+ "url": "/app/app-a",
+ },
+ Object {
+ "euiIconType": undefined,
+ "icon": undefined,
+ "id": "app-b",
+ "linkToLastSubUrl": true,
+ "order": 0,
+ "title": "AppB",
+ "url": "/app/app-b",
+ },
+]
+`;
diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
index 9867274d224bd..44f02f0c90d4e 100644
--- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
+++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts
@@ -30,69 +30,8 @@ import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/u
import { LoggerFactory } from '../../logging';
import { PackageInfo } from '../../config';
-
-import {
- LegacyUiExports,
- LegacyNavLink,
- LegacyPluginSpec,
- LegacyPluginPack,
- LegacyConfig,
-} from '../types';
-
-const REMOVE_FROM_ARRAY: LegacyNavLink[] = [];
-
-function getUiAppsNavLinks({ uiAppSpecs = [] }: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) {
- return uiAppSpecs.flatMap(spec => {
- if (!spec) {
- return REMOVE_FROM_ARRAY;
- }
-
- const id = spec.pluginId || spec.id;
-
- if (!id) {
- throw new Error('Every app must specify an id');
- }
-
- if (spec.pluginId && !pluginSpecs.some(plugin => plugin.getId() === spec.pluginId)) {
- throw new Error(`Unknown plugin id "${spec.pluginId}"`);
- }
-
- const listed = typeof spec.listed === 'boolean' ? spec.listed : true;
-
- if (spec.hidden || !listed) {
- return REMOVE_FROM_ARRAY;
- }
-
- return {
- id,
- title: spec.title,
- order: typeof spec.order === 'number' ? spec.order : 0,
- icon: spec.icon,
- euiIconType: spec.euiIconType,
- url: spec.url || `/app/${id}`,
- linkToLastSubUrl: spec.linkToLastSubUrl,
- };
- });
-}
-
-function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) {
- return (uiExports.navLinkSpecs || [])
- .map(spec => ({
- id: spec.id,
- title: spec.title,
- order: typeof spec.order === 'number' ? spec.order : 0,
- url: spec.url,
- subUrlBase: spec.subUrlBase || spec.url,
- icon: spec.icon,
- euiIconType: spec.euiIconType,
- linkToLastSub: 'linkToLastSubUrl' in spec ? spec.linkToLastSubUrl : false,
- hidden: 'hidden' in spec ? spec.hidden : false,
- disabled: 'disabled' in spec ? spec.disabled : false,
- tooltip: spec.tooltip || '',
- }))
- .concat(getUiAppsNavLinks(uiExports, pluginSpecs))
- .sort((a, b) => a.order - b.order);
-}
+import { LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types';
+import { getNavLinks } from './get_nav_links';
export async function findLegacyPluginSpecs(
settings: unknown,
diff --git a/src/core/server/legacy/plugins/get_nav_links.test.ts b/src/core/server/legacy/plugins/get_nav_links.test.ts
new file mode 100644
index 0000000000000..b93ff41510676
--- /dev/null
+++ b/src/core/server/legacy/plugins/get_nav_links.test.ts
@@ -0,0 +1,271 @@
+/*
+ * 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 { LegacyUiExports, LegacyPluginSpec, LegacyAppSpec, LegacyNavLinkSpec } from '../types';
+import { getNavLinks } from './get_nav_links';
+
+const createLegacyExports = ({
+ uiAppSpecs = [],
+ navLinkSpecs = [],
+}: {
+ uiAppSpecs?: LegacyAppSpec[];
+ navLinkSpecs?: LegacyNavLinkSpec[];
+}): LegacyUiExports => ({
+ uiAppSpecs,
+ navLinkSpecs,
+ injectedVarsReplacers: [],
+ defaultInjectedVarProviders: [],
+ savedObjectMappings: [],
+ savedObjectSchemas: {},
+ savedObjectMigrations: {},
+ savedObjectValidations: {},
+});
+
+const createPluginSpecs = (...ids: string[]): LegacyPluginSpec[] =>
+ ids.map(
+ id =>
+ ({
+ getId: () => id,
+ } as LegacyPluginSpec)
+ );
+
+describe('getNavLinks', () => {
+ describe('generating from uiAppSpecs', () => {
+ it('generates navlinks from legacy app specs', () => {
+ const navlinks = getNavLinks(
+ createLegacyExports({
+ uiAppSpecs: [
+ {
+ id: 'app-a',
+ title: 'AppA',
+ pluginId: 'pluginA',
+ },
+ {
+ id: 'app-b',
+ title: 'AppB',
+ pluginId: 'pluginA',
+ },
+ ],
+ }),
+ createPluginSpecs('pluginA')
+ );
+
+ expect(navlinks.length).toEqual(2);
+ expect(navlinks[0]).toEqual(
+ expect.objectContaining({
+ id: 'app-a',
+ title: 'AppA',
+ url: '/app/app-a',
+ })
+ );
+ expect(navlinks[1]).toEqual(
+ expect.objectContaining({
+ id: 'app-b',
+ title: 'AppB',
+ url: '/app/app-b',
+ })
+ );
+ });
+
+ it('uses the app id to generates the navlink id even if pluginId is specified', () => {
+ const navlinks = getNavLinks(
+ createLegacyExports({
+ uiAppSpecs: [
+ {
+ id: 'app-a',
+ title: 'AppA',
+ pluginId: 'pluginA',
+ },
+ {
+ id: 'app-b',
+ title: 'AppB',
+ pluginId: 'pluginA',
+ },
+ ],
+ }),
+ createPluginSpecs('pluginA')
+ );
+
+ expect(navlinks.length).toEqual(2);
+ expect(navlinks[0].id).toEqual('app-a');
+ expect(navlinks[1].id).toEqual('app-b');
+ });
+
+ it('throws if an app reference a missing plugin', () => {
+ expect(() => {
+ getNavLinks(
+ createLegacyExports({
+ uiAppSpecs: [
+ {
+ id: 'app-a',
+ title: 'AppA',
+ pluginId: 'notExistingPlugin',
+ },
+ ],
+ }),
+ createPluginSpecs('pluginA')
+ );
+ }).toThrowErrorMatchingInlineSnapshot(`"Unknown plugin id \\"notExistingPlugin\\""`);
+ });
+
+ it('uses all known properties of the navlink', () => {
+ const navlinks = getNavLinks(
+ createLegacyExports({
+ uiAppSpecs: [
+ {
+ id: 'app-a',
+ title: 'AppA',
+ order: 42,
+ url: '/some-custom-url',
+ icon: 'fa-snowflake',
+ euiIconType: 'euiIcon',
+ linkToLastSubUrl: true,
+ hidden: false,
+ },
+ ],
+ }),
+ []
+ );
+ expect(navlinks.length).toBe(1);
+ expect(navlinks[0]).toEqual({
+ id: 'app-a',
+ title: 'AppA',
+ order: 42,
+ url: '/some-custom-url',
+ icon: 'fa-snowflake',
+ euiIconType: 'euiIcon',
+ linkToLastSubUrl: true,
+ });
+ });
+ });
+
+ describe('generating from navLinkSpecs', () => {
+ it('generates navlinks from legacy navLink specs', () => {
+ const navlinks = getNavLinks(
+ createLegacyExports({
+ navLinkSpecs: [
+ {
+ id: 'link-a',
+ title: 'AppA',
+ url: '/some-custom-url',
+ },
+ {
+ id: 'link-b',
+ title: 'AppB',
+ url: '/some-other-url',
+ disableSubUrlTracking: true,
+ },
+ ],
+ }),
+ createPluginSpecs('pluginA')
+ );
+
+ expect(navlinks.length).toEqual(2);
+ expect(navlinks[0]).toEqual(
+ expect.objectContaining({
+ id: 'link-a',
+ title: 'AppA',
+ url: '/some-custom-url',
+ hidden: false,
+ disabled: false,
+ })
+ );
+ expect(navlinks[1]).toEqual(
+ expect.objectContaining({
+ id: 'link-b',
+ title: 'AppB',
+ url: '/some-other-url',
+ disableSubUrlTracking: true,
+ })
+ );
+ });
+
+ it('only uses known properties to create the navlink', () => {
+ const navlinks = getNavLinks(
+ createLegacyExports({
+ navLinkSpecs: [
+ {
+ id: 'link-a',
+ title: 'AppA',
+ order: 72,
+ url: '/some-other-custom',
+ subUrlBase: '/some-other-custom/sub',
+ disableSubUrlTracking: true,
+ icon: 'fa-corn',
+ euiIconType: 'euiIconBis',
+ linkToLastSubUrl: false,
+ hidden: false,
+ tooltip: 'My other tooltip',
+ },
+ ],
+ }),
+ []
+ );
+ expect(navlinks.length).toBe(1);
+ expect(navlinks[0]).toEqual({
+ id: 'link-a',
+ title: 'AppA',
+ order: 72,
+ url: '/some-other-custom',
+ subUrlBase: '/some-other-custom/sub',
+ disableSubUrlTracking: true,
+ icon: 'fa-corn',
+ euiIconType: 'euiIconBis',
+ linkToLastSubUrl: false,
+ hidden: false,
+ disabled: false,
+ tooltip: 'My other tooltip',
+ });
+ });
+ });
+
+ describe('generating from both apps and navlinks', () => {
+ const navlinks = getNavLinks(
+ createLegacyExports({
+ uiAppSpecs: [
+ {
+ id: 'app-a',
+ title: 'AppA',
+ },
+ {
+ id: 'app-b',
+ title: 'AppB',
+ },
+ ],
+ navLinkSpecs: [
+ {
+ id: 'link-a',
+ title: 'AppA',
+ url: '/some-custom-url',
+ },
+ {
+ id: 'link-b',
+ title: 'AppB',
+ url: '/url-b',
+ disableSubUrlTracking: true,
+ },
+ ],
+ }),
+ []
+ );
+
+ expect(navlinks.length).toBe(4);
+ expect(navlinks).toMatchSnapshot();
+ });
+});
diff --git a/src/core/server/legacy/plugins/get_nav_links.ts b/src/core/server/legacy/plugins/get_nav_links.ts
new file mode 100644
index 0000000000000..cffc363488fc1
--- /dev/null
+++ b/src/core/server/legacy/plugins/get_nav_links.ts
@@ -0,0 +1,80 @@
+/*
+ * 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 {
+ LegacyUiExports,
+ LegacyNavLink,
+ LegacyPluginSpec,
+ LegacyNavLinkSpec,
+ LegacyAppSpec,
+} from '../types';
+
+function legacyAppToNavLink(spec: LegacyAppSpec): LegacyNavLink {
+ if (!spec.id) {
+ throw new Error('Every app must specify an id');
+ }
+ return {
+ id: spec.id,
+ title: spec.title ?? spec.id,
+ order: typeof spec.order === 'number' ? spec.order : 0,
+ icon: spec.icon,
+ euiIconType: spec.euiIconType,
+ url: spec.url || `/app/${spec.id}`,
+ linkToLastSubUrl: spec.linkToLastSubUrl ?? true,
+ };
+}
+
+function legacyLinkToNavLink(spec: LegacyNavLinkSpec): LegacyNavLink {
+ return {
+ id: spec.id,
+ title: spec.title,
+ order: typeof spec.order === 'number' ? spec.order : 0,
+ url: spec.url,
+ subUrlBase: spec.subUrlBase || spec.url,
+ disableSubUrlTracking: spec.disableSubUrlTracking,
+ icon: spec.icon,
+ euiIconType: spec.euiIconType,
+ linkToLastSubUrl: spec.linkToLastSubUrl ?? true,
+ hidden: spec.hidden ?? false,
+ disabled: spec.disabled ?? false,
+ tooltip: spec.tooltip ?? '',
+ };
+}
+
+function isHidden(app: LegacyAppSpec) {
+ return app.listed === false || app.hidden === true;
+}
+
+export function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) {
+ const navLinkSpecs = uiExports.navLinkSpecs || [];
+ const appSpecs = (uiExports.uiAppSpecs || []).filter(
+ app => app !== undefined && !isHidden(app)
+ ) as LegacyAppSpec[];
+
+ const pluginIds = (pluginSpecs || []).map(spec => spec.getId());
+ appSpecs.forEach(spec => {
+ if (spec.pluginId && !pluginIds.includes(spec.pluginId)) {
+ throw new Error(`Unknown plugin id "${spec.pluginId}"`);
+ }
+ });
+
+ return [...navLinkSpecs.map(legacyLinkToNavLink), ...appSpecs.map(legacyAppToNavLink)].sort(
+ (a, b) => a.order - b.order
+ );
+}
diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts
index 40b8244a31890..0c1a7730f92a7 100644
--- a/src/core/server/legacy/types.ts
+++ b/src/core/server/legacy/types.ts
@@ -131,16 +131,20 @@ export type VarsReplacer = (
* @internal
* @deprecated
*/
-export type LegacyNavLinkSpec = Record & ChromeNavLink;
+export type LegacyNavLinkSpec = Partial & {
+ id: string;
+ title: string;
+ url: string;
+};
/**
* @internal
* @deprecated
*/
-export type LegacyAppSpec = Pick<
- ChromeNavLink,
- 'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden'
-> & { pluginId?: string; id?: string; listed?: boolean };
+export type LegacyAppSpec = Partial & {
+ pluginId?: string;
+ listed?: boolean;
+};
/**
* @internal
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 13b5c6f277e8c..95daf2db1f713 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1976,11 +1976,11 @@ export const validBodyOutput: readonly ["data", "stream"];
// Warnings were encountered during analysis:
//
// src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:158:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:159:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:160:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:161:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:162:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:162:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:163:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:166:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts