From e1f721ca0963329432333bb93edb912b0624dbed Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:42:16 +0800 Subject: [PATCH] [Navigation] Add recent items popup in top navigation (#7257) (#7388) * feat: add recent items * test: update chrome tests * Changeset file for PR #7257 created/updated * add navGroupEnabled flag * use createRecentNavLink * update icon and add empty state * test: update snapshots * update typing * update name and style --------- (cherry picked from commit 4f094a84e8371652078a8c4daa7a063d6ea69e1b) Signed-off-by: tygao Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Yulong Ruan --- changelogs/fragments/7257.yml | 2 + src/core/public/chrome/chrome_service.tsx | 1 + .../header/__snapshots__/header.test.tsx.snap | 22 ++++ .../__snapshots__/recent_items.test.tsx.snap | 37 ++++++ .../chrome/ui/header/assets/recent_items.svg | 3 + .../public/chrome/ui/header/header.test.tsx | 1 + src/core/public/chrome/ui/header/header.tsx | 16 +++ .../chrome/ui/header/recent_items.test.tsx | 86 ++++++++++++++ .../public/chrome/ui/header/recent_items.tsx | 111 ++++++++++++++++++ src/core/typings.ts | 7 ++ 10 files changed, 286 insertions(+) create mode 100644 changelogs/fragments/7257.yml create mode 100644 src/core/public/chrome/ui/header/__snapshots__/recent_items.test.tsx.snap create mode 100644 src/core/public/chrome/ui/header/assets/recent_items.svg create mode 100644 src/core/public/chrome/ui/header/recent_items.test.tsx create mode 100644 src/core/public/chrome/ui/header/recent_items.tsx diff --git a/changelogs/fragments/7257.yml b/changelogs/fragments/7257.yml new file mode 100644 index 000000000000..981342618131 --- /dev/null +++ b/changelogs/fragments/7257.yml @@ -0,0 +1,2 @@ +feat: +- Add recent items popup in top navigation ([#7257](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7257)) \ No newline at end of file diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 4f0fc7dc08e4..8365247201e5 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -307,6 +307,7 @@ export class ChromeService { currentNavGroup$={navGroup.getCurrentNavGroup$()} navGroupsMap$={navGroup.getNavGroupsMap$()} setCurrentNavGroup={navGroup.setCurrentNavGroup} + workspaceList$={workspaces.workspaceList$} /> ), diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 597cb26f7e45..4cc46fd921db 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -2053,6 +2053,17 @@ exports[`Header handles visibility and lock changes 1`] = ` } } survey="/" + workspaceList$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } >
+
+
+
+ +
+
+
+ +`; diff --git a/src/core/public/chrome/ui/header/assets/recent_items.svg b/src/core/public/chrome/ui/header/assets/recent_items.svg new file mode 100644 index 000000000000..b6427c5e59e4 --- /dev/null +++ b/src/core/public/chrome/ui/header/assets/recent_items.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 16e77a353cc6..98f82d7efd76 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -82,6 +82,7 @@ function mockProps() { navGroupsMap$: new BehaviorSubject({}), navControlsLeftBottom$: new BehaviorSubject([]), setCurrentNavGroup: jest.fn(() => {}), + workspaceList$: new BehaviorSubject([]), }; } diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index dc0876ab59ca..b58d450aefea 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -68,6 +68,9 @@ import type { Logos } from '../../../../common/types'; import { ISidecarConfig, getOsdSidecarPaddingStyle } from '../../../overlays'; import { CollapsibleNavGroupEnabled } from './collapsible_nav_group_enabled'; import { ChromeNavGroupServiceStartContract, NavGroupItemInMap } from '../../nav_group'; +import { RecentItems } from './recent_items'; +import { WorkspaceObject } from '../../../../public/workspace'; + export interface HeaderProps { opensearchDashboardsVersion: string; application: InternalApplicationStart; @@ -102,6 +105,7 @@ export interface HeaderProps { currentNavGroup$: Observable; navGroupsMap$: Observable>; setCurrentNavGroup: ChromeNavGroupServiceStartContract['setCurrentNavGroup']; + workspaceList$: Observable; } export function Header({ @@ -225,6 +229,18 @@ export function Header({ loadingCount$={observables.loadingCount$} /> + {/* Only display recent items when navGroup is enabled */} + {navGroupEnabled && ( + + + + )} ({ + createRecentNavLink: jest.fn().mockImplementation(() => { + return { + href: '/recent_nav_link', + }; + }), +})); + +const defaultMockProps = { + navigateToUrl: applicationServiceMock.createStartContract().navigateToUrl, + workspaceList$: new BehaviorSubject([]), + recentlyAccessed$: new BehaviorSubject([]), + navLinks$: new BehaviorSubject([]), + basePath: httpServiceMock.createStartContract().basePath, +}; +const setup = (props: Props) => { + return render(); +}; +describe('Recent items', () => { + it('should render base element normally', () => { + const { baseElement } = setup(defaultMockProps); + expect(baseElement).toMatchSnapshot(); + }); + + it('should get workspace name through workspace id and render it with brackets wrapper', () => { + const workspaceList$ = new BehaviorSubject([ + { + id: 'workspace_id', + name: 'workspace_name', + }, + ]); + const recentlyAccessed$ = new BehaviorSubject([ + { + label: 'item_label', + link: 'item_link', + id: 'item_id', + workspaceId: 'workspace_id', + }, + ]); + + setup({ + ...defaultMockProps, + workspaceList$, + recentlyAccessed$, + navigateToUrl: defaultMockProps.navigateToUrl, + }); + const button = screen.getByTestId('recentItemsSectionButton'); + fireEvent.click(button); + expect(screen.getByText('(workspace_name)')).toBeInTheDocument(); + }); + + it('should call navigateToUrl with link generated from createRecentNavLink when clicking item', () => { + const workspaceList$ = new BehaviorSubject([]); + const recentlyAccessed$ = new BehaviorSubject([ + { + label: 'item_label', + link: 'item_link', + id: 'item_id', + }, + ]); + const navigateToUrl = jest.fn(); + setup({ + ...defaultMockProps, + workspaceList$, + recentlyAccessed$, + navigateToUrl, + }); + const button = screen.getByTestId('recentItemsSectionButton'); + fireEvent.click(button); + const item = screen.getByText('item_label'); + expect(navigateToUrl).not.toHaveBeenCalled(); + fireEvent.click(item); + expect(navigateToUrl).toHaveBeenCalledWith('/recent_nav_link'); + }); +}); diff --git a/src/core/public/chrome/ui/header/recent_items.tsx b/src/core/public/chrome/ui/header/recent_items.tsx new file mode 100644 index 000000000000..1d91ac286a91 --- /dev/null +++ b/src/core/public/chrome/ui/header/recent_items.tsx @@ -0,0 +1,111 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React, { useMemo, useState } from 'react'; +import * as Rx from 'rxjs'; +import { + EuiPopover, + EuiHeaderSectionItemButton, + EuiTextColor, + EuiListGroup, + EuiListGroupItem, + EuiTitle, + EuiIcon, + EuiText, +} from '@elastic/eui'; +import useObservable from 'react-use/lib/useObservable'; +import { ChromeRecentlyAccessedHistoryItem } from '../..'; +import { WorkspaceObject } from '../../../workspace'; +import { createRecentNavLink } from './nav_link'; +import { HttpStart } from '../../../http'; +import { ChromeNavLink } from '../../../'; +// TODO: replace this icon once added to OUI +import recent_items from './assets/recent_items.svg'; + +export interface Props { + recentlyAccessed$: Rx.Observable; + workspaceList$: Rx.Observable; + navigateToUrl: (url: string) => Promise; + basePath: HttpStart['basePath']; + navLinks$: Rx.Observable; +} + +export const RecentItems = ({ + recentlyAccessed$, + workspaceList$, + navigateToUrl, + navLinks$, + basePath, +}: Props) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const recentlyAccessedItems = useObservable(recentlyAccessed$, []); + const workspaceList = useObservable(workspaceList$, []); + const navLinks = useObservable(navLinks$, []).filter((link) => !link.hidden); + + const items = useMemo(() => { + // Only display five most latest items + return recentlyAccessedItems.slice(0, 5).map((item) => { + return { + link: createRecentNavLink(item, navLinks, basePath, navigateToUrl).href, + label: item.label, + workspaceId: item.workspaceId, + workspaceName: + workspaceList.find((workspace) => workspace.id === item.workspaceId)?.name ?? '', + }; + }); + }, [recentlyAccessedItems, workspaceList, basePath, navLinks, navigateToUrl]); + + const handleItemClick = (link: string) => { + navigateToUrl(link); + setIsPopoverOpen(false); + }; + + return ( + { + setIsPopoverOpen((prev) => !prev); + }} + data-test-subj="recentItemsSectionButton" + > + + + } + isOpen={isPopoverOpen} + closePopover={() => { + setIsPopoverOpen(false); + }} + anchorPosition="downCenter" + repositionOnScroll + initialFocus={false} + > + +

Recents

+
+ {items.length > 0 ? ( + + {items.map((item) => ( + handleItemClick(item.link)} + key={item.link} + label={ + <> + {item.label} + {item.workspaceName ? ( + ({item.workspaceName}) + ) : null} + + } + color="text" + /> + ))} + + ) : ( + No recently viewed items + )} +
+ ); +}; diff --git a/src/core/typings.ts b/src/core/typings.ts index 97b83c91e031..c5e97e2b1d7c 100644 --- a/src/core/typings.ts +++ b/src/core/typings.ts @@ -36,3 +36,10 @@ type DeeplyMockedKeys = { T; type MockedKeys = { [P in keyof T]: jest.Mocked }; + +// Need to declare like typings/index.d.ts otherwise would be overwritten +declare module '*.svg' { + const content: string; + // eslint-disable-next-line import/no-default-export + export default content; +}