"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.SearchBar = void 0; var _eui = require("@elastic/eui"); var _react = require("@emotion/react"); var _react2 = _interopRequireWildcard(require("react")); var _useDebounce = _interopRequireDefault(require("react-use/lib/useDebounce")); var _useEvent = _interopRequireDefault(require("react-use/lib/useEvent")); var _useMountedState = _interopRequireDefault(require("react-use/lib/useMountedState")); var _useObservable = _interopRequireDefault(require("react-use/lib/useObservable")); var _ = require("."); var _lib = require("../lib"); var _search_syntax = require("../search_syntax"); var _strings = require("../strings"); var _suggestions = require("../suggestions"); var _popover_footer = require("./popover_footer"); var _popover_placeholder = require("./popover_placeholder"); require("./search_bar.scss"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ const NoMatchesMessage = props => /*#__PURE__*/_react2.default.createElement(_popover_placeholder.PopoverPlaceholder, { darkMode: props.darkMode, basePath: props.basePathUrl }); const EmptyMessage = () => /*#__PURE__*/_react2.default.createElement(_eui.EuiFlexGroup, { direction: "column", justifyContent: "center", style: { minHeight: '300px' } }, /*#__PURE__*/_react2.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react2.default.createElement(_eui.EuiLoadingSpinner, { size: "xl" }))); const SearchBar = opts => { const { globalSearch, taggingApi, navigateToUrl, reportEvent, chromeStyle$, ...props } = opts; const isMounted = (0, _useMountedState.default)(); const { euiTheme } = (0, _eui.useEuiTheme)(); const chromeStyle = (0, _useObservable.default)(chromeStyle$); // These hooks are used when on chromeStyle set to 'project' const [isVisible, setIsVisible] = (0, _react2.useState)(false); const visibilityButtonRef = (0, _react2.useRef)(null); // General hooks const [initialLoad, setInitialLoad] = (0, _react2.useState)(false); const [searchValue, setSearchValue] = (0, _react2.useState)(''); const [searchTerm, setSearchTerm] = (0, _react2.useState)(''); const [searchRef, setSearchRef] = (0, _react2.useState)(null); const [buttonRef, setButtonRef] = (0, _react2.useState)(null); const searchSubscription = (0, _react2.useRef)(null); const [options, _setOptions] = (0, _react2.useState)([]); const [searchableTypes, setSearchableTypes] = (0, _react2.useState)([]); const [showAppend, setShowAppend] = (0, _react2.useState)(true); const UNKNOWN_TAG_ID = '__unknown__'; (0, _react2.useEffect)(() => { if (initialLoad) { const fetch = async () => { const types = await globalSearch.getSearchableTypes(); setSearchableTypes(types); }; fetch(); } }, [globalSearch, initialLoad]); const loadSuggestions = (0, _react2.useCallback)(term => { return (0, _suggestions.getSuggestions)({ searchTerm: term, searchableTypes, tagCache: taggingApi === null || taggingApi === void 0 ? void 0 : taggingApi.cache }); }, [taggingApi, searchableTypes]); const setOptions = (0, _react2.useCallback)((_options, suggestions, searchTagIds = []) => { if (!isMounted()) { return; } _setOptions([...suggestions.map(_lib.suggestionToOption), ..._options.map(option => { var _searchTagIds$filter; return (0, _lib.resultToOption)(option, (_searchTagIds$filter = searchTagIds === null || searchTagIds === void 0 ? void 0 : searchTagIds.filter(id => id !== UNKNOWN_TAG_ID)) !== null && _searchTagIds$filter !== void 0 ? _searchTagIds$filter : [], taggingApi === null || taggingApi === void 0 ? void 0 : taggingApi.ui.getTag); })]); }, [isMounted, _setOptions, taggingApi]); (0, _useDebounce.default)(() => { if (initialLoad) { var _rawParams$term; // cancel pending search if not completed yet if (searchSubscription.current) { searchSubscription.current.unsubscribe(); searchSubscription.current = null; } const suggestions = loadSuggestions(searchValue); let aggregatedResults = []; if (searchValue.length !== 0) { reportEvent.searchRequest(); } const rawParams = (0, _search_syntax.parseSearchParams)(searchValue); const tagIds = taggingApi && rawParams.filters.tags ? rawParams.filters.tags.map(tagName => { var _taggingApi$ui$getTag; return (_taggingApi$ui$getTag = taggingApi.ui.getTagIdFromName(tagName)) !== null && _taggingApi$ui$getTag !== void 0 ? _taggingApi$ui$getTag : UNKNOWN_TAG_ID; }) : undefined; const searchParams = { 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 = rawParams.term) !== null && _rawParams$term !== void 0 ? _rawParams$term : ''); searchSubscription.current = globalSearch.find(searchParams, {}).subscribe({ next: ({ results }) => { if (searchValue.length > 0) { aggregatedResults = [...results, ...aggregatedResults].sort(_.sort.byScore); setOptions(aggregatedResults, suggestions, searchParams.tags); return; } // if searchbar is empty, filter to only applications and sort alphabetically results = results.filter(({ type }) => type === 'application'); aggregatedResults = [...results, ...aggregatedResults].sort(_.sort.byTitle); setOptions(aggregatedResults, suggestions, searchParams.tags); }, error: err => { // 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 reportEvent.error({ message: err, searchValue }); }, complete: () => {} }); } }, 350, [searchValue, loadSuggestions, searchableTypes, initialLoad]); const onKeyDown = (0, _react2.useCallback)(event => { if (event.key === '/' && (_.isMac ? event.metaKey : event.ctrlKey)) { event.preventDefault(); reportEvent.shortcutUsed(); if (chromeStyle === 'project' && !isVisible) { var _visibilityButtonRef$; (_visibilityButtonRef$ = visibilityButtonRef.current) === null || _visibilityButtonRef$ === void 0 ? void 0 : _visibilityButtonRef$.click(); } else if (searchRef) { searchRef.focus(); } else if (buttonRef) { buttonRef.children[0].click(); } } }, [chromeStyle, isVisible, buttonRef, searchRef, reportEvent]); const onChange = (0, _react2.useCallback)(selection => { var _selected$label; let selectedRank = null; const selected = selection.find(({ checked }, rank) => { const isChecked = checked === 'on'; if (isChecked) { selectedRank = rank + 1; } return isChecked; }); if (!selected) { return; } const selectedLabel = (_selected$label = selected.label) !== null && _selected$label !== void 0 ? _selected$label : null; // @ts-ignore - ts error is "union type is too complex to express" const { url, type, suggestion } = selected; // if the type is a suggestion, we change the query on the input and trigger a new search // by setting the searchValue (only setting the field value does not trigger a search) if (type === '__suggestion__') { setSearchValue(suggestion); return; } // errors in tracking should not prevent selection behavior try { if (type === 'application') { var _selected$key; const key = (_selected$key = selected.key) !== null && _selected$key !== void 0 ? _selected$key : 'unknown'; const application = `${key.toLowerCase().replaceAll(' ', '_')}`; reportEvent.navigateToApplication({ application, searchValue, selectedLabel, selectedRank }); } else { reportEvent.navigateToSavedObject({ type, searchValue, selectedLabel, selectedRank }); } } catch (err) { reportEvent.error({ message: err, searchValue }); // eslint-disable-next-line no-console console.log('Error trying to track searchbar metrics', err); } navigateToUrl(url); document.activeElement.blur(); if (searchRef) { clearField(); searchRef.dispatchEvent(_.blurEvent); } }, [reportEvent, navigateToUrl, searchRef, searchValue]); const clearField = () => setSearchValue(''); const keyboardShortcutTooltip = `${_strings.i18nStrings.keyboardShortcutTooltip.prefix}: ${_.isMac ? _strings.i18nStrings.keyboardShortcutTooltip.onMac : _strings.i18nStrings.keyboardShortcutTooltip.onNotMac}`; (0, _useEvent.default)('keydown', onKeyDown); if (chromeStyle === 'project' && !isVisible) { return /*#__PURE__*/_react2.default.createElement(_eui.EuiButtonIcon, { "aria-label": _strings.i18nStrings.showSearchAriaText, buttonRef: visibilityButtonRef, color: "text", "data-test-subj": "nav-search-reveal", iconType: "search", onClick: () => { setIsVisible(true); } }); } const getAppendForChromeStyle = () => { if (chromeStyle === 'project') { return /*#__PURE__*/_react2.default.createElement(_eui.EuiButtonIcon, { "aria-label": _strings.i18nStrings.closeSearchAriaText, color: "text", "data-test-subj": "nav-search-conceal", iconType: "cross", onClick: () => { reportEvent.searchBlur(); setIsVisible(false); } }); } if (showAppend) { return /*#__PURE__*/_react2.default.createElement(_eui.EuiFormLabel, { title: keyboardShortcutTooltip, css: { fontFamily: euiTheme.font.familyCode } }, _.isMac ? '⌘/' : '^/'); } }; return /*#__PURE__*/_react2.default.createElement(_eui.EuiSelectableTemplateSitewide, { isPreFiltered: true, onChange: onChange, options: options, className: "kbnSearchBar", popoverButtonBreakpoints: ['xs', 's'], singleSelection: true, renderOption: option => (0, _eui.euiSelectableTemplateSitewideRenderOptions)(option, searchTerm), listProps: { className: 'eui-yScroll', css: (0, _react.css)` max-block-size: 75vh; ` }, searchProps: { autoFocus: chromeStyle === 'project', value: searchValue, onInput: e => setSearchValue(e.currentTarget.value), 'data-test-subj': 'nav-search-input', inputRef: setSearchRef, compressed: true, 'aria-label': _strings.i18nStrings.placeholderText, placeholder: _strings.i18nStrings.placeholderText, onFocus: () => { reportEvent.searchFocus(); setInitialLoad(true); setShowAppend(false); }, onBlur: () => { reportEvent.searchBlur(); setShowAppend(!searchValue.length); }, fullWidth: true, append: getAppendForChromeStyle() }, emptyMessage: /*#__PURE__*/_react2.default.createElement(EmptyMessage, null), noMatchesMessage: /*#__PURE__*/_react2.default.createElement(NoMatchesMessage, props), popoverProps: { 'data-test-subj': 'nav-search-popover', panelClassName: 'navSearch__panel', repositionOnScroll: true, buttonRef: setButtonRef, panelStyle: { marginTop: '6px' } }, popoverButton: /*#__PURE__*/_react2.default.createElement(_eui.EuiHeaderSectionItemButton, { "aria-label": _strings.i18nStrings.popoverButton }, /*#__PURE__*/_react2.default.createElement(_eui.EuiIcon, { type: "search", size: "m" })), popoverFooter: /*#__PURE__*/_react2.default.createElement(_popover_footer.PopoverFooter, { isMac: _.isMac }) }); }; exports.SearchBar = SearchBar;