Skip to content

Commit d8afda8

Browse files
authored
feat: show list of active connections COMPASS-7654 (#5593)
1 parent 7e5d6ae commit d8afda8

File tree

14 files changed

+682
-7
lines changed

14 files changed

+682
-7
lines changed

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.nyc_output
22
dist
3+
coverage

packages/compass-connections/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
},
5353
"dependencies": {
5454
"@mongodb-js/compass-components": "^1.22.1",
55+
"bson": "^6.5.0",
5556
"@mongodb-js/compass-connection-import-export": "^0.21.1",
5657
"@mongodb-js/compass-logging": "^1.2.14",
5758
"@mongodb-js/compass-maybe-protect-connection-string": "^0.16.1",
@@ -72,6 +73,7 @@
7273
"@mongodb-js/mocha-config-compass": "^1.3.7",
7374
"@mongodb-js/prettier-config-compass": "^1.0.1",
7475
"@mongodb-js/tsconfig-compass": "^1.0.3",
76+
"@testing-library/dom": "^8.11.1",
7577
"@testing-library/react": "^12.1.4",
7678
"@testing-library/react-hooks": "^7.0.2",
7779
"@testing-library/user-event": "^13.5.0",

packages/compass-connections/src/provider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +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';
1112

1213
const ConnectionsManagerContext = createContext<ConnectionsManager | null>(
1314
null
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { ConnectionRepository } from '@mongodb-js/connection-storage/main';
2+
import {
3+
ConnectionsManager,
4+
ConnectionsManagerProvider,
5+
useActiveConnections,
6+
} from '../../provider';
7+
import { renderHook } from '@testing-library/react-hooks';
8+
import { createElement } from 'react';
9+
import {
10+
ConnectionRepositoryContext,
11+
ConnectionStorageContext,
12+
} from '@mongodb-js/connection-storage/provider';
13+
import {
14+
ConnectionStorageEvents,
15+
type ConnectionInfo,
16+
type ConnectionStorage,
17+
} from '@mongodb-js/connection-storage/renderer';
18+
import { expect } from 'chai';
19+
import Sinon from 'sinon';
20+
import { waitFor } from '@testing-library/dom';
21+
import { ConnectionsManagerEvents } from '../connections-manager';
22+
import EventEmitter from 'events';
23+
24+
const mockConnections: ConnectionInfo[] = [
25+
{
26+
id: 'turtle',
27+
connectionOptions: {
28+
connectionString: 'mongodb://turtle',
29+
},
30+
favorite: {
31+
name: 'turtles',
32+
},
33+
savedConnectionType: 'favorite',
34+
},
35+
{
36+
id: 'oranges',
37+
connectionOptions: {
38+
connectionString: 'mongodb://peaches',
39+
},
40+
favorite: {
41+
name: 'peaches',
42+
},
43+
savedConnectionType: 'favorite',
44+
},
45+
];
46+
47+
describe('useActiveConnections', function () {
48+
let renderHookWithContext: typeof renderHook;
49+
let connectionRepository: ConnectionRepository;
50+
let connectionsManager: ConnectionsManager;
51+
let mockConnectionStorage: typeof ConnectionStorage;
52+
53+
before(function () {
54+
renderHookWithContext = (callback, options) => {
55+
const wrapper: React.FC = ({ children }) =>
56+
createElement(ConnectionRepositoryContext.Provider, {
57+
value: connectionRepository,
58+
children: [
59+
createElement(ConnectionStorageContext.Provider, {
60+
value: mockConnectionStorage,
61+
children: [
62+
createElement(ConnectionsManagerProvider, {
63+
value: connectionsManager,
64+
children: children,
65+
}),
66+
],
67+
}),
68+
],
69+
});
70+
return renderHook(callback, { wrapper, ...options });
71+
};
72+
});
73+
74+
beforeEach(function () {
75+
connectionsManager = new ConnectionsManager({} as any);
76+
mockConnectionStorage = { loadAll: Sinon.stub().resolves([]) } as any;
77+
connectionRepository = new ConnectionRepository(mockConnectionStorage);
78+
});
79+
80+
it('should return empty list of connections', function () {
81+
const { result } = renderHookWithContext(() => useActiveConnections());
82+
expect(result.current).to.have.length(0);
83+
});
84+
85+
it('should return active connections', async function () {
86+
mockConnectionStorage = {
87+
loadAll: Sinon.stub().resolves(mockConnections),
88+
} as any;
89+
connectionRepository = new ConnectionRepository(mockConnectionStorage);
90+
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
91+
const { result } = renderHookWithContext(() => useActiveConnections());
92+
93+
await waitFor(() => {
94+
expect(result.current).to.have.length(1);
95+
expect(result.current[0]).to.have.property('id', 'turtle');
96+
});
97+
});
98+
99+
it('should listen to connections manager updates', async function () {
100+
mockConnectionStorage = {
101+
loadAll: Sinon.stub().resolves(mockConnections),
102+
} as any;
103+
connectionRepository = new ConnectionRepository(mockConnectionStorage);
104+
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
105+
const { result } = renderHookWithContext(() => useActiveConnections());
106+
107+
await waitFor(() => {
108+
expect(result.current).to.have.length(1);
109+
});
110+
111+
(connectionsManager as any).connectionStatuses.set('oranges', 'connected');
112+
connectionsManager.emit(
113+
ConnectionsManagerEvents.ConnectionAttemptSuccessful,
114+
'orange',
115+
{} as any
116+
);
117+
118+
await waitFor(() => {
119+
expect(result.current).to.have.length(2);
120+
});
121+
});
122+
123+
it('should listen to connections storage updates', async function () {
124+
const loadAllStub = Sinon.stub().resolves(mockConnections);
125+
mockConnectionStorage = {
126+
loadAll: loadAllStub,
127+
events: new EventEmitter(),
128+
} as any;
129+
connectionRepository = new ConnectionRepository(mockConnectionStorage);
130+
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
131+
const { result } = renderHookWithContext(() => useActiveConnections());
132+
133+
loadAllStub.resolves([
134+
{
135+
...mockConnections[0],
136+
savedConnectionType: 'recent',
137+
},
138+
mockConnections[1],
139+
]);
140+
mockConnectionStorage.events.emit(
141+
ConnectionStorageEvents.ConnectionsChanged
142+
);
143+
144+
await waitFor(() => {
145+
expect(result.current).to.have.length(1);
146+
expect(result.current[0]).to.have.property(
147+
'savedConnectionType',
148+
'recent'
149+
);
150+
});
151+
});
152+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { ConnectionInfo } from '@mongodb-js/connection-info';
2+
import { useCallback, useEffect, useState } from 'react';
3+
import { BSON } from 'bson';
4+
import {
5+
useConnectionRepositoryContext,
6+
useConnectionStorageContext,
7+
} from '@mongodb-js/connection-storage/provider';
8+
import {
9+
ConnectionsManagerEvents,
10+
useConnectionsManagerContext,
11+
} from '../provider';
12+
import isEqual from 'lodash/isEqual';
13+
import { ConnectionStorageEvents } from '@mongodb-js/connection-storage/renderer';
14+
15+
/**
16+
* Same as _.isEqual, except it takes key order into account
17+
*/
18+
function areConnectionsEqual(
19+
listA: ConnectionInfo[],
20+
listB: ConnectionInfo[]
21+
): boolean {
22+
return isEqual(
23+
listA.map((a: any) => BSON.serialize(a)),
24+
listB.map((b: any) => BSON.serialize(b))
25+
);
26+
}
27+
28+
export function useActiveConnections(): ConnectionInfo[] {
29+
// TODO(COMPASS-7397): services should not be used directly in render method,
30+
// when this code is refactored to use the hadron plugin interface, storage
31+
// should be handled through the plugin activation lifecycle
32+
const connectionManager = useConnectionsManagerContext();
33+
const connectionRepository = useConnectionRepositoryContext();
34+
const connectionStorage = useConnectionStorageContext();
35+
36+
const [activeConnections, setActiveConnections] = useState<ConnectionInfo[]>(
37+
[]
38+
);
39+
40+
const updateList = useCallback(
41+
() =>
42+
void (async () => {
43+
const newList = [
44+
...(await connectionRepository.listFavoriteConnections()),
45+
...(await connectionRepository.listNonFavoriteConnections()),
46+
].filter(({ id }) => connectionManager.statusOf(id) === 'connected');
47+
setActiveConnections((prevList) => {
48+
return areConnectionsEqual(prevList, newList) ? prevList : newList;
49+
});
50+
})(),
51+
[connectionRepository, connectionManager]
52+
);
53+
54+
useEffect(() => {
55+
updateList();
56+
57+
// reacting to connection status updates
58+
for (const event of Object.values(ConnectionsManagerEvents)) {
59+
connectionManager.on(event, updateList);
60+
}
61+
62+
// reacting to connection info updates
63+
connectionStorage.events?.on(
64+
ConnectionStorageEvents.ConnectionsChanged,
65+
updateList
66+
);
67+
68+
return () => {
69+
for (const event of Object.values(ConnectionsManagerEvents)) {
70+
connectionManager.off(event, updateList);
71+
}
72+
connectionStorage.events?.off(
73+
ConnectionStorageEvents.ConnectionsChanged,
74+
updateList
75+
);
76+
};
77+
}, [updateList, connectionManager, connectionStorage]);
78+
79+
return activeConnections;
80+
}

packages/compass-sidebar/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"redux-thunk": "^2.4.2"
7171
},
7272
"devDependencies": {
73+
"mongodb-data-service": "^22.18.1",
7374
"@mongodb-js/eslint-config-compass": "^1.0.17",
7475
"@mongodb-js/mocha-config-compass": "^1.3.7",
7576
"@mongodb-js/prettier-config-compass": "^1.0.1",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import { expect } from 'chai';
3+
import { render, screen, waitFor } from '@testing-library/react';
4+
import type { ConnectionInfo } from '@mongodb-js/connection-info';
5+
import { ActiveConnectionList } from './active-connection-list';
6+
import {
7+
ConnectionRepositoryContext,
8+
ConnectionStorageContext,
9+
} from '@mongodb-js/connection-storage/provider';
10+
import {
11+
ConnectionsManager,
12+
ConnectionsManagerProvider,
13+
} from '@mongodb-js/compass-connections/provider';
14+
import { ConnectionRepository } from '@mongodb-js/connection-storage/main';
15+
import type { ConnectionStorage } from '@mongodb-js/connection-storage/renderer';
16+
import Sinon from 'sinon';
17+
18+
const mockConnections: ConnectionInfo[] = [
19+
{
20+
id: 'turtle',
21+
connectionOptions: {
22+
connectionString: 'mongodb://turtle',
23+
},
24+
savedConnectionType: 'recent',
25+
},
26+
{
27+
id: 'oranges',
28+
connectionOptions: {
29+
connectionString: 'mongodb://peaches',
30+
},
31+
favorite: {
32+
name: 'peaches',
33+
},
34+
savedConnectionType: 'favorite',
35+
},
36+
];
37+
38+
describe('<ActiveConnectionList />', function () {
39+
let connectionRepository: ConnectionRepository;
40+
let connectionsManager: ConnectionsManager;
41+
let mockConnectionStorage: typeof ConnectionStorage;
42+
43+
beforeEach(() => {
44+
connectionsManager = new ConnectionsManager({} as any);
45+
(connectionsManager as any).connectionStatuses.set('turtle', 'connected');
46+
(connectionsManager as any).connectionStatuses.set('oranges', 'connected');
47+
mockConnectionStorage = {
48+
loadAll: Sinon.stub().resolves(mockConnections),
49+
} as any;
50+
connectionRepository = new ConnectionRepository(mockConnectionStorage);
51+
52+
render(
53+
<ConnectionStorageContext.Provider value={mockConnectionStorage}>
54+
<ConnectionRepositoryContext.Provider value={connectionRepository}>
55+
<ConnectionsManagerProvider value={connectionsManager}>
56+
<ActiveConnectionList />
57+
</ConnectionsManagerProvider>
58+
</ConnectionRepositoryContext.Provider>
59+
</ConnectionStorageContext.Provider>
60+
);
61+
});
62+
63+
it('Should render all active connections - using their correct titles', async function () {
64+
await waitFor(() => {
65+
expect(screen.queryByText('(2)')).to.be.visible;
66+
expect(screen.queryByText('turtle')).to.be.visible;
67+
expect(screen.queryByText('peaches')).to.be.visible;
68+
});
69+
});
70+
});

0 commit comments

Comments
 (0)