From 77764448c0e459d120cce60dc07fd7a89f107925 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 7 Dec 2020 16:09:59 +0100 Subject: [PATCH] initial draft --- .../public/api.mock.ts | 28 +++++-- .../saved_objects_tagging_oss/public/api.ts | 22 +++++- .../saved_objects_tagging_oss/public/index.ts | 1 + x-pack/plugins/global_search/public/mocks.ts | 1 + x-pack/plugins/global_search/public/plugin.ts | 3 +- .../services/fetch_server_searchable_types.ts | 18 +++++ .../public/services/search_service.mock.ts | 2 + .../public/services/search_service.test.ts | 1 + .../public/services/search_service.ts | 24 ++++++ x-pack/plugins/global_search/public/types.ts | 8 +- x-pack/plugins/global_search/server/mocks.ts | 2 + x-pack/plugins/global_search/server/plugin.ts | 2 + .../server/routes/get_searchable_types.ts | 24 ++++++ .../global_search/server/routes/index.test.ts | 10 ++- .../global_search/server/routes/index.ts | 2 + .../server/services/search_service.mock.ts | 2 + .../server/services/search_service.test.ts | 1 + .../server/services/search_service.ts | 17 ++++ x-pack/plugins/global_search/server/types.ts | 12 ++- .../public/components/search_bar.test.tsx | 6 +- .../public/components/search_bar.tsx | 79 ++++++++++++++----- .../global_search_bar/public/plugin.tsx | 2 +- .../public/suggestions/index.ts | 56 +++++++++++++ .../public/providers/application.ts | 5 +- .../providers/saved_objects/provider.ts | 9 +++ x-pack/plugins/lens/public/search_provider.ts | 1 + .../saved_objects_tagging/public/plugin.ts | 1 + .../public/services/tags/tags_cache.ts | 6 +- 28 files changed, 304 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/global_search/public/services/fetch_server_searchable_types.ts create mode 100644 x-pack/plugins/global_search/server/routes/get_searchable_types.ts create mode 100644 x-pack/plugins/global_search_bar/public/suggestions/index.ts diff --git a/src/plugins/saved_objects_tagging_oss/public/api.mock.ts b/src/plugins/saved_objects_tagging_oss/public/api.mock.ts index 87a3fd8f5b499..1e66a9baa812e 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.mock.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.mock.ts @@ -18,10 +18,10 @@ */ import { ITagsClient } from '../common'; -import { SavedObjectsTaggingApiUi, SavedObjectsTaggingApiUiComponent } from './api'; +import { SavedObjectsTaggingApiUi, SavedObjectsTaggingApiUiComponent, ITagsCache } from './api'; -const createClientMock = (): jest.Mocked => { - const mock = { +const createClientMock = () => { + const mock: jest.Mocked = { create: jest.fn(), get: jest.fn(), getAll: jest.fn(), @@ -32,14 +32,25 @@ const createClientMock = (): jest.Mocked => { return mock; }; +const createCacheMock = () => { + const mock: jest.Mocked = { + getState: jest.fn(), + getState$: jest.fn(), + }; + + return mock; +}; + interface SavedObjectsTaggingApiMock { client: jest.Mocked; + cache: jest.Mocked; ui: SavedObjectsTaggingApiUiMock; } const createApiMock = (): SavedObjectsTaggingApiMock => { - const mock = { + const mock: SavedObjectsTaggingApiMock = { client: createClientMock(), + cache: createCacheMock(), ui: createApiUiMock(), }; @@ -50,8 +61,8 @@ type SavedObjectsTaggingApiUiMock = Omit, components: SavedObjectsTaggingApiUiComponentMock; }; -const createApiUiMock = (): SavedObjectsTaggingApiUiMock => { - const mock = { +const createApiUiMock = () => { + const mock: SavedObjectsTaggingApiUiMock = { components: createApiUiComponentsMock(), // TS is very picky with type guards hasTagDecoration: jest.fn() as any, @@ -69,8 +80,8 @@ const createApiUiMock = (): SavedObjectsTaggingApiUiMock => { type SavedObjectsTaggingApiUiComponentMock = jest.Mocked; -const createApiUiComponentsMock = (): SavedObjectsTaggingApiUiComponentMock => { - const mock = { +const createApiUiComponentsMock = () => { + const mock: SavedObjectsTaggingApiUiComponentMock = { TagList: jest.fn(), TagSelector: jest.fn(), SavedObjectSaveModalTagSelector: jest.fn(), @@ -82,6 +93,7 @@ const createApiUiComponentsMock = (): SavedObjectsTaggingApiUiComponentMock => { export const taggingApiMock = { create: createApiMock, createClient: createClientMock, + createCache: createCacheMock, createUi: createApiUiMock, createComponents: createApiUiComponentsMock, }; diff --git a/src/plugins/saved_objects_tagging_oss/public/api.ts b/src/plugins/saved_objects_tagging_oss/public/api.ts index 81f7cc9326a77..0b8e6ceb20d8e 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.ts @@ -17,22 +17,42 @@ * under the License. */ +import { Observable } from 'rxjs'; import { SearchFilterConfig, EuiTableFieldDataColumnType } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import { SavedObject, SavedObjectReference } from '../../../core/types'; import { SavedObjectsFindOptionsReference } from '../../../core/public'; import { SavedObject as SavedObjectClass } from '../../saved_objects/public'; import { TagDecoratedSavedObject } from './decorator'; -import { ITagsClient } from '../common'; +import { ITagsClient, Tag } from '../common'; /** * @public */ export interface SavedObjectsTaggingApi { + /** + * The client to perform tag-related operations on the server-side + */ client: ITagsClient; + /** + * A client-side auto-refreshing cache of the existing tags. Can be used + * to synchronously access the list of tags. + */ + cache: ITagsCache; + /** + * UI API to use to add tagging capabilities to an application + */ ui: SavedObjectsTaggingApiUi; } +/** + * @public + */ +export interface ITagsCache { + getState(): Tag[]; + getState$(): Observable; +} + /** * @public */ diff --git a/src/plugins/saved_objects_tagging_oss/public/index.ts b/src/plugins/saved_objects_tagging_oss/public/index.ts index bc824621830d2..ef3087f944add 100644 --- a/src/plugins/saved_objects_tagging_oss/public/index.ts +++ b/src/plugins/saved_objects_tagging_oss/public/index.ts @@ -26,6 +26,7 @@ export { SavedObjectsTaggingApi, SavedObjectsTaggingApiUi, SavedObjectsTaggingApiUiComponent, + ITagsCache, TagListComponentProps, TagSelectorComponentProps, GetSearchBarFilterOptions, diff --git a/x-pack/plugins/global_search/public/mocks.ts b/x-pack/plugins/global_search/public/mocks.ts index 97dc01e92dbfe..8b0bfec66f61d 100644 --- a/x-pack/plugins/global_search/public/mocks.ts +++ b/x-pack/plugins/global_search/public/mocks.ts @@ -20,6 +20,7 @@ const createStartMock = (): jest.Mocked => { return { find: searchMock.find, + getSearchableTypes: searchMock.getSearchableTypes, }; }; diff --git a/x-pack/plugins/global_search/public/plugin.ts b/x-pack/plugins/global_search/public/plugin.ts index 6af8ec32a581d..a861911d935b4 100644 --- a/x-pack/plugins/global_search/public/plugin.ts +++ b/x-pack/plugins/global_search/public/plugin.ts @@ -45,13 +45,14 @@ export class GlobalSearchPlugin start({ http }: CoreStart, { licensing }: GlobalSearchPluginStartDeps) { this.licenseChecker = new LicenseChecker(licensing.license$); - const { find } = this.searchService.start({ + const { find, getSearchableTypes } = this.searchService.start({ http, licenseChecker: this.licenseChecker, }); return { find, + getSearchableTypes, }; } diff --git a/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.ts b/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.ts new file mode 100644 index 0000000000000..c4a0724991870 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.ts @@ -0,0 +1,18 @@ +/* + * 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 { HttpStart } from 'src/core/public'; + +interface ServerSearchableTypesResponse { + types: string[]; +} + +export const fetchServerSearchableTypes = async (http: HttpStart) => { + const { types } = await http.get( + '/internal/global_search/searchable_types' + ); + return types; +}; diff --git a/x-pack/plugins/global_search/public/services/search_service.mock.ts b/x-pack/plugins/global_search/public/services/search_service.mock.ts index eca69148288b9..54ae9ee89a3be 100644 --- a/x-pack/plugins/global_search/public/services/search_service.mock.ts +++ b/x-pack/plugins/global_search/public/services/search_service.mock.ts @@ -16,8 +16,10 @@ const createSetupMock = (): jest.Mocked => { const createStartMock = (): jest.Mocked => { const mock = { find: jest.fn(), + getSearchableTypes: jest.fn(), }; mock.find.mockReturnValue(of({ results: [] })); + mock.getSearchableTypes.mockReturnValue(of([])); return mock; }; diff --git a/x-pack/plugins/global_search/public/services/search_service.test.ts b/x-pack/plugins/global_search/public/services/search_service.test.ts index 419ad847d6c29..88139035cc677 100644 --- a/x-pack/plugins/global_search/public/services/search_service.test.ts +++ b/x-pack/plugins/global_search/public/services/search_service.test.ts @@ -45,6 +45,7 @@ describe('SearchService', () => { ): jest.Mocked => ({ id, find: jest.fn().mockImplementation((term, options, context) => source), + getSearchableTypes: jest.fn().mockReturnValue(['test-type']), }); const expectedResult = (id: string) => expect.objectContaining({ id }); diff --git a/x-pack/plugins/global_search/public/services/search_service.ts b/x-pack/plugins/global_search/public/services/search_service.ts index 64bd2fd6c930f..015143d34886f 100644 --- a/x-pack/plugins/global_search/public/services/search_service.ts +++ b/x-pack/plugins/global_search/public/services/search_service.ts @@ -6,6 +6,7 @@ import { merge, Observable, timer, throwError } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; +import { uniq } from 'lodash'; import { duration } from 'moment'; import { i18n } from '@kbn/i18n'; import { HttpStart } from 'src/core/public'; @@ -24,6 +25,7 @@ import { GlobalSearchClientConfigType } from '../config'; import { GlobalSearchFindOptions } from './types'; import { getDefaultPreference } from './utils'; import { fetchServerResults } from './fetch_server_results'; +import { fetchServerSearchableTypes } from './fetch_server_searchable_types'; /** @public */ export interface SearchServiceSetup { @@ -75,6 +77,11 @@ export interface SearchServiceStart { params: GlobalSearchFindParams, options: GlobalSearchFindOptions ): Observable; + + /** + * Returns all the searchable types registered by the underlying result providers. + */ + getSearchableTypes(): Promise; } interface SetupDeps { @@ -96,6 +103,7 @@ export class SearchService { private http?: HttpStart; private maxProviderResults = defaultMaxProviderResults; private licenseChecker?: ILicenseChecker; + private serverTypes?: string[]; setup({ config, maxProviderResults = defaultMaxProviderResults }: SetupDeps): SearchServiceSetup { this.config = config; @@ -118,9 +126,25 @@ export class SearchService { return { find: (params, options) => this.performFind(params, options), + getSearchableTypes: () => this.getSearchableTypes(), }; } + private async getSearchableTypes() { + const providerTypes = ( + await Promise.all( + [...this.providers.values()].map((provider) => provider.getSearchableTypes()) + ) + ).flat(); + + // only need to fetch from server once + if (!this.serverTypes) { + this.serverTypes = await fetchServerSearchableTypes(this.http!); + } + + return uniq([...providerTypes, ...this.serverTypes]); + } + private performFind(params: GlobalSearchFindParams, options: GlobalSearchFindOptions) { const licenseState = this.licenseChecker!.getState(); if (!licenseState.valid) { diff --git a/x-pack/plugins/global_search/public/types.ts b/x-pack/plugins/global_search/public/types.ts index 2707a2fded222..7235347d4aa38 100644 --- a/x-pack/plugins/global_search/public/types.ts +++ b/x-pack/plugins/global_search/public/types.ts @@ -13,7 +13,7 @@ import { import { SearchServiceSetup, SearchServiceStart } from './services'; export type GlobalSearchPluginSetup = Pick; -export type GlobalSearchPluginStart = Pick; +export type GlobalSearchPluginStart = Pick; /** * GlobalSearch result provider, to be registered using the {@link GlobalSearchPluginSetup | global search API} @@ -44,4 +44,10 @@ export interface GlobalSearchResultProvider { search: GlobalSearchProviderFindParams, options: GlobalSearchProviderFindOptions ): Observable; + + /** + * Method that should return all the possible {@link GlobalSearchProviderResult.type | type} of results that + * this provider can return. + */ + getSearchableTypes: () => string[] | Promise; } diff --git a/x-pack/plugins/global_search/server/mocks.ts b/x-pack/plugins/global_search/server/mocks.ts index e7c133edf95c8..88be7f6e861a1 100644 --- a/x-pack/plugins/global_search/server/mocks.ts +++ b/x-pack/plugins/global_search/server/mocks.ts @@ -26,12 +26,14 @@ const createStartMock = (): jest.Mocked => { return { find: searchMock.find, + getSearchableTypes: searchMock.getSearchableTypes, }; }; const createRouteHandlerContextMock = (): jest.Mocked => { const handlerContextMock = { find: jest.fn(), + getSearchableTypes: jest.fn(), }; handlerContextMock.find.mockReturnValue(of([])); diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts index 87e7f96b34c0c..9d6844dde50f0 100644 --- a/x-pack/plugins/global_search/server/plugin.ts +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -59,6 +59,7 @@ export class GlobalSearchPlugin core.http.registerRouteHandlerContext('globalSearch', (_, req) => { return { find: (term, options) => this.searchServiceStart!.find(term, options, req), + getSearchableTypes: () => this.searchServiceStart!.getSearchableTypes(req), }; }); @@ -75,6 +76,7 @@ export class GlobalSearchPlugin }); return { find: this.searchServiceStart.find, + getSearchableTypes: this.searchServiceStart.getSearchableTypes, }; } diff --git a/x-pack/plugins/global_search/server/routes/get_searchable_types.ts b/x-pack/plugins/global_search/server/routes/get_searchable_types.ts new file mode 100644 index 0000000000000..f9cc69e4a28ae --- /dev/null +++ b/x-pack/plugins/global_search/server/routes/get_searchable_types.ts @@ -0,0 +1,24 @@ +/* + * 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 { IRouter } from 'src/core/server'; + +export const registerInternalSearchableTypesRoute = (router: IRouter) => { + router.get( + { + path: '/internal/global_search/searchable_types', + validate: false, + }, + async (ctx, req, res) => { + const types = await ctx.globalSearch!.getSearchableTypes(); + return res.ok({ + body: { + types, + }, + }); + } + ); +}; diff --git a/x-pack/plugins/global_search/server/routes/index.test.ts b/x-pack/plugins/global_search/server/routes/index.test.ts index 64675bc13cb1c..1111f01d13055 100644 --- a/x-pack/plugins/global_search/server/routes/index.test.ts +++ b/x-pack/plugins/global_search/server/routes/index.test.ts @@ -14,7 +14,6 @@ describe('registerRoutes', () => { registerRoutes(router); expect(router.post).toHaveBeenCalledTimes(1); - expect(router.post).toHaveBeenCalledWith( expect.objectContaining({ path: '/internal/global_search/find', @@ -22,7 +21,14 @@ describe('registerRoutes', () => { expect.any(Function) ); - expect(router.get).toHaveBeenCalledTimes(0); + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/internal/global_search/searchable_types', + }), + expect.any(Function) + ); + expect(router.delete).toHaveBeenCalledTimes(0); expect(router.put).toHaveBeenCalledTimes(0); }); diff --git a/x-pack/plugins/global_search/server/routes/index.ts b/x-pack/plugins/global_search/server/routes/index.ts index 7840b95614993..0eeb443b72b53 100644 --- a/x-pack/plugins/global_search/server/routes/index.ts +++ b/x-pack/plugins/global_search/server/routes/index.ts @@ -6,7 +6,9 @@ import { IRouter } from 'src/core/server'; import { registerInternalFindRoute } from './find'; +import { registerInternalSearchableTypesRoute } from './get_searchable_types'; export const registerRoutes = (router: IRouter) => { registerInternalFindRoute(router); + registerInternalSearchableTypesRoute(router); }; diff --git a/x-pack/plugins/global_search/server/services/search_service.mock.ts b/x-pack/plugins/global_search/server/services/search_service.mock.ts index eca69148288b9..54ae9ee89a3be 100644 --- a/x-pack/plugins/global_search/server/services/search_service.mock.ts +++ b/x-pack/plugins/global_search/server/services/search_service.mock.ts @@ -16,8 +16,10 @@ const createSetupMock = (): jest.Mocked => { const createStartMock = (): jest.Mocked => { const mock = { find: jest.fn(), + getSearchableTypes: jest.fn(), }; mock.find.mockReturnValue(of({ results: [] })); + mock.getSearchableTypes.mockReturnValue(of([])); return mock; }; diff --git a/x-pack/plugins/global_search/server/services/search_service.test.ts b/x-pack/plugins/global_search/server/services/search_service.test.ts index c8d656a524e94..a79aba5e8b6fc 100644 --- a/x-pack/plugins/global_search/server/services/search_service.test.ts +++ b/x-pack/plugins/global_search/server/services/search_service.test.ts @@ -40,6 +40,7 @@ describe('SearchService', () => { ): jest.Mocked => ({ id, find: jest.fn().mockImplementation((term, options, context) => source), + getSearchableTypes: jest.fn().mockReturnValue(['test-type']), }); const expectedResult = (id: string) => expect.objectContaining({ id }); diff --git a/x-pack/plugins/global_search/server/services/search_service.ts b/x-pack/plugins/global_search/server/services/search_service.ts index 9ea62abac704c..88250820861a6 100644 --- a/x-pack/plugins/global_search/server/services/search_service.ts +++ b/x-pack/plugins/global_search/server/services/search_service.ts @@ -6,6 +6,7 @@ import { Observable, timer, merge, throwError } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; +import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, CoreStart, IBasePath } from 'src/core/server'; import { @@ -71,6 +72,11 @@ export interface SearchServiceStart { options: GlobalSearchFindOptions, request: KibanaRequest ): Observable; + + /** + * Returns all the searchable types registered by the underlying result providers. + */ + getSearchableTypes(request: KibanaRequest): Promise; } interface SetupDeps { @@ -119,9 +125,20 @@ export class SearchService { this.contextFactory = getContextFactory(core); return { find: (params, options, request) => this.performFind(params, options, request), + getSearchableTypes: (request) => this.getSearchableTypes(request), }; } + private async getSearchableTypes(request: KibanaRequest) { + const context = this.contextFactory!(request); + const allTypes = ( + await Promise.all( + [...this.providers.values()].map((provider) => provider.getSearchableTypes(context)) + ) + ).flat(); + return uniq(allTypes); + } + private performFind( params: GlobalSearchFindParams, options: GlobalSearchFindOptions, diff --git a/x-pack/plugins/global_search/server/types.ts b/x-pack/plugins/global_search/server/types.ts index 0878a965ea8c3..48c40fdb66e13 100644 --- a/x-pack/plugins/global_search/server/types.ts +++ b/x-pack/plugins/global_search/server/types.ts @@ -22,7 +22,7 @@ import { import { SearchServiceSetup, SearchServiceStart } from './services'; export type GlobalSearchPluginSetup = Pick; -export type GlobalSearchPluginStart = Pick; +export type GlobalSearchPluginStart = Pick; /** * globalSearch route handler context. @@ -37,6 +37,10 @@ export interface RouteHandlerGlobalSearchContext { params: GlobalSearchFindParams, options: GlobalSearchFindOptions ): Observable; + /** + * See {@link SearchServiceStart.getSearchableTypes | the getSearchableTypes API} + */ + getSearchableTypes: () => Promise; } /** @@ -114,4 +118,10 @@ export interface GlobalSearchResultProvider { options: GlobalSearchProviderFindOptions, context: GlobalSearchProviderContext ): Observable; + + /** + * Method that should return all the possible {@link GlobalSearchProviderResult.type | type} of results that + * this provider can return. + */ + getSearchableTypes: (context: GlobalSearchProviderContext) => string[] | Promise; } diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx index 5ba00c293d213..9e139c2a02fbc 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -86,7 +86,7 @@ describe('SearchBar', () => { component = mountWithIntl( { it('supports keyboard shortcuts', () => { mountWithIntl( { component = mountWithIntl( void; taggingApi?: SavedObjectTaggingPluginStart; @@ -42,16 +43,19 @@ interface Props { darkMode: boolean; } -const clearField = (field: HTMLInputElement) => { +const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; + +const setFieldValue = (field: HTMLInputElement, value: string) => { const nativeInputValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); const nativeInputValueSetter = nativeInputValue ? nativeInputValue.set : undefined; if (nativeInputValueSetter) { - nativeInputValueSetter.call(field, ''); + nativeInputValueSetter.call(field, value); } - field.dispatchEvent(new Event('change')); }; +const clearField = (field: HTMLInputElement) => setFieldValue(field, ''); + const cleanMeta = (str: string) => (str.charAt(0).toUpperCase() + str.slice(1)).replace(/-/g, ' '); const blurEvent = new FocusEvent('blur'); @@ -91,6 +95,18 @@ const resultToOption = (result: GlobalSearchResult): EuiSelectableTemplateSitewi return option; }; +const suggestionToOption = (suggestion: SearchSuggestion): EuiSelectableTemplateSitewideOption => { + const { key, label, description, icon, suggestedSearch } = suggestion; + return { + key, + label, + type: '__suggestion__', + icon: { type: icon }, + suggestion: suggestedSearch, + meta: [{ text: description }], + }; +}; + export function SearchBar({ globalSearch, taggingApi, @@ -104,16 +120,34 @@ export function SearchBar({ const [searchRef, setSearchRef] = useState(null); const [buttonRef, setButtonRef] = useState(null); const searchSubscription = useRef(null); - const [options, _setOptions] = useState([] as EuiSelectableTemplateSitewideOption[]); - const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; + const [options, _setOptions] = useState([]); + const [searchableTypes, setSearchableTypes] = useState([]); + + useEffect(() => { + const fetch = async () => { + const types = await globalSearch.getSearchableTypes(); + setSearchableTypes(types); + }; + fetch(); + }, [globalSearch]); + + const loadSuggestions = useCallback( + (searchTerm: string) => { + return getSuggestions({ + searchTerm, + searchableTypes, + tagCache: taggingApi?.cache, + }); + }, + [taggingApi, searchableTypes] + ); const setOptions = useCallback( - (_options: GlobalSearchResult[]) => { + (_options: GlobalSearchResult[], suggestions: SearchSuggestion[]) => { if (!isMounted()) { return; } - - _setOptions(_options.map(resultToOption)); + _setOptions([...suggestions.map(suggestionToOption), ..._options.map(resultToOption)]); }, [isMounted, _setOptions] ); @@ -126,7 +160,9 @@ export function SearchBar({ searchSubscription.current = null; } - let arr: GlobalSearchResult[] = []; + const suggestions = loadSuggestions(searchValue); + + let aggregatedResults: GlobalSearchResult[] = []; if (searchValue.length !== 0) { trackUiMetric(METRIC_TYPE.COUNT, 'search_request'); } @@ -144,20 +180,20 @@ export function SearchBar({ tags: tagIds, }; - searchSubscription.current = globalSearch(searchParams, {}).subscribe({ + searchSubscription.current = globalSearch.find(searchParams, {}).subscribe({ next: ({ results }) => { if (searchValue.length > 0) { - arr = [...results, ...arr].sort(sortByScore); - setOptions(arr); + aggregatedResults = [...results, ...aggregatedResults].sort(sortByScore); + setOptions(aggregatedResults, suggestions); return; } // if searchbar is empty, filter to only applications and sort alphabetically results = results.filter(({ type }: GlobalSearchResult) => type === 'application'); - arr = [...results, ...arr].sort(sortByTitle); + aggregatedResults = [...results, ...aggregatedResults].sort(sortByTitle); - setOptions(arr); + setOptions(aggregatedResults, suggestions); }, error: () => { // Not doing anything on error right now because it'll either just show the previous @@ -168,7 +204,7 @@ export function SearchBar({ }); }, 350, - [searchValue] + [searchValue, loadSuggestions] ); const onKeyDown = (event: KeyboardEvent) => { @@ -190,7 +226,14 @@ export function SearchBar({ } // @ts-ignore - ts error is "union type is too complex to express" - const { url, type } = selected; + const { url, type, suggestion } = selected; + + // TODO doc + if (type === '__suggestion__') { + setFieldValue(searchRef!, suggestion); + setSearchValue(suggestion); + return; + } // errors in tracking should not prevent selection behavior try { diff --git a/x-pack/plugins/global_search_bar/public/plugin.tsx b/x-pack/plugins/global_search_bar/public/plugin.tsx index 0d17bf4612737..80111e7746a75 100644 --- a/x-pack/plugins/global_search_bar/public/plugin.tsx +++ b/x-pack/plugins/global_search_bar/public/plugin.tsx @@ -70,7 +70,7 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> { ReactDOM.render( { + const results: SearchSuggestion[] = []; + + const trimmedTerm = searchTerm.trim(); + + if (searchableTypes.includes(trimmedTerm)) { + results.push({ + key: '__type__suggestion__', + label: `type: ${trimmedTerm}`, + icon: 'tag', + description: 'Filter by type', + suggestedSearch: `type:${searchTerm}`, // TODO: escape if necessary + }); + } + + if (tagCache && searchTerm) { + const matchingTag = tagCache.getState().find((tag) => tag.name === trimmedTerm); + if (matchingTag) { + results.push({ + key: '__tag__suggestion__', + label: `tag: ${matchingTag.name}`, + icon: 'tag', + description: 'Filter by tag name', + suggestedSearch: `tag:${searchTerm}`, // TODO: escape if necessary + }); + } + } + + return results; +}; diff --git a/x-pack/plugins/global_search_providers/public/providers/application.ts b/x-pack/plugins/global_search_providers/public/providers/application.ts index fd6eb0dc1878b..5b4c58161c0ae 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.ts @@ -10,6 +10,8 @@ import { ApplicationStart } from 'src/core/public'; import { GlobalSearchResultProvider } from '../../../global_search/public'; import { getAppResults } from './get_app_results'; +const applicationType = 'application'; + export const createApplicationResultProvider = ( applicationPromise: Promise ): GlobalSearchResultProvider => { @@ -27,7 +29,7 @@ export const createApplicationResultProvider = ( return { id: 'application', find: ({ term, types, tags }, { aborted$, maxResults }) => { - if (tags || (types && !types.includes('application'))) { + if (tags || (types && !types.includes(applicationType))) { return of([]); } return searchableApps$.pipe( @@ -39,5 +41,6 @@ export const createApplicationResultProvider = ( }) ); }, + getSearchableTypes: () => [applicationType], }; }; diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts index 3e2c42e7896fd..dfd6da87402d1 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts @@ -51,6 +51,15 @@ export const createSavedObjectsResultProvider = (): GlobalSearchResultProvider = map(([res, cap]) => mapToResults(res.saved_objects, typeRegistry, cap)) ); }, + getSearchableTypes: ({ core }) => { + const { + savedObjects: { typeRegistry }, + } = core; + return typeRegistry + .getVisibleTypes() + .filter((type) => type.management?.defaultSearchField && type.management?.getInAppUrl) + .map((type) => type.name); + }, }; }; diff --git a/x-pack/plugins/lens/public/search_provider.ts b/x-pack/plugins/lens/public/search_provider.ts index 02b7900a4c003..55454b54dde79 100644 --- a/x-pack/plugins/lens/public/search_provider.ts +++ b/x-pack/plugins/lens/public/search_provider.ts @@ -79,4 +79,5 @@ export const getSearchProvider: ( }) ); }, + getSearchableTypes: () => ['application'], }); diff --git a/x-pack/plugins/saved_objects_tagging/public/plugin.ts b/x-pack/plugins/saved_objects_tagging/public/plugin.ts index a8614f74125f4..70ba6c86e04cb 100644 --- a/x-pack/plugins/saved_objects_tagging/public/plugin.ts +++ b/x-pack/plugins/saved_objects_tagging/public/plugin.ts @@ -81,6 +81,7 @@ export class SavedObjectTaggingPlugin return { client: this.tagClient, + cache: this.tagCache, ui: getUiApi({ cache: this.tagCache, client: this.tagClient, diff --git a/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts b/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts index 712b4665f32ef..0df62eb600428 100644 --- a/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts +++ b/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts @@ -7,12 +7,10 @@ import { Duration } from 'moment'; import { Observable, BehaviorSubject, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { ITagsCache } from '../../../../../../src/plugins/saved_objects_tagging_oss/public'; import { Tag, TagAttributes } from '../../../common/types'; -export interface ITagsCache { - getState(): Tag[]; - getState$(): Observable; -} +export { ITagsCache }; export interface ITagsChangeListener { onDelete: (id: string) => void;