From 6ebffef9c54c58a78ebcc8deafa4d4445bf218d3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 23 Sep 2020 17:53:21 +0200 Subject: [PATCH] [Lens] Add search provider for global search (#77448) --- x-pack/plugins/lens/common/constants.ts | 4 +- x-pack/plugins/lens/kibana.json | 2 +- x-pack/plugins/lens/public/plugin.ts | 18 +++++ x-pack/plugins/lens/public/search_provider.ts | 79 +++++++++++++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/lens/public/search_provider.ts diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index ea2331a57774..9099554cb8c7 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -17,6 +17,6 @@ export function getEditPath(id: string) { return `#/edit/${encodeURIComponent(id)}`; } -export function getFullPath(id: string) { - return `/app/${PLUGIN_ID}${getEditPath(id)}`; +export function getFullPath(id?: string) { + return `/app/${PLUGIN_ID}${id ? getEditPath(id) : getBasePath()}`; } diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 67d9d5ef6448..f5fba766e60e 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -13,7 +13,7 @@ "dashboard", "charts" ], - "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"], + "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions", "globalSearch"], "configPath": ["xpack", "lens"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable"] diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index f9c63f54d671..60c7011d5530 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -11,6 +11,7 @@ import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/publ import { VisualizationsSetup } from 'src/plugins/visualizations/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { UrlForwardingSetup } from 'src/plugins/url_forwarding/public'; +import { GlobalSearchPluginSetup } from '../../global_search/public'; import { ChartsPluginSetup } from '../../../../src/plugins/charts/public'; import { EditorFrameService } from './editor_frame_service'; import { @@ -31,6 +32,7 @@ import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; +import { getSearchProvider } from './search_provider'; import './index.scss'; @@ -41,6 +43,7 @@ export interface LensPluginSetupDependencies { embeddable?: EmbeddableSetup; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + globalSearch?: GlobalSearchPluginSetup; } export interface LensPluginStartDependencies { @@ -78,6 +81,7 @@ export class LensPlugin { embeddable, visualizations, charts, + globalSearch, }: LensPluginSetupDependencies ) { const editorFrameSetupInterface = this.editorFrameService.setup(core, { @@ -116,6 +120,20 @@ export class LensPlugin { }, }); + if (globalSearch) { + globalSearch.registerResultProvider( + getSearchProvider( + core.getStartServices().then( + ([ + { + application: { capabilities }, + }, + ]) => capabilities + ) + ) + ); + } + urlForwarding.forwardApp('lens', 'lens'); } diff --git a/x-pack/plugins/lens/public/search_provider.ts b/x-pack/plugins/lens/public/search_provider.ts new file mode 100644 index 000000000000..c19e7970b45a --- /dev/null +++ b/x-pack/plugins/lens/public/search_provider.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import levenshtein from 'js-levenshtein'; +import { ApplicationStart } from 'kibana/public'; +import { from } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { GlobalSearchResultProvider } from '../../global_search/public'; +import { getFullPath } from '../common'; + +/** + * Global search provider adding a Lens entry. + * This is necessary because Lens does not show up in the nav bar and is filtered out by the + * default app provider. + * + * It is inlining the same search term matching logic as the application search provider. + * + * TODO: This is a workaround and can be removed once there is a generic way to register sub features + * of apps. In this case, Lens should be considered a feature of Visualize. + */ +export const getSearchProvider: ( + uiCapabilities: Promise +) => GlobalSearchResultProvider = (uiCapabilities) => ({ + id: 'lens', + find: (term) => { + return from( + uiCapabilities.then(({ navLinks: { visualize: visualizeNavLink } }) => { + if (!visualizeNavLink) { + return []; + } + const title = i18n.translate('xpack.lens.searchTitle', { + defaultMessage: 'Lens: create visualizations', + description: 'Lens is a product name and should not be translated', + }); + const searchableTitle = title.toLowerCase(); + + term = term.toLowerCase(); + let score = 0; + + // shortcuts to avoid calculating the distance when there is an exact match somewhere. + if (searchableTitle === term) { + score = 100; + } else if (searchableTitle.startsWith(term)) { + score = 90; + } else if (searchableTitle.includes(term)) { + score = 75; + } else { + const length = Math.max(term.length, searchableTitle.length); + const distance = levenshtein(term, searchableTitle); + + // maximum lev distance is length, we compute the match ratio (lower distance is better) + const ratio = Math.floor((1 - distance / length) * 100); + if (ratio >= 60) { + score = ratio; + } + } + if (score === 0) return []; + return [ + { + id: 'lens', + title, + type: 'application', + icon: 'logoKibana', + meta: { + categoryId: DEFAULT_APP_CATEGORIES.kibana.id, + categoryLabel: DEFAULT_APP_CATEGORIES.kibana.label, + }, + score, + url: getFullPath(), + }, + ]; + }) + ); + }, +});