Skip to content

Commit

Permalink
[Navigation] Add recent items popup in top navigation (opensearch-pro…
Browse files Browse the repository at this point in the history
…ject#7257)

* feat: add recent items

Signed-off-by: tygao <[email protected]>

* test: update chrome tests

Signed-off-by: tygao <[email protected]>

* Changeset file for PR opensearch-project#7257 created/updated

* add navGroupEnabled flag

Signed-off-by: tygao <[email protected]>

* use createRecentNavLink

Signed-off-by: tygao <[email protected]>

* update icon and add empty state

Signed-off-by: tygao <[email protected]>

* test: update snapshots

Signed-off-by: tygao <[email protected]>

* update typing

Signed-off-by: tygao <[email protected]>

* update name and style

Signed-off-by: tygao <[email protected]>

---------

Signed-off-by: tygao <[email protected]>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
Co-authored-by: Yulong Ruan <[email protected]>
  • Loading branch information
3 people authored Jul 22, 2024
1 parent c1dd5c1 commit 4f094a8
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 0 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7257.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Add recent items popup in top navigation ([#7257](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7257))
1 change: 1 addition & 0 deletions src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ export class ChromeService {
currentNavGroup$={navGroup.getCurrentNavGroup$()}
navGroupsMap$={navGroup.getNavGroupsMap$()}
setCurrentNavGroup={navGroup.setCurrentNavGroup}
workspaceList$={workspaces.workspaceList$}
/>
),

Expand Down

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

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

3 changes: 3 additions & 0 deletions src/core/public/chrome/ui/header/assets/recent_items.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/core/public/chrome/ui/header/header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function mockProps() {
navGroupsMap$: new BehaviorSubject({}),
navControlsLeftBottom$: new BehaviorSubject([]),
setCurrentNavGroup: jest.fn(() => {}),
workspaceList$: new BehaviorSubject([]),
};
}

Expand Down
16 changes: 16 additions & 0 deletions src/core/public/chrome/ui/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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;
Expand Down Expand Up @@ -103,6 +106,7 @@ export interface HeaderProps {
currentNavGroup$: Observable<NavGroupItemInMap | undefined>;
navGroupsMap$: Observable<Record<string, NavGroupItemInMap>>;
setCurrentNavGroup: ChromeNavGroupServiceStartContract['setCurrentNavGroup'];
workspaceList$: Observable<WorkspaceObject[]>;
}

export function Header({
Expand Down Expand Up @@ -228,6 +232,18 @@ export function Header({
loadingCount$={observables.loadingCount$}
/>
</EuiHeaderSectionItem>
{/* Only display recent items when navGroup is enabled */}
{navGroupEnabled && (
<EuiHeaderSectionItem border="right">
<RecentItems
recentlyAccessed$={observables.recentlyAccessed$}
workspaceList$={observables.workspaceList$}
navigateToUrl={application.navigateToUrl}
navLinks$={observables.navLinks$}
basePath={basePath}
/>
</EuiHeaderSectionItem>
)}
</EuiHeaderSection>

<HeaderBreadcrumbs
Expand Down
86 changes: 86 additions & 0 deletions src/core/public/chrome/ui/header/recent_items.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { RecentItems, Props } from './recent_items';
import { applicationServiceMock, httpServiceMock } from '../../../mocks';
import { BehaviorSubject } from 'rxjs';

jest.mock('./nav_link', () => ({
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(<RecentItems {...props} />);
};
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');
});
});
111 changes: 111 additions & 0 deletions src/core/public/chrome/ui/header/recent_items.tsx
Original file line number Diff line number Diff line change
@@ -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<ChromeRecentlyAccessedHistoryItem[]>;
workspaceList$: Rx.Observable<WorkspaceObject[]>;
navigateToUrl: (url: string) => Promise<void>;
basePath: HttpStart['basePath'];
navLinks$: Rx.Observable<ChromeNavLink[]>;
}

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 (
<EuiPopover
button={
<EuiHeaderSectionItemButton
onClick={() => {
setIsPopoverOpen((prev) => !prev);
}}
data-test-subj="recentItemsSectionButton"
>
<EuiIcon type={recent_items} size="m" />
</EuiHeaderSectionItemButton>
}
isOpen={isPopoverOpen}
closePopover={() => {
setIsPopoverOpen(false);
}}
anchorPosition="downCenter"
repositionOnScroll
initialFocus={false}
>
<EuiTitle size="xxs">
<h4>Recents</h4>
</EuiTitle>
{items.length > 0 ? (
<EuiListGroup>
{items.map((item) => (
<EuiListGroupItem
onClick={() => handleItemClick(item.link)}
key={item.link}
label={
<>
{item.label}
{item.workspaceName ? (
<EuiTextColor color="subdued">({item.workspaceName})</EuiTextColor>
) : null}
</>
}
color="text"
/>
))}
</EuiListGroup>
) : (
<EuiText color="subdued">No recently viewed items</EuiText>
)}
</EuiPopover>
);
};
7 changes: 7 additions & 0 deletions src/core/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ type DeeplyMockedKeys<T> = {
T;

type MockedKeys<T> = { [P in keyof T]: jest.Mocked<T[P]> };

// 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;
}

0 comments on commit 4f094a8

Please sign in to comment.