[Global search] Prevent unnecessary /internal/global_search/find http call at startup (#112535)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2021-10-28 15:26:28 +02:00 committed by GitHub
parent aba80059ce
commit 9e7f1c8e35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 148 additions and 175 deletions

View file

@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchBar correctly filters and sorts results 1`] = `
Array [
"Canvas • Kibana",
"Discover • Kibana",
"Graph • Kibana",
]
`;
exports[`SearchBar correctly filters and sorts results 2`] = `
Array [
"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",
]
`;
exports[`SearchBar supports keyboard shortcuts 1`] = `
<input
aria-activedescendant=""
aria-haspopup="listbox"
aria-label="Search Elastic"
autocomplete="off"
class="euiFieldSearch euiFieldSearch--fullWidth euiFieldSearch--compressed euiSelectableSearch euiSelectableTemplateSitewide__search kbnSearchBar"
data-test-subj="nav-search-input"
placeholder="Search Elastic"
type="search"
value=""
/>
`;

View file

@ -6,15 +6,21 @@
*/
import React from 'react';
import { waitFor, act } from '@testing-library/react';
import { ReactWrapper } from 'enzyme';
import { act, render, screen, fireEvent } from '@testing-library/react';
import { of, BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { mountWithIntl } from '@kbn/test/jest';
import { applicationServiceMock } from '../../../../../src/core/public/mocks';
import { globalSearchPluginMock } from '../../../global_search/public/mocks';
import { GlobalSearchBatchedResults, GlobalSearchResult } from '../../../global_search/public';
import { SearchBar } from './search_bar';
import { __IntlProvider as IntlProvider } from '@kbn/i18n/react';
jest.mock(
'react-virtualized-auto-sizer',
() =>
({ children }: any) =>
children({ height: 600, width: 600 })
);
type Result = { id: string; type: string } | string;
@ -36,9 +42,7 @@ const createResult = (result: Result): GlobalSearchResult => {
const createBatch = (...results: Result[]): GlobalSearchBatchedResults => ({
results: results.map(createResult),
});
const getSelectableProps: any = (component: any) => component.find('EuiSelectable').props();
const getSearchProps: any = (component: any) => component.find('EuiFieldSearch').props();
jest.useFakeTimers();
describe('SearchBar', () => {
let searchService: ReturnType<typeof globalSearchPluginMock.createStartContract>;
@ -46,31 +50,37 @@ describe('SearchBar', () => {
const basePathUrl = '/plugins/globalSearchBar/assets/';
const darkMode = false;
let component: ReactWrapper<any>;
beforeEach(() => {
applications = applicationServiceMock.createStartContract();
searchService = globalSearchPluginMock.createStartContract();
jest.useFakeTimers();
});
const triggerFocus = () => {
component.find('input[data-test-subj="nav-search-input"]').simulate('focus');
};
const update = () => {
act(() => {
jest.runAllTimers();
});
component.update();
};
const simulateTypeChar = async (text: string) => {
await waitFor(() => getSearchProps(component).onInput({ currentTarget: { value: text } }));
const focusAndUpdate = async () => {
await act(async () => {
(await screen.findByTestId('nav-search-input')).focus();
jest.runAllTimers();
});
};
const getDisplayedOptionsTitle = () => {
return getSelectableProps(component).options.map((option: any) => option.title);
const simulateTypeChar = (text: string) => {
fireEvent.input(screen.getByTestId('nav-search-input'), { target: { value: text } });
act(() => {
jest.runAllTimers();
});
};
const assertSearchResults = async (list: string[]) => {
for (let i = 0; i < list.length; i++) {
expect(await screen.findByTitle(list[i])).toBeInTheDocument();
}
expect(await screen.findAllByTestId('nav-search-option')).toHaveLength(list.length);
};
it('correctly filters and sorts results', async () => {
@ -83,53 +93,52 @@ describe('SearchBar', () => {
)
.mockReturnValueOnce(of(createBatch('Discover', { id: 'My Dashboard', type: 'test' })));
component = mountWithIntl(
<SearchBar
globalSearch={searchService}
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
render(
<IntlProvider locale="en">
<SearchBar
globalSearch={searchService}
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
</IntlProvider>
);
expect(searchService.find).toHaveBeenCalledTimes(0);
triggerFocus();
update();
await focusAndUpdate();
expect(searchService.find).toHaveBeenCalledTimes(1);
expect(searchService.find).toHaveBeenCalledWith({}, {});
expect(getDisplayedOptionsTitle()).toMatchSnapshot();
await assertSearchResults(['Canvas • Kibana', 'Discover • Kibana', 'Graph • Kibana']);
await simulateTypeChar('d');
update();
simulateTypeChar('d');
expect(getDisplayedOptionsTitle()).toMatchSnapshot();
await assertSearchResults(['Discover • Kibana', 'My Dashboard • Test']);
expect(searchService.find).toHaveBeenCalledTimes(2);
expect(searchService.find).toHaveBeenCalledWith({ term: 'd' }, {});
expect(searchService.find).toHaveBeenLastCalledWith({ term: 'd' }, {});
});
it('supports keyboard shortcuts', () => {
mountWithIntl(
<SearchBar
globalSearch={searchService}
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>,
{ attachTo: document.body }
it('supports keyboard shortcuts', async () => {
render(
<IntlProvider locale="en">
<SearchBar
globalSearch={searchService}
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
</IntlProvider>
);
act(() => {
fireEvent.keyDown(window, { key: '/', ctrlKey: true, metaKey: true });
});
const searchEvent = new KeyboardEvent('keydown', {
key: '/',
ctrlKey: true,
metaKey: true,
} as any);
window.dispatchEvent(searchEvent);
const inputElement = await screen.findByTestId('nav-search-input');
expect(document.activeElement).toMatchSnapshot();
expect(document.activeElement).toEqual(inputElement);
});
it('only display results from the last search', async () => {
@ -144,30 +153,29 @@ describe('SearchBar', () => {
searchService.find.mockReturnValueOnce(firstSearch).mockReturnValueOnce(secondSearch);
component = mountWithIntl(
<SearchBar
globalSearch={searchService}
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
render(
<IntlProvider locale="en">
<SearchBar
globalSearch={searchService}
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
trackUiMetric={jest.fn()}
/>
</IntlProvider>
);
triggerFocus();
update();
await focusAndUpdate();
expect(searchService.find).toHaveBeenCalledTimes(1);
await simulateTypeChar('d');
update();
expect(getDisplayedOptionsTitle()).toMatchSnapshot();
//
simulateTypeChar('d');
await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']);
firstSearchTrigger.next(true);
update();
expect(getDisplayedOptionsTitle()).toMatchSnapshot();
await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']);
});
});

View file

@ -180,6 +180,7 @@ export function SearchBar({
darkMode,
}: Props) {
const isMounted = useMountedState();
const [initialLoad, setInitialLoad] = useState(false);
const [searchValue, setSearchValue] = useState<string>('');
const [searchTerm, setSearchTerm] = useState<string>('');
const [searchRef, setSearchRef] = useState<HTMLInputElement | null>(null);
@ -190,12 +191,14 @@ export function SearchBar({
const UNKNOWN_TAG_ID = '__unknown__';
useEffect(() => {
const fetch = async () => {
const types = await globalSearch.getSearchableTypes();
setSearchableTypes(types);
};
fetch();
}, [globalSearch]);
if (initialLoad) {
const fetch = async () => {
const types = await globalSearch.getSearchableTypes();
setSearchableTypes(types);
};
fetch();
}
}, [globalSearch, initialLoad]);
const loadSuggestions = useCallback(
(term: string) => {
@ -234,75 +237,80 @@ export function SearchBar({
useDebounce(
() => {
// cancel pending search if not completed yet
if (searchSubscription.current) {
searchSubscription.current.unsubscribe();
searchSubscription.current = null;
}
if (initialLoad) {
// cancel pending search if not completed yet
if (searchSubscription.current) {
searchSubscription.current.unsubscribe();
searchSubscription.current = null;
}
const suggestions = loadSuggestions(searchValue);
const suggestions = loadSuggestions(searchValue);
let aggregatedResults: GlobalSearchResult[] = [];
if (searchValue.length !== 0) {
trackUiMetric(METRIC_TYPE.COUNT, 'search_request');
}
let aggregatedResults: GlobalSearchResult[] = [];
if (searchValue.length !== 0) {
trackUiMetric(METRIC_TYPE.COUNT, 'search_request');
}
const rawParams = parseSearchParams(searchValue);
const tagIds =
taggingApi && rawParams.filters.tags
? rawParams.filters.tags.map(
(tagName) => taggingApi.ui.getTagIdFromName(tagName) ?? UNKNOWN_TAG_ID
)
: undefined;
const searchParams: GlobalSearchFindParams = {
term: rawParams.term,
types: rawParams.filters.types,
tags: tagIds,
};
// TODO technically a subtle bug here
// this term won't be set until the next time the debounce is fired
// so the SearchOption won't highlight anything if only one call is fired
// in practice, this is hard to spot, unlikely to happen, and is a negligible issue
setSearchTerm(rawParams.term ?? '');
const rawParams = parseSearchParams(searchValue);
const tagIds =
taggingApi && rawParams.filters.tags
? rawParams.filters.tags.map(
(tagName) => taggingApi.ui.getTagIdFromName(tagName) ?? UNKNOWN_TAG_ID
)
: undefined;
const searchParams: GlobalSearchFindParams = {
term: rawParams.term,
types: rawParams.filters.types,
tags: tagIds,
};
// TODO technically a subtle bug here
// this term won't be set until the next time the debounce is fired
// so the SearchOption won't highlight anything if only one call is fired
// in practice, this is hard to spot, unlikely to happen, and is a negligible issue
setSearchTerm(rawParams.term ?? '');
searchSubscription.current = globalSearch.find(searchParams, {}).subscribe({
next: ({ results }) => {
if (searchValue.length > 0) {
aggregatedResults = [...results, ...aggregatedResults].sort(sortByScore);
setOptions(aggregatedResults, suggestions, searchParams.tags);
return;
}
// if searchbar is empty, filter to only applications and sort alphabetically
results = results.filter(({ type }: GlobalSearchResult) => type === 'application');
aggregatedResults = [...results, ...aggregatedResults].sort(sortByTitle);
searchSubscription.current = globalSearch.find(searchParams, {}).subscribe({
next: ({ results }) => {
if (searchValue.length > 0) {
aggregatedResults = [...results, ...aggregatedResults].sort(sortByScore);
setOptions(aggregatedResults, suggestions, searchParams.tags);
return;
}
// if searchbar is empty, filter to only applications and sort alphabetically
results = results.filter(({ type }: GlobalSearchResult) => type === 'application');
aggregatedResults = [...results, ...aggregatedResults].sort(sortByTitle);
setOptions(aggregatedResults, suggestions, searchParams.tags);
},
error: () => {
// 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: () => {},
});
},
error: () => {
// 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: () => {},
});
}
},
350,
[searchValue, loadSuggestions]
[searchValue, loadSuggestions, searchableTypes, initialLoad]
);
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
trackUiMetric(METRIC_TYPE.COUNT, 'shortcut_used');
if (searchRef) {
searchRef.focus();
} else if (buttonRef) {
(buttonRef.children[0] as HTMLButtonElement).click();
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
trackUiMetric(METRIC_TYPE.COUNT, 'shortcut_used');
if (searchRef) {
searchRef.focus();
} else if (buttonRef) {
(buttonRef.children[0] as HTMLButtonElement).click();
}
}
}
};
},
[buttonRef, searchRef, trackUiMetric]
);
const onChange = (selection: EuiSelectableTemplateSitewideOption[]) => {
const selected = selection.find(({ checked }) => checked === 'on');
@ -411,6 +419,7 @@ export function SearchBar({
}),
onFocus: () => {
trackUiMetric(METRIC_TYPE.COUNT, 'search_focus');
setInitialLoad(true);
},
}}
popoverProps={{