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