Skip to content

Commit d9383fe

Browse files
feat(compass-sidebar): unified sidebar experience for multiple connections - COMPASS-8016 (#5877)
1 parent 487e54f commit d9383fe

34 files changed

+1885
-2242
lines changed

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { createGlyphComponent, palette, useDarkMode } from '..';
3+
4+
export const ChevronCollapse = createGlyphComponent(
5+
'ChevronCollapse',
6+
(props) => {
7+
const isDarkMode = useDarkMode();
8+
const strokeColor = isDarkMode ? palette.gray.light1 : palette.gray.dark1;
9+
return (
10+
<svg
11+
width="16"
12+
height="16"
13+
viewBox="0 0 16 16"
14+
fill="none"
15+
xmlns="http://www.w3.org/2000/svg"
16+
{...props}
17+
>
18+
<path
19+
d="M4.25 3.25L8 6.5L11.75 3.25M4.25 12.75L8 9.5L11.75 12.75"
20+
stroke={strokeColor}
21+
strokeWidth="1.5"
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
/>
25+
</svg>
26+
);
27+
}
28+
);

packages/compass-components/src/components/item-action-controls.spec.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,29 @@ describe('item action controls components', function () {
124124
expect(within(menuActions).queryByTestId('test-actions-delete-action')).to
125125
.exist;
126126
});
127+
128+
it('renders action icons disabled when collapseAfter is provided with disabled prop', function () {
129+
render(
130+
<ItemActionControls
131+
actions={[
132+
{
133+
action: 'connect',
134+
label: 'Connection',
135+
icon: 'Plus',
136+
isDisabled: true,
137+
},
138+
{ action: 'edit', label: 'Edit', icon: 'Edit' },
139+
{ action: 'delete', label: 'Delete', icon: 'Trash' },
140+
]}
141+
onAction={() => {}}
142+
data-testid="test-actions"
143+
collapseAfter={1}
144+
></ItemActionControls>
145+
);
146+
147+
const actionButton = screen.getByTestId('test-actions-connect-action');
148+
expect(actionButton).to.exist;
149+
expect(actionButton).to.have.attribute('aria-disabled', 'true');
150+
});
127151
});
128152
});

packages/compass-components/src/components/item-action-controls.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type ItemAction<Action extends string> = {
2727
variant?: 'default' | 'destructive';
2828
isDisabled?: boolean;
2929
disabledDescription?: string;
30+
tooltip?: string;
3031
};
3132

3233
export type ItemSeparator = { separator: true };
@@ -326,7 +327,8 @@ export function ItemActionGroup<Action extends string>({
326327
return <MenuSeparator key={`separator-${idx}`} />;
327328
}
328329

329-
const { action, icon, label, tooltip, tooltipProps } = menuItem;
330+
const { action, icon, label, isDisabled, tooltip, tooltipProps } =
331+
menuItem;
330332
const button = (
331333
<ItemActionButton
332334
key={action}
@@ -339,6 +341,7 @@ export function ItemActionGroup<Action extends string>({
339341
onClick={onClick}
340342
className={cx(actionGroupButtonStyle, iconClassName)}
341343
style={iconStyle}
344+
disabled={isDisabled}
342345
/>
343346
);
344347

packages/compass-components/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import type {
4646
ItemAction,
4747
GroupedItemAction,
4848
MenuAction,
49+
ItemSeparator,
4950
} from './components/item-action-controls';
5051
import {
5152
ItemActionControls,
@@ -94,6 +95,7 @@ export type {
9495
ItemAction,
9596
GroupedItemAction,
9697
MenuAction,
98+
ItemSeparator,
9799
ElectronFileDialogOptions,
98100
ElectronShowFileDialogProvider,
99101
};
@@ -196,3 +198,4 @@ export {
196198
RequiredURLSearchParamsProvider,
197199
useRequiredURLSearchParams,
198200
} from './components/links/link';
201+
export { ChevronCollapse } from './components/chevron-collapse-icon';

packages/compass-connections-navigation/src/base-navigation-item.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ const baseItemLabelStyles = css({
5050
marginLeft: spacing[200],
5151
});
5252

53+
const menuStyles = css({
54+
width: '240px',
55+
maxHeight: 'unset',
56+
});
57+
5358
export const NavigationBaseItem = ({
5459
isActive,
5560
actionProps,
@@ -65,6 +70,7 @@ export const NavigationBaseItem = ({
6570
const [hoverProps, isHovered] = useHoverState();
6671
return (
6772
<ItemContainer
73+
data-testid="base-navigation-item"
6874
isActive={isActive}
6975
className={baseItemContainerStyles}
7076
{...hoverProps}
@@ -90,6 +96,7 @@ export const NavigationBaseItem = ({
9096
</ItemLabel>
9197
</ItemButtonWrapper>
9298
<ItemActionControls<Actions>
99+
menuClassName={menuStyles}
93100
isVisible={isActive || isHovered || isFocused}
94101
data-testid="sidebar-navigation-item-actions"
95102
iconSize="small"

packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import userEvent from '@testing-library/user-event';
1111
import { expect } from 'chai';
1212
import Sinon from 'sinon';
1313
import { ConnectionsNavigationTree } from './connections-navigation-tree';
14-
import type { ConnectedConnection } from './tree-data';
14+
import type { ConnectedConnection, Connection } from './tree-data';
1515
import type {
1616
AllPreferences,
1717
PreferencesAccess,
@@ -21,7 +21,7 @@ import { PreferencesProvider } from 'compass-preferences-model/provider';
2121
import { type WorkspaceTab } from '@mongodb-js/compass-workspaces';
2222
import { ConnectionStatus } from '@mongodb-js/compass-connections/provider';
2323

24-
const connections: ConnectedConnection[] = [
24+
const connections: Connection[] = [
2525
{
2626
connectionInfo: {
2727
id: 'connection_ready',
@@ -101,6 +101,17 @@ const connections: ConnectedConnection[] = [
101101
isPerformanceTabSupported: false,
102102
connectionStatus: ConnectionStatus.Connected,
103103
},
104+
{
105+
connectionInfo: {
106+
id: 'connection_disconnected',
107+
connectionOptions: {
108+
connectionString: 'mongodb://connection-disconnected',
109+
},
110+
savedConnectionType: 'recent',
111+
},
112+
name: 'connection_disconnected',
113+
connectionStatus: ConnectionStatus.Disconnected,
114+
},
104115
];
105116

106117
const props: React.ComponentProps<typeof ConnectionsNavigationTree> = {
@@ -399,11 +410,11 @@ describe('ConnectionsNavigationTree', function () {
399410
) {
400411
const readonlyConnections: ConnectedConnection[] = [
401412
{
402-
...connections[0],
413+
...(connections[0] as ConnectedConnection),
403414
isWritable: false,
404415
},
405416
{
406-
...connections[1],
417+
...(connections[1] as ConnectedConnection),
407418
},
408419
];
409420
await renderConnectionsNavigationTree({
@@ -421,11 +432,11 @@ describe('ConnectionsNavigationTree', function () {
421432
) {
422433
const readonlyConnections: ConnectedConnection[] = [
423434
{
424-
...connections[0],
435+
...(connections[0] as ConnectedConnection),
425436
isDataLake: true,
426437
},
427438
{
428-
...connections[1],
439+
...(connections[1] as ConnectedConnection),
429440
},
430441
];
431442
await renderConnectionsNavigationTree({
@@ -441,7 +452,10 @@ describe('ConnectionsNavigationTree', function () {
441452
React.ComponentProps<typeof ConnectionsNavigationTree>
442453
> = {}
443454
) {
444-
const readonlyConnections: ConnectedConnection[] = [...connections];
455+
const readonlyConnections: ConnectedConnection[] = [
456+
connections[0] as ConnectedConnection,
457+
connections[1] as ConnectedConnection,
458+
];
445459
await renderConnectionsNavigationTree(
446460
{
447461
...props,
@@ -607,6 +621,60 @@ describe('ConnectionsNavigationTree', function () {
607621
expect(action).to.equal('create-database');
608622
});
609623

624+
it('should render the connect action for a disconnected connection', async function () {
625+
const spy = Sinon.spy();
626+
await renderConnectionsNavigationTree({
627+
expanded: { connection_ready: { db_ready: true } },
628+
onItemAction: spy,
629+
});
630+
631+
userEvent.hover(screen.getByText('connection_disconnected'));
632+
633+
const connectButton = screen.getByTestId('connection_disconnected');
634+
expect(within(connectButton).getByLabelText('Connect')).to.exist;
635+
636+
userEvent.click(connectButton);
637+
638+
expect(spy).to.be.calledOnce;
639+
const [[item, action]] = spy.args;
640+
expect(item.type).to.equal('connection');
641+
expect(item.connectionInfo.id).to.equal('connection_disconnected');
642+
expect(action).to.equal('connection-connect');
643+
});
644+
645+
context(
646+
'when number of active connections are equal to max allowed active connections',
647+
function () {
648+
it('should render the connect action disabled', async function () {
649+
const spy = Sinon.spy();
650+
await renderConnectionsNavigationTree(
651+
{
652+
expanded: { connection_ready: { db_ready: true } },
653+
onItemAction: spy,
654+
},
655+
{
656+
maximumNumberOfActiveConnections: 2,
657+
}
658+
);
659+
660+
userEvent.hover(screen.getByText('connection_disconnected'));
661+
662+
const disconnectConnectionNavItem = screen.getByTestId(
663+
'connection_disconnected'
664+
);
665+
const connectBtn = within(
666+
disconnectConnectionNavItem
667+
).getByLabelText('Connect');
668+
expect(connectBtn).to.exist;
669+
expect(connectBtn).to.have.attribute('aria-disabled', 'true');
670+
671+
userEvent.click(connectBtn);
672+
673+
expect(spy).to.not.be.called;
674+
});
675+
}
676+
);
677+
610678
context('when performance tab is supported', function () {
611679
it('should show performance action for connection item and activate callback with `connection-performance-metrics` when clicked', async function () {
612680
const spy = Sinon.spy();
@@ -632,7 +700,10 @@ describe('ConnectionsNavigationTree', function () {
632700
await renderConnectionsNavigationTree({
633701
onItemAction: spy,
634702
connections: [
635-
{ ...connections[0], isPerformanceTabSupported: false },
703+
{
704+
...(connections[0] as ConnectedConnection),
705+
isPerformanceTabSupported: false,
706+
},
636707
{ ...connections[1] },
637708
],
638709
});

packages/compass-connections-navigation/src/connections-navigation-tree.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ const ConnectionsNavigationTree: React.FunctionComponent<
6363
const isRenameCollectionEnabled = usePreference(
6464
'enableRenameCollectionModal'
6565
);
66+
const maxAllowedActiveConnections = usePreference(
67+
'maximumNumberOfActiveConnections'
68+
) as number;
69+
const { isConnectDisabled, connectDisabledDescription } = useMemo(() => {
70+
const numberOfActiveConnections = connections.filter(
71+
(connection) => connection.connectionStatus === ConnectionStatus.Connected
72+
).length;
73+
return {
74+
isConnectDisabled:
75+
numberOfActiveConnections >= maxAllowedActiveConnections,
76+
connectDisabledDescription: `Only ${maxAllowedActiveConnections} connection${
77+
numberOfActiveConnections > 1 ? 's' : ''
78+
} can be open at the same time. First disconnect from another cluster.`,
79+
};
80+
}, [maxAllowedActiveConnections, connections]);
81+
6682
const id = useId();
6783

6884
const treeData = useMemo(() => {
@@ -77,7 +93,15 @@ const ConnectionsNavigationTree: React.FunctionComponent<
7793
const onDefaultAction: OnDefaultAction<SidebarActionableItem> = useCallback(
7894
(item, evt) => {
7995
if (item.type === 'connection') {
80-
onItemAction(item, 'select-connection');
96+
if (item.connectionStatus === ConnectionStatus.Connected) {
97+
onItemAction(item, 'select-connection');
98+
} else if (
99+
(item.connectionStatus === ConnectionStatus.Disconnected ||
100+
item.connectionStatus === ConnectionStatus.Failed) &&
101+
!isConnectDisabled
102+
) {
103+
onItemAction(item, 'connection-connect');
104+
}
81105
} else if (item.type === 'database') {
82106
onItemAction(item, 'select-database');
83107
} else {
@@ -88,7 +112,7 @@ const ConnectionsNavigationTree: React.FunctionComponent<
88112
}
89113
}
90114
},
91-
[onItemAction]
115+
[onItemAction, isConnectDisabled]
92116
);
93117

94118
const activeItemId = useMemo(() => {
@@ -113,34 +137,34 @@ const ConnectionsNavigationTree: React.FunctionComponent<
113137
case 'placeholder':
114138
return [];
115139
case 'connection': {
116-
const isFavorite =
117-
item.connectionInfo?.savedConnectionType === 'favorite';
118140
if (item.connectionStatus === ConnectionStatus.Connected) {
119141
return connectedConnectionItemActions({
120-
hasWriteActionsEnabled: item.hasWriteActionsEnabled,
142+
hasWriteActionsDisabled: item.hasWriteActionsDisabled,
121143
isShellEnabled: item.isShellEnabled,
122-
isFavorite,
144+
connectionInfo: item.connectionInfo,
123145
isPerformanceTabSupported: item.isPerformanceTabSupported,
124146
});
125147
} else {
126148
return notConnectedConnectionItemActions({
127149
connectionInfo: item.connectionInfo,
150+
isConnectDisabled,
151+
connectDisabledTooltip: connectDisabledDescription,
128152
});
129153
}
130154
}
131155
case 'database':
132156
return databaseItemActions({
133-
hasWriteActionsEnabled: item.hasWriteActionsEnabled,
157+
hasWriteActionsDisabled: item.hasWriteActionsDisabled,
134158
});
135159
default:
136160
return collectionItemActions({
137-
hasWriteActionsEnabled: item.hasWriteActionsEnabled,
161+
hasWriteActionsDisabled: item.hasWriteActionsDisabled,
138162
type: item.type,
139163
isRenameCollectionEnabled,
140164
});
141165
}
142166
},
143-
[isRenameCollectionEnabled]
167+
[isRenameCollectionEnabled, isConnectDisabled, connectDisabledDescription]
144168
);
145169

146170
const isTestEnv = process.env.NODE_ENV === 'test';

packages/compass-connections-navigation/src/constants.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { spacing } from '@mongodb-js/compass-components';
55
export const MAX_COLLECTION_PLACEHOLDER_ITEMS = Infinity;
66
export const MAX_DATABASE_PLACEHOLDER_ITEMS = Infinity;
77
export const MIN_DATABASE_PLACEHOLDER_ITEMS = 5;
8-
export const ROW_HEIGHT = spacing[5];
8+
export const ROW_HEIGHT = spacing[800];
9+
export const SIDEBAR_COLLAPSE_ICON_WIDTH = 26;
910
// export const COLLETIONS_MARGIN_BOTTOM = spacing[1];
1011

1112
export type Actions =

0 commit comments

Comments
 (0)