forked from opensearch-project/OpenSearch-Dashboards
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a workspace dropdown menu in left navigation bar (opensearch-proj…
…ect#282) In this commit, the workspace plugin registered a workspace dropdown menu on the top of left navigation bar. This allows user to quickly switch the current workspace and navigate to workspace create page and workspace list page from the dropdown sticky on the top of the left nav bar. Signed-off-by: Yulong Ruan <[email protected]> --------- Signed-off-by: Yulong Ruan <[email protected]>
- Loading branch information
Showing
10 changed files
with
367 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React from 'react'; | ||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; | ||
|
||
import { WorkspaceMenu } from './workspace_menu'; | ||
import { coreMock } from '../../../../../core/public/mocks'; | ||
import { CoreStart } from '../../../../../core/public'; | ||
|
||
describe('<WorkspaceMenu />', () => { | ||
let coreStartMock: CoreStart; | ||
|
||
beforeEach(() => { | ||
coreStartMock = coreMock.createStart(); | ||
coreStartMock.workspaces.initialized$.next(true); | ||
jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => { | ||
return `https://test.com/app/${appId}`; | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it('should display a list of workspaces in the dropdown', () => { | ||
coreStartMock.workspaces.workspaceList$.next([ | ||
{ id: 'workspace-1', name: 'workspace 1' }, | ||
{ id: 'workspace-2', name: 'workspace 2' }, | ||
]); | ||
|
||
render(<WorkspaceMenu coreStart={coreStartMock} />); | ||
fireEvent.click(screen.getByText(/select a workspace/i)); | ||
|
||
expect(screen.getByText(/workspace 1/i)).toBeInTheDocument(); | ||
expect(screen.getByText(/workspace 2/i)).toBeInTheDocument(); | ||
}); | ||
|
||
it('should display current workspace name', () => { | ||
coreStartMock.workspaces.currentWorkspace$.next({ id: 'workspace-1', name: 'workspace 1' }); | ||
render(<WorkspaceMenu coreStart={coreStartMock} />); | ||
expect(screen.getByText(/workspace 1/i)).toBeInTheDocument(); | ||
}); | ||
|
||
it('should close the workspace dropdown list', async () => { | ||
render(<WorkspaceMenu coreStart={coreStartMock} />); | ||
fireEvent.click(screen.getByText(/select a workspace/i)); | ||
|
||
expect(screen.getByLabelText(/close workspace dropdown/i)).toBeInTheDocument(); | ||
fireEvent.click(screen.getByLabelText(/close workspace dropdown/i)); | ||
await waitFor(() => { | ||
expect(screen.queryByLabelText(/close workspace dropdown/i)).not.toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('should navigate to the workspace', () => { | ||
coreStartMock.workspaces.workspaceList$.next([ | ||
{ id: 'workspace-1', name: 'workspace 1' }, | ||
{ id: 'workspace-2', name: 'workspace 2' }, | ||
]); | ||
|
||
const originalLocation = window.location; | ||
Object.defineProperty(window, 'location', { | ||
value: { | ||
assign: jest.fn(), | ||
}, | ||
}); | ||
|
||
render(<WorkspaceMenu coreStart={coreStartMock} />); | ||
fireEvent.click(screen.getByText(/select a workspace/i)); | ||
fireEvent.click(screen.getByText(/workspace 1/i)); | ||
|
||
expect(window.location.assign).toHaveBeenCalledWith( | ||
'https://test.com/w/workspace-1/app/workspace_overview' | ||
); | ||
|
||
Object.defineProperty(window, 'location', { | ||
value: originalLocation, | ||
}); | ||
}); | ||
|
||
it('should navigate to create workspace page', () => { | ||
const originalLocation = window.location; | ||
Object.defineProperty(window, 'location', { | ||
value: { | ||
assign: jest.fn(), | ||
}, | ||
}); | ||
|
||
render(<WorkspaceMenu coreStart={coreStartMock} />); | ||
fireEvent.click(screen.getByText(/select a workspace/i)); | ||
fireEvent.click(screen.getByText(/create workspace/i)); | ||
expect(window.location.assign).toHaveBeenCalledWith('https://test.com/app/workspace_create'); | ||
|
||
Object.defineProperty(window, 'location', { | ||
value: originalLocation, | ||
}); | ||
}); | ||
|
||
it('should navigate to workspace list page', () => { | ||
const originalLocation = window.location; | ||
Object.defineProperty(window, 'location', { | ||
value: { | ||
assign: jest.fn(), | ||
}, | ||
}); | ||
|
||
render(<WorkspaceMenu coreStart={coreStartMock} />); | ||
fireEvent.click(screen.getByText(/select a workspace/i)); | ||
fireEvent.click(screen.getByText(/all workspace/i)); | ||
expect(window.location.assign).toHaveBeenCalledWith('https://test.com/app/workspace_list'); | ||
|
||
Object.defineProperty(window, 'location', { | ||
value: originalLocation, | ||
}); | ||
}); | ||
}); |
188 changes: 188 additions & 0 deletions
188
src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { i18n } from '@osd/i18n'; | ||
import React, { useState } from 'react'; | ||
import { useObservable } from 'react-use'; | ||
import { | ||
EuiButtonIcon, | ||
EuiContextMenu, | ||
EuiFlexGroup, | ||
EuiFlexItem, | ||
EuiIcon, | ||
EuiListGroup, | ||
EuiListGroupItem, | ||
EuiPopover, | ||
EuiText, | ||
} from '@elastic/eui'; | ||
import type { EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; | ||
|
||
import { | ||
WORKSPACE_CREATE_APP_ID, | ||
WORKSPACE_LIST_APP_ID, | ||
WORKSPACE_OVERVIEW_APP_ID, | ||
} from '../../../common/constants'; | ||
import { cleanWorkspaceId, formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; | ||
import { CoreStart, WorkspaceObject } from '../../../../../core/public'; | ||
|
||
interface Props { | ||
coreStart: CoreStart; | ||
} | ||
|
||
function getFilteredWorkspaceList( | ||
workspaceList: WorkspaceObject[], | ||
currentWorkspace: WorkspaceObject | null | ||
): WorkspaceObject[] { | ||
// list top5 workspaces and place the current workspace at the top | ||
return [ | ||
...(currentWorkspace ? [currentWorkspace] : []), | ||
...workspaceList.filter((workspace) => workspace.id !== currentWorkspace?.id), | ||
].slice(0, 5); | ||
} | ||
|
||
export const WorkspaceMenu = ({ coreStart }: Props) => { | ||
const [isPopoverOpen, setPopover] = useState(false); | ||
const currentWorkspace = useObservable(coreStart.workspaces.currentWorkspace$, null); | ||
const workspaceList = useObservable(coreStart.workspaces.workspaceList$, []); | ||
|
||
const defaultHeaderName = i18n.translate( | ||
'core.ui.primaryNav.workspacePickerMenu.defaultHeaderName', | ||
{ | ||
defaultMessage: 'Select a workspace', | ||
} | ||
); | ||
const filteredWorkspaceList = getFilteredWorkspaceList(workspaceList, currentWorkspace); | ||
const currentWorkspaceName = currentWorkspace?.name ?? defaultHeaderName; | ||
|
||
const openPopover = () => { | ||
setPopover(!isPopoverOpen); | ||
}; | ||
|
||
const closePopover = () => { | ||
setPopover(false); | ||
}; | ||
|
||
const workspaceToItem = (workspace: WorkspaceObject) => { | ||
const workspaceURL = formatUrlWithWorkspaceId( | ||
coreStart.application.getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, { | ||
absolute: false, | ||
}), | ||
workspace.id, | ||
coreStart.http.basePath | ||
); | ||
const name = | ||
currentWorkspace?.name === workspace.name ? ( | ||
<EuiText> | ||
<strong>{workspace.name}</strong> | ||
</EuiText> | ||
) : ( | ||
workspace.name | ||
); | ||
return { | ||
name, | ||
key: workspace.id, | ||
icon: <EuiIcon type="stopFilled" color={workspace.color ?? 'primary'} />, | ||
onClick: () => { | ||
window.location.assign(workspaceURL); | ||
}, | ||
}; | ||
}; | ||
|
||
const getWorkspaceListItems = () => { | ||
const workspaceListItems: EuiContextMenuPanelItemDescriptor[] = filteredWorkspaceList.map( | ||
workspaceToItem | ||
); | ||
workspaceListItems.push({ | ||
icon: <EuiIcon type="plus" />, | ||
name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.createWorkspace', { | ||
defaultMessage: 'Create workspace', | ||
}), | ||
key: WORKSPACE_CREATE_APP_ID, | ||
onClick: () => { | ||
window.location.assign( | ||
cleanWorkspaceId( | ||
coreStart.application.getUrlForApp(WORKSPACE_CREATE_APP_ID, { | ||
absolute: false, | ||
}) | ||
) | ||
); | ||
}, | ||
}); | ||
workspaceListItems.push({ | ||
icon: <EuiIcon type="folderClosed" />, | ||
name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.allWorkspace', { | ||
defaultMessage: 'All workspaces', | ||
}), | ||
key: WORKSPACE_LIST_APP_ID, | ||
onClick: () => { | ||
window.location.assign( | ||
cleanWorkspaceId( | ||
coreStart.application.getUrlForApp(WORKSPACE_LIST_APP_ID, { | ||
absolute: false, | ||
}) | ||
) | ||
); | ||
}, | ||
}); | ||
return workspaceListItems; | ||
}; | ||
|
||
const currentWorkspaceButton = ( | ||
<> | ||
<EuiListGroup style={{ width: 318 }} maxWidth={false}> | ||
<EuiListGroupItem | ||
iconType="spacesApp" | ||
label={currentWorkspaceName} | ||
onClick={openPopover} | ||
extraAction={{ | ||
color: 'subdued', | ||
onClick: openPopover, | ||
iconType: isPopoverOpen ? 'arrowDown' : 'arrowRight', | ||
iconSize: 's', | ||
'aria-label': 'Show workspace dropdown selector', | ||
alwaysShow: true, | ||
}} | ||
/> | ||
</EuiListGroup> | ||
</> | ||
); | ||
|
||
const currentWorkspaceTitle = ( | ||
<EuiFlexGroup alignItems="center"> | ||
<EuiFlexItem grow={true}> | ||
<EuiText size="s">{currentWorkspaceName}</EuiText> | ||
</EuiFlexItem> | ||
<EuiFlexItem grow={false}> | ||
<EuiButtonIcon | ||
iconType="cross" | ||
onClick={closePopover} | ||
aria-label="close workspace dropdown" | ||
/> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
); | ||
|
||
const panels = [ | ||
{ | ||
id: 0, | ||
title: currentWorkspaceTitle, | ||
items: getWorkspaceListItems(), | ||
}, | ||
]; | ||
|
||
return ( | ||
<EuiPopover | ||
id="contextMenuExample" | ||
display="block" | ||
button={currentWorkspaceButton} | ||
isOpen={isPopoverOpen} | ||
closePopover={closePopover} | ||
panelPaddingSize="none" | ||
anchorPosition="downCenter" | ||
> | ||
<EuiContextMenu initialPanelId={0} panels={panels} /> | ||
</EuiPopover> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.