Skip to content

Commit

Permalink
feat: show list of active connections COMPASS-7654 (#5593)
Browse files Browse the repository at this point in the history
  • Loading branch information
paula-stacho authored Mar 25, 2024
1 parent 7e5d6ae commit d8afda8
Show file tree
Hide file tree
Showing 14 changed files with 682 additions and 7 deletions.
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/compass-connections/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.nyc_output
dist
coverage
2 changes: 2 additions & 0 deletions packages/compass-connections/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
},
"dependencies": {
"@mongodb-js/compass-components": "^1.22.1",
"bson": "^6.5.0",
"@mongodb-js/compass-connection-import-export": "^0.21.1",
"@mongodb-js/compass-logging": "^1.2.14",
"@mongodb-js/compass-maybe-protect-connection-string": "^0.16.1",
Expand All @@ -72,6 +73,7 @@
"@mongodb-js/mocha-config-compass": "^1.3.7",
"@mongodb-js/prettier-config-compass": "^1.0.1",
"@mongodb-js/tsconfig-compass": "^1.0.3",
"@testing-library/dom": "^8.11.1",
"@testing-library/react": "^12.1.4",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
Expand Down
1 change: 1 addition & 0 deletions packages/compass-connections/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +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';

const ConnectionsManagerContext = createContext<ConnectionsManager | null>(
null
Expand Down
152 changes: 152 additions & 0 deletions packages/compass-connections/src/stores/active-connections.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { ConnectionRepository } from '@mongodb-js/connection-storage/main';
import {
ConnectionsManager,
ConnectionsManagerProvider,
useActiveConnections,
} from '../../provider';
import { renderHook } from '@testing-library/react-hooks';
import { createElement } from 'react';
import {
ConnectionRepositoryContext,
ConnectionStorageContext,
} from '@mongodb-js/connection-storage/provider';
import {
ConnectionStorageEvents,
type ConnectionInfo,
type ConnectionStorage,
} from '@mongodb-js/connection-storage/renderer';
import { expect } from 'chai';
import Sinon from 'sinon';
import { waitFor } from '@testing-library/dom';
import { ConnectionsManagerEvents } from '../connections-manager';
import EventEmitter from 'events';

const mockConnections: ConnectionInfo[] = [
{
id: 'turtle',
connectionOptions: {
connectionString: 'mongodb://turtle',
},
favorite: {
name: 'turtles',
},
savedConnectionType: 'favorite',
},
{
id: 'oranges',
connectionOptions: {
connectionString: 'mongodb://peaches',
},
favorite: {
name: 'peaches',
},
savedConnectionType: 'favorite',
},
];

describe('useActiveConnections', function () {
let renderHookWithContext: typeof renderHook;
let connectionRepository: ConnectionRepository;
let connectionsManager: ConnectionsManager;
let mockConnectionStorage: typeof ConnectionStorage;

before(function () {
renderHookWithContext = (callback, options) => {
const wrapper: React.FC = ({ children }) =>
createElement(ConnectionRepositoryContext.Provider, {
value: connectionRepository,
children: [
createElement(ConnectionStorageContext.Provider, {
value: mockConnectionStorage,
children: [
createElement(ConnectionsManagerProvider, {
value: connectionsManager,
children: children,
}),
],
}),
],
});
return renderHook(callback, { wrapper, ...options });
};
});

beforeEach(function () {
connectionsManager = new ConnectionsManager({} as any);
mockConnectionStorage = { loadAll: Sinon.stub().resolves([]) } as any;
connectionRepository = new ConnectionRepository(mockConnectionStorage);
});

it('should return empty list of connections', function () {
const { result } = renderHookWithContext(() => useActiveConnections());
expect(result.current).to.have.length(0);
});

it('should return active connections', async function () {
mockConnectionStorage = {
loadAll: Sinon.stub().resolves(mockConnections),
} as any;
connectionRepository = new ConnectionRepository(mockConnectionStorage);
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
const { result } = renderHookWithContext(() => useActiveConnections());

await waitFor(() => {
expect(result.current).to.have.length(1);
expect(result.current[0]).to.have.property('id', 'turtle');
});
});

it('should listen to connections manager updates', async function () {
mockConnectionStorage = {
loadAll: Sinon.stub().resolves(mockConnections),
} as any;
connectionRepository = new ConnectionRepository(mockConnectionStorage);
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
const { result } = renderHookWithContext(() => useActiveConnections());

await waitFor(() => {
expect(result.current).to.have.length(1);
});

(connectionsManager as any).connectionStatuses.set('oranges', 'connected');
connectionsManager.emit(
ConnectionsManagerEvents.ConnectionAttemptSuccessful,
'orange',
{} as any
);

await waitFor(() => {
expect(result.current).to.have.length(2);
});
});

it('should listen to connections storage updates', async function () {
const loadAllStub = Sinon.stub().resolves(mockConnections);
mockConnectionStorage = {
loadAll: loadAllStub,
events: new EventEmitter(),
} as any;
connectionRepository = new ConnectionRepository(mockConnectionStorage);
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
const { result } = renderHookWithContext(() => useActiveConnections());

loadAllStub.resolves([
{
...mockConnections[0],
savedConnectionType: 'recent',
},
mockConnections[1],
]);
mockConnectionStorage.events.emit(
ConnectionStorageEvents.ConnectionsChanged
);

await waitFor(() => {
expect(result.current).to.have.length(1);
expect(result.current[0]).to.have.property(
'savedConnectionType',
'recent'
);
});
});
});
80 changes: 80 additions & 0 deletions packages/compass-connections/src/stores/active-connections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { ConnectionInfo } from '@mongodb-js/connection-info';
import { useCallback, useEffect, useState } from 'react';
import { BSON } from 'bson';
import {
useConnectionRepositoryContext,
useConnectionStorageContext,
} from '@mongodb-js/connection-storage/provider';
import {
ConnectionsManagerEvents,
useConnectionsManagerContext,
} from '../provider';
import isEqual from 'lodash/isEqual';
import { ConnectionStorageEvents } from '@mongodb-js/connection-storage/renderer';

/**
* Same as _.isEqual, except it takes key order into account
*/
function areConnectionsEqual(
listA: ConnectionInfo[],
listB: ConnectionInfo[]
): boolean {
return isEqual(
listA.map((a: any) => BSON.serialize(a)),
listB.map((b: any) => BSON.serialize(b))
);
}

export function useActiveConnections(): ConnectionInfo[] {
// TODO(COMPASS-7397): services should not be used directly in render method,
// when this code is refactored to use the hadron plugin interface, storage
// should be handled through the plugin activation lifecycle
const connectionManager = useConnectionsManagerContext();
const connectionRepository = useConnectionRepositoryContext();
const connectionStorage = useConnectionStorageContext();

const [activeConnections, setActiveConnections] = useState<ConnectionInfo[]>(
[]
);

const updateList = useCallback(
() =>
void (async () => {
const newList = [
...(await connectionRepository.listFavoriteConnections()),
...(await connectionRepository.listNonFavoriteConnections()),
].filter(({ id }) => connectionManager.statusOf(id) === 'connected');
setActiveConnections((prevList) => {
return areConnectionsEqual(prevList, newList) ? prevList : newList;
});
})(),
[connectionRepository, connectionManager]
);

useEffect(() => {
updateList();

// reacting to connection status updates
for (const event of Object.values(ConnectionsManagerEvents)) {
connectionManager.on(event, updateList);
}

// reacting to connection info updates
connectionStorage.events?.on(
ConnectionStorageEvents.ConnectionsChanged,
updateList
);

return () => {
for (const event of Object.values(ConnectionsManagerEvents)) {
connectionManager.off(event, updateList);
}
connectionStorage.events?.off(
ConnectionStorageEvents.ConnectionsChanged,
updateList
);
};
}, [updateList, connectionManager, connectionStorage]);

return activeConnections;
}
1 change: 1 addition & 0 deletions packages/compass-sidebar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"redux-thunk": "^2.4.2"
},
"devDependencies": {
"mongodb-data-service": "^22.18.1",
"@mongodb-js/eslint-config-compass": "^1.0.17",
"@mongodb-js/mocha-config-compass": "^1.3.7",
"@mongodb-js/prettier-config-compass": "^1.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import { expect } from 'chai';
import { render, screen, waitFor } from '@testing-library/react';
import type { ConnectionInfo } from '@mongodb-js/connection-info';
import { ActiveConnectionList } from './active-connection-list';
import {
ConnectionRepositoryContext,
ConnectionStorageContext,
} from '@mongodb-js/connection-storage/provider';
import {
ConnectionsManager,
ConnectionsManagerProvider,
} from '@mongodb-js/compass-connections/provider';
import { ConnectionRepository } from '@mongodb-js/connection-storage/main';
import type { ConnectionStorage } from '@mongodb-js/connection-storage/renderer';
import Sinon from 'sinon';

const mockConnections: ConnectionInfo[] = [
{
id: 'turtle',
connectionOptions: {
connectionString: 'mongodb://turtle',
},
savedConnectionType: 'recent',
},
{
id: 'oranges',
connectionOptions: {
connectionString: 'mongodb://peaches',
},
favorite: {
name: 'peaches',
},
savedConnectionType: 'favorite',
},
];

describe('<ActiveConnectionList />', function () {
let connectionRepository: ConnectionRepository;
let connectionsManager: ConnectionsManager;
let mockConnectionStorage: typeof ConnectionStorage;

beforeEach(() => {
connectionsManager = new ConnectionsManager({} as any);
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
(connectionsManager as any).connectionStatuses.set('oranges', 'connected');
mockConnectionStorage = {
loadAll: Sinon.stub().resolves(mockConnections),
} as any;
connectionRepository = new ConnectionRepository(mockConnectionStorage);

render(
<ConnectionStorageContext.Provider value={mockConnectionStorage}>
<ConnectionRepositoryContext.Provider value={connectionRepository}>
<ConnectionsManagerProvider value={connectionsManager}>
<ActiveConnectionList />
</ConnectionsManagerProvider>
</ConnectionRepositoryContext.Provider>
</ConnectionStorageContext.Provider>
);
});

it('Should render all active connections - using their correct titles', async function () {
await waitFor(() => {
expect(screen.queryByText('(2)')).to.be.visible;
expect(screen.queryByText('turtle')).to.be.visible;
expect(screen.queryByText('peaches')).to.be.visible;
});
});
});
Loading

0 comments on commit d8afda8

Please sign in to comment.