diff --git a/x-pack/plugins/global_search_bar/kibana.json b/x-pack/plugins/global_search_bar/kibana.json index 2d4ffa34d346f..bf0ae83a0d863 100644 --- a/x-pack/plugins/global_search_bar/kibana.json +++ b/x-pack/plugins/global_search_bar/kibana.json @@ -5,6 +5,6 @@ "server": false, "ui": true, "requiredPlugins": ["globalSearch"], - "optionalPlugins": [], + "optionalPlugins": ["usageCollection"], "configPath": ["xpack", "global_search_bar"] } diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap index b93e27efccaef..ff9d603bc6375 100644 --- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -2,81 +2,30 @@ exports[`SearchBar correctly filters and sorts results 1`] = ` Array [ - Object { - "append": undefined, - "className": "euiSelectableTemplateSitewide__listItem", - "key": "Canvas", - "label": "Canvas", - "meta": Array [ - Object { - "text": "Kibana", - }, - ], - "prepend": undefined, - "title": "Canvas • Kibana", - "url": "/app/test/Canvas", - }, - Object { - "append": undefined, - "className": "euiSelectableTemplateSitewide__listItem", - "key": "Discover", - "label": "Discover", - "meta": Array [ - Object { - "text": "Kibana", - }, - ], - "prepend": undefined, - "title": "Discover • Kibana", - "url": "/app/test/Discover", - }, - Object { - "append": undefined, - "className": "euiSelectableTemplateSitewide__listItem", - "key": "Graph", - "label": "Graph", - "meta": Array [ - Object { - "text": "Kibana", - }, - ], - "prepend": undefined, - "title": "Graph • Kibana", - "url": "/app/test/Graph", - }, + "Canvas • Kibana", + "Discover • Kibana", + "Graph • Kibana", ] `; exports[`SearchBar correctly filters and sorts results 2`] = ` Array [ - Object { - "append": undefined, - "className": "euiSelectableTemplateSitewide__listItem", - "key": "Discover", - "label": "Discover", - "meta": Array [ - Object { - "text": "Kibana", - }, - ], - "prepend": undefined, - "title": "Discover • Kibana", - "url": "/app/test/Discover", - }, - Object { - "append": undefined, - "className": "euiSelectableTemplateSitewide__listItem", - "key": "My Dashboard", - "label": "My Dashboard", - "meta": Array [ - Object { - "text": "Test", - }, - ], - "prepend": undefined, - "title": "My Dashboard • Test", - "url": "/app/test/My Dashboard", - }, + "Discover • Kibana", + "My Dashboard • Test", +] +`; + +exports[`SearchBar only display results from the last search 1`] = ` +Array [ + "Visualize • Kibana", + "Map • Kibana", +] +`; + +exports[`SearchBar only display results from the last search 2`] = ` +Array [ + "Visualize • Kibana", + "Map • Kibana", ] `; 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 3c86c4e70e346..b669499c63f05 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 @@ -15,10 +15,6 @@ import { GlobalSearchBatchedResults, GlobalSearchResult } from '../../../global_ import { globalSearchPluginMock } from '../../../global_search/public/mocks'; import { SearchBar } from './search_bar'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => 'mockId', -})); - type Result = { id: string; type: string } | string; const createResult = (result: Result): GlobalSearchResult => { @@ -74,8 +70,8 @@ describe('SearchBar', () => { ); }; - const getDisplayedOptionsLabel = () => { - return getSelectableProps(component).options.map((option: any) => option.label); + const getDisplayedOptionsTitle = () => { + return getSelectableProps(component).options.map((option: any) => option.title); }; it('correctly filters and sorts results', async () => { @@ -94,6 +90,7 @@ describe('SearchBar', () => { navigateToUrl={applications.navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + trackUiMetric={jest.fn()} /> ); @@ -104,12 +101,12 @@ describe('SearchBar', () => { expect(searchService.find).toHaveBeenCalledTimes(1); expect(searchService.find).toHaveBeenCalledWith('', {}); - expect(getSelectableProps(component).options).toMatchSnapshot(); + expect(getDisplayedOptionsTitle()).toMatchSnapshot(); await simulateTypeChar('d'); update(); - expect(getSelectableProps(component).options).toMatchSnapshot(); + expect(getDisplayedOptionsTitle()).toMatchSnapshot(); expect(searchService.find).toHaveBeenCalledTimes(2); expect(searchService.find).toHaveBeenCalledWith('d', {}); }); @@ -121,6 +118,7 @@ describe('SearchBar', () => { navigateToUrl={applications.navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + trackUiMetric={jest.fn()} /> ); @@ -152,6 +150,7 @@ describe('SearchBar', () => { navigateToUrl={applications.navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + trackUiMetric={jest.fn()} /> ); @@ -163,14 +162,12 @@ describe('SearchBar', () => { await simulateTypeChar('d'); update(); - expect(getDisplayedOptionsLabel().length).toBe(2); - expect(getDisplayedOptionsLabel()).toEqual(expect.arrayContaining(['Visualize', 'Map'])); + expect(getDisplayedOptionsTitle()).toMatchSnapshot(); firstSearchTrigger.next(true); update(); - expect(getDisplayedOptionsLabel().length).toBe(2); - expect(getDisplayedOptionsLabel()).toEqual(expect.arrayContaining(['Visualize', 'Map'])); + expect(getDisplayedOptionsTitle()).toMatchSnapshot(); }); }); 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 ea2271286883d..e73f9d954d5ad 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 @@ -8,27 +8,29 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, - EuiSelectableTemplateSitewide, - EuiSelectableTemplateSitewideOption, - EuiText, + EuiHeaderSectionItemButton, EuiIcon, EuiImage, - EuiHeaderSectionItemButton, EuiSelectableMessage, + EuiSelectableTemplateSitewide, + EuiSelectableTemplateSitewideOption, + EuiText, } from '@elastic/eui'; +import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Subscription } from 'rxjs'; import { ApplicationStart } from 'kibana/public'; -import React, { useCallback, useState, useRef } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import useEvent from 'react-use/lib/useEvent'; import useMountedState from 'react-use/lib/useMountedState'; +import { Subscription } from 'rxjs'; import { GlobalSearchPluginStart, GlobalSearchResult } from '../../../global_search/public'; interface Props { globalSearch: GlobalSearchPluginStart['find']; navigateToUrl: ApplicationStart['navigateToUrl']; + trackUiMetric: (metricType: UiStatsMetricType, eventName: string | string[]) => void; basePathUrl: string; darkMode: boolean; } @@ -66,6 +68,7 @@ const resultToOption = (result: GlobalSearchResult): EuiSelectableTemplateSitewi key: id, label: title, url, + type, }; if (icon) { @@ -81,10 +84,17 @@ const resultToOption = (result: GlobalSearchResult): EuiSelectableTemplateSitewi return option; }; -export function SearchBar({ globalSearch, navigateToUrl, basePathUrl, darkMode }: Props) { +export function SearchBar({ + globalSearch, + navigateToUrl, + trackUiMetric, + basePathUrl, + darkMode, +}: Props) { const isMounted = useMountedState(); const [searchValue, setSearchValue] = useState(''); 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; @@ -109,6 +119,7 @@ export function SearchBar({ globalSearch, navigateToUrl, basePathUrl, darkMode } } let arr: GlobalSearchResult[] = []; + if (searchValue.length !== 0) trackUiMetric(METRIC_TYPE.COUNT, 'search_request'); searchSubscription.current = globalSearch(searchValue, {}).subscribe({ next: ({ results }) => { if (searchValue.length > 0) { @@ -125,9 +136,9 @@ export function SearchBar({ globalSearch, navigateToUrl, basePathUrl, darkMode } setOptions(arr); }, error: () => { - // TODO #74430 - add telemetry to see if errors are happening // Not doing anything on error right now because it'll either just show the previous // results or empty results which is basically what we want anyways + trackUiMetric(METRIC_TYPE.COUNT, 'unhandled_error'); }, complete: () => {}, }); @@ -138,16 +149,31 @@ export function SearchBar({ globalSearch, navigateToUrl, basePathUrl, darkMode } const onKeyDown = (event: KeyboardEvent) => { if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) { + event.preventDefault(); + trackUiMetric(METRIC_TYPE.COUNT, 'shortcut_used'); if (searchRef) { - event.preventDefault(); searchRef.focus(); + } else if (buttonRef) { + (buttonRef.children[0] as HTMLButtonElement).click(); } } }; const onChange = (selected: EuiSelectableTemplateSitewideOption[]) => { // @ts-ignore - ts error is "union type is too complex to express" - const { url } = selected.find(({ checked }) => checked === 'on'); + const { url, type, key } = selected.find(({ checked }) => checked === 'on'); + + if (type === 'application') { + trackUiMetric(METRIC_TYPE.CLICK, [ + 'user_navigated_to_application', + `user_navigated_to_application_${key.toLowerCase().replaceAll(' ', '_')}`, // which application + ]); + } else { + trackUiMetric(METRIC_TYPE.CLICK, [ + 'user_navigated_to_saved_object', + `user_navigated_to_saved_object_${type}`, // which type of saved object + ]); + } navigateToUrl(url); (document.activeElement as HTMLElement).blur(); @@ -211,9 +237,13 @@ export function SearchBar({ globalSearch, navigateToUrl, basePathUrl, darkMode } placeholder: i18n.translate('xpack.globalSearchBar.searchBar.placeholder', { defaultMessage: 'Search Elastic', }), + onFocus: () => { + trackUiMetric(METRIC_TYPE.COUNT, 'search_focus'); + }, }} popoverProps={{ repositionOnScroll: true, + buttonRef: setButtonRef, }} emptyMessage={emptyMessage} noMatchesMessage={emptyMessage} diff --git a/x-pack/plugins/global_search_bar/public/plugin.tsx b/x-pack/plugins/global_search_bar/public/plugin.tsx index 9bc6b824b8716..14ac0935467d7 100644 --- a/x-pack/plugins/global_search_bar/public/plugin.tsx +++ b/x-pack/plugins/global_search_bar/public/plugin.tsx @@ -4,16 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart, Plugin } from 'src/core/public'; -import React from 'react'; +import { UiStatsMetricType } from '@kbn/analytics'; import { I18nProvider } from '@kbn/i18n/react'; -import ReactDOM from 'react-dom'; import { ApplicationStart } from 'kibana/public'; -import { SearchBar } from '../public/components/search_bar'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { CoreStart, Plugin } from 'src/core/public'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { GlobalSearchPluginStart } from '../../global_search/public'; +import { SearchBar } from '../public/components/search_bar'; export interface GlobalSearchBarPluginStartDeps { globalSearch: GlobalSearchPluginStart; + usageCollection: UsageCollectionSetup; } export class GlobalSearchBarPlugin implements Plugin<{}, {}> { @@ -21,7 +24,13 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> { return {}; } - public start(core: CoreStart, { globalSearch }: GlobalSearchBarPluginStartDeps) { + public start(core: CoreStart, { globalSearch, usageCollection }: GlobalSearchBarPluginStartDeps) { + let trackUiMetric = (metricType: UiStatsMetricType, eventName: string | string[]) => {}; + + if (usageCollection) { + trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'global_search_bar'); + } + core.chrome.navControls.registerCenter({ order: 1000, mount: (target) => @@ -30,7 +39,8 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> { globalSearch, core.application.navigateToUrl, core.http.basePath.prepend('/plugins/globalSearchBar/assets/'), - core.uiSettings.get('theme:darkMode') + core.uiSettings.get('theme:darkMode'), + trackUiMetric ), }); return {}; @@ -41,7 +51,8 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> { globalSearch: GlobalSearchPluginStart, navigateToUrl: ApplicationStart['navigateToUrl'], basePathUrl: string, - darkMode: boolean + darkMode: boolean, + trackUiMetric: (metricType: UiStatsMetricType, eventName: string | string[]) => void ) { ReactDOM.render( @@ -50,6 +61,7 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> { navigateToUrl={navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + trackUiMetric={trackUiMetric} /> , targetDomElement