-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: show list of active connections COMPASS-7654 (#5593)
- Loading branch information
1 parent
7e5d6ae
commit d8afda8
Showing
14 changed files
with
682 additions
and
7 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
.nyc_output | ||
dist | ||
coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 152 additions & 0 deletions
152
packages/compass-connections/src/stores/active-connections.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
80
packages/compass-connections/src/stores/active-connections.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
...ar/src/components/multiple-connections/active-connections/active-connection-list.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.