diff --git a/package.json b/package.json index 34838b5dd..1eb7be226 100644 --- a/package.json +++ b/package.json @@ -173,8 +173,8 @@ "title": "Create MongoDB Playground" }, { - "command": "mdb.showActiveConnectionInPlayground", - "title": "MongoDB: Show Active Connection In Playground" + "command": "mdb.changeActiveConnection", + "title": "MongoDB: Change Active Connection" }, { "command": "mdb.runSelectedPlaygroundBlocks", @@ -465,7 +465,7 @@ "when": "false" }, { - "command": "mdb.showActiveConnectionInPlayground", + "command": "mdb.changeActiveConnection", "when": "false" }, { diff --git a/src/connectionController.ts b/src/connectionController.ts index c52494c3b..52fc1a5ba 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -37,6 +37,11 @@ export type SavedConnectionInformation = { // A loaded connection contains connection information. export type LoadedConnection = SavedConnection & SavedConnectionInformation; +export enum NewConnectionType { + NEW_CONNECTION = 'NEW_CONNECTION', + SAVED_CONNECTION = 'SAVED_CONNECTION' +} + export default class ConnectionController { // This is a map of connection ids to their configurations. // These connections can be saved on the session (runtime), @@ -586,13 +591,13 @@ export default class ConnectionController { const connectionNameToRemove: | string | undefined = await vscode.window.showQuickPick( - connectionIds.map( - (id, index) => `${index + 1}: ${this._connections[id].name}` - ), - { - placeHolder: 'Choose a connection to remove...' - } - ); + connectionIds.map( + (id, index) => `${index + 1}: ${this._connections[id].name}` + ), + { + placeHolder: 'Choose a connection to remove...' + } + ); if (!connectionNameToRemove) { return Promise.resolve(false); @@ -777,4 +782,63 @@ export default class ConnectionController { public setDisconnecting(disconnecting: boolean): void { this._disconnecting = disconnecting; } + + public getСonnectionQuickPicks(): any[] { + if (!this._connections) { + return [ + { + label: 'Add new connection', + data: { + type: NewConnectionType.NEW_CONNECTION + } + } + ]; + } + + return [ + { + label: 'Add new connection', + data: { + type: NewConnectionType.NEW_CONNECTION + } + }, + ...Object.values(this._connections) + .sort((connectionA: any, connectionB: any) => + (connectionA.name || '').localeCompare(connectionB.name || '') + ) + .map((item: any) => ({ + label: item.name, + data: { + type: NewConnectionType.SAVED_CONNECTION, + connectionId: item.id + } + })) + ]; + } + + public changeActiveConnection(): Promise { + return new Promise(async (resolve) => { + const selectedQuickPickItem = await vscode.window.showQuickPick( + this.getСonnectionQuickPicks(), + { + placeHolder: 'Select new connection...' + } + ); + + if (!selectedQuickPickItem) { + return resolve(true); + } + + if ( + selectedQuickPickItem.data.type === NewConnectionType.NEW_CONNECTION + ) { + return this.connectWithURI(); + } + + // Get the saved connection by id and return as the current connection. + return this.connectWithConnectionId( + selectedQuickPickItem.data.connectionId + ); + }); + } } diff --git a/src/editors/activeConnectionCodeLensProvider.ts b/src/editors/activeConnectionCodeLensProvider.ts index f5c1e8309..bccd14d97 100644 --- a/src/editors/activeConnectionCodeLensProvider.ts +++ b/src/editors/activeConnectionCodeLensProvider.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; -export default class ActiveConnectionCodeLensProvider implements vscode.CodeLensProvider { - private _codeLenses: vscode.CodeLens[] = []; +export default class ActiveConnectionCodeLensProvider + implements vscode.CodeLensProvider { private _connectionController: any; private _onDidChangeCodeLenses: vscode.EventEmitter< void @@ -22,27 +22,23 @@ export default class ActiveConnectionCodeLensProvider implements vscode.CodeLens } public provideCodeLenses(): vscode.CodeLens[] { - const activeConnection = this._connectionController.getActiveDataService(); - - if (!activeConnection) { - return []; + const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 0)); + let message = ''; + + if (this._connectionController.isConnecting()) { + message = 'Connecting...'; + } else if (this._connectionController.getActiveDataService()) { + message = `Currently connected to ${this._connectionController.getActiveConnectionName()}. Click here to change connection.`; + } else { + message = 'Disconnected. Click here to connect.'; } - this._codeLenses = [new vscode.CodeLens(new vscode.Range(0, 0, 0, 0))]; - - return this._codeLenses; - } - - public resolveCodeLens?(codeLens: vscode.CodeLens): vscode.CodeLens { - const name = this._connectionController.getActiveConnectionName(); - const message = `Currently connected to ${name}`; - codeLens.command = { title: message, - command: "mdb.showActiveConnectionInPlayground", - arguments: [message] + command: 'mdb.changeActiveConnection', + arguments: [] }; - return codeLens; + return [codeLens]; } } diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index d92d49f1a..078c89644 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -171,16 +171,6 @@ export default class PlaygroundController { }); } - public showActiveConnectionInPlayground(message: string): Promise { - return new Promise((resolve) => { - this._outputChannel.clear(); - this._outputChannel.append(message); - this._outputChannel.show(true); - - resolve(true); - }); - } - public async evaluate(codeToEvaluate: string): Promise { // Send a request to the language server to execute scripts from a playground. const result = await this._languageServerController.executeAll( diff --git a/src/explorer/databaseTreeItem.ts b/src/explorer/databaseTreeItem.ts index 56024b9e5..290acd664 100644 --- a/src/explorer/databaseTreeItem.ts +++ b/src/explorer/databaseTreeItem.ts @@ -106,15 +106,9 @@ export default class DatabaseTreeItem extends vscode.TreeItem // Create new collection tree items, using previously cached items // where possible. collections - .sort((collectionA: any, collectionB: any) => { - if (collectionA.name < collectionB.name) { - return -1; - } - if (collectionA.name > collectionB.name) { - return 1; - } - return 0; - }) + .sort((collectionA: any, collectionB: any) => + (collectionA.name || '').localeCompare(collectionB.name || '') + ) .forEach((collection: any) => { if (pastChildrenCache[collection.name]) { this._childrenCache[collection.name] = new CollectionTreeItem( diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 0571d296a..d5d71d545 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -126,10 +126,8 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommand('mdb.runPlayground', () => this._playgroundController.runAllOrSelectedPlaygroundBlocks() ); - this.registerCommand( - 'mdb.showActiveConnectionInPlayground', - (message: string) => - this._playgroundController.showActiveConnectionInPlayground(message) + this.registerCommand('mdb.changeActiveConnection', () => + this._connectionController.changeActiveConnection() ); this.registerCommand('mdb.startStreamLanguageServerLogs', () => @@ -315,8 +313,9 @@ export default class MDBExtensionController implements vscode.Disposable { return false; } - const successfullyAddedCollection = await element - .onAddCollectionClicked(this._context); + const successfullyAddedCollection = await element.onAddCollectionClicked( + this._context + ); if (successfullyAddedCollection) { vscode.window.showInformationMessage( 'Collection successfully created.' @@ -398,9 +397,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommand( 'mdb.copySchemaFieldName', async (fieldTreeItem: FieldTreeItem): Promise => { - await vscode.env.clipboard.writeText( - fieldTreeItem.getFieldName() - ); + await vscode.env.clipboard.writeText(fieldTreeItem.getFieldName()); vscode.window.showInformationMessage('Copied to clipboard.'); return true; diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 79f4cbd71..4336db120 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -826,4 +826,87 @@ suite('Connection Controller Test Suite', function () { assert(false); } }); + + test('СonnectionQuickPicks list is displayed in the alphanumerical case insensitive order', async () => { + try { + await vscode.workspace + .getConfiguration('mdb.connectionSaving') + .update( + 'defaultConnectionSavingLocation', + DefaultSavingLocations.Workspace + ); + await testConnectionController.addNewConnectionStringAndConnect( + TEST_DATABASE_URI + ); + await testConnectionController.addNewConnectionStringAndConnect( + TEST_DATABASE_URI + ); + await testConnectionController.disconnect(); + + testConnectionController.clearAllConnections(); + + await testConnectionController.loadSavedConnections(); + + let connections = testConnectionController._connections; + let connectionIds = Object.keys(connections); + + assert( + connectionIds.length === 2, + `Expected 2 connection configurations found ${connectionIds.length}` + ); + assert( + connections[connectionIds[0]].name === 'localhost:27018', + `Expected the first connection name to be 'localhost:27018', found '${ + connections[connectionIds[0]].name + }'.` + ); + assert( + connections[connectionIds[1]].name === 'localhost:27018', + `Expected the second connection name to be 'localhost:27018', found '${ + connections[connectionIds[1]].name + }'.` + ); + + const mockInputBoxResolves = sinon.stub(); + + mockInputBoxResolves.onCall(0).resolves('Lynx'); + sinon.replace(vscode.window, 'showInputBox', mockInputBoxResolves); + + const renameSuccess = await testConnectionController.renameConnection( + connectionIds[0] + ); + + assert(renameSuccess); + + await testConnectionController.loadSavedConnections(); + + connections = testConnectionController._connections; + + assert( + connectionIds.length === 2, + `Expected 2 connection configurations found ${connectionIds.length}` + ); + + const connectionQuickPicks = testConnectionController.getСonnectionQuickPicks(); + + assert( + connectionQuickPicks.length === 3, + `Expected 3 connections found ${connectionIds.length} in connectionQuickPicks` + ); + assert( + connectionQuickPicks[0].label === 'Add new connection', + `Expected the first quick pick label to be 'Add new connection', found '${connectionQuickPicks[0].name}'.` + ); + assert( + connectionQuickPicks[1].label === 'localhost:27018', + `Expected the second quick pick label to be 'localhost:27018', found '${connectionQuickPicks[1].name}'.` + ); + assert( + connectionQuickPicks[2].label === 'Lynx', + `Expected the third quick pick labele to be 'Lynx', found '${connectionQuickPicks[2].name}'.` + ); + } catch (error) { + assert(false); + } + }); }); diff --git a/src/test/suite/editors/activeDBCodeLensProvider.test.ts b/src/test/suite/editors/activeDBCodeLensProvider.test.ts index ab1a7c50c..3b72c4a0a 100644 --- a/src/test/suite/editors/activeDBCodeLensProvider.test.ts +++ b/src/test/suite/editors/activeDBCodeLensProvider.test.ts @@ -1,10 +1,15 @@ -import * as assert from 'assert'; +import * as vscode from 'vscode'; import ConnectionController from '../../../connectionController'; import { StatusView } from '../../../views'; import ActiveDBCodeLensProvider from '../../../editors/activeConnectionCodeLensProvider'; -import { TestExtensionContext, mockVSCodeTextDocument } from '../stubs'; +import { TestExtensionContext } from '../stubs'; import { StorageController } from '../../../storage'; import TelemetryController from '../../../telemetry/telemetryController'; +import { beforeEach, afterEach } from 'mocha'; + +const sinon = require('sinon'); +const chai = require('chai'); +const expect = chai.expect; suite('Active DB CodeLens Provider Test Suite', () => { const mockExtensionContext = new TestExtensionContext(); @@ -14,7 +19,7 @@ suite('Active DB CodeLens Provider Test Suite', () => { mockExtensionContext ); - test('expected provideCodeLenses to return empty array when user is not connected', () => { + suite('user is not connected', () => { const testConnectionController = new ConnectionController( new StatusView(mockExtensionContext), mockStorageController, @@ -23,13 +28,30 @@ suite('Active DB CodeLens Provider Test Suite', () => { const testCodeLensProvider = new ActiveDBCodeLensProvider( testConnectionController ); - const codeLens = testCodeLensProvider.provideCodeLenses(); + const mockShowQuickPick = sinon.fake.resolves(); + + beforeEach(() => { + sinon.replace(vscode.window, 'showQuickPick', mockShowQuickPick); + }); + + afterEach(() => { + sinon.restore(); + }); - assert(!!codeLens); - assert(codeLens.length === 0); + test('show disconnected message in code lenses', () => { + const codeLens = testCodeLensProvider.provideCodeLenses(); + + expect(codeLens).to.be.an('array'); + expect(codeLens.length).to.be.equal(1); + expect(codeLens[0].command?.title).to.be.equal( + 'Disconnected. Click here to connect.' + ); + expect(codeLens[0].range.start.line).to.be.equal(0); + expect(codeLens[0].range.end.line).to.be.equal(0); + }); }); - test('expected provideCodeLenses to return a code lens with positions at the first line of the document', () => { + suite('user is connected', () => { const testConnectionController = new ConnectionController( new StatusView(mockExtensionContext), mockStorageController, @@ -44,22 +66,34 @@ suite('Active DB CodeLens Provider Test Suite', () => { }, client: {} }; + testConnectionController.setActiveConnection(mockActiveConnection); - const codeLens = testCodeLensProvider.provideCodeLenses(); + beforeEach(() => { + sinon.replace( + testConnectionController, + 'getActiveConnectionName', + sinon.fake.returns('fakeName') + ); + }); - assert(!!codeLens); - assert(codeLens.length === 1); - const range = codeLens[0].range; - const expectedStartLine = 0; - assert( - range.start.line === expectedStartLine, - `Expected a codeLens position to be at line ${expectedStartLine}, found ${range.start.line}` - ); - const expectedEnd = 0; - assert( - range.end.line === expectedEnd, - `Expected a codeLens position to be at line ${expectedEnd}, found ${range.end.line}` - ); + afterEach(() => { + sinon.restore(); + }); + + test('show active connection in code lenses', () => { + const codeLens = testCodeLensProvider.provideCodeLenses(); + + expect(codeLens).to.be.an('array'); + expect(codeLens.length).to.be.equal(1); + expect(codeLens[0].command?.title).to.be.equal( + 'Currently connected to fakeName. Click here to change connection.' + ); + expect(codeLens[0].range.start.line).to.be.equal(0); + expect(codeLens[0].range.end.line).to.be.equal(0); + expect(codeLens[0].command?.command).to.be.equal( + 'mdb.changeActiveConnection' + ); + }); }); }); diff --git a/src/test/suite/explorer/databaseTreeItem.test.ts b/src/test/suite/explorer/databaseTreeItem.test.ts index 524bbc22b..6401413e2 100644 --- a/src/test/suite/explorer/databaseTreeItem.test.ts +++ b/src/test/suite/explorer/databaseTreeItem.test.ts @@ -151,7 +151,7 @@ suite('DatabaseTreeItem Test Suite', () => { ); }); - test('collections are displayed in the alphanumerical order', (done) => { + test('collections are displayed in the alphanumerical case insensitive order', (done) => { const testDatabaseTreeItem = new DatabaseTreeItem( mockDatabaseNames[2], new DataServiceStub(), @@ -163,10 +163,10 @@ suite('DatabaseTreeItem Test Suite', () => { const expectedCollectionsOrder = [ '111_abc', '222_abc', - 'AAA', - 'ZZZ', 'aaa', - 'zzz' + 'AAA', + 'zzz', + 'ZZZ' ]; testDatabaseTreeItem diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 41231057b..a9d265026 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -1328,20 +1328,20 @@ suite('MDBExtensionController Test Suite', () => { .then(done, done); }); - test('mdb.showActiveConnectionInPlayground command should call showActiveConnectionInPlayground on the playground controller', (done) => { - const mockShowActiveConnectionInPlayground = sinon.fake.resolves(); + test('mdb.changeActiveConnection command should call changeActiveConnection on the playground controller', (done) => { + const mockChangeActiveConnection = sinon.fake.resolves(); sinon.replace( - mdbTestExtension.testExtensionController._playgroundController, - 'showActiveConnectionInPlayground', - mockShowActiveConnectionInPlayground + mdbTestExtension.testExtensionController._connectionController, + 'changeActiveConnection', + mockChangeActiveConnection ); vscode.commands - .executeCommand('mdb.showActiveConnectionInPlayground') + .executeCommand('mdb.changeActiveConnection') .then(() => { assert( - mockShowActiveConnectionInPlayground.called, - 'Expected "showActiveConnectionInPlayground" to be called on the playground controller.' + mockChangeActiveConnection.called, + 'Expected "changeActiveConnection" to be called on the playground controller.' ); }) .then(done, done); diff --git a/src/test/suite/telemetry/telemetryController.test.ts b/src/test/suite/telemetry/telemetryController.test.ts index 5eb7982fc..0bc88c75e 100644 --- a/src/test/suite/telemetry/telemetryController.test.ts +++ b/src/test/suite/telemetry/telemetryController.test.ts @@ -103,12 +103,9 @@ suite('Telemetry Controller Test Suite', () => { test('track command run event', (done) => { vscode.commands - .executeCommand('mdb.showActiveConnectionInPlayground', 'Test') + .executeCommand('mdb.addConnection') .then(() => { - sinon.assert.calledWith( - mockTrackCommandRun, - 'mdb.showActiveConnectionInPlayground' - ); + sinon.assert.calledWith(mockTrackCommandRun, 'mdb.addConnection'); }) .then(done, done); });