Skip to content

Commit

Permalink
Navigational search UI metrics (elastic#79238)
Browse files Browse the repository at this point in the history
  • Loading branch information
Michail Yasonik authored Oct 5, 2020
1 parent 96d3b77 commit b9613af
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 100 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/global_search_bar/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"server": false,
"ui": true,
"requiredPlugins": ["globalSearch"],
"optionalPlugins": [],
"optionalPlugins": ["usageCollection"],
"configPath": ["xpack", "global_search_bar"]
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -94,6 +90,7 @@ describe('SearchBar', () => {
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
);

Expand All @@ -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', {});
});
Expand All @@ -121,6 +118,7 @@ describe('SearchBar', () => {
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
);

Expand Down Expand Up @@ -152,6 +150,7 @@ describe('SearchBar', () => {
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
);

Expand All @@ -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();
});
});
50 changes: 40 additions & 10 deletions x-pack/plugins/global_search_bar/public/components/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -66,6 +68,7 @@ const resultToOption = (result: GlobalSearchResult): EuiSelectableTemplateSitewi
key: id,
label: title,
url,
type,
};

if (icon) {
Expand All @@ -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<string>('');
const [searchRef, setSearchRef] = useState<HTMLInputElement | null>(null);
const [buttonRef, setButtonRef] = useState<HTMLDivElement | null>(null);
const searchSubscription = useRef<Subscription | null>(null);
const [options, _setOptions] = useState([] as EuiSelectableTemplateSitewideOption[]);
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
Expand All @@ -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) {
Expand All @@ -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: () => {},
});
Expand All @@ -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();
Expand Down Expand Up @@ -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}
Expand Down
26 changes: 19 additions & 7 deletions x-pack/plugins/global_search_bar/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,33 @@
* 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<{}, {}> {
public async setup() {
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) =>
Expand All @@ -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 {};
Expand All @@ -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(
<I18nProvider>
Expand All @@ -50,6 +61,7 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> {
navigateToUrl={navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={trackUiMetric}
/>
</I18nProvider>,
targetDomElement
Expand Down

0 comments on commit b9613af

Please sign in to comment.