diff --git a/packages/compass-home/src/components/home.tsx b/packages/compass-home/src/components/home.tsx index 67ecd5285eb..077f8faff97 100644 --- a/packages/compass-home/src/components/home.tsx +++ b/packages/compass-home/src/components/home.tsx @@ -246,7 +246,7 @@ function Home({ appName }: { appName: string }): React.ReactElement | null { return (
- +
); diff --git a/packages/connections/src/components/connections.spec.tsx b/packages/connections/src/components/connections.spec.tsx index 1088b9374e9..1fa54a21e61 100644 --- a/packages/connections/src/components/connections.spec.tsx +++ b/packages/connections/src/components/connections.spec.tsx @@ -23,19 +23,20 @@ function getMockConnectionStorage( return Promise.resolve(mockConnections); }, save: () => Promise.resolve(), + delete: () => Promise.resolve(), }; } -async function loadSavedConnectionAndConnect(savedConnectionId: string) { +async function loadSavedConnectionAndConnect(connectionInfo: ConnectionInfo) { const savedConnectionButton = screen.getByTestId( - `saved-connection-button-${savedConnectionId}` + `saved-connection-button-${connectionInfo.id}` ); fireEvent.click(savedConnectionButton); // Wait for the connection to load in the form. await waitFor(() => expect(screen.queryByRole('textbox').textContent).to.equal( - 'mongodb://localhost:27018/?readPreference=primary&ssl=false' + connectionInfo.connectionOptions.connectionString ) ); @@ -51,13 +52,9 @@ describe('Connections Component', function () { beforeEach(function () { onConnectedSpy = sinon.spy(); - this.clock = sinon.useFakeTimers({ - now: 1483228800000, - }); }); afterEach(function () { - this.clock.restore(); sinon.restore(); cleanup(); }); @@ -72,6 +69,7 @@ describe('Connections Component', function () { ); }); @@ -117,16 +115,19 @@ describe('Connections Component', function () { let mockConnectFn: sinon.SinonSpy; let mockStorage: ConnectionStore; let savedConnectionId: string; + let savedConnectionWithAppNameId: string; let saveConnectionSpy: sinon.SinonSpy; + let connections: ConnectionInfo[]; beforeEach(async function () { mockConnectFn = sinon.fake.resolves({ mockDataService: 'yes', }); savedConnectionId = uuid(); + savedConnectionWithAppNameId = uuid(); saveConnectionSpy = sinon.spy(); - mockStorage = getMockConnectionStorage([ + connections = [ { id: savedConnectionId, connectionOptions: { @@ -134,7 +135,15 @@ describe('Connections Component', function () { 'mongodb://localhost:27018/?readPreference=primary&ssl=false', }, }, - ]); + { + id: savedConnectionWithAppNameId, + connectionOptions: { + connectionString: + 'mongodb://localhost:27019/?appName=Some+App+Name', + }, + }, + ]; + mockStorage = getMockConnectionStorage(connections); sinon.replace(mockStorage, 'save', saveConnectionSpy); render( @@ -142,21 +151,22 @@ describe('Connections Component', function () { onConnected={onConnectedSpy} connectFn={mockConnectFn} connectionStorage={mockStorage} + appName="Test App Name" /> ); - await waitFor(() => expect(screen.queryByRole('listitem')).to.be.visible); + await waitFor(() => expect(screen.queryAllByRole('listitem')).to.exist); }); it('should render the saved connections', function () { const listItems = screen.getAllByRole('listitem'); - expect(listItems.length).to.equal(1); + expect(listItems.length).to.equal(2); const favorites = screen.queryAllByTestId('favorite-connection'); expect(favorites.length).to.equal(0); const recents = screen.getAllByTestId('recent-connection'); - expect(recents.length).to.equal(1); + expect(recents.length).to.equal(2); }); it('renders the title of the saved connection', function () { @@ -169,7 +179,9 @@ describe('Connections Component', function () { throw new Error('Error: pineapples'); }; - await loadSavedConnectionAndConnect(savedConnectionId); + await loadSavedConnectionAndConnect( + connections.find(({ id }) => id === savedConnectionId) + ); }); it('displays the error that occurred when saving', function () { @@ -194,14 +206,23 @@ describe('Connections Component', function () { describe('when a saved connection is clicked on and connected to', function () { beforeEach(async function () { - await loadSavedConnectionAndConnect(savedConnectionId); + this.clock = sinon.useFakeTimers({ + now: 1483228800000, + }); + await loadSavedConnectionAndConnect( + connections.find(({ id }) => id === savedConnectionId) + ); + }); + + afterEach(function () { + this.clock.restore(); }); it('should call the connect function with the connection options to connect', function () { expect(mockConnectFn.callCount).to.equal(1); expect(mockConnectFn.firstCall.args[0]).to.deep.equal({ connectionString: - 'mongodb://localhost:27018/?readPreference=primary&ssl=false', + 'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=Test+App+Name', }); }); @@ -241,6 +262,21 @@ describe('Connections Component', function () { ); }); }); + + describe('when a saved connection with appName is clicked on and connected to', function () { + beforeEach(async function () { + await loadSavedConnectionAndConnect( + connections.find(({ id }) => id === savedConnectionWithAppNameId) + ); + }); + + it('should call the connect function without replacing appName', function () { + expect(mockConnectFn.callCount).to.equal(1); + expect(mockConnectFn.firstCall.args[0]).to.deep.equal({ + connectionString: 'mongodb://localhost:27019/?appName=Some+App+Name', + }); + }); + }); }); describe('connecting to a connection that is not succeeding', function () { @@ -248,6 +284,7 @@ describe('Connections Component', function () { let saveConnectionSpy: sinon.SinonSpy; let savedConnectableId: string; let savedUnconnectableId: string; + let connections: ConnectionInfo[]; beforeEach(async function () { saveConnectionSpy = sinon.spy(); @@ -258,7 +295,7 @@ describe('Connections Component', function () { async (connectionOptions: ConnectionOptions) => { if ( connectionOptions.connectionString === - 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000' + 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000&appName=Test+App+Name' ) { return new Promise((resolve) => { // On first call we want this attempt to be cancelled before @@ -272,7 +309,7 @@ describe('Connections Component', function () { } ); - const mockStorage = getMockConnectionStorage([ + connections = [ { id: savedConnectableId, connectionOptions: { @@ -287,7 +324,8 @@ describe('Connections Component', function () { 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000', }, }, - ]); + ]; + const mockStorage = getMockConnectionStorage(connections); sinon.replace(mockStorage, 'save', saveConnectionSpy); render( @@ -295,6 +333,7 @@ describe('Connections Component', function () { onConnected={onConnectedSpy} connectFn={mockConnectFn} connectionStorage={mockStorage} + appName="Test App Name" /> ); @@ -322,16 +361,13 @@ describe('Connections Component', function () { const connectButton = screen.getByText('Connect'); fireEvent.click(connectButton); - // Speedup the modal showing animation. - this.clock.tick(300); - // Wait for the connecting... modal to be shown. await waitFor(() => expect(screen.queryByText('Cancel')).to.be.visible); }); describe('when the connection attempt is cancelled', function () { beforeEach(async function () { - const cancelButton = screen.getByText('Cancel').closest('Button'); + const cancelButton = screen.getByText('Cancel'); fireEvent.click(cancelButton); // Wait for the connecting... modal to hide. @@ -359,13 +395,15 @@ describe('Connections Component', function () { expect(mockConnectFn.callCount).to.equal(1); expect(mockConnectFn.firstCall.args[0]).to.deep.equal({ connectionString: - 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000', + 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000&appName=Test+App+Name', }); }); describe('connecting to a successful connection after cancelling a connect', function () { beforeEach(async function () { - await loadSavedConnectionAndConnect(savedConnectableId); + await loadSavedConnectionAndConnect( + connections.find(({ id }) => id === savedConnectableId) + ); }); it('should call onConnected once', function () { @@ -392,7 +430,7 @@ describe('Connections Component', function () { expect(mockConnectFn.callCount).to.equal(2); expect(mockConnectFn.secondCall.args[0]).to.deep.equal({ connectionString: - 'mongodb://localhost:27018/?readPreference=primary&ssl=false', + 'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=Test+App+Name', }); }); diff --git a/packages/connections/src/components/connections.tsx b/packages/connections/src/components/connections.tsx index 1c45dfdad38..46b0967ab1c 100644 --- a/packages/connections/src/components/connections.tsx +++ b/packages/connections/src/components/connections.tsx @@ -57,6 +57,7 @@ const formContainerStyles = css({ function Connections({ onConnected, connectionStorage = new ConnectionStorage(), + appName, connectFn = connect, }: { onConnected: ( @@ -64,6 +65,7 @@ function Connections({ dataService: DataService ) => void; connectionStorage?: ConnectionStore; + appName: string; connectFn?: (connectionOptions: ConnectionOptions) => Promise; }): React.ReactElement { const { @@ -77,7 +79,7 @@ function Connections({ removeAllRecentsConnections, removeConnection, saveConnection, - } = useConnections(onConnected, connectionStorage, connectFn); + } = useConnections({ onConnected, connectionStorage, connectFn, appName }); const { activeConnectionId, activeConnectionInfo, diff --git a/packages/connections/src/stores/connections-store.spec.ts b/packages/connections/src/stores/connections-store.spec.ts index 7f9b7ef75cc..cb1aa43b74c 100644 --- a/packages/connections/src/stores/connections-store.spec.ts +++ b/packages/connections/src/stores/connections-store.spec.ts @@ -56,7 +56,12 @@ describe('use-connections hook', function () { mockConnectionStorage.loadAll = loadAllSpyWithData; const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); // Wait for the async loading of connections to complete. @@ -76,7 +81,12 @@ describe('use-connections hook', function () { mockConnectionStorage.loadAll = () => Promise.resolve(mockConnections); const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); // Wait for the async loading of connections to complete. @@ -130,7 +140,12 @@ describe('use-connections hook', function () { let hookResult: RenderResult>; beforeEach(async function () { const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); await act(async () => { @@ -172,7 +187,12 @@ describe('use-connections hook', function () { mockConnectionStorage.loadAll = () => Promise.resolve(mockConnections); const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); // Wait for the async loading of connections to complete. @@ -256,7 +276,12 @@ describe('use-connections hook', function () { mockConnectionStorage.loadAll = loadAllSpyWithData; const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); await waitFor(() => { expect(result.current.state.connections.length).to.equal(3); @@ -278,7 +303,12 @@ describe('use-connections hook', function () { mockConnectionStorage.loadAll = loadAllSpyWithData; const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); await waitFor(() => { expect(result.current.state.connections.length).to.equal(2); @@ -321,7 +351,12 @@ describe('use-connections hook', function () { mockConnectionStorage.loadAll = loadAllSpyWithData; const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); await waitFor(() => { expect(result.current.state.connections.length).to.equal(2); @@ -374,7 +409,12 @@ describe('use-connections hook', function () { mockConnectionStorage.loadAll = loadAllSpyWithData; const { result } = renderHook(() => - useConnections(noop, mockConnectionStorage, noop) + useConnections({ + onConnected: noop, + connectionStorage: mockConnectionStorage, + connectFn: noop, + appName: 'Test App Name', + }) ); await waitFor(() => { expect(result.current.state.connections.length).to.equal(2); diff --git a/packages/connections/src/stores/connections-store.ts b/packages/connections/src/stores/connections-store.ts index 70137ac51cd..293a13ee0f7 100644 --- a/packages/connections/src/stores/connections-store.ts +++ b/packages/connections/src/stores/connections-store.ts @@ -16,6 +16,9 @@ import { trackNewConnectionEvent, trackConnectionFailedEvent, } from '../modules/telemetry'; +import ConnectionString from 'mongodb-connection-string-url'; +import type { MongoClientOptions } from 'mongodb'; + const debug = debugModule('mongodb-compass:connections:connections-store'); export function createNewConnectionInfo(): ConnectionInfo { @@ -26,13 +29,37 @@ export function createNewConnectionInfo(): ConnectionInfo { }, }; } - export interface ConnectionStore { loadAll: () => Promise; save: (connectionInfo: ConnectionInfo) => Promise; delete: (connectionInfo: ConnectionInfo) => Promise; } +function setAppNameParamIfMissing( + connectionString: string, + appName: string +): string { + let connectionStringUrl; + + try { + connectionStringUrl = new ConnectionString(connectionString); + } catch (e) { + // + } + + if (!connectionStringUrl) { + return connectionString; + } + + const searchParams = + connectionStringUrl.typedSearchParams(); + if (!searchParams.has('appName')) { + searchParams.set('appName', appName); + } + + return connectionStringUrl.href; +} + type State = { activeConnectionId?: string; activeConnectionInfo: ConnectionInfo; @@ -184,14 +211,20 @@ async function loadConnections( } } -export function useConnections( +export function useConnections({ + onConnected, + connectionStorage, + appName, + connectFn, +}: { onConnected: ( connectionInfo: ConnectionInfo, dataService: DataService - ) => void, - connectionStorage: ConnectionStore, - connectFn: (connectionOptions: ConnectionOptions) => Promise -): { + ) => void; + connectionStorage: ConnectionStore; + connectFn: (connectionOptions: ConnectionOptions) => Promise; + appName: string; +}): { state: State; cancelConnectionAttempt: () => void; connect: (connectionInfo: ConnectionInfo) => Promise; @@ -318,9 +351,14 @@ export function useConnections( debug('connecting with connectionInfo', connectionInfo); try { - const newConnectionDataService = await newConnectionAttempt.connect( - connectionInfo.connectionOptions + const connectionStringWithAppName = setAppNameParamIfMissing( + connectionInfo.connectionOptions.connectionString, + appName ); + const newConnectionDataService = await newConnectionAttempt.connect({ + ...connectionInfo.connectionOptions, + connectionString: connectionStringWithAppName, + }); connectingConnectionAttempt.current = undefined; if (!newConnectionDataService || newConnectionAttempt.isClosed()) { diff --git a/packages/data-service/src/legacy/legacy-connection-model.spec.ts b/packages/data-service/src/legacy/legacy-connection-model.spec.ts index 70174753ff9..ee7a99f40f5 100644 --- a/packages/data-service/src/legacy/legacy-connection-model.spec.ts +++ b/packages/data-service/src/legacy/legacy-connection-model.spec.ts @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import ConnectionString from 'mongodb-connection-string-url'; import util from 'util'; import type { ConnectionInfo } from '../connection-info'; @@ -23,7 +24,7 @@ async function createAndConvertModel( describe('LegacyConnectionModel', function () { describe('convertConnectionModelToInfo', function () { - it('converts a raw model to the connection model instance', function () { + it('converts a raw model to connection info', function () { const rawModel = { _id: '1234-1234-1234-1234', hostname: 'localhost', @@ -38,6 +39,32 @@ describe('LegacyConnectionModel', function () { expect(id).to.deep.equal('1234-1234-1234-1234'); }); + it('removes appName if matches MongoDB Compass', async function () { + const { connectionOptions } = await createAndConvertModel( + 'mongodb://localhost:27017/admin?appName=MongoDB+Compass', + { _id: '1234-1234-1234-1234' } + ); + + expect( + new ConnectionString( + connectionOptions.connectionString + ).searchParams.has('appName') + ).to.be.false; + }); + + it('preserves appName if does not match MongoDB Compass', async function () { + const { connectionOptions } = await createAndConvertModel( + 'mongodb://localhost:27017/admin?appName=Some+Other+App', + { _id: '1234-1234-1234-1234' } + ); + + expect( + new ConnectionString( + connectionOptions.connectionString + ).searchParams.get('appName') + ).to.deep.equal('Some Other App'); + }); + it('converts _id', async function () { const { id } = await createAndConvertModel( 'mongodb://localhost:27017/admin', diff --git a/packages/data-service/src/legacy/legacy-connection-model.ts b/packages/data-service/src/legacy/legacy-connection-model.ts index 6e6259b5fcf..2906f85143c 100644 --- a/packages/data-service/src/legacy/legacy-connection-model.ts +++ b/packages/data-service/src/legacy/legacy-connection-model.ts @@ -26,6 +26,36 @@ type SslMethod = | 'SERVER' | 'ALL'; +function deleteCompassAppNameParam( + connectionInfo: ConnectionInfo +): ConnectionInfo { + let connectionStringUrl; + + try { + connectionStringUrl = new ConnectionString( + connectionInfo.connectionOptions.connectionString + ); + } catch { + return connectionInfo; + } + + if ( + /^mongodb compass/i.exec( + connectionStringUrl.searchParams.get('appName') || '' + ) + ) { + connectionStringUrl.searchParams.delete('appName'); + } + + return { + ...connectionInfo, + connectionOptions: { + ...connectionInfo.connectionOptions, + connectionString: connectionStringUrl.href, + }, + }; +} + export interface LegacyConnectionModelProperties { _id: string; hostname: string; @@ -173,7 +203,7 @@ export function convertConnectionModelToInfo( connectionInfo.lastUsed = new Date(connectionInfo.lastUsed); } - return connectionInfo; + return deleteCompassAppNameParam(connectionInfo); } // Not migrated yet, has to be converted @@ -223,7 +253,7 @@ export function convertConnectionModelToInfo( info.lastUsed = legacyModel.lastUsed; } - return info; + return deleteCompassAppNameParam(info); } function setConnectionStringParam(