Skip to content

Commit

Permalink
feat: do not allow opening a new connection when the limit is reached C…
Browse files Browse the repository at this point in the history
…OMPASS-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
  • Loading branch information
kmruiz committed Mar 27, 2024
1 parent 7d97b71 commit 266bbb6
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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,
};
}
7 changes: 6 additions & 1 deletion packages/compass-connections/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectionsManager | null>(
null
Expand All @@ -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'
Expand Down Expand Up @@ -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';
12 changes: 12 additions & 0 deletions packages/compass-preferences-model/src/preferences-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags &
enableAggregationBuilderExtraOptions: boolean;
enableHackoladeBanner: boolean;
enablePerformanceAdvisorBanner: boolean;
maximumNumberOfActiveConnections?: number;
};

export type InternalUserPreferences = {
Expand Down Expand Up @@ -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,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
<ConnectionsManagerProvider value={connectionManager}>
<SavedConnectionList
favoriteConnections={favoriteInfo}
nonFavoriteConnections={nonFavoriteInfo}
onNewConnection={onNewConnectionSpy}
onConnect={onConnectSpy}
onEditConnection={onEditConnectionSpy}
onDeleteConnection={onDeleteConnectionSpy}
onDuplicateConnection={onDuplicateConnectionSpy}
onToggleFavoriteConnection={onToggleFavoriteConnectionSpy}
/>
</ConnectionsManagerProvider>
<ConnectionStorageContext.Provider value={connectionStorage}>
<ConnectionRepositoryContextProvider>
<ConnectionsManagerProvider value={connectionManager}>
<SavedConnectionList
favoriteConnections={favoriteInfo}
nonFavoriteConnections={nonFavoriteInfo}
onNewConnection={onNewConnectionSpy}
onConnect={onConnectSpy}
onEditConnection={onEditConnectionSpy}
onDeleteConnection={onDeleteConnectionSpy}
onDuplicateConnection={onDuplicateConnectionSpy}
onToggleFavoriteConnection={onToggleFavoriteConnectionSpy}
/>
</ConnectionsManagerProvider>
</ConnectionRepositoryContextProvider>
</ConnectionStorageContext.Provider>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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%',
Expand Down Expand Up @@ -79,6 +80,12 @@ export function SavedConnectionList({
onDuplicateConnection,
onToggleFavoriteConnection,
}: SavedConnectionListProps): React.ReactElement {
const {
maximumNumberOfConnectionsOpen,
canOpenNewConnection,
canNotOpenReason,
} = useCanOpenNewConnections();

const connectionCount =
favoriteConnections.length + nonFavoriteConnections.length;

Expand Down Expand Up @@ -106,6 +113,9 @@ export function SavedConnectionList({
<ul className={savedConnectionListPaddingStyles}>
{favoriteConnections.map((conn) => (
<SavedConnection
canOpenNewConnection={canOpenNewConnection}
canNotOpenReason={canNotOpenReason}
maximumNumberOfConnectionsOpen={maximumNumberOfConnectionsOpen}
onConnect={onConnect}
onEditConnection={onEditConnection}
onDuplicateConnection={onDuplicateConnection}
Expand All @@ -117,6 +127,9 @@ export function SavedConnectionList({
))}
{nonFavoriteConnections.map((conn) => (
<SavedConnection
canOpenNewConnection={canOpenNewConnection}
canNotOpenReason={canNotOpenReason}
maximumNumberOfConnectionsOpen={maximumNumberOfConnectionsOpen}
onConnect={onConnect}
onEditConnection={onEditConnection}
onDuplicateConnection={onDuplicateConnection}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ describe('SavedConnection Component', function () {
onDeleteConnection={onDeleteConnectionSpy}
onDuplicateConnection={onDuplicateConnectionSpy}
onToggleFavoriteConnection={onToggleFavoriteConnectionSpy}
canOpenNewConnection={true}
maximumNumberOfConnectionsOpen={10}
canNotOpenReason={undefined}
connectionInfo={info}
/>
</ConnectionsManagerProvider>
Expand Down
Loading

0 comments on commit 266bbb6

Please sign in to comment.