From 5f96bf779689e5cf48043e82f0454f2c467de9d0 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 13 Nov 2020 12:18:17 -0700 Subject: [PATCH 1/6] Add subLinks to application registration --- .../public/kibana-plugin-core-public.app.md | 1 + .../kibana-plugin-core-public.app.sublinks.md | 15 ++++++ .../kibana-plugin-core-public.appsublink.md | 26 +++++++++ ...a-plugin-core-public.appupdatablefields.md | 2 +- .../core/public/kibana-plugin-core-public.md | 2 + ...kibana-plugin-core-public.publicappinfo.md | 3 +- ...plugin-core-public.publicappsublinkinfo.md | 15 ++++++ src/core/public/application/index.ts | 2 + src/core/public/application/types.ts | 53 ++++++++++++++++++- .../application/utils/get_app_info.test.ts | 36 +++++++++++++ .../public/application/utils/get_app_info.ts | 27 +++++++++- .../chrome/nav_links/to_nav_link.test.ts | 1 + src/core/public/index.ts | 4 +- src/core/public/public.api.md | 23 +++++++- 14 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.app.sublinks.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.appsublink.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 7bdee9dc4c53e..c4fc7527e366a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -28,6 +28,7 @@ export interface App | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | | [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | +| [subLinks](./kibana-plugin-core-public.app.sublinks.md) | AppSubLink[] | Array of links that represent secondary in-app locations for the given app.Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See [AppSubLink](./kibana-plugin-core-public.appsublink.md) for more details. | | [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.app.tooltip.md) | string | A tooltip shown when hovering over app link. | | [updater$](./kibana-plugin-core-public.app.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.sublinks.md b/docs/development/core/public/kibana-plugin-core-public.app.sublinks.md new file mode 100644 index 0000000000000..d44c1b6ddb66e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.sublinks.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [subLinks](./kibana-plugin-core-public.app.sublinks.md) + +## App.subLinks property + +Array of links that represent secondary in-app locations for the given app. + +Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See [AppSubLink](./kibana-plugin-core-public.appsublink.md) for more details. + +Signature: + +```typescript +subLinks?: AppSubLink[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appsublink.md b/docs/development/core/public/kibana-plugin-core-public.appsublink.md new file mode 100644 index 0000000000000..f6b25491e97ee --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appsublink.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppSubLink](./kibana-plugin-core-public.appsublink.md) + +## AppSubLink type + +Input type for registering secondary in-app locations for an application. + +Sublinks must include at least one of `path` or `subLinks`. A sublink that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + +Used to populate navigational search results (where available). + +Signature: + +```typescript +export declare type AppSubLink = { + id: string; + title: string; +} & ({ + path: string; + subLinks?: AppSubLink[]; +} | { + path?: string; + subLinks: AppSubLink[]; +}); +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index 1232b7f940255..be4c3e7d2f76a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 6a90fd49f1d66..c2f5af90571a8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -138,6 +138,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm to to prompt a message to the user before leaving the page, or default to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. | | [AppMount](./kibana-plugin-core-public.appmount.md) | A mount function called when the user navigates to this app's route. | | [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | +| [AppSubLink](./kibana-plugin-core-public.appsublink.md) | Input type for registering secondary in-app locations for an application.Sublinks must include at least one of path or subLinks. A sublink that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible.Used to populate navigational search results (where available). | | [AppUnmount](./kibana-plugin-core-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-core-public.appupdater.md). | | [AppUpdater](./kibana-plugin-core-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | @@ -160,6 +161,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | +| [PublicAppSubLinkInfo](./kibana-plugin-core-public.publicappsublinkinfo.md) | Public information about a registered app's [subLinks](./kibana-plugin-core-public.appsublink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index 3717dc847db25..d18c9b3081c7d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -9,9 +9,10 @@ Public information about a registered [application](./kibana-plugin-core-public. Signature: ```typescript -export declare type PublicAppInfo = Omit & { +export declare type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; + subLinks: PublicAppSubLinkInfo[]; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md new file mode 100644 index 0000000000000..0d2052b213b09 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppSubLinkInfo](./kibana-plugin-core-public.publicappsublinkinfo.md) + +## PublicAppSubLinkInfo type + +Public information about a registered app's [subLinks](./kibana-plugin-core-public.appsublink.md) + +Signature: + +```typescript +export declare type PublicAppSubLinkInfo = Omit & { + subLinks: PublicAppSubLinkInfo[]; +}; +``` diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 4f3b113a29c9b..b9df52311f287 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -31,6 +31,7 @@ export { AppNavLinkStatus, AppUpdatableFields, AppUpdater, + AppSubLink, ApplicationSetup, ApplicationStart, AppLeaveHandler, @@ -40,6 +41,7 @@ export { AppLeaveConfirmAction, NavigateToAppOptions, PublicAppInfo, + PublicAppSubLinkInfo, // Internal types InternalApplicationSetup, InternalApplicationStart, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 02d2d3a52a01a..579989735a5c8 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -81,7 +81,10 @@ export enum AppNavLinkStatus { * Defines the list of fields that can be updated via an {@link AppUpdater}. * @public */ -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick< + App, + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'subLinks' +>; /** * Updater for applications. @@ -232,18 +235,64 @@ export interface App { * ``` */ exactRoute?: boolean; + + /** + * Array of links that represent secondary in-app locations for the given app. + * + * Can be updated using the {@link App.updater$} observable. See {@link AppSubLink} for more details. + */ + subLinks?: AppSubLink[]; } +/** + * Input type for registering secondary in-app locations for an application. + * + * Sublinks must include at least one of `path` or `subLinks`. A sublink that does not have a `path` represents a + * topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + * + * Used to populate navigational search results (where available). + * @public + */ +export type AppSubLink = { + /** Identifier to represent this sublink, should be unique for this application */ + id: string; + /** Title to label represent this sublink */ + title: string; +} & ( + | { + /** URL path to access this link, relative to the application's appRoute. */ + path: string; + /** Optional array of links that are 'underneath' this section in the hierarchy */ + subLinks?: AppSubLink[]; + } + | { + /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ + path?: string; + /** Array links that are 'underneath' this section in this hierarchy. */ + subLinks: AppSubLink[]; + } +); + +/** + * Public information about a registered app's {@link AppSubLink | subLinks} + * + * @public + */ +export type PublicAppSubLinkInfo = Omit & { + subLinks: PublicAppSubLinkInfo[]; +}; + /** * Public information about a registered {@link App | application} * * @public */ -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; + subLinks: PublicAppSubLinkInfo[]; }; /** diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index 055f7d1a5ada9..aa19251c379fc 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -43,6 +43,42 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, + subLinks: [], + }); + }); + + it('populates default values for nested subLinks', () => { + const app = createApp({ + subLinks: [ + { + id: 'sub-id', + title: 'sub-title', + subLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], + }, + ], + }); + const info = getAppInfo(app); + + expect(info).toEqual({ + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.visible, + appRoute: `/app/some-id`, + subLinks: [ + { + id: 'sub-id', + title: 'sub-title', + subLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + subLinks: [], // default empty array added + }, + ], + }, + ], }); }); diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index 71cd8a3e14929..3dbb88e7fa69c 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -17,9 +17,16 @@ * under the License. */ -import { App, AppNavLinkStatus, AppStatus, PublicAppInfo } from '../types'; +import { + App, + AppNavLinkStatus, + AppStatus, + AppSubLink, + PublicAppInfo, + PublicAppSubLinkInfo, +} from '../types'; -export function getAppInfo(app: App): PublicAppInfo { +export function getAppInfo(app: App): PublicAppInfo { const navLinkStatus = app.navLinkStatus === AppNavLinkStatus.default ? app.status === AppStatus.inaccessible @@ -32,5 +39,21 @@ export function getAppInfo(app: App): PublicAppInfo { status: app.status!, navLinkStatus, appRoute: app.appRoute!, + subLinks: getSubLinkInfos(app, app.subLinks), }; } + +function getSubLinkInfos(app: App, subLinks?: AppSubLink[]): PublicAppSubLinkInfo[] { + if (!subLinks) { + return []; + } + + return subLinks.map((rawSubLink) => { + return { + id: rawSubLink.id, + title: rawSubLink.title, + path: rawSubLink.path, + subLinks: getSubLinkInfos(app, rawSubLink.subLinks), + }; + }); +} diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 7e2c1fc1f89f8..ce79cda71bc57 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -28,6 +28,7 @@ const app = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, + subLinks: [], ...props, }); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 564bbd712c535..e925e7a4ba501 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -95,7 +95,6 @@ export { ApplicationSetup, ApplicationStart, App, - PublicAppInfo, AppMount, AppMountDeprecated, AppUnmount, @@ -110,6 +109,9 @@ export { AppNavLinkStatus, AppUpdatableFields, AppUpdater, + AppSubLink, + PublicAppInfo, + PublicAppSubLinkInfo, ScopedHistory, NavigateToAppOptions, } from './application'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 37e57a9ee606e..085c7833bfe6a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -60,6 +60,7 @@ export interface App { navLinkStatus?: AppNavLinkStatus; order?: number; status?: AppStatus; + subLinks?: AppSubLink[]; title: string; tooltip?: string; updater$?: Observable; @@ -181,11 +182,23 @@ export enum AppStatus { inaccessible = 1 } +// @public +export type AppSubLink = { + id: string; + title: string; +} & ({ + path: string; + subLinks?: AppSubLink[]; +} | { + path?: string; + subLinks: AppSubLink[]; +}); + // @public export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: App) => Partial | undefined; @@ -967,10 +980,16 @@ export interface PluginInitializerContext export type PluginOpaqueId = symbol; // @public -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; + subLinks: PublicAppSubLinkInfo[]; +}; + +// @public +export type PublicAppSubLinkInfo = Omit & { + subLinks: PublicAppSubLinkInfo[]; }; // @public From e9f6469c50868107f63226c9c1e4a92345e90ca5 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 13 Nov 2020 12:18:45 -0700 Subject: [PATCH 2/6] Register subLinks for management sections --- src/plugins/management/public/plugin.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 122e73796753c..c4bd89b3e1eff 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -31,6 +31,7 @@ import { AppUpdater, AppStatus, AppNavLinkStatus, + AppSubLink, } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; @@ -38,6 +39,7 @@ import { ManagementSectionsService, getSectionsServiceStartPrivate, } from './management_sections_service'; +import { ManagementSection } from './utils'; interface ManagementSetupDependencies { home?: HomePublicPluginSetup; @@ -46,7 +48,23 @@ interface ManagementSetupDependencies { export class ManagementPlugin implements Plugin { private readonly managementSections = new ManagementSectionsService(); - private readonly appUpdater = new BehaviorSubject(() => ({})); + private readonly appUpdater = new BehaviorSubject(() => { + const subLinks: AppSubLink[] = Object.values(this.managementSections.definedSections).map( + (section: ManagementSection) => ({ + id: section.id, + title: section.title, + subLinks: section.getAppsEnabled().map((mgmtApp) => ({ + id: mgmtApp.id, + title: mgmtApp.title, + path: mgmtApp.basePath, + })), + }) + ); + + return { + subLinks, + }; + }); private hasAnyEnabledApps = true; From c7fcaf1241abb95cce1adc2d7d5be0f6cdeadb40 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 13 Nov 2020 12:19:02 -0700 Subject: [PATCH 3/6] Include subLinks in GS application results --- .../public/providers/application.test.ts | 1 + .../public/providers/get_app_results.test.ts | 99 +++++++++++++--- .../public/providers/get_app_results.ts | 84 +++++++++++--- .../global_search/global_search_providers.ts | 106 ++++++++++-------- 4 files changed, 216 insertions(+), 74 deletions(-) diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index 2831550da00d9..a85c38ce2d39c 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -28,6 +28,7 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, + subLinks: [], ...props, }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts index 5ef15a8cf2ea4..9dca61ec331f5 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts @@ -10,7 +10,7 @@ import { PublicAppInfo, DEFAULT_APP_CATEGORIES, } from 'src/core/public'; -import { appToResult, getAppResults, scoreApp } from './get_app_results'; +import { AppLink, appToResult, getAppResults, scoreApp } from './get_app_results'; const createApp = (props: Partial = {}): PublicAppInfo => ({ id: 'app1', @@ -19,9 +19,17 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, + subLinks: [], ...props, }); +const createAppLink = (props: Partial = {}): AppLink => ({ + id: props.id ?? 'app1', + path: props.appRoute ?? '/app/app1', + subLinkTitles: [], + app: createApp(props), +}); + describe('getAppResults', () => { it('retrieves the matching results', () => { const apps = [ @@ -34,43 +42,80 @@ describe('getAppResults', () => { expect(results.length).toBe(1); expect(results[0]).toEqual(expect.objectContaining({ id: 'dashboard', score: 100 })); }); + + it('creates multiple links for apps with sublinks', () => { + const apps = [ + createApp({ + subLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] }, + { + id: 'sub2', + title: 'Sub2', + path: '/sub2', + subLinks: [{ id: 'sub2sub1', title: 'Sub2Sub1', path: '/sub2/sub1', subLinks: [] }], + }, + ], + }), + ]; + + const results = getAppResults('App 1', apps); + + expect(results.length).toBe(4); + expect(results.map(({ title }) => title)).toEqual([ + 'App 1', + 'App 1 / Sub1', + 'App 1 / Sub2', + 'App 1 / Sub2 / Sub2Sub1', + ]); + }); + + it('only includes sublinks when search term is non-empty', () => { + const apps = [ + createApp({ + subLinks: [{ id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] }], + }), + ]; + + expect(getAppResults('', apps).length).toBe(1); + expect(getAppResults('App 1', apps).length).toBe(2); + }); }); describe('scoreApp', () => { describe('when the term is included in the title', () => { it('returns 100 if the app title is an exact match', () => { - expect(scoreApp('dashboard', createApp({ title: 'dashboard' }))).toBe(100); - expect(scoreApp('dashboard', createApp({ title: 'DASHBOARD' }))).toBe(100); - expect(scoreApp('DASHBOARD', createApp({ title: 'DASHBOARD' }))).toBe(100); - expect(scoreApp('dashBOARD', createApp({ title: 'DASHboard' }))).toBe(100); + expect(scoreApp('dashboard', createAppLink({ title: 'dashboard' }))).toBe(100); + expect(scoreApp('dashboard', createAppLink({ title: 'DASHBOARD' }))).toBe(100); + expect(scoreApp('DASHBOARD', createAppLink({ title: 'DASHBOARD' }))).toBe(100); + expect(scoreApp('dashBOARD', createAppLink({ title: 'DASHboard' }))).toBe(100); }); it('returns 90 if the app title starts with the term', () => { - expect(scoreApp('dash', createApp({ title: 'dashboard' }))).toBe(90); - expect(scoreApp('DASH', createApp({ title: 'dashboard' }))).toBe(90); + expect(scoreApp('dash', createAppLink({ title: 'dashboard' }))).toBe(90); + expect(scoreApp('DASH', createAppLink({ title: 'dashboard' }))).toBe(90); }); it('returns 75 if the term in included in the app title', () => { - expect(scoreApp('board', createApp({ title: 'dashboard' }))).toBe(75); - expect(scoreApp('shboa', createApp({ title: 'dashboard' }))).toBe(75); + expect(scoreApp('board', createAppLink({ title: 'dashboard' }))).toBe(75); + expect(scoreApp('shboa', createAppLink({ title: 'dashboard' }))).toBe(75); }); }); describe('when the term is not included in the title', () => { it('returns the levenshtein ratio if superior or equal to 60', () => { - expect(scoreApp('0123456789', createApp({ title: '012345' }))).toBe(60); - expect(scoreApp('--1234567-', createApp({ title: '123456789' }))).toBe(60); + expect(scoreApp('0123456789', createAppLink({ title: '012345' }))).toBe(60); + expect(scoreApp('--1234567-', createAppLink({ title: '123456789' }))).toBe(60); }); it('returns 0 if the levenshtein ratio is inferior to 60', () => { - expect(scoreApp('0123456789', createApp({ title: '12345' }))).toBe(0); - expect(scoreApp('1-2-3-4-5', createApp({ title: '123456789' }))).toBe(0); + expect(scoreApp('0123456789', createAppLink({ title: '12345' }))).toBe(0); + expect(scoreApp('1-2-3-4-5', createAppLink({ title: '123456789' }))).toBe(0); }); }); }); describe('appToResult', () => { it('converts an app to a result', () => { - const app = createApp({ + const app = createAppLink({ id: 'foo', title: 'Foo', euiIconType: 'fooIcon', @@ -92,7 +137,7 @@ describe('appToResult', () => { }); it('converts an app without category to a result', () => { - const app = createApp({ + const app = createAppLink({ id: 'foo', title: 'Foo', euiIconType: 'fooIcon', @@ -111,4 +156,28 @@ describe('appToResult', () => { score: 42, }); }); + + it('includes the app name in sub links', () => { + const app = createApp(); + const appLink: AppLink = { + id: 'app1-sub', + app, + path: '/sub1', + subLinkTitles: ['Sub1'], + }; + + expect(appToResult(appLink, 42).title).toEqual('App 1 / Sub1'); + }); + + it('does not include the app name in sub links for Stack Management', () => { + const app = createApp({ id: 'management' }); + const appLink: AppLink = { + id: 'management-sub', + app, + path: '/sub1', + subLinkTitles: ['Sub1'], + }; + + expect(appToResult(appLink, 42).title).toEqual('Sub1'); + }); }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts index c4e1a9532d144..bf381e035e514 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts @@ -5,22 +5,39 @@ */ import levenshtein from 'js-levenshtein'; -import { PublicAppInfo } from 'src/core/public'; +import { PublicAppInfo, PublicAppSubLinkInfo } from 'src/core/public'; import { GlobalSearchProviderResult } from '../../../global_search/public'; +/** Type used internally to represent an application unrolled into its separate sublinks */ +export interface AppLink { + id: string; + app: PublicAppInfo; + subLinkTitles: string[]; + path: string; +} + export const getAppResults = ( term: string, apps: PublicAppInfo[] ): GlobalSearchProviderResult[] => { - return apps - .map((app) => ({ app, score: scoreApp(term, app) })) - .filter(({ score }) => score > 0) - .map(({ app, score }) => appToResult(app, score)); + return ( + apps + // Unroll all sublinks + .flatMap((app) => flattenSubLinks(app)) + // Only include sublinks if there is a search term + .filter((appLink) => term.length > 0 || appLink.subLinkTitles.length === 0) + .map((appLink) => ({ + appLink, + score: scoreApp(term, appLink), + })) + .filter(({ score }) => score > 0) + .map(({ appLink, score }) => appToResult(appLink, score)) + ); }; -export const scoreApp = (term: string, { title }: PublicAppInfo): number => { +export const scoreApp = (term: string, appLink: AppLink): number => { term = term.toLowerCase(); - title = title.toLowerCase(); + const title = [appLink.app.title, ...appLink.subLinkTitles].join(' ').toLowerCase(); // shortcuts to avoid calculating the distance when there is an exact match somewhere. if (title === term) { @@ -43,17 +60,56 @@ export const scoreApp = (term: string, { title }: PublicAppInfo): number => { return 0; }; -export const appToResult = (app: PublicAppInfo, score: number): GlobalSearchProviderResult => { +export const appToResult = (appLink: AppLink, score: number): GlobalSearchProviderResult => { + const titleParts = + // Stack Management app should not include the app title in the concatenated link label + appLink.app.id === 'management' && appLink.subLinkTitles.length > 0 + ? appLink.subLinkTitles + : [appLink.app.title, ...appLink.subLinkTitles]; + return { - id: app.id, - title: app.title, + id: appLink.id, + // Concatenate title using slashes + title: titleParts.join(' / '), type: 'application', - icon: app.euiIconType, - url: app.appRoute, + icon: appLink.app.euiIconType, + url: appLink.path, meta: { - categoryId: app.category?.id ?? null, - categoryLabel: app.category?.label ?? null, + categoryId: appLink.app.category?.id ?? null, + categoryLabel: appLink.app.category?.label ?? null, }, score, }; }; + +const flattenSubLinks = (app: PublicAppInfo, subLink?: PublicAppSubLinkInfo): AppLink[] => { + if (!subLink) { + return [ + { + id: app.id, + app, + path: app.appRoute, + subLinkTitles: [], + }, + ...app.subLinks.flatMap((appSubLink) => flattenSubLinks(app, appSubLink)), + ]; + } + + const appLink: AppLink = { + id: `${app.id}-${subLink.id}`, + app, + subLinkTitles: [subLink.title], + path: `${app.appRoute}${subLink.path}`, + }; + + return [ + ...(subLink.path ? [appLink] : []), + ...subLink.subLinks + .flatMap((subSubLink) => flattenSubLinks(app, subSubLink)) + .map((subAppLink) => ({ + ...subAppLink, + // shift current sublink title into array of sub-sublink titles + subLinkTitles: [subLink.title, ...subAppLink.subLinkTitles], + })), + ]; +}; diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts index 16dc7b379214a..29ee26866bd71 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts @@ -14,7 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); - const findResultsWithAPI = async (t: string): Promise => { + const findResultsWithApi = async (t: string): Promise => { return browser.executeAsync(async (term, cb) => { const { start } = window._coreProvider; const globalSearchTestApi: GlobalSearchTestApi = start.plugins.globalSearchTest; @@ -22,60 +22,76 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, t); }; - describe('GlobalSearch - SavedObject provider', function () { - before(async () => { - await esArchiver.load('global_search/basic'); - }); - - after(async () => { - await esArchiver.unload('global_search/basic'); - }); - + describe('GlobalSearch', function () { beforeEach(async () => { await pageObjects.common.navigateToApp('globalSearchTestApp'); }); - it('can search for index patterns', async () => { - const results = await findResultsWithAPI('logstash'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('index-pattern'); - expect(results[0].title).to.be('logstash-*'); - expect(results[0].score).to.be.greaterThan(0.9); - }); + describe('SavedObject provider', function () { + before(async () => { + await esArchiver.load('global_search/basic'); + }); - it('can search for visualizations', async () => { - const results = await findResultsWithAPI('pie'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('visualization'); - expect(results[0].title).to.be('A Pie'); - }); + after(async () => { + await esArchiver.unload('global_search/basic'); + }); - it('can search for maps', async () => { - const results = await findResultsWithAPI('just'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('map'); - expect(results[0].title).to.be('just a map'); - }); + it('can search for index patterns', async () => { + const results = await findResultsWithApi('logstash'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('index-pattern'); + expect(results[0].title).to.be('logstash-*'); + expect(results[0].score).to.be.greaterThan(0.9); + }); - it('can search for dashboards', async () => { - const results = await findResultsWithAPI('Amazing'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('dashboard'); - expect(results[0].title).to.be('Amazing Dashboard'); - }); + it('can search for visualizations', async () => { + const results = await findResultsWithApi('pie'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('visualization'); + expect(results[0].title).to.be('A Pie'); + }); + + it('can search for maps', async () => { + const results = await findResultsWithApi('just'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('map'); + expect(results[0].title).to.be('just a map'); + }); - it('returns all objects matching the search', async () => { - const results = await findResultsWithAPI('dashboard'); - expect(results.length).to.be.greaterThan(2); - expect(results.map((r) => r.title)).to.contain('dashboard with map'); - expect(results.map((r) => r.title)).to.contain('Amazing Dashboard'); + it('can search for dashboards', async () => { + const results = await findResultsWithApi('Amazing'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('dashboard'); + expect(results[0].title).to.be('Amazing Dashboard'); + }); + + it('returns all objects matching the search', async () => { + const results = await findResultsWithApi('dashboard'); + expect(results.length).to.be.greaterThan(2); + expect(results.map((r) => r.title)).to.contain('dashboard with map'); + expect(results.map((r) => r.title)).to.contain('Amazing Dashboard'); + }); + + it('can search by prefix', async () => { + const results = await findResultsWithApi('Amaz'); + expect(results.length).to.be(1); + expect(results[0].type).to.be('dashboard'); + expect(results[0].title).to.be('Amazing Dashboard'); + }); }); - it('can search by prefix', async () => { - const results = await findResultsWithAPI('Amaz'); - expect(results.length).to.be(1); - expect(results[0].type).to.be('dashboard'); - expect(results[0].title).to.be('Amazing Dashboard'); + describe('Applications provider', function () { + it('can search for root-level applications', async () => { + const results = await findResultsWithApi('discover'); + expect(results.length).to.be(1); + expect(results[0].title).to.be('Discover'); + }); + + it('can search for application sublinks', async () => { + const results = await findResultsWithApi('saved objects'); + expect(results.length).to.be(1); + expect(results[0].title).to.be('Kibana / Saved Objects'); + }); }); }); } From 6460c07931b3a85ca5425417992df81c87e06cc4 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 30 Nov 2020 11:41:24 -0700 Subject: [PATCH 4/6] Rename to searchDeepLinks --- .../public/kibana-plugin-core-public.app.md | 2 +- ...plugin-core-public.app.searchdeeplinks.md} | 10 ++-- ...na-plugin-core-public.appsearchdeeplink.md | 24 +++++++++ .../kibana-plugin-core-public.appsublink.md | 26 ---------- ...a-plugin-core-public.appupdatablefields.md | 2 +- .../core/public/kibana-plugin-core-public.md | 4 +- ...kibana-plugin-core-public.publicappinfo.md | 4 +- ...core-public.publicappsearchdeeplinkinfo.md | 15 ++++++ ...plugin-core-public.publicappsublinkinfo.md | 15 ------ src/core/public/application/index.ts | 4 +- src/core/public/application/types.ts | 32 ++++++------ .../application/utils/get_app_info.test.ts | 14 ++--- .../public/application/utils/get_app_info.ts | 31 ++++++----- src/core/public/index.ts | 4 +- src/core/public/public.api.md | 31 +++++------ src/plugins/management/public/plugin.ts | 26 +++++----- .../public/providers/application.test.ts | 2 +- .../public/providers/get_app_results.test.ts | 16 +++--- .../public/providers/get_app_results.ts | 51 +++++++++++-------- 19 files changed, 163 insertions(+), 150 deletions(-) rename docs/development/core/public/{kibana-plugin-core-public.app.sublinks.md => kibana-plugin-core-public.app.searchdeeplinks.md} (57%) create mode 100644 docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appsublink.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index c4fc7527e366a..c3117ee193f7c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -27,8 +27,8 @@ export interface App | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app.Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See for more details. | | [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | -| [subLinks](./kibana-plugin-core-public.app.sublinks.md) | AppSubLink[] | Array of links that represent secondary in-app locations for the given app.Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See [AppSubLink](./kibana-plugin-core-public.appsublink.md) for more details. | | [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.app.tooltip.md) | string | A tooltip shown when hovering over app link. | | [updater$](./kibana-plugin-core-public.app.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.sublinks.md b/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md similarity index 57% rename from docs/development/core/public/kibana-plugin-core-public.app.sublinks.md rename to docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md index d44c1b6ddb66e..2e5d6b7f8b61d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.sublinks.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md @@ -1,15 +1,15 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [subLinks](./kibana-plugin-core-public.app.sublinks.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) -## App.subLinks property +## App.searchDeepLinks property -Array of links that represent secondary in-app locations for the given app. +Array of links that represent secondary in-app locations for the app. -Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See [AppSubLink](./kibana-plugin-core-public.appsublink.md) for more details. +Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See for more details. Signature: ```typescript -subLinks?: AppSubLink[]; +searchDeepLinks?: AppSearchDeepLink[]; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md new file mode 100644 index 0000000000000..7e5ccf7d06ed1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) + +## AppSearchDeepLink type + +Input type for registering secondary in-app locations for an application. + +Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + +Signature: + +```typescript +export declare type AppSearchDeepLink = { + id: string; + title: string; +} & ({ + path: string; + searchDeepLinks?: AppSearchDeepLink[]; +} | { + path?: string; + searchDeepLinks: AppSearchDeepLink[]; +}); +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appsublink.md b/docs/development/core/public/kibana-plugin-core-public.appsublink.md deleted file mode 100644 index f6b25491e97ee..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appsublink.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppSubLink](./kibana-plugin-core-public.appsublink.md) - -## AppSubLink type - -Input type for registering secondary in-app locations for an application. - -Sublinks must include at least one of `path` or `subLinks`. A sublink that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. - -Used to populate navigational search results (where available). - -Signature: - -```typescript -export declare type AppSubLink = { - id: string; - title: string; -} & ({ - path: string; - subLinks?: AppSubLink[]; -} | { - path?: string; - subLinks: AppSubLink[]; -}); -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index be4c3e7d2f76a..b6f404c3d11aa 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index c2f5af90571a8..5f656b9ca510d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -138,7 +138,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm to to prompt a message to the user before leaving the page, or default to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. | | [AppMount](./kibana-plugin-core-public.appmount.md) | A mount function called when the user navigates to this app's route. | | [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | -| [AppSubLink](./kibana-plugin-core-public.appsublink.md) | Input type for registering secondary in-app locations for an application.Sublinks must include at least one of path or subLinks. A sublink that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible.Used to populate navigational search results (where available). | +| [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or searchDeepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [AppUnmount](./kibana-plugin-core-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-core-public.appupdater.md). | | [AppUpdater](./kibana-plugin-core-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | @@ -161,7 +161,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | -| [PublicAppSubLinkInfo](./kibana-plugin-core-public.publicappsublinkinfo.md) | Public information about a registered app's [subLinks](./kibana-plugin-core-public.appsublink.md) | +| [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) | Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index d18c9b3081c7d..d56b0ac58cd9b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -9,10 +9,10 @@ Public information about a registered [application](./kibana-plugin-core-public. Signature: ```typescript -export declare type PublicAppInfo = Omit & { +export declare type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - subLinks: PublicAppSubLinkInfo[]; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md new file mode 100644 index 0000000000000..9814f0408d047 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) + +## PublicAppSearchDeepLinkInfo type + +Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) + +Signature: + +```typescript +export declare type PublicAppSearchDeepLinkInfo = Omit & { + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md deleted file mode 100644 index 0d2052b213b09..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.publicappsublinkinfo.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppSubLinkInfo](./kibana-plugin-core-public.publicappsublinkinfo.md) - -## PublicAppSubLinkInfo type - -Public information about a registered app's [subLinks](./kibana-plugin-core-public.appsublink.md) - -Signature: - -```typescript -export declare type PublicAppSubLinkInfo = Omit & { - subLinks: PublicAppSubLinkInfo[]; -}; -``` diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index b9df52311f287..b39aa70c888fe 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -31,7 +31,7 @@ export { AppNavLinkStatus, AppUpdatableFields, AppUpdater, - AppSubLink, + AppSearchDeepLink, ApplicationSetup, ApplicationStart, AppLeaveHandler, @@ -41,7 +41,7 @@ export { AppLeaveConfirmAction, NavigateToAppOptions, PublicAppInfo, - PublicAppSubLinkInfo, + PublicAppSearchDeepLinkInfo, // Internal types InternalApplicationSetup, InternalApplicationStart, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 579989735a5c8..9b80afa114c03 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -83,7 +83,7 @@ export enum AppNavLinkStatus { */ export type AppUpdatableFields = Pick< App, - 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'subLinks' + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'searchDeepLinks' >; /** @@ -237,49 +237,49 @@ export interface App { exactRoute?: boolean; /** - * Array of links that represent secondary in-app locations for the given app. + * Array of links that represent secondary in-app locations for the app. * + * Used to populate navigational search results (where available). * Can be updated using the {@link App.updater$} observable. See {@link AppSubLink} for more details. */ - subLinks?: AppSubLink[]; + searchDeepLinks?: AppSearchDeepLink[]; } /** * Input type for registering secondary in-app locations for an application. * - * Sublinks must include at least one of `path` or `subLinks`. A sublink that does not have a `path` represents a - * topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. - * - * Used to populate navigational search results (where available). + * Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` + * represents a topological level in the application's hierarchy, but does not have a destination URL that is + * user-accessible. * @public */ -export type AppSubLink = { +export type AppSearchDeepLink = { /** Identifier to represent this sublink, should be unique for this application */ id: string; - /** Title to label represent this sublink */ + /** Title to label represent this deep link */ title: string; } & ( | { /** URL path to access this link, relative to the application's appRoute. */ path: string; /** Optional array of links that are 'underneath' this section in the hierarchy */ - subLinks?: AppSubLink[]; + searchDeepLinks?: AppSearchDeepLink[]; } | { /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ path?: string; /** Array links that are 'underneath' this section in this hierarchy. */ - subLinks: AppSubLink[]; + searchDeepLinks: AppSearchDeepLink[]; } ); /** - * Public information about a registered app's {@link AppSubLink | subLinks} + * Public information about a registered app's {@link AppSearchDeepLink | searchDeepLinks} * * @public */ -export type PublicAppSubLinkInfo = Omit & { - subLinks: PublicAppSubLinkInfo[]; +export type PublicAppSearchDeepLinkInfo = Omit & { + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; /** @@ -287,12 +287,12 @@ export type PublicAppSubLinkInfo = Omit & { * * @public */ -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - subLinks: PublicAppSubLinkInfo[]; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; /** diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index aa19251c379fc..ee0bd4f1eadfa 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -43,17 +43,17 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - subLinks: [], + searchDeepLinks: [], }); }); - it('populates default values for nested subLinks', () => { + it('populates default values for nested searchDeepLinks', () => { const app = createApp({ - subLinks: [ + searchDeepLinks: [ { id: 'sub-id', title: 'sub-title', - subLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], + searchDeepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], }, ], }); @@ -65,16 +65,16 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - subLinks: [ + searchDeepLinks: [ { id: 'sub-id', title: 'sub-title', - subLinks: [ + searchDeepLinks: [ { id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub', - subLinks: [], // default empty array added + searchDeepLinks: [], // default empty array added }, ], }, diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index 3dbb88e7fa69c..7316080816da7 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -21,9 +21,9 @@ import { App, AppNavLinkStatus, AppStatus, - AppSubLink, + AppSearchDeepLink, PublicAppInfo, - PublicAppSubLinkInfo, + PublicAppSearchDeepLinkInfo, } from '../types'; export function getAppInfo(app: App): PublicAppInfo { @@ -39,21 +39,26 @@ export function getAppInfo(app: App): PublicAppInfo { status: app.status!, navLinkStatus, appRoute: app.appRoute!, - subLinks: getSubLinkInfos(app, app.subLinks), + searchDeepLinks: getSearchDeepLinkInfos(app, app.searchDeepLinks), }; } -function getSubLinkInfos(app: App, subLinks?: AppSubLink[]): PublicAppSubLinkInfo[] { - if (!subLinks) { +function getSearchDeepLinkInfos( + app: App, + searchDeepLinks?: AppSearchDeepLink[] +): PublicAppSearchDeepLinkInfo[] { + if (!searchDeepLinks) { return []; } - return subLinks.map((rawSubLink) => { - return { - id: rawSubLink.id, - title: rawSubLink.title, - path: rawSubLink.path, - subLinks: getSubLinkInfos(app, rawSubLink.subLinks), - }; - }); + return searchDeepLinks.map( + (rawDeepLink): PublicAppSearchDeepLinkInfo => { + return { + id: rawDeepLink.id, + title: rawDeepLink.title, + path: rawDeepLink.path, + searchDeepLinks: getSearchDeepLinkInfos(app, rawDeepLink.searchDeepLinks), + }; + } + ); } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index e925e7a4ba501..557529fc94dc4 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -109,9 +109,9 @@ export { AppNavLinkStatus, AppUpdatableFields, AppUpdater, - AppSubLink, + AppSearchDeepLink, PublicAppInfo, - PublicAppSubLinkInfo, + PublicAppSearchDeepLinkInfo, ScopedHistory, NavigateToAppOptions, } from './application'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 085c7833bfe6a..aaea8f2f7c3fd 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -59,8 +59,9 @@ export interface App { mount: AppMount | AppMountDeprecated; navLinkStatus?: AppNavLinkStatus; order?: number; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppSubLink" + searchDeepLinks?: AppSearchDeepLink[]; status?: AppStatus; - subLinks?: AppSubLink[]; title: string; tooltip?: string; updater$?: Observable; @@ -177,28 +178,28 @@ export enum AppNavLinkStatus { } // @public -export enum AppStatus { - accessible = 0, - inaccessible = 1 -} - -// @public -export type AppSubLink = { +export type AppSearchDeepLink = { id: string; title: string; } & ({ path: string; - subLinks?: AppSubLink[]; + searchDeepLinks?: AppSearchDeepLink[]; } | { path?: string; - subLinks: AppSubLink[]; + searchDeepLinks: AppSearchDeepLink[]; }); +// @public +export enum AppStatus { + accessible = 0, + inaccessible = 1 +} + // @public export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: App) => Partial | undefined; @@ -980,16 +981,16 @@ export interface PluginInitializerContext export type PluginOpaqueId = symbol; // @public -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - subLinks: PublicAppSubLinkInfo[]; + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; // @public -export type PublicAppSubLinkInfo = Omit & { - subLinks: PublicAppSubLinkInfo[]; +export type PublicAppSearchDeepLinkInfo = Omit & { + searchDeepLinks: PublicAppSearchDeepLinkInfo[]; }; // @public diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index c4bd89b3e1eff..bf03c649fa6b4 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -31,7 +31,7 @@ import { AppUpdater, AppStatus, AppNavLinkStatus, - AppSubLink, + AppSearchDeepLink, } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; @@ -49,20 +49,20 @@ export class ManagementPlugin implements Plugin(() => { - const subLinks: AppSubLink[] = Object.values(this.managementSections.definedSections).map( - (section: ManagementSection) => ({ - id: section.id, - title: section.title, - subLinks: section.getAppsEnabled().map((mgmtApp) => ({ - id: mgmtApp.id, - title: mgmtApp.title, - path: mgmtApp.basePath, - })), - }) - ); + const deepLinks: AppSearchDeepLink[] = Object.values( + this.managementSections.definedSections + ).map((section: ManagementSection) => ({ + id: section.id, + title: section.title, + searchDeepLinks: section.getAppsEnabled().map((mgmtApp) => ({ + id: mgmtApp.id, + title: mgmtApp.title, + path: mgmtApp.basePath, + })), + })); return { - subLinks, + searchDeepLinks: deepLinks, }; }); diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index a85c38ce2d39c..7beed42de4c4f 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -28,7 +28,7 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, - subLinks: [], + searchDeepLinks: [], ...props, }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts index 9dca61ec331f5..33fd358f61aca 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts @@ -19,7 +19,7 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, - subLinks: [], + searchDeepLinks: [], ...props, }); @@ -43,16 +43,18 @@ describe('getAppResults', () => { expect(results[0]).toEqual(expect.objectContaining({ id: 'dashboard', score: 100 })); }); - it('creates multiple links for apps with sublinks', () => { + it('creates multiple links for apps with searchDeepLinks', () => { const apps = [ createApp({ - subLinks: [ - { id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] }, + searchDeepLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [] }, { id: 'sub2', title: 'Sub2', path: '/sub2', - subLinks: [{ id: 'sub2sub1', title: 'Sub2Sub1', path: '/sub2/sub1', subLinks: [] }], + searchDeepLinks: [ + { id: 'sub2sub1', title: 'Sub2Sub1', path: '/sub2/sub1', searchDeepLinks: [] }, + ], }, ], }), @@ -69,10 +71,10 @@ describe('getAppResults', () => { ]); }); - it('only includes sublinks when search term is non-empty', () => { + it('only includes searchDeepLinks when search term is non-empty', () => { const apps = [ createApp({ - subLinks: [{ id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] }], + searchDeepLinks: [{ id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [] }], }), ]; diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts index bf381e035e514..01e6e87f30c94 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts @@ -5,10 +5,10 @@ */ import levenshtein from 'js-levenshtein'; -import { PublicAppInfo, PublicAppSubLinkInfo } from 'src/core/public'; +import { PublicAppInfo, PublicAppSearchDeepLinkInfo } from 'src/core/public'; import { GlobalSearchProviderResult } from '../../../global_search/public'; -/** Type used internally to represent an application unrolled into its separate sublinks */ +/** Type used internally to represent an application unrolled into its separate searchDeepLinks */ export interface AppLink { id: string; app: PublicAppInfo; @@ -22,10 +22,12 @@ export const getAppResults = ( ): GlobalSearchProviderResult[] => { return ( apps - // Unroll all sublinks - .flatMap((app) => flattenSubLinks(app)) - // Only include sublinks if there is a search term - .filter((appLink) => term.length > 0 || appLink.subLinkTitles.length === 0) + // Unroll all searchDeepLinks, only if there is a search term + .flatMap((app) => + term.length > 0 + ? flattenDeepLinks(app) + : [{ id: app.id, app, path: app.appRoute, subLinkTitles: [] }] + ) .map((appLink) => ({ appLink, score: scoreApp(term, appLink), @@ -82,8 +84,11 @@ export const appToResult = (appLink: AppLink, score: number): GlobalSearchProvid }; }; -const flattenSubLinks = (app: PublicAppInfo, subLink?: PublicAppSubLinkInfo): AppLink[] => { - if (!subLink) { +const flattenDeepLinks = ( + app: PublicAppInfo, + deepLink?: PublicAppSearchDeepLinkInfo +): AppLink[] => { + if (!deepLink) { return [ { id: app.id, @@ -91,25 +96,27 @@ const flattenSubLinks = (app: PublicAppInfo, subLink?: PublicAppSubLinkInfo): Ap path: app.appRoute, subLinkTitles: [], }, - ...app.subLinks.flatMap((appSubLink) => flattenSubLinks(app, appSubLink)), + ...app.searchDeepLinks.flatMap((appDeepLink) => flattenDeepLinks(app, appDeepLink)), ]; } - const appLink: AppLink = { - id: `${app.id}-${subLink.id}`, - app, - subLinkTitles: [subLink.title], - path: `${app.appRoute}${subLink.path}`, - }; - return [ - ...(subLink.path ? [appLink] : []), - ...subLink.subLinks - .flatMap((subSubLink) => flattenSubLinks(app, subSubLink)) - .map((subAppLink) => ({ - ...subAppLink, + ...(deepLink.path + ? [ + { + id: `${app.id}-${deepLink.id}`, + app, + subLinkTitles: [deepLink.title], + path: `${app.appRoute}${deepLink.path}`, + }, + ] + : []), + ...deepLink.searchDeepLinks + .flatMap((deepDeepLink) => flattenDeepLinks(app, deepDeepLink)) + .map((deepAppLink) => ({ + ...deepAppLink, // shift current sublink title into array of sub-sublink titles - subLinkTitles: [subLink.title, ...subAppLink.subLinkTitles], + subLinkTitles: [deepLink.title, ...deepAppLink.subLinkTitles], })), ]; }; From da1cd8e93968fe0ad934ab8ebbd6ffae8868ffad Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 30 Nov 2020 11:46:52 -0700 Subject: [PATCH 5/6] Fix tests and propType error --- .../public/components/search_bar.tsx | 2 +- .../global_search/global_search_providers.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index 3746e636066a9..ecd1c92bfcee6 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -83,7 +83,7 @@ const resultToOption = (result: GlobalSearchResult): EuiSelectableTemplateSitewi }; if (type === 'application') { - option.meta = [{ text: meta?.categoryLabel as string }]; + option.meta = [{ text: (meta?.categoryLabel as string) ?? '' }]; } else { option.meta = [{ text: cleanMeta(type) }]; } diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts index 29ee26866bd71..170548811def5 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_providers.ts @@ -22,8 +22,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, t); }; - describe('GlobalSearch', function () { - beforeEach(async () => { + describe('GlobalSearch providers', function () { + before(async () => { await pageObjects.common.navigateToApp('globalSearchTestApp'); }); @@ -37,7 +37,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('can search for index patterns', async () => { - const results = await findResultsWithApi('logstash'); + const results = await findResultsWithApi('type:index-pattern logstash'); expect(results.length).to.be(1); expect(results[0].type).to.be('index-pattern'); expect(results[0].title).to.be('logstash-*'); @@ -45,35 +45,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('can search for visualizations', async () => { - const results = await findResultsWithApi('pie'); + const results = await findResultsWithApi('type:visualization pie'); expect(results.length).to.be(1); expect(results[0].type).to.be('visualization'); expect(results[0].title).to.be('A Pie'); }); it('can search for maps', async () => { - const results = await findResultsWithApi('just'); + const results = await findResultsWithApi('type:map just'); expect(results.length).to.be(1); expect(results[0].type).to.be('map'); expect(results[0].title).to.be('just a map'); }); it('can search for dashboards', async () => { - const results = await findResultsWithApi('Amazing'); + const results = await findResultsWithApi('type:dashboard Amazing'); expect(results.length).to.be(1); expect(results[0].type).to.be('dashboard'); expect(results[0].title).to.be('Amazing Dashboard'); }); it('returns all objects matching the search', async () => { - const results = await findResultsWithApi('dashboard'); - expect(results.length).to.be.greaterThan(2); + const results = await findResultsWithApi('type:dashboard dashboard'); + expect(results.length).to.be(2); expect(results.map((r) => r.title)).to.contain('dashboard with map'); expect(results.map((r) => r.title)).to.contain('Amazing Dashboard'); }); it('can search by prefix', async () => { - const results = await findResultsWithApi('Amaz'); + const results = await findResultsWithApi('type:dashboard Amaz'); expect(results.length).to.be(1); expect(results[0].type).to.be('dashboard'); expect(results[0].title).to.be('Amazing Dashboard'); @@ -87,7 +87,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(results[0].title).to.be('Discover'); }); - it('can search for application sublinks', async () => { + it('can search for application deep links', async () => { const results = await findResultsWithApi('saved objects'); expect(results.length).to.be(1); expect(results[0].title).to.be('Kibana / Saved Objects'); From 321ff0ea68bccc8f540a8b1243cbed6e51fc58a0 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 30 Nov 2020 11:55:56 -0700 Subject: [PATCH 6/6] Fix types + add better example --- ...ibana-plugin-core-public.app.exactroute.md | 2 +- .../public/kibana-plugin-core-public.app.md | 2 +- ...-plugin-core-public.app.searchdeeplinks.md | 31 +++++++++++++++++-- src/core/public/application/types.ts | 27 +++++++++++++++- .../chrome/nav_links/to_nav_link.test.ts | 2 +- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md index d1e0be17a92b2..eb050b62c7d43 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md @@ -18,7 +18,7 @@ exactRoute?: boolean; ```ts core.application.register({ id: 'my_app', - title: 'My App' + title: 'My App', exactRoute: true, mount: () => { ... }, }) diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index c3117ee193f7c..8e8bae5ad9c58 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -27,7 +27,7 @@ export interface App | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app.Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See for more details. | +| [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app. | | [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | | [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.app.tooltip.md) | string | A tooltip shown when hovering over app link. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md b/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md index 2e5d6b7f8b61d..667fddbc212a5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.searchdeeplinks.md @@ -6,10 +6,37 @@ Array of links that represent secondary in-app locations for the app. -Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See for more details. - Signature: ```typescript searchDeepLinks?: AppSearchDeepLink[]; ``` + +## Remarks + +Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See for more details. + +## Example + +The `path` property on deep links should not include the application's `appRoute`: + +```ts +core.application.register({ + id: 'my_app', + title: 'My App', + searchDeepLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1' }, + { + id: 'sub2', + title: 'Sub2', + searchDeepLinks: [ + { id: 'subsub', title: 'SubSub', path: '/sub2/sub' } + ] + } + ], + mount: () => { ... }, +}) + +``` +Will produce deep links on these paths: - `/app/my_app/sub1` - `/app/my_app/sub2/sub` + diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 9b80afa114c03..d9f326c7a59ab 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -225,7 +225,7 @@ export interface App { * ```ts * core.application.register({ * id: 'my_app', - * title: 'My App' + * title: 'My App', * exactRoute: true, * mount: () => { ... }, * }) @@ -239,8 +239,33 @@ export interface App { /** * Array of links that represent secondary in-app locations for the app. * + * @remarks * Used to populate navigational search results (where available). * Can be updated using the {@link App.updater$} observable. See {@link AppSubLink} for more details. + * + * @example + * The `path` property on deep links should not include the application's `appRoute`: + * ```ts + * core.application.register({ + * id: 'my_app', + * title: 'My App', + * searchDeepLinks: [ + * { id: 'sub1', title: 'Sub1', path: '/sub1' }, + * { + * id: 'sub2', + * title: 'Sub2', + * searchDeepLinks: [ + * { id: 'subsub', title: 'SubSub', path: '/sub2/sub' } + * ] + * } + * ], + * mount: () => { ... }, + * }) + * ``` + * + * Will produce deep links on these paths: + * - `/app/my_app/sub1` + * - `/app/my_app/sub2/sub` */ searchDeepLinks?: AppSearchDeepLink[]; } diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index ce79cda71bc57..606370c5afd0a 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -28,7 +28,7 @@ const app = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, - subLinks: [], + searchDeepLinks: [], ...props, });