From b607813330627a0fe0dd45c540dede3df521f703 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 22 Aug 2024 17:04:25 +0800 Subject: [PATCH 1/9] feature/refractor-style-of-recent-items Signed-off-by: Qxisylolo --- src/core/public/chrome/chrome_service.tsx | 3 + .../header/__snapshots__/header.test.tsx.snap | 542 +++++++----------- .../public/chrome/ui/header/header.test.tsx | 1 + src/core/public/chrome/ui/header/header.tsx | 7 +- .../chrome/ui/header/recent_items.test.tsx | 176 ++++-- .../public/chrome/ui/header/recent_items.tsx | 247 ++++++-- 6 files changed, 525 insertions(+), 451 deletions(-) diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 82511b32bc0..0f4276b1f2b 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -43,6 +43,7 @@ import { } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { EuiLink } from '@elastic/eui'; +import { CoreStart } from 'opensearch-dashboards/public'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; @@ -115,6 +116,7 @@ export interface StartDeps { application: InternalApplicationStart; docLinks: DocLinksStart; http: HttpStart; + // core: CoreStart; injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; @@ -349,6 +351,7 @@ export class ChromeService { getHeaderComponent: () => (
@@ -8951,7 +8901,7 @@ exports[`Header renders application header without title and breadcrumbs 1`] = ` initialFocus={false} isOpen={false} ownFocus={true} - panelPaddingSize="s" + panelPaddingSize="m" repositionOnScroll={true} >
@@ -9548,43 +9498,6 @@ exports[`Header renders application header without title and breadcrumbs 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, } @@ -11048,6 +10961,34 @@ exports[`Header renders condensed header 1`] = ` } } homeHref="/" + http={ + Object { + "addLoadingCountSource": [MockFunction], + "anonymousPaths": Object { + "isAnonymous": [MockFunction], + "register": [MockFunction], + }, + "basePath": BasePath { + "basePath": "/test", + "clientBasePath": "", + "get": [Function], + "getBasePath": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "/test", + }, + "delete": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "put": [MockFunction], + } + } intl={ Object { "defaultFormats": Object {}, @@ -16023,6 +15964,34 @@ exports[`Header renders page header with application title 1`] = ` } } homeHref="/" + http={ + Object { + "addLoadingCountSource": [MockFunction], + "anonymousPaths": Object { + "isAnonymous": [MockFunction], + "register": [MockFunction], + }, + "basePath": BasePath { + "basePath": "/test", + "clientBasePath": "", + "get": [Function], + "getBasePath": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "/test", + }, + "delete": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "put": [MockFunction], + } + } intl={ Object { "defaultFormats": Object {}, @@ -16612,43 +16581,6 @@ exports[`Header renders page header with application title 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, } @@ -16982,102 +16914,33 @@ exports[`Header renders page header with application title 1`] = ` className="euiHeaderSectionItem" > @@ -17441,7 +17304,7 @@ exports[`Header renders page header with application title 1`] = ` initialFocus={false} isOpen={false} ownFocus={true} - panelPaddingSize="s" + panelPaddingSize="m" repositionOnScroll={true} >
@@ -18886,43 +18749,6 @@ exports[`Header renders page header with application title 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, } @@ -20386,6 +20212,34 @@ exports[`Header toggles primary navigation menu when clicked 1`] = ` } } homeHref="/" + http={ + Object { + "addLoadingCountSource": [MockFunction], + "anonymousPaths": Object { + "isAnonymous": [MockFunction], + "register": [MockFunction], + }, + "basePath": BasePath { + "basePath": "/test", + "clientBasePath": "", + "get": [Function], + "getBasePath": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "/test", + }, + "delete": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "put": [MockFunction], + } + } intl={ Object { "defaultFormats": Object {}, diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 9cc8652c3e4..7c2f0b90f9e 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -50,6 +50,7 @@ function mockProps() { const application = applicationServiceMock.createInternalStartContract(); return { + http, application, opensearchDashboardsVersion: '1.0.0', appTitle$: new BehaviorSubject('test'), diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 492d9c7b3e7..b31398c72de 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -79,6 +79,7 @@ import { HomeLoader } from './home_loader'; import { RecentItems } from './recent_items'; export interface HeaderProps { + http: HttpStart; opensearchDashboardsVersion: string; application: InternalApplicationStart; appTitle$: Observable; @@ -120,6 +121,7 @@ export interface HeaderProps { } export function Header({ + http, opensearchDashboardsVersion, opensearchDashboardsDocLink, application, @@ -351,11 +353,12 @@ export function Header({ const renderRecentItems = () => ( diff --git a/src/core/public/chrome/ui/header/recent_items.test.tsx b/src/core/public/chrome/ui/header/recent_items.test.tsx index a6b9a65f9e1..186f9adc310 100644 --- a/src/core/public/chrome/ui/header/recent_items.test.tsx +++ b/src/core/public/chrome/ui/header/recent_items.test.tsx @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, act } from '@testing-library/react'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; import { applicationServiceMock, httpServiceMock } from '../../../mocks'; -import { Props, RecentItems } from './recent_items'; +import { SavedObjectWithMetadata } from './recent_items'; +import { RecentItems } from './recent_items'; jest.mock('./nav_link', () => ({ createRecentNavLink: jest.fn().mockImplementation(() => { @@ -17,73 +18,146 @@ jest.mock('./nav_link', () => ({ }), })); +const mockRecentlyAccessed = new BehaviorSubject([ + { + id: '6ef856c0-5f86-11ef-b7df-1bb1cf26ce5b', + label: 'visualizeMock', + link: '/app/visualize', + workspaceId: 'workspace_1', + meta: { type: 'visualization' }, + }, +]); + +const mockWorkspaceList = new BehaviorSubject([ + { + id: 'workspace_1', + name: 'WorkspaceMock_1', + }, +]); + +const savedObjectsFromServer: SavedObjectWithMetadata[] = [ + { + type: 'visualization', + id: '6ef856c0-5f86-11ef-b7df-1bb1cf26ce5b', + attributes: {}, + references: [], + updated_at: '2024-07-20T00:00:00.000Z', + meta: {}, + }, +]; const defaultMockProps = { navigateToUrl: applicationServiceMock.createStartContract().navigateToUrl, workspaceList$: new BehaviorSubject([]), recentlyAccessed$: new BehaviorSubject([]), - navLinks$: new BehaviorSubject([]), - basePath: httpServiceMock.createStartContract().basePath, + http: httpServiceMock.createSetupContract(), renderBreadcrumbs: <>, }; -const setup = (props: Props) => { - return render(); -}; + +jest.spyOn(defaultMockProps.http, 'get').mockImplementation( + (url): Promise => { + if (typeof url === 'string') { + if ((url as string).includes('6ef856c0-5f86-11ef-b7df-1bb1cf26ce5b')) { + return Promise.resolve(savedObjectsFromServer[0]); + } else { + return Promise.resolve(savedObjectsFromServer[1]); + } + } + return Promise.reject(new Error('Invalid URL')); + } +); + describe('Recent items', () => { - it('should render base element normally', () => { - const { baseElement } = setup(defaultMockProps); - expect(baseElement).toMatchSnapshot(); + it('render with empty recent work', () => { + const { getByText, getByTestId } = render(); + const mockRecentButton = getByTestId('recentItemsSectionButton'); + fireEvent.click(mockRecentButton); + expect(getByText('No recently viewed items')).toBeInTheDocument(); }); - 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({ + it('should be able to render recent works', async () => { + const mockProps = { ...defaultMockProps, - workspaceList$, - recentlyAccessed$, - navigateToUrl: defaultMockProps.navigateToUrl, + recentlyAccessed$: mockRecentlyAccessed, + }; + + let getByText: (text: string) => HTMLElement; + let getByTestId: (testId: string) => HTMLElement; + + await act(async () => { + const renderResult = render(); + getByText = renderResult.getByText; + getByTestId = renderResult.getByTestId; }); - const button = screen.getByTestId('recentItemsSectionButton'); - fireEvent.click(button); - expect(screen.getByText('(workspace_name)')).toBeInTheDocument(); + + const mockRecentButton = getByTestId!('recentItemsSectionButton'); + fireEvent.click(mockRecentButton); + expect(getByText!('visualizeMock')).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(); - const renderBreadcrumbs = <>; - setup({ + it('shoulde be able to display workspace name if the asset is attched to a workspace and render it with brackets wrapper ', async () => { + const mockProps = { + ...defaultMockProps, + recentlyAccessed$: mockRecentlyAccessed, + workspaceList$: mockWorkspaceList, + }; + + let getByText: (text: string) => HTMLElement; + let getByTestId: (testId: string) => HTMLElement; + + await act(async () => { + const renderResult = render(); + getByText = renderResult.getByText; + getByTestId = renderResult.getByTestId; + }); + + const mockRecentButton = getByTestId!('recentItemsSectionButton'); + fireEvent.click(mockRecentButton); + expect(getByText!('(WorkspaceMock_1)')).toBeInTheDocument(); + }); + + it('should call navigateToUrl with link generated from createRecentNavLink when clicking a recent item', async () => { + const mockProps = { ...defaultMockProps, - workspaceList$, - recentlyAccessed$, - navigateToUrl, - renderBreadcrumbs, + recentlyAccessed$: mockRecentlyAccessed, + workspaceList$: mockWorkspaceList, + }; + + let getByText: (text: string) => HTMLElement; + let getByTestId: (testId: string) => HTMLElement; + const navigateToUrl = jest.fn(); + + await act(async () => { + const renderResult = render(); + getByText = renderResult.getByText; + getByTestId = renderResult.getByTestId; }); + const button = screen.getByTestId('recentItemsSectionButton'); fireEvent.click(button); - const item = screen.getByText('item_label'); + const item = screen.getByText('visualizeMock'); expect(navigateToUrl).not.toHaveBeenCalled(); fireEvent.click(item); - expect(navigateToUrl).toHaveBeenCalledWith('/recent_nav_link'); + expect(navigateToUrl).toHaveBeenCalledWith('/app/visualize'); + }); + + it('should be able to display the preferences popover setting when clicking Preferences button', async () => { + const mockProps = { + ...defaultMockProps, + recentlyAccessed$: mockRecentlyAccessed, + }; + let getByText: (text: string) => HTMLElement; + let getByTestId: (testId: string) => HTMLElement; + + await act(async () => { + const renderResult = render(); + getByText = renderResult.getByText; + getByTestId = renderResult.getByTestId; + }); + const button = screen.getByTestId('recentItemsSectionButton'); + fireEvent.click(button); + + const preferencesButton = screen.getByTestId('preferencesSettingButton'); + fireEvent.click(preferencesButton); + expect(getByTestId!('preferencesSettingPopover')).toBeInTheDocument(); }); }); diff --git a/src/core/public/chrome/ui/header/recent_items.tsx b/src/core/public/chrome/ui/header/recent_items.tsx index c7894104f16..a545cf6951b 100644 --- a/src/core/public/chrome/ui/header/recent_items.tsx +++ b/src/core/public/chrome/ui/header/recent_items.tsx @@ -2,76 +2,129 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import * as Rx from 'rxjs'; +import moment from 'moment'; import { - EuiPopover, - EuiTextColor, + EuiPanel, EuiListGroup, EuiListGroupItem, EuiTitle, + EuiPopoverTitle, + EuiIcon, EuiText, + EuiButtonEmpty, EuiSpacer, EuiHeaderSectionItemButtonProps, EuiButtonIcon, + EuiRadioGroup, + EuiTextColor, + EuiPopover, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import useObservable from 'react-use/lib/useObservable'; -import { ChromeRecentlyAccessedHistoryItem } from '../..'; +import { SavedObjectsNamespaceType } from 'src/core/public'; +import { ChromeRecentlyAccessedHistoryItem, SavedObject } from 'opensearch-dashboards/public'; import { WorkspaceObject } from '../../../workspace'; -import { createRecentNavLink } from './nav_link'; import { HttpStart } from '../../../http'; -import { ChromeNavLink } from '../../../'; import './recent_items.scss'; +const widthForRightMargin = 8; + export interface Props { recentlyAccessed$: Rx.Observable; workspaceList$: Rx.Observable; navigateToUrl: (url: string) => Promise; - basePath: HttpStart['basePath']; - navLinks$: Rx.Observable; renderBreadcrumbs: React.JSX.Element; buttonSize?: EuiHeaderSectionItemButtonProps['size']; + http: HttpStart; } +interface SavedObjectMetadata { + icon?: string; + title?: string; + editUrl?: string; + inAppUrl?: { path: string; uiCapabilitiesPath: string }; + namespaceType?: SavedObjectsNamespaceType; +} +export type SavedObjectWithMetadata = SavedObject & { + meta: SavedObjectMetadata; +}; + +type DetailedRecentlyAccessedItem = ChromeRecentlyAccessedHistoryItem & + SavedObjectWithMetadata & + ChromeRecentlyAccessedHistoryItem['meta'] & { + updatedAt: number; + workspaceName?: string; + }; + +export const bulkGetDetail = ( + savedObjects: Array>, + http: HttpStart +) => { + return Promise.all( + savedObjects.map((obj) => + http + .get( + `/api/opensearch-dashboards/management/saved_objects/${encodeURIComponent( + obj.type + )}/${encodeURIComponent(obj.id)}` + ) + .catch((error) => ({ + id: obj.id, + type: obj.type, + error, + attributes: {}, + references: [], + meta: {}, + updated_at: '', + })) + ) + ); +}; + +const recentsRadios = [ + { + id: 'recent_5', + label: '5 pages', + }, + { + id: 'recent_10', + label: '10 pages', + }, + { + id: 'recent_15', + label: '15 pages', + }, +]; + export const RecentItems = ({ recentlyAccessed$, workspaceList$, navigateToUrl, - navLinks$, - basePath, renderBreadcrumbs, buttonSize = 's', + http, }: Props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - + const [isPreferencesPopoverOpen, setIsPreferencesPopoverOpen] = useState(false); const recentlyAccessedItems = useObservable(recentlyAccessed$, []); + const [recentsRadioIdSelected, setRecentsRadioIdSelected] = useState('recent_5'); const workspaceList = useObservable(workspaceList$, []); - const navLinks = useObservable(navLinks$, []).filter((link) => !link.hidden); - - const items = useMemo(() => { - // Only display five most recent 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 [detailedSavedObjects, setDetailedSavedObjects] = useState( + [] + ); const handleItemClick = (link: string) => { navigateToUrl(link); setIsPopoverOpen(false); }; - const button = ( + const recentButton = ( ); + useEffect(() => { + const savedObjects = recentlyAccessedItems + .filter((item) => item.meta?.type) + .map((item) => ({ + type: item.meta?.type || '', + id: item.id, + })); + + if (savedObjects.length) { + bulkGetDetail(savedObjects, http).then((res) => { + const formatDetailedSavedObjects = res.map((obj) => { + const recentAccessItem = recentlyAccessedItems.find( + (item) => item.id === obj.id + ) as ChromeRecentlyAccessedHistoryItem; + + const findWorkspace = workspaceList.find( + (workspace) => workspace.id === recentAccessItem.workspaceId + ); + + return { + ...recentAccessItem, + ...obj, + ...recentAccessItem.meta, + updatedAt: moment(obj?.updated_at).valueOf(), + workspaceName: findWorkspace?.name, + }; + }); + setDetailedSavedObjects(formatDetailedSavedObjects); + }); + } + }, [recentlyAccessedItems, http, workspaceList]); + + const selectedRecentsItems = useMemo(() => { + return detailedSavedObjects.slice(0, Number(recentsRadioIdSelected.split('recent_')[1])); + }, [detailedSavedObjects, recentsRadioIdSelected]); + return ( { setIsPopoverOpen(false); @@ -104,36 +193,86 @@ export const RecentItems = ({ anchorPosition="downCenter" repositionOnScroll initialFocus={false} - panelPaddingSize="s" + panelPaddingSize="m" > {renderBreadcrumbs} + + +

Recent

+
+ + {selectedRecentsItems.length > 0 ? ( + + {selectedRecentsItems.map((item) => ( + handleItemClick(item.link)} + key={item.link} + style={{ padding: '1px' }} + label={ + <> + + {item.label} + {item.workspaceName ? ( + ({item.workspaceName}) + ) : null} + + } + color="text" + size="s" + /> + ))} + + ) : ( + No recently viewed items + )} + - -

Recents

-
- {items.length > 0 ? ( - - {items.map((item) => ( - handleItemClick(item.link)} - key={item.link} - label={ - <> - {item.label} - {item.workspaceName ? ( - ({item.workspaceName}) - ) : null} - - } - color="text" - size="s" - /> - ))} - - ) : ( - No recently viewed items - )} + { + setIsPreferencesPopoverOpen((IsPreferencesPopoverOpe) => !IsPreferencesPopoverOpe); + }} + > + Preferences + + } + isOpen={isPreferencesPopoverOpen} + anchorPosition="downLeft" + closePopover={() => { + setIsPreferencesPopoverOpen(false); + }} + > + Preferences + { + setRecentsRadioIdSelected(id); + setIsPreferencesPopoverOpen(false); + }} + name="radio group" + legend={{ + children: Recents, + }} + /> + +
); }; From e679066ada2e4a88a094ce7b576176b26616c994 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 22 Aug 2024 17:30:03 +0800 Subject: [PATCH 2/9] solve bugs and delete annotation Signed-off-by: Qxisylolo --- src/core/public/chrome/chrome_service.tsx | 2 - .../chrome/ui/header/recent_items.test.tsx | 37 +++++-------------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 0f4276b1f2b..e737ecec289 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -43,7 +43,6 @@ import { } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { EuiLink } from '@elastic/eui'; -import { CoreStart } from 'opensearch-dashboards/public'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; @@ -116,7 +115,6 @@ export interface StartDeps { application: InternalApplicationStart; docLinks: DocLinksStart; http: HttpStart; - // core: CoreStart; injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; diff --git a/src/core/public/chrome/ui/header/recent_items.test.tsx b/src/core/public/chrome/ui/header/recent_items.test.tsx index 186f9adc310..5b2ce099819 100644 --- a/src/core/public/chrome/ui/header/recent_items.test.tsx +++ b/src/core/public/chrome/ui/header/recent_items.test.tsx @@ -80,18 +80,13 @@ describe('Recent items', () => { recentlyAccessed$: mockRecentlyAccessed, }; - let getByText: (text: string) => HTMLElement; - let getByTestId: (testId: string) => HTMLElement; - await act(async () => { - const renderResult = render(); - getByText = renderResult.getByText; - getByTestId = renderResult.getByTestId; + render(); }); - const mockRecentButton = getByTestId!('recentItemsSectionButton'); + const mockRecentButton = screen.getByTestId('recentItemsSectionButton'); fireEvent.click(mockRecentButton); - expect(getByText!('visualizeMock')).toBeInTheDocument(); + expect(screen.getByText('visualizeMock')).toBeInTheDocument(); }); it('shoulde be able to display workspace name if the asset is attched to a workspace and render it with brackets wrapper ', async () => { @@ -101,18 +96,13 @@ describe('Recent items', () => { workspaceList$: mockWorkspaceList, }; - let getByText: (text: string) => HTMLElement; - let getByTestId: (testId: string) => HTMLElement; - await act(async () => { - const renderResult = render(); - getByText = renderResult.getByText; - getByTestId = renderResult.getByTestId; + render(); }); - const mockRecentButton = getByTestId!('recentItemsSectionButton'); + const mockRecentButton = screen.getByTestId('recentItemsSectionButton'); fireEvent.click(mockRecentButton); - expect(getByText!('(WorkspaceMock_1)')).toBeInTheDocument(); + expect(screen.getByText('(WorkspaceMock_1)')).toBeInTheDocument(); }); it('should call navigateToUrl with link generated from createRecentNavLink when clicking a recent item', async () => { @@ -122,14 +112,10 @@ describe('Recent items', () => { workspaceList$: mockWorkspaceList, }; - let getByText: (text: string) => HTMLElement; - let getByTestId: (testId: string) => HTMLElement; const navigateToUrl = jest.fn(); await act(async () => { - const renderResult = render(); - getByText = renderResult.getByText; - getByTestId = renderResult.getByTestId; + render(); }); const button = screen.getByTestId('recentItemsSectionButton'); @@ -145,19 +131,16 @@ describe('Recent items', () => { ...defaultMockProps, recentlyAccessed$: mockRecentlyAccessed, }; - let getByText: (text: string) => HTMLElement; - let getByTestId: (testId: string) => HTMLElement; await act(async () => { - const renderResult = render(); - getByText = renderResult.getByText; - getByTestId = renderResult.getByTestId; + render(); }); + const button = screen.getByTestId('recentItemsSectionButton'); fireEvent.click(button); const preferencesButton = screen.getByTestId('preferencesSettingButton'); fireEvent.click(preferencesButton); - expect(getByTestId!('preferencesSettingPopover')).toBeInTheDocument(); + expect(screen.getByTestId('preferencesSettingPopover')).toBeInTheDocument(); }); }); From 9d358b6f06238eadbe25e3e4a376578ab0921b0d Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 22 Aug 2024 18:05:17 +0800 Subject: [PATCH 3/9] solve bugs Signed-off-by: Qxisylolo --- .../public/chrome/ui/header/recent_items.tsx | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/src/core/public/chrome/ui/header/recent_items.tsx b/src/core/public/chrome/ui/header/recent_items.tsx index a545cf6951b..0d1e61ce398 100644 --- a/src/core/public/chrome/ui/header/recent_items.tsx +++ b/src/core/public/chrome/ui/header/recent_items.tsx @@ -59,10 +59,7 @@ type DetailedRecentlyAccessedItem = ChromeRecentlyAccessedHistoryItem & workspaceName?: string; }; -export const bulkGetDetail = ( - savedObjects: Array>, - http: HttpStart -) => { +const bulkGetDetail = (savedObjects: Array>, http: HttpStart) => { return Promise.all( savedObjects.map((obj) => http @@ -86,15 +83,15 @@ export const bulkGetDetail = ( const recentsRadios = [ { - id: 'recent_5', + id: '5', label: '5 pages', }, { - id: 'recent_10', + id: '10', label: '10 pages', }, { - id: 'recent_15', + id: '15', label: '15 pages', }, ]; @@ -110,7 +107,7 @@ export const RecentItems = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isPreferencesPopoverOpen, setIsPreferencesPopoverOpen] = useState(false); const recentlyAccessedItems = useObservable(recentlyAccessed$, []); - const [recentsRadioIdSelected, setRecentsRadioIdSelected] = useState('recent_5'); + const [recentsRadioIdSelected, setRecentsRadioIdSelected] = useState(recentsRadios[0].id); const workspaceList = useObservable(workspaceList$, []); const [detailedSavedObjects, setDetailedSavedObjects] = useState( [] @@ -121,6 +118,44 @@ export const RecentItems = ({ setIsPopoverOpen(false); }; + const preferencesPopover = ( + { + setIsPreferencesPopoverOpen((IsPreferencesPopoverOpe) => !IsPreferencesPopoverOpe); + }} + > + Preferences + + } + isOpen={isPreferencesPopoverOpen} + anchorPosition="downLeft" + closePopover={() => { + setIsPreferencesPopoverOpen(false); + }} + > + Preferences + { + setRecentsRadioIdSelected(id); + setIsPreferencesPopoverOpen(false); + }} + name="radio group" + legend={{ + children: Recents, + }} + /> + + ); const recentButton = ( { - return detailedSavedObjects.slice(0, Number(recentsRadioIdSelected.split('recent_')[1])); + return detailedSavedObjects.slice(0, Number(recentsRadioIdSelected)); }, [detailedSavedObjects, recentsRadioIdSelected]); return ( @@ -232,47 +267,13 @@ export const RecentItems = ({ ))} ) : ( - No recently viewed items + + No recently viewed items + )} - - { - setIsPreferencesPopoverOpen((IsPreferencesPopoverOpe) => !IsPreferencesPopoverOpe); - }} - > - Preferences - - } - isOpen={isPreferencesPopoverOpen} - anchorPosition="downLeft" - closePopover={() => { - setIsPreferencesPopoverOpen(false); - }} - > - Preferences - { - setRecentsRadioIdSelected(id); - setIsPreferencesPopoverOpen(false); - }} - name="radio group" - legend={{ - children: Recents, - }} - /> - + {preferencesPopover} ); }; From f58a7bf596b8e71470dc2477f746eb154e902e82 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 22 Aug 2024 18:29:41 +0800 Subject: [PATCH 4/9] fix bugs Signed-off-by: Qxisylolo --- src/core/public/chrome/ui/header/recent_items.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/public/chrome/ui/header/recent_items.tsx b/src/core/public/chrome/ui/header/recent_items.tsx index 0d1e61ce398..355534a7382 100644 --- a/src/core/public/chrome/ui/header/recent_items.tsx +++ b/src/core/public/chrome/ui/header/recent_items.tsx @@ -118,7 +118,7 @@ export const RecentItems = ({ setIsPopoverOpen(false); }; - const preferencesPopover = ( + const preferencePopover = ( - {preferencesPopover} + {preferencePopover} ); }; From d9d1ff6ed6598cc69e2168f4958d37dd92e2a25c Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Fri, 23 Aug 2024 13:39:05 +0800 Subject: [PATCH 5/9] fix bugs-1 Signed-off-by: Qxisylolo --- src/core/public/chrome/ui/header/recent_items.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/public/chrome/ui/header/recent_items.test.tsx b/src/core/public/chrome/ui/header/recent_items.test.tsx index 5b2ce099819..809807c59c2 100644 --- a/src/core/public/chrome/ui/header/recent_items.test.tsx +++ b/src/core/public/chrome/ui/header/recent_items.test.tsx @@ -67,6 +67,11 @@ jest.spyOn(defaultMockProps.http, 'get').mockImplementation( ); describe('Recent items', () => { + it('should render base element normally', () => { + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); + }); + it('render with empty recent work', () => { const { getByText, getByTestId } = render(); const mockRecentButton = getByTestId('recentItemsSectionButton'); From 1235c929b635c24951ab230bf158e417dde5fbd4 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Tue, 27 Aug 2024 09:49:59 +0800 Subject: [PATCH 6/9] fix bugs-delete-lines Signed-off-by: Qxisylolo --- src/core/public/chrome/ui/header/header.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index b31398c72de..c29cfd898d9 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -357,8 +357,6 @@ export function Header({ recentlyAccessed$={observables.recentlyAccessed$} workspaceList$={observables.workspaceList$} navigateToUrl={application.navigateToUrl} - // navLinks$={observables.navLinks$} - // basePath={basePath} renderBreadcrumbs={renderBreadcrumbs(true)} buttonSize={useApplicationHeader ? 's' : 'xs'} /> From c3f1f41f9acf34fb32b6a3717e39dd4cd7f11d0c Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Tue, 27 Aug 2024 16:27:31 +0800 Subject: [PATCH 7/9] fix refine-popover-padding Signed-off-by: Qxisylolo --- src/core/public/chrome/ui/header/recent_items.test.tsx | 8 -------- src/core/public/chrome/ui/header/recent_items.tsx | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/core/public/chrome/ui/header/recent_items.test.tsx b/src/core/public/chrome/ui/header/recent_items.test.tsx index 809807c59c2..0b6f45b0637 100644 --- a/src/core/public/chrome/ui/header/recent_items.test.tsx +++ b/src/core/public/chrome/ui/header/recent_items.test.tsx @@ -10,14 +10,6 @@ import { applicationServiceMock, httpServiceMock } from '../../../mocks'; import { SavedObjectWithMetadata } from './recent_items'; import { RecentItems } from './recent_items'; -jest.mock('./nav_link', () => ({ - createRecentNavLink: jest.fn().mockImplementation(() => { - return { - href: '/recent_nav_link', - }; - }), -})); - const mockRecentlyAccessed = new BehaviorSubject([ { id: '6ef856c0-5f86-11ef-b7df-1bb1cf26ce5b', diff --git a/src/core/public/chrome/ui/header/recent_items.tsx b/src/core/public/chrome/ui/header/recent_items.tsx index 355534a7382..280115fb82b 100644 --- a/src/core/public/chrome/ui/header/recent_items.tsx +++ b/src/core/public/chrome/ui/header/recent_items.tsx @@ -122,7 +122,7 @@ export const RecentItems = ({ Date: Tue, 27 Aug 2024 18:54:34 +0800 Subject: [PATCH 8/9] fix, add createRecentNavLink Signed-off-by: Qxisylolo --- .../header/__snapshots__/header.test.tsx.snap | 342 ++++++++++++++++++ src/core/public/chrome/ui/header/header.tsx | 2 + .../chrome/ui/header/recent_items.test.tsx | 12 +- .../public/chrome/ui/header/recent_items.tsx | 24 +- 4 files changed, 376 insertions(+), 4 deletions(-) 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 c1f78575fe7..3c5c3584c08 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 @@ -8290,6 +8290,43 @@ exports[`Header renders application header without title and breadcrumbs 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, ], "thrownError": null, } @@ -8624,6 +8661,17 @@ exports[`Header renders application header without title and breadcrumbs 1`] = ` className="euiHeaderSectionItem" > ({ + createRecentNavLink: jest.fn().mockImplementation(() => { + return { + href: '/recent_nav_link', + }; + }), +})); + const mockRecentlyAccessed = new BehaviorSubject([ { id: '6ef856c0-5f86-11ef-b7df-1bb1cf26ce5b', @@ -41,6 +49,8 @@ const defaultMockProps = { navigateToUrl: applicationServiceMock.createStartContract().navigateToUrl, workspaceList$: new BehaviorSubject([]), recentlyAccessed$: new BehaviorSubject([]), + navLinks$: new BehaviorSubject([]), + basePath: httpServiceMock.createStartContract().basePath, http: httpServiceMock.createSetupContract(), renderBreadcrumbs: <>, }; @@ -120,7 +130,7 @@ describe('Recent items', () => { const item = screen.getByText('visualizeMock'); expect(navigateToUrl).not.toHaveBeenCalled(); fireEvent.click(item); - expect(navigateToUrl).toHaveBeenCalledWith('/app/visualize'); + expect(navigateToUrl).toHaveBeenCalledWith('/recent_nav_link'); }); it('should be able to display the preferences popover setting when clicking Preferences button', async () => { diff --git a/src/core/public/chrome/ui/header/recent_items.tsx b/src/core/public/chrome/ui/header/recent_items.tsx index 280115fb82b..153193780a2 100644 --- a/src/core/public/chrome/ui/header/recent_items.tsx +++ b/src/core/public/chrome/ui/header/recent_items.tsx @@ -28,6 +28,8 @@ import { SavedObjectsNamespaceType } from 'src/core/public'; import { ChromeRecentlyAccessedHistoryItem, SavedObject } from 'opensearch-dashboards/public'; import { WorkspaceObject } from '../../../workspace'; import { HttpStart } from '../../../http'; +import { createRecentNavLink } from './nav_link'; +import { ChromeNavLink } from '../../../'; import './recent_items.scss'; const widthForRightMargin = 8; @@ -36,6 +38,8 @@ export interface Props { recentlyAccessed$: Rx.Observable; workspaceList$: Rx.Observable; navigateToUrl: (url: string) => Promise; + basePath: HttpStart['basePath']; + navLinks$: Rx.Observable; renderBreadcrumbs: React.JSX.Element; buttonSize?: EuiHeaderSectionItemButtonProps['size']; http: HttpStart; @@ -100,6 +104,8 @@ export const RecentItems = ({ recentlyAccessed$, workspaceList$, navigateToUrl, + navLinks$, + basePath, renderBreadcrumbs, buttonSize = 's', http, @@ -112,6 +118,7 @@ export const RecentItems = ({ const [detailedSavedObjects, setDetailedSavedObjects] = useState( [] ); + const navLinks = useObservable(navLinks$, []).filter((link) => !link.hidden); const handleItemClick = (link: string) => { navigateToUrl(link); @@ -200,19 +207,30 @@ export const RecentItems = ({ const findWorkspace = workspaceList.find( (workspace) => workspace.id === recentAccessItem.workspaceId ); - return { ...recentAccessItem, ...obj, ...recentAccessItem.meta, updatedAt: moment(obj?.updated_at).valueOf(), workspaceName: findWorkspace?.name, + link: createRecentNavLink(recentAccessItem, navLinks, basePath, navigateToUrl).href, }; }); - setDetailedSavedObjects(formatDetailedSavedObjects); + // here I write this argument to avoid Unnecessary re-rendering + if (JSON.stringify(formatDetailedSavedObjects) !== JSON.stringify(detailedSavedObjects)) { + setDetailedSavedObjects(formatDetailedSavedObjects); + } }); } - }, [recentlyAccessedItems, http, workspaceList]); + }, [ + navLinks, + basePath, + navigateToUrl, + recentlyAccessedItems, + http, + workspaceList, + detailedSavedObjects, + ]); const selectedRecentsItems = useMemo(() => { return detailedSavedObjects.slice(0, Number(recentsRadioIdSelected)); From 69fb6d551ad7bf555f0bc5c0da80263e27776b40 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:40:48 +0000 Subject: [PATCH 9/9] Changeset file for PR #7805 created/updated --- changelogs/fragments/7805.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7805.yml diff --git a/changelogs/fragments/7805.yml b/changelogs/fragments/7805.yml new file mode 100644 index 00000000000..f4a1832c142 --- /dev/null +++ b/changelogs/fragments/7805.yml @@ -0,0 +1,2 @@ +feat: +- Refractor the style of recent items card ([#7805](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7805)) \ No newline at end of file