mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
parent
3118c934a9
commit
6a76c1ccfe
21 changed files with 507 additions and 90 deletions
|
@ -18,7 +18,7 @@ exactRoute?: boolean;
|
|||
```ts
|
||||
core.application.register({
|
||||
id: 'my_app',
|
||||
title: 'My App'
|
||||
title: 'My App',
|
||||
exactRoute: true,
|
||||
mount: () => { ... },
|
||||
})
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface App<HistoryLocationState = unknown>
|
|||
| [mount](./kibana-plugin-core-public.app.mount.md) | <code>AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState></code> | 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) | <code>AppNavLinkStatus</code> | The initial status of the application's navLink. Defaulting to <code>visible</code> if <code>status</code> is <code>accessible</code> and <code>hidden</code> if status is <code>inaccessible</code> See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) |
|
||||
| [order](./kibana-plugin-core-public.app.order.md) | <code>number</code> | An ordinal used to sort nav links relative to one another for display. |
|
||||
| [searchDeepLinks](./kibana-plugin-core-public.app.searchdeeplinks.md) | <code>AppSearchDeepLink[]</code> | Array of links that represent secondary in-app locations for the app. |
|
||||
| [status](./kibana-plugin-core-public.app.status.md) | <code>AppStatus</code> | The initial status of the application. Defaulting to <code>accessible</code> |
|
||||
| [title](./kibana-plugin-core-public.app.title.md) | <code>string</code> | The title of the application. |
|
||||
| [tooltip](./kibana-plugin-core-public.app.tooltip.md) | <code>string</code> | A tooltip shown when hovering over app link. |
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.searchDeepLinks property
|
||||
|
||||
Array of links that represent secondary in-app locations for the app.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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`
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AppSearchDeepLink = {
|
||||
id: string;
|
||||
title: string;
|
||||
} & ({
|
||||
path: string;
|
||||
searchDeepLinks?: AppSearchDeepLink[];
|
||||
} | {
|
||||
path?: string;
|
||||
searchDeepLinks: AppSearchDeepLink[];
|
||||
});
|
||||
```
|
|
@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath'>;
|
||||
export declare type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'searchDeepLinks'>;
|
||||
```
|
||||
|
|
|
@ -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 <code>confirm</code> to to prompt a message to the user before leaving the page, or <code>default</code> 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. |
|
||||
| [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 <code>path</code> or <code>searchDeepLinks</code>. A deep link that does not have a <code>path</code> 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) |
|
||||
|
@ -160,6 +161,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> 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) |
|
||||
| [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) |
|
||||
|
|
|
@ -9,9 +9,10 @@ Public information about a registered [application](./kibana-plugin-core-public.
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PublicAppInfo = Omit<App, 'mount' | 'updater$'> & {
|
||||
export declare type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'searchDeepLinks'> & {
|
||||
status: AppStatus;
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
appRoute: string;
|
||||
searchDeepLinks: PublicAppSearchDeepLinkInfo[];
|
||||
};
|
||||
```
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PublicAppSearchDeepLinkInfo = Omit<AppSearchDeepLink, 'searchDeepLinks'> & {
|
||||
searchDeepLinks: PublicAppSearchDeepLinkInfo[];
|
||||
};
|
||||
```
|
|
@ -31,6 +31,7 @@ export {
|
|||
AppNavLinkStatus,
|
||||
AppUpdatableFields,
|
||||
AppUpdater,
|
||||
AppSearchDeepLink,
|
||||
ApplicationSetup,
|
||||
ApplicationStart,
|
||||
AppLeaveHandler,
|
||||
|
@ -40,6 +41,7 @@ export {
|
|||
AppLeaveConfirmAction,
|
||||
NavigateToAppOptions,
|
||||
PublicAppInfo,
|
||||
PublicAppSearchDeepLinkInfo,
|
||||
// Internal types
|
||||
InternalApplicationSetup,
|
||||
InternalApplicationStart,
|
||||
|
|
|
@ -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<App, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath'>;
|
||||
export type AppUpdatableFields = Pick<
|
||||
App,
|
||||
'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'searchDeepLinks'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Updater for applications.
|
||||
|
@ -222,7 +225,7 @@ export interface App<HistoryLocationState = unknown> {
|
|||
* ```ts
|
||||
* core.application.register({
|
||||
* id: 'my_app',
|
||||
* title: 'My App'
|
||||
* title: 'My App',
|
||||
* exactRoute: true,
|
||||
* mount: () => { ... },
|
||||
* })
|
||||
|
@ -232,18 +235,89 @@ export interface App<HistoryLocationState = unknown> {
|
|||
* ```
|
||||
*/
|
||||
exactRoute?: boolean;
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @public
|
||||
*/
|
||||
export type AppSearchDeepLink = {
|
||||
/** Identifier to represent this sublink, should be unique for this application */
|
||||
id: string;
|
||||
/** 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 */
|
||||
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. */
|
||||
searchDeepLinks: AppSearchDeepLink[];
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Public information about a registered app's {@link AppSearchDeepLink | searchDeepLinks}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PublicAppSearchDeepLinkInfo = Omit<AppSearchDeepLink, 'searchDeepLinks'> & {
|
||||
searchDeepLinks: PublicAppSearchDeepLinkInfo[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Public information about a registered {@link App | application}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PublicAppInfo = Omit<App, 'mount' | 'updater$'> & {
|
||||
export type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'searchDeepLinks'> & {
|
||||
// remove optional on fields populated with default values
|
||||
status: AppStatus;
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
appRoute: string;
|
||||
searchDeepLinks: PublicAppSearchDeepLinkInfo[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -43,6 +43,42 @@ describe('getAppInfo', () => {
|
|||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
appRoute: `/app/some-id`,
|
||||
searchDeepLinks: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('populates default values for nested searchDeepLinks', () => {
|
||||
const app = createApp({
|
||||
searchDeepLinks: [
|
||||
{
|
||||
id: 'sub-id',
|
||||
title: 'sub-title',
|
||||
searchDeepLinks: [{ 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`,
|
||||
searchDeepLinks: [
|
||||
{
|
||||
id: 'sub-id',
|
||||
title: 'sub-title',
|
||||
searchDeepLinks: [
|
||||
{
|
||||
id: 'sub-sub-id',
|
||||
title: 'sub-sub-title',
|
||||
path: '/sub-sub',
|
||||
searchDeepLinks: [], // default empty array added
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,9 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { App, AppNavLinkStatus, AppStatus, PublicAppInfo } from '../types';
|
||||
import {
|
||||
App,
|
||||
AppNavLinkStatus,
|
||||
AppStatus,
|
||||
AppSearchDeepLink,
|
||||
PublicAppInfo,
|
||||
PublicAppSearchDeepLinkInfo,
|
||||
} from '../types';
|
||||
|
||||
export function getAppInfo(app: App<unknown>): PublicAppInfo {
|
||||
export function getAppInfo(app: App): PublicAppInfo {
|
||||
const navLinkStatus =
|
||||
app.navLinkStatus === AppNavLinkStatus.default
|
||||
? app.status === AppStatus.inaccessible
|
||||
|
@ -32,5 +39,26 @@ export function getAppInfo(app: App<unknown>): PublicAppInfo {
|
|||
status: app.status!,
|
||||
navLinkStatus,
|
||||
appRoute: app.appRoute!,
|
||||
searchDeepLinks: getSearchDeepLinkInfos(app, app.searchDeepLinks),
|
||||
};
|
||||
}
|
||||
|
||||
function getSearchDeepLinkInfos(
|
||||
app: App,
|
||||
searchDeepLinks?: AppSearchDeepLink[]
|
||||
): PublicAppSearchDeepLinkInfo[] {
|
||||
if (!searchDeepLinks) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return searchDeepLinks.map(
|
||||
(rawDeepLink): PublicAppSearchDeepLinkInfo => {
|
||||
return {
|
||||
id: rawDeepLink.id,
|
||||
title: rawDeepLink.title,
|
||||
path: rawDeepLink.path,
|
||||
searchDeepLinks: getSearchDeepLinkInfos(app, rawDeepLink.searchDeepLinks),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ const app = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
|
|||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
appRoute: `/app/some-id`,
|
||||
searchDeepLinks: [],
|
||||
...props,
|
||||
});
|
||||
|
||||
|
|
|
@ -96,7 +96,6 @@ export {
|
|||
ApplicationSetup,
|
||||
ApplicationStart,
|
||||
App,
|
||||
PublicAppInfo,
|
||||
AppMount,
|
||||
AppMountDeprecated,
|
||||
AppUnmount,
|
||||
|
@ -111,6 +110,9 @@ export {
|
|||
AppNavLinkStatus,
|
||||
AppUpdatableFields,
|
||||
AppUpdater,
|
||||
AppSearchDeepLink,
|
||||
PublicAppInfo,
|
||||
PublicAppSearchDeepLinkInfo,
|
||||
ScopedHistory,
|
||||
NavigateToAppOptions,
|
||||
} from './application';
|
||||
|
|
|
@ -59,6 +59,8 @@ export interface App<HistoryLocationState = unknown> {
|
|||
mount: AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState>;
|
||||
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;
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
|
@ -175,6 +177,18 @@ export enum AppNavLinkStatus {
|
|||
visible = 1
|
||||
}
|
||||
|
||||
// @public
|
||||
export type AppSearchDeepLink = {
|
||||
id: string;
|
||||
title: string;
|
||||
} & ({
|
||||
path: string;
|
||||
searchDeepLinks?: AppSearchDeepLink[];
|
||||
} | {
|
||||
path?: string;
|
||||
searchDeepLinks: AppSearchDeepLink[];
|
||||
});
|
||||
|
||||
// @public
|
||||
export enum AppStatus {
|
||||
accessible = 0,
|
||||
|
@ -185,7 +199,7 @@ export enum AppStatus {
|
|||
export type AppUnmount = () => void;
|
||||
|
||||
// @public
|
||||
export type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath'>;
|
||||
export type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'searchDeepLinks'>;
|
||||
|
||||
// @public
|
||||
export type AppUpdater = (app: App) => Partial<AppUpdatableFields> | undefined;
|
||||
|
@ -967,10 +981,16 @@ export interface PluginInitializerContext<ConfigSchema extends object = object>
|
|||
export type PluginOpaqueId = symbol;
|
||||
|
||||
// @public
|
||||
export type PublicAppInfo = Omit<App, 'mount' | 'updater$'> & {
|
||||
export type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'searchDeepLinks'> & {
|
||||
status: AppStatus;
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
appRoute: string;
|
||||
searchDeepLinks: PublicAppSearchDeepLinkInfo[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export type PublicAppSearchDeepLinkInfo = Omit<AppSearchDeepLink, 'searchDeepLinks'> & {
|
||||
searchDeepLinks: PublicAppSearchDeepLinkInfo[];
|
||||
};
|
||||
|
||||
// @public
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
AppUpdater,
|
||||
AppStatus,
|
||||
AppNavLinkStatus,
|
||||
AppSearchDeepLink,
|
||||
} 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<ManagementSetup, ManagementStart> {
|
||||
private readonly managementSections = new ManagementSectionsService();
|
||||
|
||||
private readonly appUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
|
||||
private readonly appUpdater = new BehaviorSubject<AppUpdater>(() => {
|
||||
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 {
|
||||
searchDeepLinks: deepLinks,
|
||||
};
|
||||
});
|
||||
|
||||
private hasAnyEnabledApps = true;
|
||||
|
||||
|
|
|
@ -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) }];
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
|
|||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
chromeless: false,
|
||||
searchDeepLinks: [],
|
||||
...props,
|
||||
});
|
||||
|
||||
|
|
|
@ -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> = {}): PublicAppInfo => ({
|
||||
id: 'app1',
|
||||
|
@ -19,9 +19,17 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
|
|||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
chromeless: false,
|
||||
searchDeepLinks: [],
|
||||
...props,
|
||||
});
|
||||
|
||||
const createAppLink = (props: Partial<PublicAppInfo> = {}): 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,82 @@ describe('getAppResults', () => {
|
|||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toEqual(expect.objectContaining({ id: 'dashboard', score: 100 }));
|
||||
});
|
||||
|
||||
it('creates multiple links for apps with searchDeepLinks', () => {
|
||||
const apps = [
|
||||
createApp({
|
||||
searchDeepLinks: [
|
||||
{ id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [] },
|
||||
{
|
||||
id: 'sub2',
|
||||
title: 'Sub2',
|
||||
path: '/sub2',
|
||||
searchDeepLinks: [
|
||||
{ id: 'sub2sub1', title: 'Sub2Sub1', path: '/sub2/sub1', searchDeepLinks: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
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 searchDeepLinks when search term is non-empty', () => {
|
||||
const apps = [
|
||||
createApp({
|
||||
searchDeepLinks: [{ id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [] }],
|
||||
}),
|
||||
];
|
||||
|
||||
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 +139,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 +158,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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,22 +5,41 @@
|
|||
*/
|
||||
|
||||
import levenshtein from 'js-levenshtein';
|
||||
import { PublicAppInfo } 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 searchDeepLinks */
|
||||
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 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),
|
||||
}))
|
||||
.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 +62,61 @@ 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 flattenDeepLinks = (
|
||||
app: PublicAppInfo,
|
||||
deepLink?: PublicAppSearchDeepLinkInfo
|
||||
): AppLink[] => {
|
||||
if (!deepLink) {
|
||||
return [
|
||||
{
|
||||
id: app.id,
|
||||
app,
|
||||
path: app.appRoute,
|
||||
subLinkTitles: [],
|
||||
},
|
||||
...app.searchDeepLinks.flatMap((appDeepLink) => flattenDeepLinks(app, appDeepLink)),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...(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: [deepLink.title, ...deepAppLink.subLinkTitles],
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const browser = getService('browser');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const findResultsWithAPI = async (t: string): Promise<GlobalSearchResult[]> => {
|
||||
const findResultsWithApi = async (t: string): Promise<GlobalSearchResult[]> => {
|
||||
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 () {
|
||||
describe('GlobalSearch providers', function () {
|
||||
before(async () => {
|
||||
await esArchiver.load('global_search/basic');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('global_search/basic');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('global_search/basic');
|
||||
});
|
||||
|
||||
it('can search for index patterns', async () => {
|
||||
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-*');
|
||||
expect(results[0].score).to.be.greaterThan(0.9);
|
||||
});
|
||||
|
||||
it('can search for visualizations', async () => {
|
||||
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('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('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('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('type:dashboard 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 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');
|
||||
});
|
||||
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 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 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 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue