diff --git a/packages/compass-connections/src/stores/active-connections.spec.ts b/packages/compass-connections/src/hooks/use-active-connections.spec.ts similarity index 100% rename from packages/compass-connections/src/stores/active-connections.spec.ts rename to packages/compass-connections/src/hooks/use-active-connections.spec.ts diff --git a/packages/compass-connections/src/stores/active-connections.ts b/packages/compass-connections/src/hooks/use-active-connections.ts similarity index 100% rename from packages/compass-connections/src/stores/active-connections.ts rename to packages/compass-connections/src/hooks/use-active-connections.ts diff --git a/packages/compass-connections/src/hooks/use-can-open-new-connections.spec.ts b/packages/compass-connections/src/hooks/use-can-open-new-connections.spec.ts new file mode 100644 index 00000000000..cb66ce90276 --- /dev/null +++ b/packages/compass-connections/src/hooks/use-can-open-new-connections.spec.ts @@ -0,0 +1,146 @@ +import { expect } from 'chai'; +import { waitFor } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { createElement } from 'react'; +import { + type ConnectionInfo, + type ConnectionStatus, +} from '@mongodb-js/connection-info'; +import { + type PreferencesAccess, + createSandboxFromDefaultPreferences, +} from 'compass-preferences-model'; +import { PreferencesProvider } from 'compass-preferences-model/provider'; +import { ConnectionsManager, ConnectionsManagerProvider } from '../provider'; +import { + ConnectionRepositoryContextProvider, + type ConnectionStorage, + ConnectionStorageContext, +} from '@mongodb-js/connection-storage/provider'; +import { ConnectionStorageBus } from '@mongodb-js/connection-storage/renderer'; +import { useCanOpenNewConnections } from './use-can-open-new-connections'; + +const FAVORITE_CONNECTION_INFO: ConnectionInfo = { + id: 'favorite', + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + savedConnectionType: 'favorite', +}; + +const NONFAVORITE_CONNECTION_INFO: ConnectionInfo = { + id: 'nonfavorite', + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + savedConnectionType: 'recent', +}; + +describe('useCanOpenNewConnections', function () { + let renderHookWithContext: typeof renderHook; + let connectionStorage: ConnectionStorage; + let connectionManager: ConnectionsManager; + let preferencesAccess: PreferencesAccess; + + function withConnectionWithStatus( + connectionId: ConnectionInfo['id'], + status: ConnectionStatus + ) { + const connectionManagerInspectable = connectionManager as any; + connectionManagerInspectable.connectionStatuses.set(connectionId, status); + } + + async function withConnectionLimit(limit: number) { + await preferencesAccess.savePreferences({ + maximumNumberOfActiveConnections: limit, + }); + } + beforeEach(async function () { + preferencesAccess = await createSandboxFromDefaultPreferences(); + connectionManager = new ConnectionsManager({} as any); + connectionStorage = { + loadAll() { + return Promise.resolve([ + FAVORITE_CONNECTION_INFO, + NONFAVORITE_CONNECTION_INFO, + ]); + }, + events: new ConnectionStorageBus(), + } as ConnectionStorage; + + renderHookWithContext = (callback, options) => { + const wrapper: React.FC = ({ children }) => + createElement(PreferencesProvider, { + value: preferencesAccess, + children: [ + createElement(ConnectionStorageContext.Provider, { + value: connectionStorage, + children: [ + createElement(ConnectionRepositoryContextProvider, { + children: [ + createElement(ConnectionsManagerProvider, { + value: connectionManager, + children, + }), + ], + }), + ], + }), + ], + }); + return renderHook(callback, { wrapper, ...options }); + }; + }); + + describe('number of active connections', function () { + it('should return the count of active connections', async function () { + withConnectionWithStatus(FAVORITE_CONNECTION_INFO.id, 'connected'); + + const { result } = renderHookWithContext(() => + useCanOpenNewConnections() + ); + + await waitFor(() => { + const { numberOfConnectionsOpen } = result.current; + expect(numberOfConnectionsOpen).to.equal(1); + }); + }); + }); + + describe('connection limiting', function () { + it('should not limit when the maximum number of connections is not reached', async function () { + await withConnectionLimit(1); + + const { result } = renderHookWithContext(() => + useCanOpenNewConnections() + ); + + await waitFor(() => { + const { numberOfConnectionsOpen, canOpenNewConnection } = + result.current; + expect(numberOfConnectionsOpen).to.equal(0); + expect(canOpenNewConnection).to.equal(true); + }); + }); + + it('should limit when the maximum number of connections is reached', async function () { + withConnectionWithStatus(FAVORITE_CONNECTION_INFO.id, 'connected'); + await withConnectionLimit(1); + + const { result } = renderHookWithContext(() => + useCanOpenNewConnections() + ); + + await waitFor(() => { + const { + numberOfConnectionsOpen, + canOpenNewConnection, + canNotOpenReason, + } = result.current; + expect(numberOfConnectionsOpen).to.equal(1); + expect(canOpenNewConnection).to.equal(false); + expect(canNotOpenReason).to.equal('maximum-number-exceeded'); + }); + }); + }); +}); diff --git a/packages/compass-connections/src/hooks/use-can-open-new-connections.ts b/packages/compass-connections/src/hooks/use-can-open-new-connections.ts new file mode 100644 index 00000000000..6a4aa3607be --- /dev/null +++ b/packages/compass-connections/src/hooks/use-can-open-new-connections.ts @@ -0,0 +1,29 @@ +import { useActiveConnections } from './use-active-connections'; +import { usePreference } from 'compass-preferences-model/provider'; + +export type CanNotOpenConnectionReason = 'maximum-number-exceeded'; + +export function useCanOpenNewConnections(): { + numberOfConnectionsOpen: number; + maximumNumberOfConnectionsOpen: number; + canOpenNewConnection: boolean; + canNotOpenReason?: CanNotOpenConnectionReason; +} { + const activeConnections = useActiveConnections(); + const maximumNumberOfConnectionsOpen = + usePreference('maximumNumberOfActiveConnections') ?? 1; + + const numberOfConnectionsOpen = activeConnections.length; + const canOpenNewConnection = + numberOfConnectionsOpen < maximumNumberOfConnectionsOpen; + const canNotOpenReason = !canOpenNewConnection + ? 'maximum-number-exceeded' + : undefined; + + return { + numberOfConnectionsOpen, + maximumNumberOfConnectionsOpen, + canOpenNewConnection, + canNotOpenReason, + }; +} diff --git a/packages/compass-connections/src/provider.ts b/packages/compass-connections/src/provider.ts index 33721c00756..d987401a39d 100644 --- a/packages/compass-connections/src/provider.ts +++ b/packages/compass-connections/src/provider.ts @@ -8,7 +8,7 @@ import type { ConnectionsManager } from './connections-manager'; export type { DataService }; export * from './connections-manager'; export { useConnections } from './stores/connections-store'; -export { useActiveConnections } from './stores/active-connections'; +export { useActiveConnections } from './hooks/use-active-connections'; const ConnectionsManagerContext = createContext( null @@ -17,6 +17,7 @@ export const ConnectionsManagerProvider = ConnectionsManagerContext.Provider; export const useConnectionsManagerContext = (): ConnectionsManager => { const connectionsManager = useContext(ConnectionsManagerContext); + if (!connectionsManager) { throw new Error( 'ConnectionsManager not available in context. Did you forget to setup ConnectionsManagerProvider' @@ -63,3 +64,7 @@ export const dataServiceLocator = createServiceLocator( ); export { useConnectionStatus } from './hooks/use-connection-status'; +export { + type CanNotOpenConnectionReason, + useCanOpenNewConnections, +} from './hooks/use-can-open-new-connections'; diff --git a/packages/compass-preferences-model/src/preferences-schema.ts b/packages/compass-preferences-model/src/preferences-schema.ts index 4e207172216..60b14e65643 100644 --- a/packages/compass-preferences-model/src/preferences-schema.ts +++ b/packages/compass-preferences-model/src/preferences-schema.ts @@ -56,6 +56,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & enableAggregationBuilderExtraOptions: boolean; enableHackoladeBanner: boolean; enablePerformanceAdvisorBanner: boolean; + maximumNumberOfActiveConnections?: number; }; export type InternalUserPreferences = { @@ -728,6 +729,17 @@ export const storedUserPreferencesProps: Required<{ type: 'boolean', }, + maximumNumberOfActiveConnections: { + ui: true, + cli: true, + global: true, + description: { + short: 'Limits the amount of open connections.', + }, + validator: z.number().default(10), + type: 'number', + }, + ...allFeatureFlagsProps, }; diff --git a/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.spec.tsx b/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.spec.tsx index 992910bf8d3..16306dc99f5 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.spec.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.spec.tsx @@ -5,6 +5,12 @@ import { render, screen, cleanup, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SavedConnectionList } from './saved-connection-list'; import type { ConnectionInfo } from '@mongodb-js/connection-info'; +import { + ConnectionRepositoryContextProvider, + ConnectionStorageContext, +} from '@mongodb-js/connection-storage/provider'; +import { ConnectionStorageBus } from '@mongodb-js/connection-storage/renderer'; + import { ConnectionsManagerProvider, ConnectionsManager, @@ -48,24 +54,38 @@ describe('SavedConnectionList Component', function () { favoriteInfo: ConnectionInfo[], nonFavoriteInfo: ConnectionInfo[] ) { + const connectionStorage = { + events: new ConnectionStorageBus(), + loadAll() { + return Promise.resolve([ + FAVOURITE_CONNECTION_INFO, + NON_FAVOURITE_CONNECTION_INFO, + ]); + }, + } as any; + const connectionManager = new ConnectionsManager({ logger: {} as any, __TEST_CONNECT_FN: connectFn, }); return render( - - - + + + + + + + ); } diff --git a/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.tsx b/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.tsx index ba95e5e1dd9..1fe1dc20c17 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/saved-connections/saved-connection-list.tsx @@ -11,6 +11,7 @@ import { palette, } from '@mongodb-js/compass-components'; import { ButtonVariant } from '@mongodb-js/compass-components'; +import { useCanOpenNewConnections } from '@mongodb-js/compass-connections/provider'; const savedConnectionListStyles = css({ width: '100%', @@ -79,6 +80,12 @@ export function SavedConnectionList({ onDuplicateConnection, onToggleFavoriteConnection, }: SavedConnectionListProps): React.ReactElement { + const { + maximumNumberOfConnectionsOpen, + canOpenNewConnection, + canNotOpenReason, + } = useCanOpenNewConnections(); + const connectionCount = favoriteConnections.length + nonFavoriteConnections.length; @@ -106,6 +113,9 @@ export function SavedConnectionList({