diff --git a/package-lock.json b/package-lock.json index 373f84b0989..00fa813d750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44630,6 +44630,7 @@ "dependencies": { "@mongodb-js/compass-components": "^1.42.0", "@mongodb-js/compass-connections": "^1.64.0", + "@mongodb-js/compass-context-menu": "^0.2.0", "@mongodb-js/compass-workspaces": "^0.45.0", "@mongodb-js/connection-form": "^1.56.0", "@mongodb-js/connection-info": "^0.15.5", @@ -47370,6 +47371,7 @@ "@mongodb-js/compass-telemetry": "^1.10.3", "@mongodb-js/compass-workspaces": "^0.45.0", "@mongodb-js/connection-info": "^0.15.5", + "@mongodb-js/mongodb-constants": "^0.12.1", "compass-preferences-model": "^2.44.0", "lodash": "^4.17.21", "mongodb": "^6.16.0", @@ -47404,6 +47406,27 @@ "xvfb-maybe": "^0.2.1" } }, + "packages/compass-sidebar/node_modules/@mongodb-js/mongodb-constants": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.12.1.tgz", + "integrity": "sha512-wqlseEtgrKu86s0n3ja2wXGZEwSommg6H1XDeqfAieX6wZKws3oL1R83QeQs0juy1SZv6LjfLk/788FcJcmVrA==", + "license": "Apache-2.0", + "dependencies": { + "semver": "^7.7.1" + } + }, + "packages/compass-sidebar/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "packages/compass-sidebar/node_modules/sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -56944,6 +56967,7 @@ "requires": { "@mongodb-js/compass-components": "^1.42.0", "@mongodb-js/compass-connections": "^1.64.0", + "@mongodb-js/compass-context-menu": "^0.2.0", "@mongodb-js/compass-workspaces": "^0.45.0", "@mongodb-js/connection-form": "^1.56.0", "@mongodb-js/connection-info": "^0.15.5", @@ -58983,6 +59007,7 @@ "@mongodb-js/connection-info": "^0.15.5", "@mongodb-js/eslint-config-compass": "^1.4.1", "@mongodb-js/mocha-config-compass": "^1.6.9", + "@mongodb-js/mongodb-constants": "^0.12.1", "@mongodb-js/prettier-config-compass": "^1.2.8", "@mongodb-js/testing-library-compass": "^1.3.4", "@mongodb-js/tsconfig-compass": "^1.2.9", @@ -59013,6 +59038,19 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { + "@mongodb-js/mongodb-constants": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.12.1.tgz", + "integrity": "sha512-wqlseEtgrKu86s0n3ja2wXGZEwSommg6H1XDeqfAieX6wZKws3oL1R83QeQs0juy1SZv6LjfLk/788FcJcmVrA==", + "requires": { + "semver": "^7.7.1" + } + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" + }, "sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", diff --git a/packages/compass-components/src/components/actions/dropdown-menu-button.tsx b/packages/compass-components/src/components/actions/dropdown-menu-button.tsx index d24e4a031aa..568e6528991 100644 --- a/packages/compass-components/src/components/actions/dropdown-menu-button.tsx +++ b/packages/compass-components/src/components/actions/dropdown-menu-button.tsx @@ -7,9 +7,9 @@ import { Button, Icon, Menu, MenuItem, MenuSeparator } from '../leafygreen'; import { WorkspaceContainer } from '../workspace-container'; import { ItemActionButtonSize } from './constants'; -import { actionTestId } from './utils'; import { ActionGlyph } from './action-glyph'; -import { isSeparatorMenuAction, type MenuAction } from './item-action-menu'; +import { actionTestId, isSeparatorMenuAction } from './utils'; +import type { MenuAction } from './types'; const getHiddenOnNarrowStyles = (narrowBreakpoint: string) => css({ diff --git a/packages/compass-components/src/components/actions/item-action-group.tsx b/packages/compass-components/src/components/actions/item-action-group.tsx index dde60f96de3..cc44fa23d4c 100644 --- a/packages/compass-components/src/components/actions/item-action-group.tsx +++ b/packages/compass-components/src/components/actions/item-action-group.tsx @@ -6,9 +6,8 @@ import { MenuSeparator, Tooltip } from '../leafygreen'; import { ItemActionButtonSize } from './constants'; import type { ItemAction, ItemSeparator } from './types'; -import { isSeparatorMenuAction } from './item-action-menu'; import { ItemActionButton } from './item-action-button'; -import { actionTestId } from './utils'; +import { actionTestId, isSeparatorMenuAction } from './utils'; export type GroupedItemAction = ItemAction & { tooltipProps?: Parameters; diff --git a/packages/compass-components/src/components/actions/item-action-menu.tsx b/packages/compass-components/src/components/actions/item-action-menu.tsx index 8e560b1ce8e..8678e2e773c 100644 --- a/packages/compass-components/src/components/actions/item-action-menu.tsx +++ b/packages/compass-components/src/components/actions/item-action-menu.tsx @@ -6,22 +6,9 @@ import { Menu, MenuItem, MenuSeparator } from '../leafygreen'; import { ItemActionButtonSize } from './constants'; import { ActionGlyph } from './action-glyph'; -import type { ItemBase, ItemSeparator } from './types'; +import type { MenuAction } from './types'; import { SmallIconButton } from './small-icon-button'; -import { actionTestId } from './utils'; - -export type MenuAction = - | ItemBase - | ItemSeparator; - -export function isSeparatorMenuAction(value: unknown): value is ItemSeparator { - return ( - typeof value === 'object' && - value !== null && - 'separator' in value && - value.separator === true - ); -} +import { actionTestId, isSeparatorMenuAction } from './utils'; const containerStyle = css({ flex: 'none', diff --git a/packages/compass-components/src/components/actions/types.ts b/packages/compass-components/src/components/actions/types.ts index 59bd3ae0694..23f8744f254 100644 --- a/packages/compass-components/src/components/actions/types.ts +++ b/packages/compass-components/src/components/actions/types.ts @@ -36,3 +36,7 @@ export type ItemAction = { } & ItemBase; export type ItemSeparator = { separator: true }; + +export type MenuAction = + | ItemBase + | ItemSeparator; diff --git a/packages/compass-components/src/components/actions/utils.spec.ts b/packages/compass-components/src/components/actions/utils.spec.ts new file mode 100644 index 00000000000..6cb5bcb3d34 --- /dev/null +++ b/packages/compass-components/src/components/actions/utils.spec.ts @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import { splitBySeparator } from './utils'; + +describe('item action utils', function () { + describe('splitBySeparator', function () { + it('returns an empty array for an empty input', function () { + const result = splitBySeparator([]); + expect(result).is.empty; + }); + + it('returns a single item for a single input', function () { + const result = splitBySeparator([{ label: 'Foo', action: 'foo' }]); + expect(result).deep.equal([[{ label: 'Foo', action: 'foo' }]]); + }); + + it('splits four items separated by a separator', function () { + const result = splitBySeparator([ + { label: 'Foo', action: 'foo' }, + { label: 'Bar', action: 'bar' }, + { separator: true }, + { label: 'Baz', action: 'baz' }, + { label: 'Qux', action: 'qux' }, + ]); + expect(result).deep.equal([ + [ + { label: 'Foo', action: 'foo' }, + { label: 'Bar', action: 'bar' }, + ], + [ + { label: 'Baz', action: 'baz' }, + { label: 'Qux', action: 'qux' }, + ], + ]); + }); + + it('disregards leading separators', function () { + const result = splitBySeparator([ + { separator: true }, + { label: 'Foo', action: 'foo' }, + ]); + expect(result).deep.equal([[{ label: 'Foo', action: 'foo' }]]); + }); + + it('disregards trailing separators', function () { + const result = splitBySeparator([ + { label: 'Foo', action: 'foo' }, + { separator: true }, + ]); + expect(result).deep.equal([[{ label: 'Foo', action: 'foo' }]]); + }); + }); +}); diff --git a/packages/compass-components/src/components/actions/utils.ts b/packages/compass-components/src/components/actions/utils.ts index 4a02d70183f..4ece3b67750 100644 --- a/packages/compass-components/src/components/actions/utils.ts +++ b/packages/compass-components/src/components/actions/utils.ts @@ -1,3 +1,35 @@ +import type { ItemBase, ItemSeparator, MenuAction } from './types'; + +export function isSeparatorMenuAction(value: unknown): value is ItemSeparator { + return ( + typeof value === 'object' && + value !== null && + 'separator' in value && + value.separator === true + ); +} + export function actionTestId(dataTestId: string | undefined, action: string) { return dataTestId ? `${dataTestId}-${action}-action` : undefined; } + +export function splitBySeparator( + actions: MenuAction[] +) { + const result: ItemBase[][] = []; + let currentGroup: ItemBase[] = []; + for (const action of actions) { + if (isSeparatorMenuAction(action)) { + if (currentGroup.length > 0) { + result.push(currentGroup); + currentGroup = []; + } + } else { + currentGroup.push(action); + } + } + if (currentGroup.length > 0) { + result.push(currentGroup); + } + return result; +} diff --git a/packages/compass-components/src/components/context-menu.tsx b/packages/compass-components/src/components/context-menu.tsx index f2285fea73f..e1931bdab1c 100644 --- a/packages/compass-components/src/components/context-menu.tsx +++ b/packages/compass-components/src/components/context-menu.tsx @@ -11,7 +11,10 @@ import { type ContextMenuWrapperProps, } from '@mongodb-js/compass-context-menu'; -export type { ContextMenuItem } from '@mongodb-js/compass-context-menu'; +export type { + ContextMenuItem, + ContextMenuItemGroup, +} from '@mongodb-js/compass-context-menu'; // TODO: Remove these once https://jira.mongodb.org/browse/LG-5013 is resolved diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index 6be7d8102eb..40d6a609b93 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -51,18 +51,19 @@ import ResizableSidebar, { defaultSidebarWidth, } from './components/resizeable-sidebar'; -import type { +export type { ItemAction, ItemComponentProps, ItemSeparator, + MenuAction, } from './components/actions/types'; -import type { GroupedItemAction } from './components/actions/item-action-group'; -import type { MenuAction } from './components/actions/item-action-menu'; +export { splitBySeparator } from './components/actions/utils'; +export type { GroupedItemAction } from './components/actions/item-action-group'; -import { ItemActionControls } from './components/actions/item-action-controls'; -import { ItemActionGroup } from './components/actions/item-action-group'; -import { ItemActionMenu } from './components/actions/item-action-menu'; -import { DropdownMenuButton } from './components/actions/dropdown-menu-button'; +export { ItemActionControls } from './components/actions/item-action-controls'; +export { ItemActionGroup } from './components/actions/item-action-group'; +export { ItemActionMenu } from './components/actions/item-action-menu'; +export { DropdownMenuButton } from './components/actions/dropdown-menu-button'; export { DocumentIcon } from './components/icons/document-icon'; export { FavoriteIcon } from './components/icons/favorite-icon'; @@ -104,15 +105,11 @@ export { useContextMenuItems, useContextMenuGroups, type ContextMenuItem, + type ContextMenuItemGroup, } from './components/context-menu'; export type { FileInputBackend, - ItemAction, - ItemComponentProps, - GroupedItemAction, - MenuAction, - ItemSeparator, ElectronFileDialogOptions, ElectronShowFileDialogProvider, }; @@ -131,10 +128,6 @@ export { ResizableSidebar, WarningSummary, WorkspaceTabs, - ItemActionControls, - ItemActionGroup, - ItemActionMenu, - DropdownMenuButton, defaultSidebarWidth, createElectronFileInputBackend, createJSDomFileInputDummyBackend, diff --git a/packages/compass-connections-navigation/package.json b/packages/compass-connections-navigation/package.json index 9d321154eef..c0f500e1ab7 100644 --- a/packages/compass-connections-navigation/package.json +++ b/packages/compass-connections-navigation/package.json @@ -49,11 +49,12 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-connections": "^1.64.0", "@mongodb-js/compass-components": "^1.42.0", - "@mongodb-js/connection-info": "^0.15.5", - "@mongodb-js/connection-form": "^1.56.0", + "@mongodb-js/compass-connections": "^1.64.0", + "@mongodb-js/compass-context-menu": "^0.2.0", "@mongodb-js/compass-workspaces": "^0.45.0", + "@mongodb-js/connection-form": "^1.56.0", + "@mongodb-js/connection-info": "^0.15.5", "compass-preferences-model": "^2.44.0", "mongodb-build-info": "^1.7.2", "react": "^17.0.2", diff --git a/packages/compass-connections-navigation/src/base-navigation-item.tsx b/packages/compass-connections-navigation/src/base-navigation-item.tsx index 65c5ffa2189..c8ddb75301d 100644 --- a/packages/compass-connections-navigation/src/base-navigation-item.tsx +++ b/packages/compass-connections-navigation/src/base-navigation-item.tsx @@ -20,7 +20,7 @@ import type { SidebarTreeItem, } from './tree-data'; -type NavigationBaseItemProps = { +type NavigationBaseItemProps = React.PropsWithChildren<{ item: SidebarTreeItem; name: string; isActive: boolean; @@ -40,7 +40,7 @@ type NavigationBaseItemProps = { onAction: (action: Actions) => void; }; toggleExpand: () => void; -}; +}>; const menuStyles = css({ width: '240px', @@ -155,26 +155,33 @@ const ClusterStateBadgeWithTooltip: React.FunctionComponent<{ return null; }; -export const NavigationBaseItem: React.FC = ({ - item, - isActive, - actionProps, - name, - style, - icon, - dataAttributes, - isExpandVisible, - isExpandDisabled, - isExpanded, - isFocused, - hasDefaultAction, - toggleExpand, - children, -}) => { +export const NavigationBaseItem = React.forwardRef< + HTMLDivElement, + NavigationBaseItemProps +>(function NavigationBaseItem( + { + item, + isActive, + actionProps, + name, + style, + icon, + dataAttributes, + isExpandVisible, + isExpandDisabled, + isExpanded, + isFocused, + hasDefaultAction, + toggleExpand, + children, + }, + ref +) { const [hoverProps, isHovered] = useHoverState(); return (
= ({
); -}; +}); diff --git a/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx b/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx index 2888c6f66c0..7cb214b3d7a 100644 --- a/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx +++ b/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx @@ -43,6 +43,7 @@ const connections: Connection[] = [ collectionsStatus: 'initial', collectionsLength: 5, collections: [], + isNonExistent: false, }, { _id: 'db_ready', @@ -56,6 +57,7 @@ const connections: Connection[] = [ type: 'collection', sourceName: '', pipeline: [], + isNonExistent: false, }, { _id: 'db_ready.woof', @@ -63,6 +65,7 @@ const connections: Connection[] = [ type: 'timeseries', sourceName: '', pipeline: [], + isNonExistent: false, }, { _id: 'db_ready.bwok', @@ -70,8 +73,10 @@ const connections: Connection[] = [ type: 'view', sourceName: '', pipeline: [], + isNonExistent: false, }, ], + isNonExistent: false, }, ], isReady: true, @@ -407,7 +412,7 @@ describe('ConnectionsNavigationTree', function () { expect(screen.getByText('View performance metrics')).to.be.visible; expect(screen.getByText('Show connection info')).to.be.visible; expect(screen.getByText('Copy connection string')).to.be.visible; - expect(screen.getByText('Unfavorite')).to.be.visible; + expect(screen.getByText('Unfavorite connection')).to.be.visible; expect(screen.getByText('Disconnect')).to.be.visible; }); @@ -582,7 +587,7 @@ describe('ConnectionsNavigationTree', function () { expect(screen.getByText('View performance metrics')).to.be.visible; expect(screen.getByText('Show connection info')).to.be.visible; expect(screen.getByText('Copy connection string')).to.be.visible; - expect(screen.getByText('Unfavorite')).to.be.visible; + expect(screen.getByText('Unfavorite connection')).to.be.visible; expect(screen.getByText('Disconnect')).to.be.visible; }); @@ -763,7 +768,6 @@ describe('ConnectionsNavigationTree', function () { connections: [ { ...(connections[0] as ConnectedConnection), - isPerformanceTabSupported: true, isPerformanceTabSupported: false, }, { ...connections[1] }, @@ -934,4 +938,293 @@ describe('ConnectionsNavigationTree', function () { }); }); }); + + describe('context menu', function () { + const assertContextMenuItems = async ( + element: HTMLElement, + items: (string | { separator: true })[] + ) => { + userEvent.click(element, { button: 2 }); + await waitFor(() => { + expect(screen.getByTestId('context-menu')).to.be.visible; + }); + let groupIndex = 0; + let itemIndex = 0; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (typeof item === 'object' && 'separator' in item) { + groupIndex++; + itemIndex = 0; + continue; + } + if (typeof item === 'string') { + expect( + screen.getByTestId(`menu-group-${groupIndex}-item-${itemIndex}`) + ).to.have.text(item); + } + itemIndex++; + } + // Expect no more items + expect( + screen.queryByTestId(`menu-group-${groupIndex}-item-${itemIndex + 1}`) + ).to.not.exist; + expect(screen.queryByTestId(`menu-group-${groupIndex + 1}-item-0`)).to.not + .exist; + }; + + describe('connection context menu', function () { + it('should show context menu for connected connection', async function () { + await renderConnectionsNavigationTree(); + + const connectionElement = within( + screen.getByTestId('connection_ready') + ).getByTestId('base-navigation-item'); + + await assertContextMenuItems(connectionElement, [ + 'Edit connection', + 'Copy connection string', + 'Unfavorite connection', + 'Duplicate connection', + 'Remove connection', + { separator: true }, + 'Open MongoDB shell', + 'View performance metrics', + 'Show connection info', + 'Refresh databases', + { separator: true }, + 'Disconnect', + ]); + }); + + it('should show context menu for disconnected connection', async function () { + await renderConnectionsNavigationTree(); + + const connectionElement = within( + screen.getByTestId('connection_disconnected') + ).getByTestId('base-navigation-item'); + userEvent.click(connectionElement, { button: 2 }); + + await waitFor(() => { + expect(screen.getByTestId('context-menu')).to.be.visible; + }); + + // Check for expected context menu items for disconnected connection + await assertContextMenuItems(connectionElement, [ + 'Connect', + 'Edit connection', + 'Copy connection string', + 'Favorite connection', + 'Duplicate connection', + 'Remove connection', + ]); + }); + }); + + describe('database context menu', function () { + it('should show context menu for database', async function () { + await renderConnectionsNavigationTree({ + expanded: { connection_ready: {} }, + }); + + const databaseElement = within( + screen.getByTestId('connection_ready.db_initial') + ).getByTestId('base-navigation-item'); + + // Check for expected context menu items for database + await assertContextMenuItems(databaseElement, [ + 'Create collection', + { separator: true }, + 'Create database', + 'Drop database', + { separator: true }, + 'Open MongoDB shell', + 'View performance metrics', + 'Show connection info', + 'Refresh databases', + { separator: true }, + 'Disconnect', + ]); + }); + + it('should show limited context menu for database when read-only', async function () { + await renderConnectionsNavigationTree( + { + expanded: { connection_ready: {} }, + }, + { + readOnly: true, + } + ); + + const databaseElement = within( + screen.getByTestId('connection_ready.db_initial') + ).getByTestId('base-navigation-item'); + userEvent.click(databaseElement, { button: 2 }); + + const contextMenu = screen.getByTestId('context-menu'); + + // Check that write actions are not present in read-only mode + expect(() => within(contextMenu).getByText('Create collection')).to + .throw; + expect(() => within(contextMenu).getByText('Create database')).to.throw; + expect(() => within(contextMenu).getByText('Drop database')).to.throw; + + // Check that read-only actions are still present + expect(within(contextMenu).getByText('View performance metrics')).to.be + .visible; + expect(within(contextMenu).getByText('Show connection info')).to.be + .visible; + expect(within(contextMenu).getByText('Refresh databases')).to.be + .visible; + expect(within(contextMenu).getByText('Disconnect')).to.be.visible; + }); + }); + + describe('collection context menu', function () { + it('should show context menu for collection', async function () { + await renderConnectionsNavigationTree({ + expanded: { connection_ready: { db_ready: true } }, + }); + + const collectionElement = within( + screen.getByTestId('connection_ready.db_ready.meow') + ).getByTestId('base-navigation-item'); + userEvent.click(collectionElement, { button: 2 }); + + await waitFor(() => { + expect(screen.getByTestId('context-menu')).to.be.visible; + }); + + // Check for expected context menu items for collection + await assertContextMenuItems(collectionElement, [ + 'Open in new tab', + { separator: true }, + 'Rename collection', + 'Create collection', + 'Drop collection', + { separator: true }, + 'Open MongoDB shell', + 'View performance metrics', + 'Show connection info', + 'Refresh databases', + { separator: true }, + 'Disconnect', + ]); + }); + + it('should show limited context menu for collection when read-only', async function () { + await renderConnectionsNavigationTree( + { + expanded: { connection_ready: { db_ready: true } }, + }, + { + readOnly: true, + } + ); + + const collectionElement = within( + screen.getByTestId('connection_ready.db_ready.meow') + ).getByTestId('base-navigation-item'); + userEvent.click(collectionElement, { button: 2 }); + + await waitFor(() => { + expect(screen.getByTestId('context-menu')).to.be.visible; + }); + + await assertContextMenuItems(collectionElement, [ + 'Open in new tab', + { separator: true }, + 'View performance metrics', + 'Show connection info', + 'Refresh databases', + { separator: true }, + 'Disconnect', + ]); + }); + }); + + describe('view context menu', function () { + it('should show context menu for view', async function () { + await renderConnectionsNavigationTree({ + expanded: { connection_ready: { db_ready: true } }, + }); + + const viewElement = within( + screen.getByTestId('connection_ready.db_ready.bwok') + ).getByTestId('base-navigation-item'); + + // Check for expected context menu items for view + await assertContextMenuItems(viewElement, [ + 'Open in new tab', + { separator: true }, + 'Duplicate view', + 'Modify view', + 'Drop view', + { separator: true }, + 'Open MongoDB shell', + 'View performance metrics', + 'Show connection info', + 'Refresh databases', + { separator: true }, + 'Disconnect', + ]); + + // Views should not have rename option + expect(() => screen.getByText('Rename collection')).to.throw; + }); + + it('should show limited context menu for view when read-only', async function () { + await renderConnectionsNavigationTree( + { + expanded: { connection_ready: { db_ready: true } }, + }, + { + readOnly: true, + } + ); + + const viewElement = within( + screen.getByTestId('connection_ready.db_ready.bwok') + ).getByTestId('base-navigation-item'); + + // Check that read-only actions are still present + await assertContextMenuItems(viewElement, [ + 'Open in new tab', + { separator: true }, + 'View performance metrics', + 'Show connection info', + 'Refresh databases', + { separator: true }, + 'Disconnect', + ]); + }); + }); + + describe('context menu actions', function () { + it('should trigger onItemAction when context menu item is clicked', async function () { + const spy = Sinon.spy(); + await renderConnectionsNavigationTree({ + expanded: { connection_ready: { db_ready: true } }, + onItemAction: spy, + }); + + const collectionElement = within( + screen.getByTestId('connection_ready.db_ready.meow') + ).getByTestId('base-navigation-item'); + userEvent.click(collectionElement, { button: 2 }); + + await waitFor(() => { + expect(screen.getByTestId('context-menu')).to.be.visible; + }); + + userEvent.click(screen.getByText('Open in new tab')); + + expect(spy).to.be.calledOnce; + const [[item, action]] = spy.args; + expect(item.type).to.equal('collection'); + expect(item.namespace).to.equal('db_ready.meow'); + expect(action).to.equal('open-in-new-tab'); + }); + }); + }); }); diff --git a/packages/compass-connections-navigation/src/connections-navigation-tree.tsx b/packages/compass-connections-navigation/src/connections-navigation-tree.tsx index a872ddd2e43..7958cda5b8f 100644 --- a/packages/compass-connections-navigation/src/connections-navigation-tree.tsx +++ b/packages/compass-connections-navigation/src/connections-navigation-tree.tsx @@ -12,6 +12,7 @@ import type { Connection, } from './tree-data'; import type { ItemAction, ItemSeparator } from '@mongodb-js/compass-components'; +import type { ContextMenuItemGroup } from '@mongodb-js/compass-context-menu'; import { VisuallyHidden, css, @@ -24,10 +25,14 @@ import { usePreference } from 'compass-preferences-model/provider'; import type { NavigationItemActions } from './item-actions'; import { collectionItemActions, + collectionContextMenuActions, connectedConnectionItemActions, databaseItemActions, + databaseContextMenuActions, notConnectedConnectionItemActions, + connectionContextMenuActions, } from './item-actions'; +import { itemActionsToContextMenuGroups } from './context-menus'; const ConnectionsNavigationContainerStyles = css({ display: 'flex', @@ -219,6 +224,78 @@ const ConnectionsNavigationTree: React.FunctionComponent< ] ); + const getContextMenuGroups = useCallback( + function getContextMenuGroups( + item: SidebarTreeItem + ): ContextMenuItemGroup[] { + switch (item.type) { + case 'placeholder': + return []; + case 'connection': + return itemActionsToContextMenuGroups( + item, + onItemAction, + item.connectionStatus === 'connected' + ? connectionContextMenuActions({ + hasWriteActionsDisabled: item.hasWriteActionsDisabled, + isShellEnabled: item.isShellEnabled, + connectionInfo: item.connectionInfo, + isPerformanceTabAvailable: item.isPerformanceTabAvailable, + isPerformanceTabSupported: item.isPerformanceTabSupported, + isAtlas: !!item.connectionInfo.atlasMetadata, + }) + : notConnectedConnectionItemActions({ + connectionInfo: item.connectionInfo, + connectionStatus: item.connectionStatus, + }) + ); + case 'database': { + const { + isPerformanceTabAvailable, + isPerformanceTabSupported, + isShellEnabled, + hasWriteActionsDisabled, + connectionInfo, + } = item.connectionItem; + return itemActionsToContextMenuGroups( + item, + onItemAction, + databaseContextMenuActions({ + hasWriteActionsDisabled, + isShellEnabled, + isPerformanceTabAvailable, + isPerformanceTabSupported, + isAtlas: !!connectionInfo.atlasMetadata, + }) + ); + } + default: { + const { + isPerformanceTabAvailable, + isPerformanceTabSupported, + isShellEnabled, + hasWriteActionsDisabled, + connectionInfo, + } = item.databaseItem.connectionItem; + return itemActionsToContextMenuGroups( + item, + onItemAction, + collectionContextMenuActions({ + hasWriteActionsDisabled, + type: item.type, + isRenameCollectionEnabled, + isShellEnabled, + isPerformanceTabAvailable, + isPerformanceTabSupported, + isAtlas: !!connectionInfo.atlasMetadata, + }) + ); + } + } + }, + [onItemAction, isRenameCollectionEnabled] + ); + const isTestEnv = process.env.NODE_ENV === 'test'; return ( @@ -243,6 +320,7 @@ const ConnectionsNavigationTree: React.FunctionComponent< onItemExpand={onItemExpand} getItemActions={getItemActionsAndConfig} getItemKey={(item) => item.id} + getContextMenuGroups={getContextMenuGroups} renderItem={({ item, isActive, @@ -250,6 +328,7 @@ const ConnectionsNavigationTree: React.FunctionComponent< onItemAction, onItemExpand, getItemActions, + getContextMenuGroups, }) => { return ( ); }} diff --git a/packages/compass-connections-navigation/src/context-menus.ts b/packages/compass-connections-navigation/src/context-menus.ts new file mode 100644 index 00000000000..49d5e5436e1 --- /dev/null +++ b/packages/compass-connections-navigation/src/context-menus.ts @@ -0,0 +1,23 @@ +import { + splitBySeparator, + type ContextMenuItemGroup, +} from '@mongodb-js/compass-components'; + +import type { NavigationItemActions } from './item-actions'; +import type { Actions } from './constants'; +import type { SidebarActionableItem } from './tree-data'; + +export function itemActionsToContextMenuGroups( + item: SidebarActionableItem, + onItemAction: (item: SidebarActionableItem, action: Actions) => void, + itemActions: NavigationItemActions +): ContextMenuItemGroup[] { + return splitBySeparator(itemActions).map((actions) => + actions.map(({ label, action }) => ({ + label, + onAction() { + onItemAction({ ...item, entrypoint: 'context-menu' }, action); + }, + })) + ); +} diff --git a/packages/compass-connections-navigation/src/item-actions.ts b/packages/compass-connections-navigation/src/item-actions.ts index 4609f0d9a76..834c5174123 100644 --- a/packages/compass-connections-navigation/src/item-actions.ts +++ b/packages/compass-connections-navigation/src/item-actions.ts @@ -49,22 +49,22 @@ export const commonConnectionItemActions = ({ action: 'connection-toggle-favorite', label: connectionInfo.savedConnectionType === 'favorite' - ? 'Unfavorite' - : 'Favorite', + ? 'Unfavorite connection' + : 'Favorite connection', icon: 'Favorite', }, isAtlas ? null : { action: 'duplicate-connection', - label: 'Duplicate', + label: 'Duplicate connection', icon: 'Clone', }, isAtlas ? null : { action: 'remove-connection', - label: 'Remove', + label: 'Remove connection', icon: 'Trash', variant: 'destructive', }, @@ -206,6 +206,7 @@ export const collectionItemActions = ({ } if (type === 'view') { + actions.push({ separator: true }); actions.push( { action: 'drop-collection', @@ -228,6 +229,7 @@ export const collectionItemActions = ({ } if (type !== 'timeseries' && isRenameCollectionEnabled) { + actions.push({ separator: true }); actions.push({ action: 'rename-collection', label: 'Rename collection', @@ -243,3 +245,200 @@ export const collectionItemActions = ({ return actions; }; + +export const connectionContextMenuActions = ({ + isPerformanceTabAvailable, + isPerformanceTabSupported, + isAtlas, + isShellEnabled, + hasWriteActionsDisabled, + connectionInfo, +}: { + isPerformanceTabAvailable: boolean; + isPerformanceTabSupported: boolean; + isAtlas: boolean; + isShellEnabled: boolean; + hasWriteActionsDisabled: boolean; + connectionInfo?: ConnectionInfo; +}): NavigationItemActions => { + return stripNullActions([ + ...(hasWriteActionsDisabled || !connectionInfo + ? [] + : [ + ...commonConnectionItemActions({ connectionInfo }), + { separator: true } as NavigationItemAction, + ]), + isShellEnabled + ? { + action: 'open-shell', + icon: 'Shell', + label: 'Open MongoDB shell', + } + : null, + isPerformanceTabAvailable + ? { + action: 'connection-performance-metrics', + icon: 'Gauge', + label: 'View performance metrics', + isDisabled: !isPerformanceTabSupported, + disabledDescription: 'Not supported', + } + : null, + isAtlas + ? null + : { + action: 'open-connection-info', + icon: 'InfoWithCircle', + label: 'Show connection info', + }, + { + action: 'refresh-databases', + label: 'Refresh databases', + icon: 'Refresh', + }, + { separator: true }, + { + action: 'connection-disconnect', + icon: 'Disconnect', + label: 'Disconnect', + variant: 'destructive', + }, + ]); +}; + +export const databaseContextMenuActions = ({ + hasWriteActionsDisabled, + isShellEnabled, + isPerformanceTabAvailable, + isPerformanceTabSupported, + isAtlas, +}: { + hasWriteActionsDisabled: boolean; + isShellEnabled: boolean; + isPerformanceTabAvailable: boolean; + isPerformanceTabSupported: boolean; + isAtlas: boolean; +}): NavigationItemActions => { + return stripNullActions([ + // Database-specific actions + hasWriteActionsDisabled + ? null + : { + action: 'create-collection', + icon: 'Plus', + label: 'Create collection', + }, + { separator: true }, + hasWriteActionsDisabled + ? null + : { + action: 'create-database', + icon: 'Plus', + label: 'Create database', + }, + hasWriteActionsDisabled + ? null + : { + action: 'drop-database', + icon: 'Trash', + label: 'Drop database', + }, + { separator: true }, + + ...connectionContextMenuActions({ + isShellEnabled, + isPerformanceTabAvailable, + isPerformanceTabSupported, + isAtlas, + hasWriteActionsDisabled, + connectionInfo: undefined, + }), + ]); +}; + +export const collectionContextMenuActions = ({ + hasWriteActionsDisabled, + type, + isRenameCollectionEnabled, + isPerformanceTabAvailable, + isPerformanceTabSupported, + isAtlas, + isShellEnabled, +}: { + hasWriteActionsDisabled: boolean; + type: 'collection' | 'view' | 'timeseries'; + isRenameCollectionEnabled: boolean; + isShellEnabled: boolean; + isPerformanceTabAvailable: boolean; + isPerformanceTabSupported: boolean; + isAtlas: boolean; +}): NavigationItemActions => { + const actions: NavigationItemActions = [ + // Collection-specific actions + { + action: 'open-in-new-tab', + label: 'Open in new tab', + icon: 'OpenNewTab', + }, + ]; + + let writeActions: NavigationItemActions = []; + + if (!hasWriteActionsDisabled) { + if (type === 'view') { + writeActions = [ + { separator: true }, + { + action: 'duplicate-view', + label: 'Duplicate view', + icon: 'Copy', + }, + { + action: 'modify-view', + label: 'Modify view', + icon: 'Edit', + }, + { + action: 'drop-collection', + label: 'Drop view', + icon: 'Trash', + }, + ]; + } else { + writeActions = stripNullActions([ + { separator: true }, + type !== 'timeseries' && isRenameCollectionEnabled + ? { + action: 'rename-collection', + label: 'Rename collection', + icon: 'Edit', + } + : null, + { + action: 'create-collection', + icon: 'Plus', + label: 'Create collection', + }, + { + action: 'drop-collection', + label: 'Drop collection', + icon: 'Trash', + }, + ]); + } + } + + return [ + ...actions, + ...writeActions, + { separator: true }, + ...connectionContextMenuActions({ + isShellEnabled, + isPerformanceTabAvailable, + isPerformanceTabSupported, + isAtlas, + hasWriteActionsDisabled, + connectionInfo: undefined, + }), + ]; +}; diff --git a/packages/compass-connections-navigation/src/navigation-item.tsx b/packages/compass-connections-navigation/src/navigation-item.tsx index 4ba31889ce7..b840de2561f 100644 --- a/packages/compass-connections-navigation/src/navigation-item.tsx +++ b/packages/compass-connections-navigation/src/navigation-item.tsx @@ -4,8 +4,10 @@ import { css, palette, ItemActionControls, - type ItemAction, useDarkMode, + useContextMenuGroups, + type ItemAction, + type ContextMenuItem, } from '@mongodb-js/compass-components'; import { PlaceholderItem } from './placeholder'; import StyledNavigationItem from './styled-navigation-item'; @@ -101,6 +103,7 @@ type NavigationItemProps = { }; onItemAction: (item: SidebarActionableItem, action: Actions) => void; onItemExpand(item: SidebarActionableItem, isExpanded: boolean): void; + getContextMenuGroups(item: SidebarTreeItem): ContextMenuItem[][]; }; export function NavigationItem({ @@ -110,6 +113,7 @@ export function NavigationItem({ onItemAction, onItemExpand, getItemActions, + getContextMenuGroups, }: NavigationItemProps) { const isDarkMode = useDarkMode(); const onAction = useCallback( @@ -138,6 +142,12 @@ export function NavigationItem({ }; }, [getItemActions, item, onAction]); + const contextMenuTriggerRef: React.RefCallback = + useContextMenuGroups( + () => getContextMenuGroups(item), + [item, getContextMenuGroups] + ); + const itemDataProps = useMemo(() => { if (item.type === 'placeholder') { return {}; @@ -212,6 +222,7 @@ export function NavigationItem({ ) : ( { return databaseToItems({ connectionId: connectionInfo.id, + connectionItem: connectionTI, database, expandedItems: expandedItems[connectionInfo.id] || {}, level: 2, @@ -262,6 +265,7 @@ const databaseToItems = ({ isNonExistent, }, connectionId, + connectionItem, expandedItems = {}, level, colorCode, @@ -271,6 +275,7 @@ const databaseToItems = ({ }: { database: Database; connectionId: string; + connectionItem: ConnectedConnectionTreeItem; expandedItems?: Record; level: number; colorCode?: string; @@ -289,6 +294,7 @@ const databaseToItems = ({ isExpanded, colorCode, connectionId, + connectionItem, dbName: id, isExpandable: true, hasWriteActionsDisabled, @@ -330,6 +336,7 @@ const databaseToItems = ({ posInSet: collectionIndex + 1, colorCode, connectionId, + databaseItem: databaseTI, namespace: id, hasWriteActionsDisabled, isExpandable: false, diff --git a/packages/compass-connections-navigation/src/virtual-list/use-virtual-navigation-tree.tsx b/packages/compass-connections-navigation/src/virtual-list/use-virtual-navigation-tree.tsx index 2af11795c53..cdc8e7226e2 100644 --- a/packages/compass-connections-navigation/src/virtual-list/use-virtual-navigation-tree.tsx +++ b/packages/compass-connections-navigation/src/virtual-list/use-virtual-navigation-tree.tsx @@ -14,6 +14,7 @@ export type VirtualTreeItem = { posInSet: number; isExpandable: boolean; isExpanded?: boolean; + entrypoint?: 'sidebar' | 'context-menu'; }; export type VirtualPlaceholderItem = { diff --git a/packages/compass-connections-navigation/src/virtual-list/virtual-list.tsx b/packages/compass-connections-navigation/src/virtual-list/virtual-list.tsx index 73de36739de..1bc2605e0bd 100644 --- a/packages/compass-connections-navigation/src/virtual-list/virtual-list.tsx +++ b/packages/compass-connections-navigation/src/virtual-list/virtual-list.tsx @@ -14,6 +14,7 @@ import { mergeProps, useFocusRing, useId, + type ContextMenuItem, } from '@mongodb-js/compass-components'; import { type SidebarActionableItem, type SidebarTreeItem } from '../tree-data'; import { type Actions } from '../constants'; @@ -73,6 +74,10 @@ type RenderItem = (props: { collapseAfter: number; }; }; + getContextMenuGroups: ( + this: void, + item: SidebarTreeItem + ) => ContextMenuItem[][]; }) => React.ReactNode; export type OnDefaultAction = ( item: T, @@ -104,6 +109,7 @@ type VirtualTreeProps = { collapseAfter: number; }; }; + getContextMenuGroups(this: void, item: SidebarTreeItem): ContextMenuItem[][]; __TEST_OVER_SCAN_COUNT?: number; }; @@ -133,6 +139,7 @@ export function VirtualTree({ onItemExpand, onItemAction, getItemActions, + getContextMenuGroups, __TEST_OVER_SCAN_COUNT, }: VirtualTreeProps) { const listRef = useRef(null); @@ -172,6 +179,7 @@ export function VirtualTree({ onItemAction, onItemExpand, getItemActions, + getContextMenuGroups, }; }, [ items, @@ -183,6 +191,7 @@ export function VirtualTree({ onItemAction, getItemActions, onItemExpand, + getContextMenuGroups, ]); const getItemKey = useCallback( @@ -241,6 +250,7 @@ type VirtualItemData = { collapseAfter: number; }; }; + getContextMenuGroups(this: void, item: SidebarTreeItem): ContextMenuItem[][]; }; function TreeItem({ index, @@ -263,6 +273,7 @@ function TreeItem({ onItemAction: data.onItemAction, onItemExpand: data.onItemExpand, getItemActions: data.getItemActions, + getContextMenuGroups: data.getContextMenuGroups, }); }, [ renderItem, @@ -274,6 +285,7 @@ function TreeItem({ data.onItemAction, data.getItemActions, data.onItemExpand, + data.getContextMenuGroups, ]); const actionProps = useDefaultAction( diff --git a/packages/compass-sidebar/package.json b/packages/compass-sidebar/package.json index f61a0807857..a9dfd457d22 100644 --- a/packages/compass-sidebar/package.json +++ b/packages/compass-sidebar/package.json @@ -48,6 +48,7 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { + "@mongodb-js/compass-app-registry": "^9.4.14", "@mongodb-js/compass-app-stores": "^7.50.0", "@mongodb-js/compass-components": "^1.42.0", "@mongodb-js/compass-connection-import-export": "^0.60.0", @@ -58,8 +59,8 @@ "@mongodb-js/compass-telemetry": "^1.10.3", "@mongodb-js/compass-workspaces": "^0.45.0", "@mongodb-js/connection-info": "^0.15.5", + "@mongodb-js/mongodb-constants": "^0.12.1", "compass-preferences-model": "^2.44.0", - "@mongodb-js/compass-app-registry": "^9.4.14", "lodash": "^4.17.21", "mongodb": "^6.16.0", "mongodb-instance-model": "^12.36.0", diff --git a/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx b/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx index f6102502a50..594e4a9bba6 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx @@ -27,9 +27,7 @@ import type { MapDispatchToProps, MapStateToProps } from 'react-redux'; import type { Actions, SidebarConnectedConnection, - SidebarConnectedConnectionTreeItem, SidebarConnection, - SidebarNotConnectedConnectionTreeItem, SidebarItem, } from '@mongodb-js/compass-connections-navigation'; import type { WorkspaceTab } from '@mongodb-js/compass-workspaces'; @@ -63,6 +61,7 @@ import { } from '@mongodb-js/compass-connection-import-export'; import { useTelemetry } from '@mongodb-js/compass-telemetry/provider'; import { usePreference } from 'compass-preferences-model/provider'; +import { wrapField } from '@mongodb-js/mongodb-constants'; const connectionsContainerStyles = css({ height: '100%', @@ -317,105 +316,125 @@ const ConnectionsNavigation: React.FC = ({ return actions; }, [supportsConnectionImportExport, enableCreatingNewConnections]); - const onConnectionItemAction = useCallback( - ( - item: - | SidebarConnectedConnectionTreeItem - | SidebarNotConnectedConnectionTreeItem, - action: Actions - ) => { + const onItemAction = useCallback( + (item: SidebarItem, action: Actions) => { + const getConnectionInfo = (item: SidebarItem) => { + switch (item.type) { + case 'connection': + return item.connectionInfo; + case 'database': + return item.connectionItem.connectionInfo; + case 'view': + case 'collection': + case 'timeseries': + return item.databaseItem.connectionItem.connectionInfo; + default: + throw new Error( + `Item type does not have connection info for action ${action}` + ); + } + }; + + const getNamespace = (item: SidebarItem) => { + if (item.type === 'connection') { + throw new Error( + `Item type ${item.type} does not have a namespace for action ${action}` + ); + } + return item.type === 'database' ? item.dbName : item.namespace; + }; + + const connectionId = + item.type === 'connection' ? item.connectionInfo.id : item.connectionId; + switch (action) { case 'select-connection': - openDatabasesWorkspace(item.connectionInfo.id); + openDatabasesWorkspace(connectionId); return; case 'refresh-databases': - _onRefreshDatabases(item.connectionInfo.id); + _onRefreshDatabases(connectionId); return; case 'create-database': - _onNamespaceAction(item.connectionInfo.id, '', action); + _onNamespaceAction(connectionId, '', action); return; - case 'open-shell': - openShellWorkspace(item.connectionInfo.id, { newTab: true }); - track('Open Shell', { entrypoint: 'sidebar' }, item.connectionInfo); + case 'open-shell': { + let initialEvaluate: string | undefined = undefined; + let initialInput: string | undefined = undefined; + + if (item.type === 'database') { + initialEvaluate = `use ${item.dbName};`; + } + + if (item.type === 'collection') { + initialEvaluate = `use ${item.databaseItem.dbName};`; + initialInput = `db[${wrapField(item.name, true)}].find()`; + } + + openShellWorkspace(connectionId, { + newTab: true, + initialEvaluate, + initialInput, + }); + track( + 'Open Shell', + { entrypoint: item.entrypoint ?? 'sidebar' }, + getConnectionInfo(item) + ); return; + } case 'connection-performance-metrics': - openPerformanceWorkspace(item.connectionInfo.id); + openPerformanceWorkspace(connectionId); return; case 'open-connection-info': - onOpenConnectionInfo(item.connectionInfo.id); + onOpenConnectionInfo(connectionId); return; case 'connection-disconnect': - onDisconnect(item.connectionInfo.id); + onDisconnect(connectionId); return; case 'connection-connect': - onConnect(item.connectionInfo); + onConnect(getConnectionInfo(item)); return; case 'connection-connect-in-new-window': - onConnectInNewWindow(item.connectionInfo); + onConnectInNewWindow(getConnectionInfo(item)); return; case 'edit-connection': - onEditConnection(item.connectionInfo); + onEditConnection(getConnectionInfo(item)); return; case 'copy-connection-string': - onCopyConnectionString(item.connectionInfo); + onCopyConnectionString(getConnectionInfo(item)); return; case 'connection-toggle-favorite': - onToggleFavoriteConnectionInfo(item.connectionInfo); + onToggleFavoriteConnectionInfo(getConnectionInfo(item)); return; case 'duplicate-connection': - onDuplicateConnection(item.connectionInfo); + onDuplicateConnection(getConnectionInfo(item)); return; case 'remove-connection': - onRemoveConnection(item.connectionInfo); + onRemoveConnection(getConnectionInfo(item)); return; case 'open-csfle-modal': - onOpenCsfleModal(item.connectionInfo.id); + onOpenCsfleModal(connectionId); return; case 'open-non-genuine-mongodb-modal': - onOpenNonGenuineMongoDBModal(item.connectionInfo.id); + onOpenNonGenuineMongoDBModal(connectionId); return; case 'show-connect-via-modal': - onOpenConnectViaModal?.(item.connectionInfo.atlasMetadata); + onOpenConnectViaModal?.(getConnectionInfo(item).atlasMetadata); return; - } - }, - [ - openDatabasesWorkspace, - _onRefreshDatabases, - _onNamespaceAction, - openShellWorkspace, - track, - openPerformanceWorkspace, - onOpenConnectionInfo, - onDisconnect, - onConnect, - onConnectInNewWindow, - onEditConnection, - onCopyConnectionString, - onToggleFavoriteConnectionInfo, - onDuplicateConnection, - onRemoveConnection, - onOpenCsfleModal, - onOpenNonGenuineMongoDBModal, - onOpenConnectViaModal, - ] - ); - - const onNamespaceAction = useCallback( - (connectionId: string, ns: string, action: Actions) => { - switch (action) { case 'select-database': - openCollectionsWorkspace(connectionId, ns); + openCollectionsWorkspace(connectionId, getNamespace(item)); return; case 'select-collection': - openCollectionWorkspace(connectionId, ns); + openCollectionWorkspace(connectionId, getNamespace(item)); return; case 'open-in-new-tab': - openCollectionWorkspace(connectionId, ns, { newTab: true }); + openCollectionWorkspace(connectionId, getNamespace(item), { + newTab: true, + }); return; case 'modify-view': { const coll = findCollection( - ns, + getNamespace(item), (connections.find( (conn): conn is SidebarConnectedConnection => conn.connectionStatus === ConnectionStatus.Connected && @@ -432,11 +451,28 @@ const ConnectionsNavigation: React.FC = ({ return; } default: - _onNamespaceAction(connectionId, ns, action); + _onNamespaceAction(connectionId, getNamespace(item), action); return; } }, [ + openDatabasesWorkspace, + _onRefreshDatabases, + openShellWorkspace, + track, + openPerformanceWorkspace, + onOpenConnectionInfo, + onDisconnect, + onConnect, + onConnectInNewWindow, + onEditConnection, + onCopyConnectionString, + onToggleFavoriteConnectionInfo, + onDuplicateConnection, + onRemoveConnection, + onOpenCsfleModal, + onOpenNonGenuineMongoDBModal, + onOpenConnectViaModal, connections, openCollectionsWorkspace, openCollectionWorkspace, @@ -445,19 +481,6 @@ const ConnectionsNavigation: React.FC = ({ ] ); - const onItemAction = useCallback( - (item: SidebarItem, action: Actions) => { - if (item.type === 'connection') { - onConnectionItemAction(item, action); - } else { - const namespace = - item.type === 'database' ? item.dbName : item.namespace; - onNamespaceAction(item.connectionId, namespace, action); - } - }, - [onConnectionItemAction, onNamespaceAction] - ); - const onItemExpand = useCallback( (item: SidebarItem, isExpanded: boolean) => { if (item.type === 'connection') { @@ -485,7 +508,7 @@ const ConnectionsNavigation: React.FC = ({ [onCollapseAll, onNewConnection, openConnectionImportExportModal] ); - const ref = useContextMenuItems( + const contextMenuRef = useContextMenuItems( () => connectionListTitleActions.map(({ label, action }) => ({ label, @@ -526,10 +549,11 @@ const ConnectionsNavigation: React.FC = ({ ) : undefined; return ( -
+
{isAtlasConnectionStorage ? 'Clusters' : 'Connections'} diff --git a/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx b/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx index db7124e0d19..823f568ff07 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx @@ -355,13 +355,13 @@ describe('Multiple Connections Sidebar Component', function () { expect(copyAction).to.be.visible; // Unfavorite because the connection is already a favorite - const favAction = screen.getByText('Unfavorite'); + const favAction = screen.getByText('Unfavorite connection'); expect(favAction).to.be.visible; - const duplicateAction = screen.getByText('Duplicate'); + const duplicateAction = screen.getByText('Duplicate connection'); expect(duplicateAction).to.be.visible; - const removeAction = screen.getByText('Remove'); + const removeAction = screen.getByText('Remove connection'); expect(removeAction).to.be.visible; }); @@ -386,13 +386,13 @@ describe('Multiple Connections Sidebar Component', function () { expect(copyAction).to.be.visible; // Favorite because the connection is not yet a favorite - const favAction = screen.getByText('Favorite'); + const favAction = screen.getByText('Favorite connection'); expect(favAction).to.be.visible; - const duplicateAction = screen.getByText('Duplicate'); + const duplicateAction = screen.getByText('Duplicate connection'); expect(duplicateAction).to.be.visible; - const removeAction = screen.getByText('Remove'); + const removeAction = screen.getByText('Remove connection'); expect(removeAction).to.be.visible; }); }); @@ -437,9 +437,9 @@ describe('Multiple Connections Sidebar Component', function () { expect(screen.getByText('Copy connection string')).to.be.visible; // because it is already a favorite - expect(screen.getByText('Unfavorite')).to.be.visible; - expect(screen.getByText('Duplicate')).to.be.visible; - expect(screen.getByText('Remove')).to.be.visible; + expect(screen.getByText('Unfavorite connection')).to.be.visible; + expect(screen.getByText('Duplicate connection')).to.be.visible; + expect(screen.getByText('Remove connection')).to.be.visible; }); it('should render the only connected connections when toggled', async () => { @@ -511,7 +511,11 @@ describe('Multiple Connections Sidebar Component', function () { expect(workspace.openShellWorkspace).to.have.been.calledWith( savedFavoriteConnection.id, - { newTab: true } + { + newTab: true, + initialEvaluate: undefined, + initialInput: undefined, + } ); await waitFor(() => { @@ -631,7 +635,7 @@ describe('Multiple Connections Sidebar Component', function () { within(connectionItem).getByLabelText('Show actions') ); - userEvent.click(screen.getByText('Unfavorite')); + userEvent.click(screen.getByText('Unfavorite connection')); await waitFor(() => { expect( @@ -652,7 +656,7 @@ describe('Multiple Connections Sidebar Component', function () { within(connectionItem).getByLabelText('Show actions') ); - userEvent.click(screen.getByText('Duplicate')); + userEvent.click(screen.getByText('Duplicate connection')); // We see the connect button in the form modal expect(screen.getByTestId('connect-button')).to.be.visible; @@ -675,7 +679,7 @@ describe('Multiple Connections Sidebar Component', function () { within(connectionItem).getByLabelText('Show actions') ); - userEvent.click(screen.getByText('Remove')); + userEvent.click(screen.getByText('Remove connection')); await waitFor(() => { expect(