From 79fed30c8c14fe29228b65675ebb0ac4010bce54 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 23 Oct 2024 11:47:53 +0800 Subject: [PATCH] [workspace] update search bar layout and align workspace selector (#8649) * update search bar layout Signed-off-by: Hailong Cui * Changeset file for PR #8649 created/updated * update search bar layout Signed-off-by: Hailong Cui * update test case Signed-off-by: Hailong Cui * style update Signed-off-by: Hailong Cui * typo Signed-off-by: Hailong Cui * support search nav items by its parent title Signed-off-by: Hailong Cui * address review comments Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8649.yml | 2 + .../header/collapsible_nav_group_enabled.scss | 6 +- .../collapsible_nav_group_enabled_top.tsx | 7 +- .../ui/header/header_search_bar.test.tsx | 66 ++++-- .../chrome/ui/header/header_search_bar.tsx | 213 +++++++++--------- .../components/global_search/page_item.tsx | 10 +- .../global_search/search_pages_command.tsx | 21 +- 7 files changed, 191 insertions(+), 134 deletions(-) create mode 100644 changelogs/fragments/8649.yml diff --git a/changelogs/fragments/8649.yml b/changelogs/fragments/8649.yml new file mode 100644 index 000000000000..1a3a2d6dbf66 --- /dev/null +++ b/changelogs/fragments/8649.yml @@ -0,0 +1,2 @@ +fix: +- Finetune search bar and workspace selector style ([#8649](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8649)) \ No newline at end of file diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss index 1310585eefea..cd0d73a0644a 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss @@ -112,7 +112,11 @@ .navGroupEnabledNavTopWrapper { padding: 0 $euiSizeS; - padding-left: $euiSize; + padding-left: $euiSizeS; + + .navGroupEnabledHomeIcon { + margin-left: $euiSizeS; + } } .searchBar-wrapper { diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx index 628894864928..caa825fe8381 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx @@ -76,7 +76,12 @@ export const CollapsibleNavTop = ({ {!shouldShrinkNavigation ? ( - + ', () => { }, ]; - it('render HeaderSearchBarIcon correctly without search results', () => { + it('render HeaderSearchBarIcon correctly without search results', async () => { const { getByTestId, queryByText } = render( ); @@ -34,22 +34,22 @@ describe('', () => { fireEvent.click(searchIcon); - expect(getByTestId('search-input')).toBeVisible(); + expect(getByTestId('global-search-input')).toBeVisible(); act(() => { - fireEvent.change(getByTestId('search-input'), { + fireEvent.change(getByTestId('global-search-input'), { target: { value: 'index' }, }); }); expect(searchFn).toHaveBeenCalled(); - waitFor(() => { + await waitFor(() => { expect(queryByText('No results found.')).toBeInTheDocument(); }); }); - it('render HeaderSearchBarIcon correctly with search results', () => { + it('render HeaderSearchBarIcon correctly with search results', async () => { const { getByTestId, queryByText } = render( ); @@ -58,18 +58,18 @@ describe('', () => { fireEvent.click(searchIcon); - expect(getByTestId('search-input')).toBeVisible(); + expect(getByTestId('global-search-input')).toBeVisible(); searchFn.mockResolvedValue([index page]); act(() => { - fireEvent.change(getByTestId('search-input'), { + fireEvent.change(getByTestId('global-search-input'), { target: { value: 'index' }, }); }); expect(searchFn).toHaveBeenCalled(); - waitFor(() => { + await waitFor(() => { expect(queryByText('index page')).toBeInTheDocument(); }); }); @@ -109,19 +109,37 @@ describe('', () => { expect(searchPanel).toBeVisible(); }); - it('render HeaderSearchBar with search result', () => { + it('render HeaderSearchBar with search input', async () => { + const { queryByTestId, getByTestId } = render( + + ); + const searchPanel = queryByTestId('search-result-panel'); + expect(searchPanel).toBeNull(); + + // focus on search input + const searchInput = getByTestId('global-search-input'); + expect(searchInput).toBeVisible(); + searchInput.focus(); + + await waitFor(() => { + expect(queryByTestId('search-result-panel')).toBeVisible(); + expect(queryByTestId('global-search-input')).toBeVisible(); + }); + }); + + it('render HeaderSearchBar with search result', async () => { const { getByTestId, queryByText } = render( ); const searchPanel = getByTestId('search-result-panel'); expect(searchPanel).toBeVisible(); - expect(getByTestId('search-input')).toBeVisible(); + expect(getByTestId('global-search-input')).toBeVisible(); searchFn.mockResolvedValue([index page]); searchFnBar.mockResolvedValue([index polices]); act(() => { - fireEvent.change(getByTestId('search-input'), { + fireEvent.change(getByTestId('global-search-input'), { target: { value: 'index' }, }); }); @@ -129,26 +147,26 @@ describe('', () => { expect(searchFn).toHaveBeenCalled(); // merge page results together - waitFor(() => { + await waitFor(() => { expect(queryByText('index page')).toBeInTheDocument(); expect(queryByText('index polices')).toBeInTheDocument(); }); }); - it('render HeaderSearchBar with reject search result', () => { + it('render HeaderSearchBar with reject search result', async () => { const { getByTestId, queryByText } = render( ); const searchPanel = getByTestId('search-result-panel'); expect(searchPanel).toBeVisible(); - expect(getByTestId('search-input')).toBeVisible(); + expect(getByTestId('global-search-input')).toBeVisible(); searchFn.mockResolvedValue([index page]); searchFnBar.mockRejectedValue(new Error('Async search error')); act(() => { - fireEvent.change(getByTestId('search-input'), { + fireEvent.change(getByTestId('global-search-input'), { target: { value: 'index' }, }); }); @@ -156,25 +174,25 @@ describe('', () => { expect(searchFn).toHaveBeenCalled(); // ignore reject and show pages for success search≠ - waitFor(() => { + await waitFor(() => { expect(queryByText('index page')).toBeInTheDocument(); }); }); - it('render HeaderSearchBar with all reject search result', () => { + it('render HeaderSearchBar with all reject search result', async () => { const { getByTestId, queryByText } = render( ); const searchPanel = getByTestId('search-result-panel'); expect(searchPanel).toBeVisible(); - expect(getByTestId('search-input')).toBeVisible(); + expect(getByTestId('global-search-input')).toBeVisible(); searchFnBar.mockRejectedValue(new Error('Async search error')); searchFn.mockRejectedValue(new Error('Async search error')); act(() => { - fireEvent.change(getByTestId('search-input'), { + fireEvent.change(getByTestId('global-search-input'), { target: { value: 'index' }, }); }); @@ -182,24 +200,24 @@ describe('', () => { expect(searchFn).toHaveBeenCalled(); // show no result for all reject search - waitFor(() => { + await waitFor(() => { expect(queryByText('No results found.')).toBeInTheDocument(); }); }); - it('render HeaderSearchBar with search saved objects', () => { + it('render HeaderSearchBar with search saved objects', async () => { const { getByTestId, queryByText } = render( ); const searchPanel = getByTestId('search-result-panel'); expect(searchPanel).toBeVisible(); - expect(getByTestId('search-input')).toBeVisible(); + expect(getByTestId('global-search-input')).toBeVisible(); searchFnBaz.mockResolvedValue([
saved objects
]); act(() => { - fireEvent.change(getByTestId('search-input'), { + fireEvent.change(getByTestId('global-search-input'), { target: { value: '@index' }, }); }); @@ -213,7 +231,7 @@ describe('', () => { } }); - waitFor(() => { + await waitFor(() => { expect(queryByText('saved objects')).toBeInTheDocument(); }); }); diff --git a/src/core/public/chrome/ui/header/header_search_bar.tsx b/src/core/public/chrome/ui/header/header_search_bar.tsx index 023b5c19566b..5c94dd28af03 100644 --- a/src/core/public/chrome/ui/header/header_search_bar.tsx +++ b/src/core/public/chrome/ui/header/header_search_bar.tsx @@ -16,7 +16,7 @@ import { EuiTitle, EuiToolTip, } from '@elastic/eui'; -import React, { ReactNode, useRef, useState } from 'react'; +import React, { ReactNode, useCallback, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; import { GlobalSearchCommand, @@ -86,12 +86,10 @@ export const HeaderSearchBar = ({ globalSearchCommands, panel, onSearchResultCli const [results, setResults] = useState([] as React.JSX.Element[]); const [isLoading, setIsLoading] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [panelWidth, setPanelWidth] = useState(0); - const inputElRef = useRef(); - const inputRef = (node: HTMLElement | null) => (inputElRef.current = node); const closePopover = () => { setIsPopoverOpen(false); + setResults([]); }; const resultSection = (items: ReactNode[], sectionHeader: string) => { @@ -108,7 +106,7 @@ export const HeaderSearchBar = ({ globalSearchCommands, panel, onSearchResultCli {items.length ? ( {items.map((item, index) => ( - + ))} ) : ( @@ -123,71 +121,80 @@ export const HeaderSearchBar = ({ globalSearchCommands, panel, onSearchResultCli ); }; - const searchResultSections = results && ( - - {results.map((result) => ( - {result} - ))} - - ); + const searchResultSections = + results && results.length ? ( + + {results.map((result) => ( + {result} + ))} + + ) : ( + + {i18n.translate('core.globalSearch.emptyResult.description', { + defaultMessage: 'No results found.', + })} + + ); - const onSearch = async (value: string) => { - const filteredCommands = globalSearchCommands.filter((command) => { - const alias = SearchCommandTypes[command.type].alias; - return alias && value.startsWith(alias); - }); - - const defaultSearchCommands = globalSearchCommands.filter((command) => { - return !SearchCommandTypes[command.type].alias; - }); - - if (filteredCommands.length === 0) { - filteredCommands.push(...defaultSearchCommands); - } - - if (value && filteredCommands && filteredCommands.length) { - setIsPopoverOpen(true); - setIsLoading(true); - - const settleResults = await Promise.allSettled( - filteredCommands.map((command) => { - const callback = onSearchResultClick || closePopover; - const alias = SearchCommandTypes[command.type].alias; - const queryValue = alias ? value.replace(alias, '').trim() : value; - return command.run(queryValue, callback).then((items) => { - return { items, type: command.type }; - }); - }) - ); - - const searchResults = settleResults - .filter((result) => result.status === 'fulfilled') - .map( - (result) => - (result as PromiseFulfilledResult<{ - items: ReactNode[]; - type: SearchCommandKeyTypes; - }>).value - ) - .reduce((acc, { items, type }) => { - return { - ...acc, - [type]: (acc[type] || []).concat(items), - }; - }, {} as Record); - - const sections = Object.entries(searchResults).map(([key, items]) => { - const sectionHeader = SearchCommandTypes[key as SearchCommandKeyTypes].description; - return resultSection(items, sectionHeader); + const onSearch = useCallback( + async (value: string) => { + const filteredCommands = globalSearchCommands.filter((command) => { + const alias = SearchCommandTypes[command.type].alias; + return alias && value.startsWith(alias); }); - setIsLoading(false); - setResults(sections); - } else { - setIsPopoverOpen(false); - setResults([]); - } - }; + const defaultSearchCommands = globalSearchCommands.filter((command) => { + return !SearchCommandTypes[command.type].alias; + }); + + if (filteredCommands.length === 0) { + filteredCommands.push(...defaultSearchCommands); + } + + if (value && filteredCommands && filteredCommands.length) { + setIsPopoverOpen(true); + setIsLoading(true); + + const settleResults = await Promise.allSettled( + filteredCommands.map((command) => { + const callback = onSearchResultClick || closePopover; + const alias = SearchCommandTypes[command.type].alias; + const queryValue = alias ? value.replace(alias, '').trim() : value; + return command.run(queryValue, callback).then((items) => { + return { items, type: command.type }; + }); + }) + ); + + const searchResults = settleResults + .filter((result) => result.status === 'fulfilled') + .map( + (result) => + (result as PromiseFulfilledResult<{ + items: ReactNode[]; + type: SearchCommandKeyTypes; + }>).value + ) + .reduce((acc, { items, type }) => { + return { + ...acc, + [type]: (acc[type] || []).concat(items), + }; + }, {} as Record); + + const sections = Object.entries(searchResults).map(([key, items]) => { + const sectionHeader = SearchCommandTypes[key as SearchCommandKeyTypes].description; + return resultSection(items, sectionHeader); + }); + + setIsLoading(false); + setResults(sections); + } else { + setResults([]); + } + }, + [globalSearchCommands, onSearchResultClick] + ); const searchBar = ( { - const inputEl = inputElRef.current; - if (inputEl) { - const width = inputEl.getBoundingClientRect().width; - setPanelWidth(width); - } + setIsPopoverOpen(true); }} /> ); + const searchBarPanel = ( + + + {searchBar} + {searchResultSections} + + + ); + if (panel) { - return ( - - - {searchBar} - {searchResultSections} - - - ); + return searchBarPanel; } else { return ( - { - setIsPopoverOpen(false); - }} - > - {searchResultSections} - + <> + {!isPopoverOpen && searchBar} + {isPopoverOpen && ( + } + zIndex={2000} + panelPaddingSize="s" + attachToAnchor={true} + ownFocus={true} + display="block" + isOpen={isPopoverOpen} + closePopover={() => { + closePopover(); + }} + > + {searchBarPanel} + + )} + ); } }; diff --git a/src/plugins/workspace/public/components/global_search/page_item.tsx b/src/plugins/workspace/public/components/global_search/page_item.tsx index c3295f0bc671..bc18342ee9ba 100644 --- a/src/plugins/workspace/public/components/global_search/page_item.tsx +++ b/src/plugins/workspace/public/components/global_search/page_item.tsx @@ -89,7 +89,15 @@ export const GlobalSearchPageItem = ({ const parentNavLinkTitle = link.navGroup.navLinks.find( (navLink) => navLink.id === link.parentNavLinkId )?.title; - breadcrumbs.push({ text: parentNavLinkTitle }); + if (parentNavLinkTitle) { + breadcrumbs.push({ + text: ( + + {parentNavLinkTitle} + + ), + }); + } } const onNavItemClick = () => { diff --git a/src/plugins/workspace/public/components/global_search/search_pages_command.tsx b/src/plugins/workspace/public/components/global_search/search_pages_command.tsx index 0aa73a11088b..7d2b348f08ac 100644 --- a/src/plugins/workspace/public/components/global_search/search_pages_command.tsx +++ b/src/plugins/workspace/public/components/global_search/search_pages_command.tsx @@ -43,14 +43,25 @@ export const searchPages = async ( // parent nav links are not clickable const parentNavLinkIds = links.map((link) => link.parentNavLinkId).filter((link) => !!link); return links - .filter( - (link) => + .filter((link) => { + const title = link.title; + let parentNavLinkTitle; + // parent title also taken into consideration for search its sub items + if (link.parentNavLinkId) { + parentNavLinkTitle = navGroup.navLinks.find( + (navLink) => navLink.id === link.parentNavLinkId + )?.title; + } + const titleMatch = title && title.toLowerCase().includes(query.toLowerCase()); + const parentTitleMatch = + parentNavLinkTitle && parentNavLinkTitle.toLowerCase().includes(query.toLowerCase()); + return ( !link.hidden && !link.disabled && - link.title && - link.title.toLowerCase().includes(query.toLowerCase()) && + (titleMatch || parentTitleMatch) && !parentNavLinkIds.includes(link.id) - ) + ); + }) .map((link) => ({ ...link, navGroup,