Skip to content

Commit 266bbb6

Browse files
authored
feat: do not allow opening a new connection when the limit is reached COMPASS-7727 (#5624)
* chore: draft, limit number of maximum open connections * chore: remove console.log * chore: put use-all-saved-connections under test * chore: enable tests * chore: add tests to hook * chore: remove .only * chore: add clue on what to do when the limit is reached * chore: add new tests * chore: fix bootstrap * chore: fix linter * chore: fix linting issues * chore: use useActiveConnections hook instead of a new hook * chore: rename toggle
1 parent 7d97b71 commit 266bbb6

File tree

12 files changed

+284
-24
lines changed

12 files changed

+284
-24
lines changed

packages/compass-connections/src/stores/active-connections.spec.ts renamed to packages/compass-connections/src/hooks/use-active-connections.spec.ts

File renamed without changes.

packages/compass-connections/src/stores/active-connections.ts renamed to packages/compass-connections/src/hooks/use-active-connections.ts

File renamed without changes.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { expect } from 'chai';
2+
import { waitFor } from '@testing-library/react';
3+
import { renderHook } from '@testing-library/react-hooks';
4+
import { createElement } from 'react';
5+
import {
6+
type ConnectionInfo,
7+
type ConnectionStatus,
8+
} from '@mongodb-js/connection-info';
9+
import {
10+
type PreferencesAccess,
11+
createSandboxFromDefaultPreferences,
12+
} from 'compass-preferences-model';
13+
import { PreferencesProvider } from 'compass-preferences-model/provider';
14+
import { ConnectionsManager, ConnectionsManagerProvider } from '../provider';
15+
import {
16+
ConnectionRepositoryContextProvider,
17+
type ConnectionStorage,
18+
ConnectionStorageContext,
19+
} from '@mongodb-js/connection-storage/provider';
20+
import { ConnectionStorageBus } from '@mongodb-js/connection-storage/renderer';
21+
import { useCanOpenNewConnections } from './use-can-open-new-connections';
22+
23+
const FAVORITE_CONNECTION_INFO: ConnectionInfo = {
24+
id: 'favorite',
25+
connectionOptions: {
26+
connectionString: 'mongodb://localhost:27017',
27+
},
28+
savedConnectionType: 'favorite',
29+
};
30+
31+
const NONFAVORITE_CONNECTION_INFO: ConnectionInfo = {
32+
id: 'nonfavorite',
33+
connectionOptions: {
34+
connectionString: 'mongodb://localhost:27017',
35+
},
36+
savedConnectionType: 'recent',
37+
};
38+
39+
describe('useCanOpenNewConnections', function () {
40+
let renderHookWithContext: typeof renderHook;
41+
let connectionStorage: ConnectionStorage;
42+
let connectionManager: ConnectionsManager;
43+
let preferencesAccess: PreferencesAccess;
44+
45+
function withConnectionWithStatus(
46+
connectionId: ConnectionInfo['id'],
47+
status: ConnectionStatus
48+
) {
49+
const connectionManagerInspectable = connectionManager as any;
50+
connectionManagerInspectable.connectionStatuses.set(connectionId, status);
51+
}
52+
53+
async function withConnectionLimit(limit: number) {
54+
await preferencesAccess.savePreferences({
55+
maximumNumberOfActiveConnections: limit,
56+
});
57+
}
58+
beforeEach(async function () {
59+
preferencesAccess = await createSandboxFromDefaultPreferences();
60+
connectionManager = new ConnectionsManager({} as any);
61+
connectionStorage = {
62+
loadAll() {
63+
return Promise.resolve([
64+
FAVORITE_CONNECTION_INFO,
65+
NONFAVORITE_CONNECTION_INFO,
66+
]);
67+
},
68+
events: new ConnectionStorageBus(),
69+
} as ConnectionStorage;
70+
71+
renderHookWithContext = (callback, options) => {
72+
const wrapper: React.FC = ({ children }) =>
73+
createElement(PreferencesProvider, {
74+
value: preferencesAccess,
75+
children: [
76+
createElement(ConnectionStorageContext.Provider, {
77+
value: connectionStorage,
78+
children: [
79+
createElement(ConnectionRepositoryContextProvider, {
80+
children: [
81+
createElement(ConnectionsManagerProvider, {
82+
value: connectionManager,
83+
children,
84+
}),
85+
],
86+
}),
87+
],
88+
}),
89+
],
90+
});
91+
return renderHook(callback, { wrapper, ...options });
92+
};
93+
});
94+
95+
describe('number of active connections', function () {
96+
it('should return the count of active connections', async function () {
97+
withConnectionWithStatus(FAVORITE_CONNECTION_INFO.id, 'connected');
98+
99+
const { result } = renderHookWithContext(() =>
100+
useCanOpenNewConnections()
101+
);
102+
103+
await waitFor(() => {
104+
const { numberOfConnectionsOpen } = result.current;
105+
expect(numberOfConnectionsOpen).to.equal(1);
106+
});
107+
});
108+
});
109+
110+
describe('connection limiting', function () {
111+
it('should not limit when the maximum number of connections is not reached', async function () {
112+
await withConnectionLimit(1);
113+
114+
const { result } = renderHookWithContext(() =>
115+
useCanOpenNewConnections()
116+
);
117+
118+
await waitFor(() => {
119+
const { numberOfConnectionsOpen, canOpenNewConnection } =
120+
result.current;
121+
expect(numberOfConnectionsOpen).to.equal(0);
122+
expect(canOpenNewConnection).to.equal(true);
123+
});
124+
});
125+
126+
it('should limit when the maximum number of connections is reached', async function () {
127+
withConnectionWithStatus(FAVORITE_CONNECTION_INFO.id, 'connected');
128+
await withConnectionLimit(1);
129+
130+
const { result } = renderHookWithContext(() =>
131+
useCanOpenNewConnections()
132+
);
133+
134+
await waitFor(() => {
135+
const {
136+
numberOfConnectionsOpen,
137+
canOpenNewConnection,
138+
canNotOpenReason,
139+
} = result.current;
140+
expect(numberOfConnectionsOpen).to.equal(1);
141+
expect(canOpenNewConnection).to.equal(false);
142+
expect(canNotOpenReason).to.equal('maximum-number-exceeded');
143+
});
144+
});
145+
});
146+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useActiveConnections } from './use-active-connections';
2+
import { usePreference } from 'compass-preferences-model/provider';
3+
4+
export type CanNotOpenConnectionReason = 'maximum-number-exceeded';
5+
6+
export function useCanOpenNewConnections(): {
7+
numberOfConnectionsOpen: number;
8+
maximumNumberOfConnectionsOpen: number;
9+
canOpenNewConnection: boolean;
10+
canNotOpenReason?: CanNotOpenConnectionReason;
11+
} {
12+
const activeConnections = useActiveConnections();
13+
const maximumNumberOfConnectionsOpen =
14+
usePreference('maximumNumberOfActiveConnections') ?? 1;
15+
16+
const numberOfConnectionsOpen = activeConnections.length;
17+
const canOpenNewConnection =
18+
numberOfConnectionsOpen < maximumNumberOfConnectionsOpen;
19+
const canNotOpenReason = !canOpenNewConnection
20+
? 'maximum-number-exceeded'
21+
: undefined;
22+
23+
return {
24+
numberOfConnectionsOpen,
25+
maximumNumberOfConnectionsOpen,
26+
canOpenNewConnection,
27+
canNotOpenReason,
28+
};
29+
}

packages/compass-connections/src/provider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { ConnectionsManager } from './connections-manager';
88
export type { DataService };
99
export * from './connections-manager';
1010
export { useConnections } from './stores/connections-store';
11-
export { useActiveConnections } from './stores/active-connections';
11+
export { useActiveConnections } from './hooks/use-active-connections';
1212

1313
const ConnectionsManagerContext = createContext<ConnectionsManager | null>(
1414
null
@@ -17,6 +17,7 @@ export const ConnectionsManagerProvider = ConnectionsManagerContext.Provider;
1717

1818
export const useConnectionsManagerContext = (): ConnectionsManager => {
1919
const connectionsManager = useContext(ConnectionsManagerContext);
20+
2021
if (!connectionsManager) {
2122
throw new Error(
2223
'ConnectionsManager not available in context. Did you forget to setup ConnectionsManagerProvider'
@@ -63,3 +64,7 @@ export const dataServiceLocator = createServiceLocator(
6364
);
6465

6566
export { useConnectionStatus } from './hooks/use-connection-status';
67+
export {
68+
type CanNotOpenConnectionReason,
69+
useCanOpenNewConnections,
70+
} from './hooks/use-can-open-new-connections';

packages/compass-preferences-model/src/preferences-schema.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags &
5656
enableAggregationBuilderExtraOptions: boolean;
5757
enableHackoladeBanner: boolean;
5858
enablePerformanceAdvisorBanner: boolean;
59+
maximumNumberOfActiveConnections?: number;
5960
};
6061

6162
export type InternalUserPreferences = {
@@ -728,6 +729,17 @@ export const storedUserPreferencesProps: Required<{
728729
type: 'boolean',
729730
},
730731

732+
maximumNumberOfActiveConnections: {
733+
ui: true,
734+
cli: true,
735+
global: true,
736+
description: {
737+
short: 'Limits the amount of open connections.',
738+
},
739+
validator: z.number().default(10),
740+
type: 'number',
741+
},
742+
731743
...allFeatureFlagsProps,
732744
};
733745

packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.spec.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { render, screen, cleanup, waitFor } from '@testing-library/react';
55
import userEvent from '@testing-library/user-event';
66
import { SavedConnectionList } from './saved-connection-list';
77
import type { ConnectionInfo } from '@mongodb-js/connection-info';
8+
import {
9+
ConnectionRepositoryContextProvider,
10+
ConnectionStorageContext,
11+
} from '@mongodb-js/connection-storage/provider';
12+
import { ConnectionStorageBus } from '@mongodb-js/connection-storage/renderer';
13+
814
import {
915
ConnectionsManagerProvider,
1016
ConnectionsManager,
@@ -48,24 +54,38 @@ describe('SavedConnectionList Component', function () {
4854
favoriteInfo: ConnectionInfo[],
4955
nonFavoriteInfo: ConnectionInfo[]
5056
) {
57+
const connectionStorage = {
58+
events: new ConnectionStorageBus(),
59+
loadAll() {
60+
return Promise.resolve([
61+
FAVOURITE_CONNECTION_INFO,
62+
NON_FAVOURITE_CONNECTION_INFO,
63+
]);
64+
},
65+
} as any;
66+
5167
const connectionManager = new ConnectionsManager({
5268
logger: {} as any,
5369
__TEST_CONNECT_FN: connectFn,
5470
});
5571

5672
return render(
57-
<ConnectionsManagerProvider value={connectionManager}>
58-
<SavedConnectionList
59-
favoriteConnections={favoriteInfo}
60-
nonFavoriteConnections={nonFavoriteInfo}
61-
onNewConnection={onNewConnectionSpy}
62-
onConnect={onConnectSpy}
63-
onEditConnection={onEditConnectionSpy}
64-
onDeleteConnection={onDeleteConnectionSpy}
65-
onDuplicateConnection={onDuplicateConnectionSpy}
66-
onToggleFavoriteConnection={onToggleFavoriteConnectionSpy}
67-
/>
68-
</ConnectionsManagerProvider>
73+
<ConnectionStorageContext.Provider value={connectionStorage}>
74+
<ConnectionRepositoryContextProvider>
75+
<ConnectionsManagerProvider value={connectionManager}>
76+
<SavedConnectionList
77+
favoriteConnections={favoriteInfo}
78+
nonFavoriteConnections={nonFavoriteInfo}
79+
onNewConnection={onNewConnectionSpy}
80+
onConnect={onConnectSpy}
81+
onEditConnection={onEditConnectionSpy}
82+
onDeleteConnection={onDeleteConnectionSpy}
83+
onDuplicateConnection={onDuplicateConnectionSpy}
84+
onToggleFavoriteConnection={onToggleFavoriteConnectionSpy}
85+
/>
86+
</ConnectionsManagerProvider>
87+
</ConnectionRepositoryContextProvider>
88+
</ConnectionStorageContext.Provider>
6989
);
7090
}
7191

packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
palette,
1212
} from '@mongodb-js/compass-components';
1313
import { ButtonVariant } from '@mongodb-js/compass-components';
14+
import { useCanOpenNewConnections } from '@mongodb-js/compass-connections/provider';
1415

1516
const savedConnectionListStyles = css({
1617
width: '100%',
@@ -79,6 +80,12 @@ export function SavedConnectionList({
7980
onDuplicateConnection,
8081
onToggleFavoriteConnection,
8182
}: SavedConnectionListProps): React.ReactElement {
83+
const {
84+
maximumNumberOfConnectionsOpen,
85+
canOpenNewConnection,
86+
canNotOpenReason,
87+
} = useCanOpenNewConnections();
88+
8289
const connectionCount =
8390
favoriteConnections.length + nonFavoriteConnections.length;
8491

@@ -106,6 +113,9 @@ export function SavedConnectionList({
106113
<ul className={savedConnectionListPaddingStyles}>
107114
{favoriteConnections.map((conn) => (
108115
<SavedConnection
116+
canOpenNewConnection={canOpenNewConnection}
117+
canNotOpenReason={canNotOpenReason}
118+
maximumNumberOfConnectionsOpen={maximumNumberOfConnectionsOpen}
109119
onConnect={onConnect}
110120
onEditConnection={onEditConnection}
111121
onDuplicateConnection={onDuplicateConnection}
@@ -117,6 +127,9 @@ export function SavedConnectionList({
117127
))}
118128
{nonFavoriteConnections.map((conn) => (
119129
<SavedConnection
130+
canOpenNewConnection={canOpenNewConnection}
131+
canNotOpenReason={canNotOpenReason}
132+
maximumNumberOfConnectionsOpen={maximumNumberOfConnectionsOpen}
120133
onConnect={onConnect}
121134
onEditConnection={onEditConnection}
122135
onDuplicateConnection={onDuplicateConnection}

packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection.spec.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ describe('SavedConnection Component', function () {
4444
onDeleteConnection={onDeleteConnectionSpy}
4545
onDuplicateConnection={onDuplicateConnectionSpy}
4646
onToggleFavoriteConnection={onToggleFavoriteConnectionSpy}
47+
canOpenNewConnection={true}
48+
maximumNumberOfConnectionsOpen={10}
49+
canNotOpenReason={undefined}
4750
connectionInfo={info}
4851
/>
4952
</ConnectionsManagerProvider>

0 commit comments

Comments
 (0)