From a05c4a2e5b44a0811d937a5069b53337882da1d0 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 5 Sep 2024 17:57:37 +0800 Subject: [PATCH] [Workspace] Content menu picker in side bar and enable searching (#7881) * 01-feat/refractor-content-menu-picker-in-side-bar Signed-off-by: Qxisylolo * 02-enable-associated-icon Signed-off-by: Qxisylolo * 03-update-side-bar Signed-off-by: Qxisylolo * 04-fix-bugs Signed-off-by: Qxisylolo * 04-fix-bugs, add test Signed-off-by: Qxisylolo * 05-delete search Signed-off-by: Qxisylolo * 06-delete search Signed-off-by: Qxisylolo * 06-fix-bug Signed-off-by: Qxisylolo * 07-fix-conflication-1 Signed-off-by: Qxisylolo * 07-fix-conflication-adjust-icon-size Signed-off-by: Qxisylolo * Changeset file for PR #7881 created/updated --------- Signed-off-by: Qxisylolo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/7881.yml | 2 + .../workspace_menu/workspace_menu.test.tsx | 75 ++++++---- .../workspace_menu/workspace_menu.tsx | 137 ++++++++---------- .../workspace_picker_content.tsx | 133 ++++++++++++++--- 4 files changed, 220 insertions(+), 127 deletions(-) create mode 100644 changelogs/fragments/7881.yml diff --git a/changelogs/fragments/7881.yml b/changelogs/fragments/7881.yml new file mode 100644 index 000000000000..49da3e4fa2e5 --- /dev/null +++ b/changelogs/fragments/7881.yml @@ -0,0 +1,2 @@ +feat: +- Refactor content menu picker in side bar and enable searching ([#7881](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7881)) \ No newline at end of file diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx index 39acffd0c42d..a0afcf3eb8bb 100644 --- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx +++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx @@ -12,7 +12,6 @@ import { CoreStart, DEFAULT_NAV_GROUPS } from '../../../../../core/public'; import { BehaviorSubject } from 'rxjs'; import { IntlProvider } from 'react-intl'; import { recentWorkspaceManager } from '../../recent_workspace_manager'; -import * as workspaceUtils from '../utils/workspace'; describe('', () => { let coreStartMock: CoreStart; @@ -91,7 +90,52 @@ describe('', () => { expect(screen.getByTestId('workspace-menu-item-recent-workspace-2')).toBeInTheDocument(); }); - it('should display current workspace name and use case name', () => { + it('should be able to display empty state when the workspace list is empty', () => { + coreStartMock.workspaces.workspaceList$.next([]); + render(); + const selectButton = screen.getByTestId('workspace-select-button'); + fireEvent.click(selectButton); + expect(screen.getByText(/no workspace available/i)).toBeInTheDocument(); + }); + + it('should be able to perform search and filter and the results will be shown in both all and recent section', () => { + coreStartMock.workspaces.workspaceList$.next([ + { id: 'workspace-1', name: 'workspace 1', features: [] }, + { id: 'test-2', name: 'test 2', features: [] }, + ]); + jest + .spyOn(recentWorkspaceManager, 'getRecentWorkspaces') + .mockReturnValue([{ id: 'workspace-1', timestamp: 1234567890 }]); + render(); + + const selectButton = screen.getByTestId('workspace-select-button'); + fireEvent.click(selectButton); + + const searchInput = screen.getByRole('searchbox'); + fireEvent.change(searchInput, { target: { value: 'works' } }); + expect(screen.getByTestId('workspace-menu-item-recent-workspace-1')).toBeInTheDocument(); + expect(screen.getByTestId('workspace-menu-item-recent-workspace-1')).toBeInTheDocument(); + }); + + it('should be able to display empty state when seach is not found', () => { + coreStartMock.workspaces.workspaceList$.next([ + { id: 'workspace-1', name: 'workspace 1', features: [] }, + { id: 'test-2', name: 'test 2', features: [] }, + ]); + jest + .spyOn(recentWorkspaceManager, 'getRecentWorkspaces') + .mockReturnValue([{ id: 'workspace-1', timestamp: 1234567890 }]); + render(); + + const selectButton = screen.getByTestId('workspace-select-button'); + fireEvent.click(selectButton); + + const searchInput = screen.getByRole('searchbox'); + fireEvent.change(searchInput, { target: { value: 'noitems' } }); + expect(screen.getByText(/no workspace available/i)).toBeInTheDocument(); + }); + + it('should display current workspace name, use case name and associated icon', () => { coreStartMock.workspaces.currentWorkspace$.next({ id: 'workspace-1', name: 'workspace 1', @@ -102,6 +146,7 @@ describe('', () => { fireEvent.click(screen.getByTestId('workspace-select-button')); expect(screen.getByTestId('workspace-menu-current-workspace-name')).toBeInTheDocument(); expect(screen.getByTestId('workspace-menu-current-use-case')).toBeInTheDocument(); + expect(screen.getByTestId('current-workspace-icon-wsObservability')).toBeInTheDocument(); expect(screen.getByText('Observability')).toBeInTheDocument(); }); @@ -155,28 +200,6 @@ describe('', () => { }); }); - it('should navigate to workspace management page', () => { - coreStartMock.workspaces.currentWorkspace$.next({ - id: 'workspace-1', - name: 'workspace 1', - features: ['use-case-observability'], - }); - const navigateToWorkspaceDetail = jest.spyOn(workspaceUtils, 'navigateToWorkspaceDetail'); - render(); - - fireEvent.click(screen.getByTestId('workspace-select-button')); - const button = screen.getByText(/Manage workspace/i); - fireEvent.click(button); - expect(navigateToWorkspaceDetail).toBeCalled(); - }); - - it('should navigate to workspaces management page', () => { - render(); - fireEvent.click(screen.getByTestId('workspace-select-button')); - fireEvent.click(screen.getByText(/manage workspaces/i)); - expect(coreStartMock.application.navigateToApp).toHaveBeenCalledWith('workspace_list'); - }); - it('should navigate to create workspace page', () => { render(); fireEvent.click(screen.getByTestId('workspace-select-button')); @@ -188,7 +211,7 @@ describe('', () => { render(); fireEvent.click(screen.getByTestId('workspace-select-button')); - fireEvent.click(screen.getByText(/View all/i)); + fireEvent.click(screen.getByText(/manage/i)); expect(coreStartMock.application.navigateToApp).toHaveBeenCalledWith('workspace_list'); }); @@ -203,7 +226,7 @@ describe('', () => { render(); fireEvent.click(screen.getByTestId('workspace-select-button')); - expect(screen.getByText(/View all/i)).toBeInTheDocument(); + expect(screen.queryByText(/manage/i)).not.toBeInTheDocument(); expect(screen.queryByText(/create workspaces/i)).toBeNull(); }); }); diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx index d70b6c4419a0..bf36fc3844ff 100644 --- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx +++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx @@ -9,21 +9,20 @@ import { useObservable } from 'react-use'; import { EuiText, EuiPanel, + EuiButton, EuiPopover, - EuiToolTip, - EuiFlexItem, - EuiFlexGroup, - EuiSmallButtonEmpty, - EuiSmallButton, EuiButtonIcon, + EuiFlexItem, EuiIcon, + EuiFlexGroup, + EuiHorizontalRule, + EuiButtonEmpty, } from '@elastic/eui'; import { BehaviorSubject } from 'rxjs'; import { WORKSPACE_CREATE_APP_ID, WORKSPACE_LIST_APP_ID } from '../../../common/constants'; import { CoreStart, WorkspaceObject } from '../../../../../core/public'; import { getFirstUseCaseOfFeatureConfigs } from '../../utils'; import { WorkspaceUseCase } from '../../types'; -import { navigateToWorkspaceDetail } from '../utils/workspace'; import { validateWorkspaceColor } from '../../../common/utils'; import { WorkspacePickerContent } from '../workspace_picker_content/workspace_picker_content'; @@ -35,16 +34,8 @@ const createWorkspaceButton = i18n.translate('workspace.menu.button.createWorksp defaultMessage: 'Create workspace', }); -const viewAllButton = i18n.translate('workspace.menu.button.viewAll', { - defaultMessage: 'View all', -}); - -const manageWorkspaceButton = i18n.translate('workspace.menu.button.manageWorkspace', { - defaultMessage: 'Manage workspace', -}); - const manageWorkspacesButton = i18n.translate('workspace.menu.button.manageWorkspaces', { - defaultMessage: 'Manage workspaces', + defaultMessage: 'Manage', }); const getValidWorkspaceColor = (color?: string) => @@ -97,8 +88,9 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { closePopover={closePopover} panelPaddingSize="s" anchorPosition="downCenter" + repositionOnScroll={true} > - + { <> - - - - {currentWorkspaceName} - - - - - {getUseCase(currentWorkspace)?.title ?? ''} - - - { - closePopover(); - navigateToWorkspaceDetail(coreStart, currentWorkspace.id); - }} + + {currentWorkspaceName} + - {manageWorkspaceButton} - + {getUseCase(currentWorkspace)?.title ?? ''} + ) : ( <> - + - {currentWorkspaceName} - - - { - closePopover(); - coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID); - }} - > - {manageWorkspacesButton} - + {currentWorkspaceName} )} - + + setPopover(false)} /> - - - - { - closePopover(); - coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID); - }} - > - {viewAllButton} - - - {isDashboardAdmin && ( + + {isDashboardAdmin && ( + + + + + { + closePopover(); + coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID); + }} + > + {manageWorkspacesButton} + + + - { @@ -197,12 +180,12 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { coreStart.application.navigateToApp(WORKSPACE_CREATE_APP_ID); }} > - {createWorkspaceButton} - + {createWorkspaceButton} + - )} - - + + + )} ); }; diff --git a/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx index 8aa9e8e97823..72fdd67b7367 100644 --- a/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx +++ b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx @@ -2,13 +2,22 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - import { i18n } from '@osd/i18n'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useObservable } from 'react-use'; -import { EuiTitle, EuiListGroup, EuiListGroupItem, EuiIcon } from '@elastic/eui'; +import { + EuiTitle, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiFieldSearch, + EuiListGroup, + EuiListGroupItem, + EuiEmptyPrompt, +} from '@elastic/eui'; + import { BehaviorSubject } from 'rxjs'; -import { MAX_WORKSPACE_PICKER_NUM } from '../../../common/constants'; import { CoreStart, WorkspaceObject } from '../../../../../core/public'; import { recentWorkspaceManager } from '../../recent_workspace_manager'; import { WorkspaceUseCase } from '../../types'; @@ -19,6 +28,10 @@ const allWorkspacesTitle = i18n.translate('workspace.menu.title.allWorkspaces', defaultMessage: 'All workspaces', }); +const searchFieldPlaceholder = i18n.translate('workspace.menu.search.placeholder', { + defaultMessage: 'Search workspace name', +}); + const recentWorkspacesTitle = i18n.translate('workspace.menu.title.recentWorkspaces', { defaultMessage: 'Recent workspaces', }); @@ -38,20 +51,39 @@ export const WorkspacePickerContent = ({ onClickWorkspace, }: Props) => { const workspaceList = useObservable(coreStart.workspaces.workspaceList$, []); + const isDashboardAdmin = coreStart.application.capabilities?.dashboards?.isDashboardAdmin; const availableUseCases = useObservable(registeredUseCases$, []); + const [search, setSearch] = useState(''); - const filteredWorkspaceList = useMemo(() => { - return workspaceList.slice(0, MAX_WORKSPACE_PICKER_NUM); - }, [workspaceList]); - - const filteredRecentWorkspaces = useMemo(() => { + const recentWorkspaces = useMemo(() => { return recentWorkspaceManager .getRecentWorkspaces() .map((workspace) => workspaceList.find((ws) => ws.id === workspace.id)) - .filter((workspace): workspace is WorkspaceObject => workspace !== undefined) - .slice(0, MAX_WORKSPACE_PICKER_NUM); + .filter((workspace): workspace is WorkspaceObject => workspace !== undefined); }, [workspaceList]); + const queryFromList = ({ list, query }: { list: WorkspaceObject[]; query: string }) => { + if (!list || list.length === 0) { + return []; + } + + if (query && query.trim() !== '') { + const normalizedQuery = query.toLowerCase(); + const result = list.filter((item) => item.name.toLowerCase().includes(normalizedQuery)); + return result; + } + + return list; + }; + + const queriedWorkspace = useMemo(() => { + return queryFromList({ list: workspaceList, query: search }); + }, [workspaceList, search]); + + const queriedRecentWorkspace = useMemo(() => { + return queryFromList({ list: recentWorkspaces, query: search }); + }, [recentWorkspaces, search]); + const getUseCase = (workspace: WorkspaceObject) => { if (!workspace.features) { return; @@ -60,10 +92,44 @@ export const WorkspacePickerContent = ({ return availableUseCases.find((useCase) => useCase.id === useCaseId); }; + const getEmptyStatePrompt = () => { + return ( + +

+ {i18n.translate('workspace.picker.empty.state.title', { + defaultMessage: 'No workspace available', + })} +

+ + } + body={ + +

+ {isDashboardAdmin + ? i18n.translate('workspace.picker.empty.state.description.admin', { + defaultMessage: 'Create a workspace to get start', + }) + : i18n.translate('workspace.picker.empty.state.description.noAdmin', { + defaultMessage: + 'Contact your administrator to create a workspace or to be added to an existing one', + })} +

+
+ } + /> + ); + }; + const getWorkspaceListGroup = (filterWorkspaceList: WorkspaceObject[], itemType: string) => { const listItems = filterWorkspaceList.map((workspace: WorkspaceObject) => { const useCase = getUseCase(workspace); const useCaseURL = getUseCaseUrl(useCase, workspace, coreStart.application, coreStart.http); + return ( - -

{itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}

- - } - /> - {listItems} - + <> + +

{itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}

+
+ + + {listItems} + + + ); }; return ( <> - {filteredRecentWorkspaces.length > 0 && - getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')} - {filteredWorkspaceList.length > 0 && getWorkspaceListGroup(filteredWorkspaceList, 'all')} + setSearch(e.target.value)} + placeholder={searchFieldPlaceholder} + /> + + + + {queriedRecentWorkspace.length > 0 && + getWorkspaceListGroup(queriedRecentWorkspace, 'recent')} + + {queriedWorkspace.length > 0 && getWorkspaceListGroup(queriedWorkspace, 'all')} + + {queriedWorkspace.length === 0 && getEmptyStatePrompt()} + ); };