From db87e397a333a6d31684d71fc9970e4cd6937253 Mon Sep 17 00:00:00 2001 From: gagik Date: Sun, 12 Jan 2025 23:00:38 +0100 Subject: [PATCH 01/10] add preset settings --- .vscode/settings.json | 8 +- package.json | 56 ++++++++++--- src/commands/index.ts | 1 + src/explorer/connectionTreeItem.ts | 24 +++--- src/explorer/explorerTreeController.ts | 82 +++++++++++-------- src/mdbExtensionController.ts | 10 +++ src/storage/connectionStorage.ts | 51 ++++++++---- .../suite/explorer/connectionTreeItem.test.ts | 11 ++- src/test/suite/mdbExtensionController.test.ts | 1 + 9 files changed, 167 insertions(+), 77 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 32baf6163..6f2a8305f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,11 @@ "out": true }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + "mdb.presetSavedConnections": [ + { + "name": "Local Test", + "connectionString": "mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000" + } + ] } diff --git a/package.json b/package.json index 4e0fac3db..d100a9235 100644 --- a/package.json +++ b/package.json @@ -320,6 +320,10 @@ "command": "mdb.copyConnectionString", "title": "Copy Connection String" }, + { + "command": "mdb.openWorkspaceSettingsFile", + "title": "Open Workspace Settings" + }, { "command": "mdb.renameConnection", "title": "Rename Connection..." @@ -499,32 +503,32 @@ "view/item/context": [ { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "inline" }, { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "1@1" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "inline" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "1@1" }, { "command": "mdb.refreshConnection", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", "group": "1@2" }, { "command": "mdb.treeViewOpenMongoDBShell", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", "group": "2@1" }, { @@ -537,14 +541,19 @@ "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", "group": "3@2" }, + { + "command": "mdb.openWorkspaceSettingsFile", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedImmutableConnectionTreeItem)", + "group": "3@2" + }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", "group": "4@1" }, { "command": "mdb.disconnectFromConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", "group": "5@1" }, { @@ -559,7 +568,7 @@ }, { "command": "mdb.connectToConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedImmutableConnectionTreeItem)", "group": "1@1" }, { @@ -572,9 +581,14 @@ "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", "group": "2@2" }, + { + "command": "mdb.openWorkspaceSettingsFile", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedImmutableConnectionTreeItem)", + "group": "2@2" + }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedImmutableConnectionTreeItem)", "group": "3@1" }, { @@ -1171,6 +1185,28 @@ "type": "string", "default": "", "description": "Specify a shell command that is run to start the browser for authenticating with the OIDC identity provider for the server connection. Leave this empty for default browser." + }, + "mdb.presetSavedConnections": { + "scope": "window", + "type": "array", + "description": "Defines preset saved connections.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the connection." + }, + "connectionString": { + "type": "string", + "description": "Connection string. Do not store sensitive credentials here." + } + }, + "required": [ + "name", + "connectionString" + ] + } } } }, diff --git a/src/commands/index.ts b/src/commands/index.ts index b1a23606c..0348b36cd 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -44,6 +44,7 @@ enum EXTENSION_COMMANDS { MDB_EDIT_CONNECTION = 'mdb.editConnection', MDB_REFRESH_CONNECTION = 'mdb.refreshConnection', MDB_COPY_CONNECTION_STRING = 'mdb.copyConnectionString', + MDB_OPEN_WORKSPACE_SETTINGS_FILE = 'mdb.openWorkspaceSettingsFile', MDB_REMOVE_CONNECTION_TREE_VIEW = 'mdb.treeItemRemoveConnection', MDB_RENAME_CONNECTION = 'mdb.renameConnection', MDB_ADD_DATABASE = 'mdb.addDatabase', diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index 9c47423a4..40baf1936 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -12,10 +12,9 @@ import { getImagesPath } from '../extensionConstants'; import type TreeItemParent from './treeItemParentInterface'; import StreamProcessorTreeItem from './streamProcessorTreeItem'; -export enum ConnectionItemContextValues { - disconnected = 'disconnectedConnectionTreeItem', - connected = 'connectedConnectionTreeItem', -} +export type ConnectionItemContextValue = `${'disconnected' | 'connected'}${ + | '' + | 'Immutable'}ConnectionTreeItem`; function getIconPath(isActiveConnection: boolean): { light: string; @@ -39,7 +38,7 @@ export default class ConnectionTreeItem extends vscode.TreeItem implements TreeItemParent, vscode.TreeDataProvider { - contextValue = ConnectionItemContextValues.disconnected; + contextValue: ConnectionItemContextValue = 'disconnectedConnectionTreeItem'; private _childrenCache: { [key: string]: DatabaseTreeItem | StreamProcessorTreeItem; @@ -50,6 +49,7 @@ export default class ConnectionTreeItem connectionId: string; isExpanded: boolean; + isMutable: boolean; constructor({ connectionId, @@ -58,6 +58,7 @@ export default class ConnectionTreeItem connectionController, cacheIsUpToDate, childrenCache, + isMutable, }: { connectionId: string; collapsibleState: vscode.TreeItemCollapsibleState; @@ -67,21 +68,24 @@ export default class ConnectionTreeItem childrenCache: { [key: string]: DatabaseTreeItem | StreamProcessorTreeItem; }; // Existing cache. + isMutable: boolean; }) { super( connectionController.getSavedConnectionName(connectionId), collapsibleState ); - if ( + const isConnected = connectionController.getActiveConnectionId() === connectionId && !connectionController.isDisconnecting() && - !connectionController.isConnecting() - ) { - this.contextValue = ConnectionItemContextValues.connected; - } + !connectionController.isConnecting(); + + this.contextValue = `${isConnected ? 'connected' : 'disconnected'}${ + isMutable ? '' : 'Immutable' + }ConnectionTreeItem`; this.connectionId = connectionId; + this.isMutable = isMutable; this._connectionController = connectionController; this.isExpanded = isExpanded; this._childrenCache = childrenCache; diff --git a/src/explorer/explorerTreeController.ts b/src/explorer/explorerTreeController.ts index 7c569cfb2..628bfa16f 100644 --- a/src/explorer/explorerTreeController.ts +++ b/src/explorer/explorerTreeController.ts @@ -8,6 +8,7 @@ import { DOCUMENT_ITEM } from './documentTreeItem'; import { DOCUMENT_LIST_ITEM, CollectionTypes } from './documentListTreeItem'; import EXTENSION_COMMANDS from '../commands'; import { sortTreeItemsByLabel } from './treeItemUtils'; +import type { LoadedConnection } from '../storage/connectionStorage'; const log = createLogger('explorer tree controller'); @@ -130,6 +131,46 @@ export default class ExplorerTreeController return element; } + private _getConnectionExpandedState(connection: LoadedConnection): { + collapsibleState: vscode.TreeItemCollapsibleState; + isExpanded: boolean; + } { + const pastConnectionTreeItems = this._connectionTreeItems; + const isActiveConnection = + connection.id === this._connectionController.getActiveConnectionId(); + const isBeingConnectedTo = + this._connectionController.isConnecting() && + connection.id === this._connectionController.getConnectingConnectionId(); + + let collapsibleState = isActiveConnection + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.Collapsed; + + if ( + pastConnectionTreeItems[connection.id] && + !pastConnectionTreeItems[connection.id].isExpanded + ) { + // Connection was manually collapsed while being active. + collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + } + if (isActiveConnection && this._connectionController.isDisconnecting()) { + // Don't show a collapsable state when the connection is being disconnected from. + collapsibleState = vscode.TreeItemCollapsibleState.None; + } + if (isBeingConnectedTo) { + // Don't show a collapsable state when the connection is being connected to. + collapsibleState = vscode.TreeItemCollapsibleState.None; + } + return { + collapsibleState, + // Set expanded when we're connecting to a connection so that it + // expands when it's connected. + isExpanded: + isBeingConnectedTo || + collapsibleState === vscode.TreeItemCollapsibleState.Expanded, + }; + } + getChildren(element?: any): Thenable { // When no element is present we are at the root. if (!element) { @@ -139,45 +180,14 @@ export default class ExplorerTreeController // Create new connection tree items, using cached children wherever possible. connections.forEach((connection) => { - const isActiveConnection = - connection.id === this._connectionController.getActiveConnectionId(); - const isBeingConnectedTo = - this._connectionController.isConnecting() && - connection.id === - this._connectionController.getConnectingConnectionId(); - - let connectionExpandedState = isActiveConnection - ? vscode.TreeItemCollapsibleState.Expanded - : vscode.TreeItemCollapsibleState.Collapsed; - - if ( - pastConnectionTreeItems[connection.id] && - !pastConnectionTreeItems[connection.id].isExpanded - ) { - // Connection was manually collapsed while being active. - connectionExpandedState = vscode.TreeItemCollapsibleState.Collapsed; - } - if ( - isActiveConnection && - this._connectionController.isDisconnecting() - ) { - // Don't show a collapsable state when the connection is being disconnected from. - connectionExpandedState = vscode.TreeItemCollapsibleState.None; - } - if (isBeingConnectedTo) { - // Don't show a collapsable state when the connection is being connected to. - connectionExpandedState = vscode.TreeItemCollapsibleState.None; - } + const { collapsibleState, isExpanded } = + this._getConnectionExpandedState(connection); this._connectionTreeItems[connection.id] = new ConnectionTreeItem({ connectionId: connection.id, - collapsibleState: connectionExpandedState, - // Set expanded when we're connecting to a connection so that it - // expands when it's connected. - isExpanded: - isBeingConnectedTo || - connectionExpandedState === - vscode.TreeItemCollapsibleState.Expanded, + collapsibleState, + isExpanded, + isMutable: connection.isMutable ?? true, connectionController: this._connectionController, cacheIsUpToDate: pastConnectionTreeItems[connection.id] ? pastConnectionTreeItems[connection.id].cacheIsUpToDate diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index b9a2de1c3..e74024ef5 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -480,6 +480,16 @@ export default class MDBExtensionController implements vscode.Disposable { return true; } ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_OPEN_WORKSPACE_SETTINGS_FILE, + async () => { + await vscode.commands.executeCommand( + 'workbench.action.openWorkspaceSettingsFile' + ); + + return true; + } + ); this.registerCommand( EXTENSION_COMMANDS.MDB_COPY_CONNECTION_STRING, async (element: ConnectionTreeItem): Promise => { diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index e233ad4d5..ba982e97a 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -11,9 +11,11 @@ import type StorageController from './storageController'; import type { SecretStorageLocationType } from './storageController'; import { DefaultSavingLocations, + SecretStorageLocation, StorageLocation, StorageVariables, } from './storageController'; +import { v4 as uuidv4 } from 'uuid'; const log = createLogger('connection storage'); @@ -21,20 +23,18 @@ export interface StoreConnectionInfo { id: string; // Connection model id or a new uuid. name: string; // Possibly user given name, not unique. storageLocation: StorageLocation; - secretStorageLocation?: SecretStorageLocationType; - connectionOptions?: ConnectionOptions; + secretStorageLocation: SecretStorageLocationType; + connectionOptions: ConnectionOptions; + isMutable?: boolean; lastUsed?: Date; // Date and time when the connection was last used, i.e. connected with. } -type StoreConnectionInfoWithConnectionOptions = StoreConnectionInfo & - Required>; - -type StoreConnectionInfoWithSecretStorageLocation = StoreConnectionInfo & - Required>; - -export type LoadedConnection = StoreConnectionInfoWithConnectionOptions & - StoreConnectionInfoWithSecretStorageLocation; +export type PresetSavedConnection = { + name: string; + connectionString: string; +}; +export type LoadedConnection = StoreConnectionInfo; export class ConnectionStorage { _storageController: StorageController; @@ -56,6 +56,7 @@ export class ConnectionStorage { return { id: connectionId, name, + isMutable: true, storageLocation: this.getPreferredStorageLocationFromConfiguration(), secretStorageLocation: 'vscode.SecretStorage', connectionOptions: connectionOptions, @@ -83,7 +84,7 @@ export class ConnectionStorage { (await this._storageController.getSecret(connectionInfo.id)) ?? ''; return this._mergedConnectionInfoWithSecrets( - connectionInfo as LoadedConnection, + connectionInfo, unparsedSecrets ); } catch (error) { @@ -166,7 +167,27 @@ export class ConnectionStorage { ); } - async loadConnections() { + _loadPresetSavedConnections(): LoadedConnection[] { + const presetSavedConnections: PresetSavedConnection[] | undefined = + vscode.workspace.getConfiguration('mdb').get('presetSavedConnections'); + + if (!presetSavedConnections) { + return []; + } + + return presetSavedConnections.map((presetConnection) => ({ + id: uuidv4(), + name: `${presetConnection.name} (From Configuration)`, + connectionOptions: { + connectionString: presetConnection.connectionString, + }, + isMutable: false, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + })); + } + + async loadConnections(): Promise { const globalAndWorkspaceConnections = Object.values({ ...this._storageController.get( StorageVariables.GLOBAL_SAVED_CONNECTIONS, @@ -203,10 +224,12 @@ export class ConnectionStorage { }) ); - return loadedConnections; + const presetSavedConnections = this._loadPresetSavedConnections(); + + return [...presetSavedConnections, ...loadedConnections]; } - async removeConnection(connectionId: string) { + async removeConnection(connectionId: string): Promise { await this._storageController.deleteSecret(connectionId); // See if the connection exists in the saved global or workspace connections diff --git a/src/test/suite/explorer/connectionTreeItem.test.ts b/src/test/suite/explorer/connectionTreeItem.test.ts index 13f7bd157..52411da4c 100644 --- a/src/test/suite/explorer/connectionTreeItem.test.ts +++ b/src/test/suite/explorer/connectionTreeItem.test.ts @@ -4,9 +4,7 @@ import { beforeEach, afterEach } from 'mocha'; import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; -import ConnectionTreeItem, { - ConnectionItemContextValues, -} from '../../../explorer/connectionTreeItem'; +import ConnectionTreeItem from '../../../explorer/connectionTreeItem'; import { DataServiceStub } from '../stubs'; import formatError from '../../../utils/formatError'; import { mdbTestExtension } from '../stubbableMdbExtension'; @@ -14,7 +12,7 @@ import { mdbTestExtension } from '../stubbableMdbExtension'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { contributes } = require('../../../../package.json'); -function getTestConnectionTreeItem() { +function getTestConnectionTreeItem(): ConnectionTreeItem { return new ConnectionTreeItem({ connectionId: 'test', collapsibleState: vscode.TreeItemCollapsibleState.Expanded, @@ -23,6 +21,7 @@ function getTestConnectionTreeItem() { mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, + isMutable: true, }); } @@ -32,10 +31,10 @@ suite('ConnectionTreeItem Test Suite', () => { let disconnectedRegisteredCommandInPackageJson = false; contributes.menus['view/item/context'].forEach((contextItem) => { - if (contextItem.when.includes(ConnectionItemContextValues.connected)) { + if (contextItem.when.includes('connected')) { connectedRegisteredCommandInPackageJson = true; } - if (contextItem.when.includes(ConnectionItemContextValues.disconnected)) { + if (contextItem.when.includes('disconnected')) { disconnectedRegisteredCommandInPackageJson = true; } }); diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 440a83262..198559e52 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -42,6 +42,7 @@ function getTestConnectionTreeItem( mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, + isMutable: true, ...options, }); } From 06b1e90d8e6ee027cc53b77af8129022df776eb6 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 13 Jan 2025 10:26:44 +0100 Subject: [PATCH 02/10] add tests --- src/storage/connectionStorage.ts | 16 ++- .../suite/storage/connectionStorage.test.ts | 111 +++++++++++++++++- 2 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index ba982e97a..8e83bec1e 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -23,8 +23,8 @@ export interface StoreConnectionInfo { id: string; // Connection model id or a new uuid. name: string; // Possibly user given name, not unique. storageLocation: StorageLocation; - secretStorageLocation: SecretStorageLocationType; - connectionOptions: ConnectionOptions; + secretStorageLocation?: SecretStorageLocationType; + connectionOptions?: ConnectionOptions; isMutable?: boolean; lastUsed?: Date; // Date and time when the connection was last used, i.e. connected with. } @@ -34,7 +34,15 @@ export type PresetSavedConnection = { connectionString: string; }; -export type LoadedConnection = StoreConnectionInfo; +type StoreConnectionInfoWithConnectionOptions = StoreConnectionInfo & + Required>; + +type StoreConnectionInfoWithSecretStorageLocation = StoreConnectionInfo & + Required>; + +export type LoadedConnection = StoreConnectionInfoWithConnectionOptions & + StoreConnectionInfoWithSecretStorageLocation; + export class ConnectionStorage { _storageController: StorageController; @@ -84,7 +92,7 @@ export class ConnectionStorage { (await this._storageController.getSecret(connectionInfo.id)) ?? ''; return this._mergedConnectionInfoWithSecrets( - connectionInfo, + connectionInfo as LoadedConnection, unparsedSecrets ); } catch (error) { diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index 30ae15719..9cc581028 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -15,12 +15,15 @@ import { TEST_DATABASE_URI_USER, TEST_USER_PASSWORD, } from '../dbTestHelper'; -import type { StoreConnectionInfo } from '../../../storage/connectionStorage'; +import type { LoadedConnection } from '../../../storage/connectionStorage'; import { ConnectionStorage } from '../../../storage/connectionStorage'; const testDatabaseConnectionName = 'localhost:27088'; -const newTestConnection = (connectionStorage: ConnectionStorage, id: string) => +const newTestConnection = ( + connectionStorage: ConnectionStorage, + id: string +): LoadedConnection => connectionStorage.createNewConnection({ connectionId: id, connectionOptions: { @@ -305,9 +308,7 @@ suite('Connection Storage Test Suite', function () { expect(connections.length).to.equal(1); const newSavedConnectionInfoWithSecrets = - await testConnectionStorage._getConnectionInfoWithSecrets( - connections[0] as StoreConnectionInfo - ); + await testConnectionStorage._getConnectionInfoWithSecrets(connections[0]); expect(newSavedConnectionInfoWithSecrets).to.deep.equal(connectionInfo); }); @@ -321,6 +322,104 @@ suite('Connection Storage Test Suite', function () { extensionSandbox.restore(); }); + suite('when there are preset connections', () => { + const presetConnections = [ + { + name: 'Preset Connection 1', + connectionString: 'mongodb://localhost:27017/', + }, + { + name: 'Preset Connection 2', + connectionString: 'mongodb://localhost:27018/', + }, + ]; + + let getConfigurationStub: sinon.SinonStub< + [ + section?: string | undefined, + scope?: vscode.ConfigurationScope | null | undefined + ], + vscode.WorkspaceConfiguration + >; + let getPresetSavedConnectionsStub: sinon.SinonStub; + + beforeEach(() => { + testSandbox.restore(); + getPresetSavedConnectionsStub = testSandbox.stub(); + }); + + test('loads the preset connections', async () => { + getConfigurationStub = testSandbox.stub( + vscode.workspace, + 'getConfiguration' + ); + getConfigurationStub.returns({ + get: getPresetSavedConnectionsStub, + } as any); + + getPresetSavedConnectionsStub + .withArgs('presetSavedConnections') + .returns(presetConnections); + + const connections = await testConnectionStorage.loadConnections(); + + expect(connections.length).to.equal(2); + + for (let i = 0; i < connections.length; i++) { + const connection = connections[i]; + const presetConnection = presetConnections[i]; + expect(connection.name).equals( + `${presetConnection.name} (From Configuration)` + ); + expect(connection.connectionOptions.connectionString).equals( + presetConnection.connectionString + ); + expect(connection.isMutable).equals(false); + } + }); + + test('loads both preset and other saved connections', async () => { + const savedConnection = newTestConnection(testConnectionStorage, '1'); + await testConnectionStorage.saveConnection(savedConnection); + + getConfigurationStub = testSandbox.stub( + vscode.workspace, + 'getConfiguration' + ); + getConfigurationStub.returns({ + get: getPresetSavedConnectionsStub, + } as any); + + getPresetSavedConnectionsStub + .withArgs('presetSavedConnections') + .returns(presetConnections); + + const loadedConnections = await testConnectionStorage.loadConnections(); + + expect(loadedConnections.length).equals(3); + + for (let i = 0; i < presetConnections.length; i++) { + const connection = loadedConnections[i]; + const presetConnection = presetConnections[i]; + expect(connection.name).equals( + `${presetConnection.name} (From Configuration)` + ); + expect(connection.connectionOptions.connectionString).equals( + presetConnection.connectionString + ); + expect(connection.isMutable).equals(false); + } + + const savedLoadedConnection = loadedConnections[2]; + + expect(savedLoadedConnection.name).equals(savedConnection.name); + expect( + savedLoadedConnection.connectionOptions.connectionString + ).contains(savedConnection.connectionOptions.connectionString); + expect(savedLoadedConnection.isMutable).equals(true); + }); + }); + suite('when connection secrets are already in SecretStorage', () => { afterEach(() => { testSandbox.restore(); @@ -341,6 +440,8 @@ suite('Connection Storage Test Suite', function () { // By default the connection secrets are already stored in SecretStorage const savedConnections = await testConnectionStorage.loadConnections(); + + expect(savedConnections.length).equals(2); expect( savedConnections.every( ({ secretStorageLocation }) => From d2d58e6de54e6690cf73333ac4345e3ade1a519b Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 16 Jan 2025 10:32:03 +0100 Subject: [PATCH 03/10] feat(tree-explorer): add reactivity, sample value and remove configuration suffix --- .vscode/settings.json | 9 ++++++--- package.json | 14 ++++++++++++++ src/connectionController.ts | 2 ++ src/mdbExtensionController.ts | 10 ++++++++++ src/storage/connectionStorage.ts | 2 +- src/test/suite/storage/connectionStorage.test.ts | 8 ++------ 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6f2a8305f..b200fdafe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,10 +17,13 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "mdb.presetSavedConnections": [ + "mdb.presetSavedConnections":[ { - "name": "Local Test", - "connectionString": "mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000" + "name": "Local Test Connection", + "connectionString": "mongodb://localhost:27017" } + ], + "conventionalCommits.scopes": [ + "tree-explorer" ] } diff --git a/package.json b/package.json index d100a9235..0cac627c1 100644 --- a/package.json +++ b/package.json @@ -1190,8 +1190,22 @@ "scope": "window", "type": "array", "description": "Defines preset saved connections.", + "examples": [ + [ + { + "name": "Local Test Connection", + "connectionString": "mongodb://localhost:27017" + } + ] + ], "items": { "type": "object", + "examples": [ + { + "name": "Local Test Connection", + "connectionString": "mongodb://localhost:27017" + } + ], "properties": { "name": { "type": "string", diff --git a/src/connectionController.ts b/src/connectionController.ts index 4bffaa094..b28de612f 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -162,6 +162,8 @@ export default class ConnectionController { } async loadSavedConnections(): Promise { + this._connections = Object.create(null); + const loadedConnections = await this._connectionStorage.loadConnections(); for (const connection of loadedConnections) { diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index e74024ef5..0b8f09d96 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -160,6 +160,15 @@ export default class MDBExtensionController implements vscode.Disposable { this._editorsController.registerProviders(); } + subscribeToConfigurationChanges(): void { + const subscription = vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('mdb.presetSavedConnections')) { + void this._connectionController.loadSavedConnections(); + } + }); + this._context.subscriptions.push(subscription); + } + async activate(): Promise { this._explorerController.activateConnectionsTreeView(); this._helpExplorer.activateHelpTreeView(this._telemetryService); @@ -172,6 +181,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommands(); this.showOverviewPageIfRecentlyInstalled(); + this.subscribeToConfigurationChanges(); const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); void vscode.commands.executeCommand( diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index 8e83bec1e..7f8c17b83 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -185,7 +185,7 @@ export class ConnectionStorage { return presetSavedConnections.map((presetConnection) => ({ id: uuidv4(), - name: `${presetConnection.name} (From Configuration)`, + name: presetConnection.name, connectionOptions: { connectionString: presetConnection.connectionString, }, diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index 9cc581028..b402c234f 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -368,9 +368,7 @@ suite('Connection Storage Test Suite', function () { for (let i = 0; i < connections.length; i++) { const connection = connections[i]; const presetConnection = presetConnections[i]; - expect(connection.name).equals( - `${presetConnection.name} (From Configuration)` - ); + expect(connection.name).equals(presetConnection.name); expect(connection.connectionOptions.connectionString).equals( presetConnection.connectionString ); @@ -401,9 +399,7 @@ suite('Connection Storage Test Suite', function () { for (let i = 0; i < presetConnections.length; i++) { const connection = loadedConnections[i]; const presetConnection = presetConnections[i]; - expect(connection.name).equals( - `${presetConnection.name} (From Configuration)` - ); + expect(connection.name).equals(presetConnection.name); expect(connection.connectionOptions.connectionString).equals( presetConnection.connectionString ); From bf9cd640c204b24a924181e45924021557982d07 Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 16 Jan 2025 11:41:52 +0100 Subject: [PATCH 04/10] feat(tree-explorer): automatically derive workspace or global settings Now when one clicks edit preset connections, we'll automatically redirect to the correct settings.json (workspace or global). --- package.json | 32 +++++++++---------- src/commands/index.ts | 2 +- src/explorer/connectionTreeItem.ts | 13 ++++---- src/explorer/explorerTreeController.ts | 2 +- src/mdbExtensionController.ts | 17 ++++++---- src/storage/connectionStorage.ts | 32 +++++++++++++++---- .../suite/explorer/connectionTreeItem.test.ts | 2 +- src/test/suite/mdbExtensionController.test.ts | 2 +- .../suite/storage/connectionStorage.test.ts | 32 ++++++++++--------- 9 files changed, 80 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 0cac627c1..15e359eb5 100644 --- a/package.json +++ b/package.json @@ -321,8 +321,8 @@ "title": "Copy Connection String" }, { - "command": "mdb.openWorkspaceSettingsFile", - "title": "Open Workspace Settings" + "command": "mdb.editPresetConnections", + "title": "Edit Preset Connections..." }, { "command": "mdb.renameConnection", @@ -503,32 +503,32 @@ "view/item/context": [ { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "inline" }, { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "1@1" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "inline" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem) && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "1@1" }, { "command": "mdb.refreshConnection", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "1@2" }, { "command": "mdb.treeViewOpenMongoDBShell", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "2@1" }, { @@ -542,18 +542,18 @@ "group": "3@2" }, { - "command": "mdb.openWorkspaceSettingsFile", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedImmutableConnectionTreeItem)", + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedPresetConnectionTreeItem)", "group": "3@2" }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "4@1" }, { "command": "mdb.disconnectFromConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedImmutableConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "5@1" }, { @@ -568,7 +568,7 @@ }, { "command": "mdb.connectToConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedImmutableConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedPresetConnectionTreeItem)", "group": "1@1" }, { @@ -582,13 +582,13 @@ "group": "2@2" }, { - "command": "mdb.openWorkspaceSettingsFile", - "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedImmutableConnectionTreeItem)", + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedPresetConnectionTreeItem)", "group": "2@2" }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedImmutableConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedPresetConnectionTreeItem)", "group": "3@1" }, { diff --git a/src/commands/index.ts b/src/commands/index.ts index 0348b36cd..348189649 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -44,7 +44,7 @@ enum EXTENSION_COMMANDS { MDB_EDIT_CONNECTION = 'mdb.editConnection', MDB_REFRESH_CONNECTION = 'mdb.refreshConnection', MDB_COPY_CONNECTION_STRING = 'mdb.copyConnectionString', - MDB_OPEN_WORKSPACE_SETTINGS_FILE = 'mdb.openWorkspaceSettingsFile', + MDB_EDIT_PRESET_CONNECTIONS = 'mdb.editPresetConnections', MDB_REMOVE_CONNECTION_TREE_VIEW = 'mdb.treeItemRemoveConnection', MDB_RENAME_CONNECTION = 'mdb.renameConnection', MDB_ADD_DATABASE = 'mdb.addDatabase', diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index 40baf1936..f7185d3f7 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -11,10 +11,11 @@ import formatError from '../utils/formatError'; import { getImagesPath } from '../extensionConstants'; import type TreeItemParent from './treeItemParentInterface'; import StreamProcessorTreeItem from './streamProcessorTreeItem'; +import type { ConnectionSource } from '../storage/connectionStorage'; export type ConnectionItemContextValue = `${'disconnected' | 'connected'}${ | '' - | 'Immutable'}ConnectionTreeItem`; + | 'Preset'}ConnectionTreeItem`; function getIconPath(isActiveConnection: boolean): { light: string; @@ -49,7 +50,7 @@ export default class ConnectionTreeItem connectionId: string; isExpanded: boolean; - isMutable: boolean; + source: ConnectionSource; constructor({ connectionId, @@ -58,7 +59,7 @@ export default class ConnectionTreeItem connectionController, cacheIsUpToDate, childrenCache, - isMutable, + source, }: { connectionId: string; collapsibleState: vscode.TreeItemCollapsibleState; @@ -68,7 +69,7 @@ export default class ConnectionTreeItem childrenCache: { [key: string]: DatabaseTreeItem | StreamProcessorTreeItem; }; // Existing cache. - isMutable: boolean; + source: ConnectionSource; }) { super( connectionController.getSavedConnectionName(connectionId), @@ -81,11 +82,11 @@ export default class ConnectionTreeItem !connectionController.isConnecting(); this.contextValue = `${isConnected ? 'connected' : 'disconnected'}${ - isMutable ? '' : 'Immutable' + source === 'user' ? '' : 'Preset' }ConnectionTreeItem`; this.connectionId = connectionId; - this.isMutable = isMutable; + this.source = source; this._connectionController = connectionController; this.isExpanded = isExpanded; this._childrenCache = childrenCache; diff --git a/src/explorer/explorerTreeController.ts b/src/explorer/explorerTreeController.ts index 628bfa16f..ceaf23d35 100644 --- a/src/explorer/explorerTreeController.ts +++ b/src/explorer/explorerTreeController.ts @@ -187,7 +187,7 @@ export default class ExplorerTreeController connectionId: connection.id, collapsibleState, isExpanded, - isMutable: connection.isMutable ?? true, + source: connection.source ?? 'user', connectionController: this._connectionController, cacheIsUpToDate: pastConnectionTreeItems[connection.id] ? pastConnectionTreeItems[connection.id].cacheIsUpToDate diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 0b8f09d96..08a5fdbaa 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -491,12 +491,17 @@ export default class MDBExtensionController implements vscode.Disposable { } ); this.registerCommand( - EXTENSION_COMMANDS.MDB_OPEN_WORKSPACE_SETTINGS_FILE, - async () => { - await vscode.commands.executeCommand( - 'workbench.action.openWorkspaceSettingsFile' - ); - + EXTENSION_COMMANDS.MDB_EDIT_PRESET_CONNECTIONS, + async (element: ConnectionTreeItem) => { + if (element.source === 'workspaceSettings') { + await vscode.commands.executeCommand( + 'workbench.action.openWorkspaceSettingsFile' + ); + } else if (element.source === 'globalSettings') { + await vscode.commands.executeCommand( + 'workbench.action.openSettingsJson' + ); + } return true; } ); diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index 7f8c17b83..f7f18c008 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -19,13 +19,14 @@ import { v4 as uuidv4 } from 'uuid'; const log = createLogger('connection storage'); +export type ConnectionSource = 'globalSettings' | 'workspaceSettings' | 'user'; export interface StoreConnectionInfo { id: string; // Connection model id or a new uuid. name: string; // Possibly user given name, not unique. storageLocation: StorageLocation; secretStorageLocation?: SecretStorageLocationType; connectionOptions?: ConnectionOptions; - isMutable?: boolean; + source?: ConnectionSource; lastUsed?: Date; // Date and time when the connection was last used, i.e. connected with. } @@ -34,6 +35,10 @@ export type PresetSavedConnection = { connectionString: string; }; +export type PresetSavedConnectionWithSource = PresetSavedConnection & { + source: ConnectionSource; +}; + type StoreConnectionInfoWithConnectionOptions = StoreConnectionInfo & Required>; @@ -64,7 +69,7 @@ export class ConnectionStorage { return { id: connectionId, name, - isMutable: true, + source: 'user', storageLocation: this.getPreferredStorageLocationFromConfiguration(), secretStorageLocation: 'vscode.SecretStorage', connectionOptions: connectionOptions, @@ -176,20 +181,33 @@ export class ConnectionStorage { } _loadPresetSavedConnections(): LoadedConnection[] { - const presetSavedConnections: PresetSavedConnection[] | undefined = - vscode.workspace.getConfiguration('mdb').get('presetSavedConnections'); + const configuration = vscode.workspace.getConfiguration('mdb'); + const presetSavedConnectionsInfo = configuration.inspect< + PresetSavedConnection[] + >('presetSavedConnections'); - if (!presetSavedConnections) { + if (!presetSavedConnectionsInfo) { return []; } - return presetSavedConnections.map((presetConnection) => ({ + const combinedPresetSavedConnections: PresetSavedConnectionWithSource[] = [ + ...(presetSavedConnectionsInfo?.workspaceValue ?? []).map((preset) => ({ + ...preset, + source: 'workspaceSettings' as const, + })), + ...(presetSavedConnectionsInfo?.globalValue ?? []).map((preset) => ({ + ...preset, + source: 'globalSettings' as const, + })), + ]; + + return combinedPresetSavedConnections.map((presetConnection) => ({ id: uuidv4(), name: presetConnection.name, connectionOptions: { connectionString: presetConnection.connectionString, }, - isMutable: false, + source: presetConnection.source, storageLocation: StorageLocation.NONE, secretStorageLocation: SecretStorageLocation.SecretStorage, })); diff --git a/src/test/suite/explorer/connectionTreeItem.test.ts b/src/test/suite/explorer/connectionTreeItem.test.ts index 52411da4c..4e65a0042 100644 --- a/src/test/suite/explorer/connectionTreeItem.test.ts +++ b/src/test/suite/explorer/connectionTreeItem.test.ts @@ -21,7 +21,7 @@ function getTestConnectionTreeItem(): ConnectionTreeItem { mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, - isMutable: true, + source: 'user', }); } diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 198559e52..518b917bc 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -42,7 +42,7 @@ function getTestConnectionTreeItem( mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, - isMutable: true, + source: 'user', ...options, }); } diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index b402c234f..64fa8944a 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -323,16 +323,18 @@ suite('Connection Storage Test Suite', function () { }); suite('when there are preset connections', () => { - const presetConnections = [ - { - name: 'Preset Connection 1', - connectionString: 'mongodb://localhost:27017/', - }, - { - name: 'Preset Connection 2', - connectionString: 'mongodb://localhost:27018/', - }, - ]; + const presetConnections = { + workspaceValue: [ + { + name: 'Preset Connection 1', + connectionString: 'mongodb://localhost:27017/', + }, + { + name: 'Preset Connection 2', + connectionString: 'mongodb://localhost:27018/', + }, + ], + }; let getConfigurationStub: sinon.SinonStub< [ @@ -354,7 +356,7 @@ suite('Connection Storage Test Suite', function () { 'getConfiguration' ); getConfigurationStub.returns({ - get: getPresetSavedConnectionsStub, + inspect: getPresetSavedConnectionsStub, } as any); getPresetSavedConnectionsStub @@ -372,7 +374,7 @@ suite('Connection Storage Test Suite', function () { expect(connection.connectionOptions.connectionString).equals( presetConnection.connectionString ); - expect(connection.isMutable).equals(false); + expect(connection.source).equals('workspaceSettings'); } }); @@ -396,14 +398,14 @@ suite('Connection Storage Test Suite', function () { expect(loadedConnections.length).equals(3); - for (let i = 0; i < presetConnections.length; i++) { + for (let i = 0; i < presetConnections.workspaceValue.length; i++) { const connection = loadedConnections[i]; const presetConnection = presetConnections[i]; expect(connection.name).equals(presetConnection.name); expect(connection.connectionOptions.connectionString).equals( presetConnection.connectionString ); - expect(connection.isMutable).equals(false); + expect(connection.source).equals('workspaceSettings'); } const savedLoadedConnection = loadedConnections[2]; @@ -412,7 +414,7 @@ suite('Connection Storage Test Suite', function () { expect( savedLoadedConnection.connectionOptions.connectionString ).contains(savedConnection.connectionOptions.connectionString); - expect(savedLoadedConnection.isMutable).equals(true); + expect(savedLoadedConnection.source).equals('workspaceSettings'); }); }); From 01288358cba446d61c282a38fa85cfa407a00586 Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 16 Jan 2025 12:36:03 +0100 Subject: [PATCH 05/10] feat(tree-explorer): allow running edit preset connections from the header Running from header will automatically determine which scope to open and create a preset to edit if none are defined --- .vscode/settings.json | 7 +-- package.json | 17 +++++-- src/connectionController.ts | 47 ++++++++++++++++++- src/mdbExtensionController.ts | 12 +---- src/telemetry/telemetryService.ts | 14 ++++++ .../suite/telemetry/telemetryService.test.ts | 2 + 6 files changed, 78 insertions(+), 21 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b200fdafe..ca7fbbbde 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,13 +17,10 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "mdb.presetSavedConnections":[ + "mdb.presetSavedConnections": [ { - "name": "Local Test Connection", + "name": "Preset Database", "connectionString": "mongodb://localhost:27017" } - ], - "conventionalCommits.scopes": [ - "tree-explorer" ] } diff --git a/package.json b/package.json index 15e359eb5..e651c6f2f 100644 --- a/package.json +++ b/package.json @@ -493,11 +493,18 @@ }, { "command": "mdb.addConnection", - "when": "view == mongoDBConnectionExplorer" + "when": "view == mongoDBConnectionExplorer", + "group": "1@1" }, { "command": "mdb.addConnectionWithURI", - "when": "view == mongoDBConnectionExplorer" + "when": "view == mongoDBConnectionExplorer", + "group": "1@2" + }, + { + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer", + "group": "2@1" } ], "view/item/context": [ @@ -1189,11 +1196,11 @@ "mdb.presetSavedConnections": { "scope": "window", "type": "array", - "description": "Defines preset saved connections.", + "description": "Defines preset saved connections. Can be used to share connection configurations in a workspace or global scope. Do not store sensitive credentials here.", "examples": [ [ { - "name": "Local Test Connection", + "name": "Preset Database", "connectionString": "mongodb://localhost:27017" } ] @@ -1202,7 +1209,7 @@ "type": "object", "examples": [ { - "name": "Local Test Connection", + "name": "Preset Database", "connectionString": "mongodb://localhost:27017" } ], diff --git a/src/connectionController.ts b/src/connectionController.ts index b28de612f..aebaf4402 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -21,10 +21,15 @@ import type { StorageController } from './storage'; import type { StatusView } from './views'; import type TelemetryService from './telemetry/telemetryService'; import { openLink } from './utils/linkHelper'; -import type { LoadedConnection } from './storage/connectionStorage'; +import type { + ConnectionSource, + LoadedConnection, +} from './storage/connectionStorage'; import { ConnectionStorage } from './storage/connectionStorage'; import LINKS from './utils/links'; import { isAtlasStream } from 'mongodb-build-info'; +import { DocumentSource } from './documentSource'; +import type { ConnectionTreeItem } from './explorer'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJSON = require('../package.json'); @@ -161,6 +166,46 @@ export default class ConnectionController { }); } + async openPresetConnectionsSettings( + originTreeItem: ConnectionTreeItem | undefined + ): Promise { + this._telemetryService.trackPresetConnectionEdited({ + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: originTreeItem ? 'tree_item' : 'header', + }); + let source: ConnectionSource | undefined = originTreeItem?.source; + if (!source) { + const mdbConfiguration = vscode.workspace.getConfiguration('mdb'); + + const presetConnections = mdbConfiguration?.inspect( + 'presetSavedConnections' + ); + + if (presetConnections?.workspaceValue) { + source = 'workspaceSettings'; + } else if (presetConnections?.globalValue) { + source = 'globalSettings'; + } else { + // If no preset settings exist in workspace and global scope, + // set a default one inside the workspace and open it. + source = 'workspaceSettings'; + await mdbConfiguration.update('presetSavedConnections', [ + { + name: 'Preset Database', + connectionString: 'mongodb://localhost:27017', + }, + ]); + } + } + if (originTreeItem?.source === 'globalSettings') { + await vscode.commands.executeCommand('workbench.action.openSettingsJson'); + } else { + await vscode.commands.executeCommand( + 'workbench.action.openWorkspaceSettingsFile' + ); + } + } + async loadSavedConnections(): Promise { this._connections = Object.create(null); diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 08a5fdbaa..55ac202c9 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -492,16 +492,8 @@ export default class MDBExtensionController implements vscode.Disposable { ); this.registerCommand( EXTENSION_COMMANDS.MDB_EDIT_PRESET_CONNECTIONS, - async (element: ConnectionTreeItem) => { - if (element.source === 'workspaceSettings') { - await vscode.commands.executeCommand( - 'workbench.action.openWorkspaceSettingsFile' - ); - } else if (element.source === 'globalSettings') { - await vscode.commands.executeCommand( - 'workbench.action.openSettingsJson' - ); - } + async (element: ConnectionTreeItem | undefined) => { + await this._connectionController.openPresetConnectionsSettings(element); return true; } ); diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 232031994..82ccd523b 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -91,6 +91,8 @@ type ConnectionEditedTelemetryEventProperties = { type SavedConnectionsLoadedProperties = { // Total number of connections saved on disk saved_connections: number; + // Total number of connections from preset settings + preset_connections: number; // Total number of connections that extension was able to load, it might // differ from saved_connections since there might be failures in loading // secrets for a connection in which case we don't list the connections in the @@ -145,6 +147,11 @@ export type ParticipantChatOpenedFromActionProperties = { command?: ParticipantCommandType; }; +export type PresetSavedConnectionEditedProperties = { + source: DocumentSource; + source_details: 'tree_item' | 'header'; +}; + export type ParticipantInputBoxSubmitted = { source: DocumentSource; input_length: number | undefined; @@ -216,6 +223,7 @@ export enum TelemetryEventTypes { /** Tracks after a participant interacts with the input box we open to let the user write the prompt for participant. */ PARTICIPANT_INPUT_BOX_SUBMITTED = 'Participant Inbox Box Submitted', PARTICIPANT_RESPONSE_GENERATED = 'Participant Response Generated', + PRESET_CONNECTION_EDITED = 'Preset Connection Edited', } /** @@ -446,6 +454,12 @@ export default class TelemetryService { ); } + trackPresetConnectionEdited( + props: PresetSavedConnectionEditedProperties + ): void { + this.track(TelemetryEventTypes.PRESET_CONNECTION_EDITED, props); + } + trackPlaygroundCreated(playgroundType: string): void { this.track(TelemetryEventTypes.PLAYGROUND_CREATED, { playground_type: playgroundType, diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index cfdcf8be1..f9830ca9e 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -672,6 +672,7 @@ suite('Telemetry Controller Test Suite', () => { testTelemetryService.trackSavedConnectionsLoaded({ saved_connections: 3, loaded_connections: 3, + preset_connections: 3, connections_with_secrets_in_keytar: 0, connections_with_secrets_in_secret_storage: 3, }); @@ -684,6 +685,7 @@ suite('Telemetry Controller Test Suite', () => { properties: { saved_connections: 3, loaded_connections: 3, + preset_connections: 3, connections_with_secrets_in_keytar: 0, connections_with_secrets_in_secret_storage: 3, }, From 08503750098c19a7a73db6da15dacee4e9e43d7b Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 16 Jan 2025 13:35:38 +0100 Subject: [PATCH 06/10] fix(tree-explorer): update tests to use inspect and check for global and workspace settings --- src/storage/connectionStorage.ts | 10 +- .../suite/storage/connectionStorage.test.ts | 92 +++++++++++++------ 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index f7f18c008..dc0731946 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -191,14 +191,14 @@ export class ConnectionStorage { } const combinedPresetSavedConnections: PresetSavedConnectionWithSource[] = [ - ...(presetSavedConnectionsInfo?.workspaceValue ?? []).map((preset) => ({ - ...preset, - source: 'workspaceSettings' as const, - })), ...(presetSavedConnectionsInfo?.globalValue ?? []).map((preset) => ({ ...preset, source: 'globalSettings' as const, })), + ...(presetSavedConnectionsInfo?.workspaceValue ?? []).map((preset) => ({ + ...preset, + source: 'workspaceSettings' as const, + })), ]; return combinedPresetSavedConnections.map((presetConnection) => ({ @@ -252,7 +252,7 @@ export class ConnectionStorage { const presetSavedConnections = this._loadPresetSavedConnections(); - return [...presetSavedConnections, ...loadedConnections]; + return [...loadedConnections, ...presetSavedConnections]; } async removeConnection(connectionId: string): Promise { diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index 64fa8944a..a5061cd91 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -324,6 +324,12 @@ suite('Connection Storage Test Suite', function () { suite('when there are preset connections', () => { const presetConnections = { + globalValue: [ + { + name: 'Global Connection 1', + connectionString: 'mongodb://localhost:27017/', + }, + ], workspaceValue: [ { name: 'Preset Connection 1', @@ -343,11 +349,11 @@ suite('Connection Storage Test Suite', function () { ], vscode.WorkspaceConfiguration >; - let getPresetSavedConnectionsStub: sinon.SinonStub; + let inspectPresetSavedConnectionsStub: sinon.SinonStub; beforeEach(() => { testSandbox.restore(); - getPresetSavedConnectionsStub = testSandbox.stub(); + inspectPresetSavedConnectionsStub = testSandbox.stub(); }); test('loads the preset connections', async () => { @@ -356,25 +362,39 @@ suite('Connection Storage Test Suite', function () { 'getConfiguration' ); getConfigurationStub.returns({ - inspect: getPresetSavedConnectionsStub, + inspect: inspectPresetSavedConnectionsStub, + get: () => undefined, } as any); - getPresetSavedConnectionsStub + inspectPresetSavedConnectionsStub .withArgs('presetSavedConnections') .returns(presetConnections); - const connections = await testConnectionStorage.loadConnections(); + const loadedConnections = await testConnectionStorage.loadConnections(); - expect(connections.length).to.equal(2); + const expectedConnectionValues = [ + ...presetConnections.globalValue.map((connection) => ({ + ...connection, + source: 'globalSettings', + })), + ...presetConnections.workspaceValue.map((connection) => ({ + ...connection, + source: 'workspaceSettings', + })), + ]; + + expect(loadedConnections.length).equals( + expectedConnectionValues.length + ); - for (let i = 0; i < connections.length; i++) { - const connection = connections[i]; - const presetConnection = presetConnections[i]; - expect(connection.name).equals(presetConnection.name); - expect(connection.connectionOptions.connectionString).equals( - presetConnection.connectionString + for (let i = 0; i < expectedConnectionValues.length; i++) { + const connection = loadedConnections[i]; + const expected = expectedConnectionValues[i]; + expect(connection.name).equals(expected.name); + expect(connection.connectionOptions.connectionString).contains( + expected.connectionString ); - expect(connection.source).equals('workspaceSettings'); + expect(connection.source).equals(expected.source); } }); @@ -387,34 +407,46 @@ suite('Connection Storage Test Suite', function () { 'getConfiguration' ); getConfigurationStub.returns({ - get: getPresetSavedConnectionsStub, + inspect: inspectPresetSavedConnectionsStub, + get: () => undefined, } as any); - getPresetSavedConnectionsStub + inspectPresetSavedConnectionsStub .withArgs('presetSavedConnections') .returns(presetConnections); const loadedConnections = await testConnectionStorage.loadConnections(); - expect(loadedConnections.length).equals(3); + const expectedConnectionValues = [ + { + name: savedConnection.name, + source: 'user', + connectionString: + savedConnection.connectionOptions.connectionString, + }, + ...presetConnections.globalValue.map((connection) => ({ + ...connection, + source: 'globalSettings', + })), + ...presetConnections.workspaceValue.map((connection) => ({ + ...connection, + source: 'workspaceSettings', + })), + ]; + + expect(loadedConnections.length).equals( + expectedConnectionValues.length + ); - for (let i = 0; i < presetConnections.workspaceValue.length; i++) { + for (let i = 0; i < expectedConnectionValues.length; i++) { const connection = loadedConnections[i]; - const presetConnection = presetConnections[i]; - expect(connection.name).equals(presetConnection.name); - expect(connection.connectionOptions.connectionString).equals( - presetConnection.connectionString + const expected = expectedConnectionValues[i]; + expect(connection.name).equals(expected.name); + expect(connection.connectionOptions.connectionString).contains( + expected.connectionString ); - expect(connection.source).equals('workspaceSettings'); + expect(connection.source).equals(expected.source); } - - const savedLoadedConnection = loadedConnections[2]; - - expect(savedLoadedConnection.name).equals(savedConnection.name); - expect( - savedLoadedConnection.connectionOptions.connectionString - ).contains(savedConnection.connectionOptions.connectionString); - expect(savedLoadedConnection.source).equals('workspaceSettings'); }); }); From bf392303c69ca45f04ed3856cf0ed167e68bdb49 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 20 Jan 2025 11:53:25 +0100 Subject: [PATCH 07/10] refactor: rename to presetConnections, update test strict equality, use switch --- .vscode/settings.json | 2 +- package.json | 6 ++--- src/connectionController.ts | 26 ++++++++++++------- src/explorer/connectionTreeItem.ts | 8 ++++-- src/mdbExtensionController.ts | 2 +- src/storage/connectionStorage.ts | 21 +++++++-------- .../suite/storage/connectionStorage.test.ts | 20 +++++++------- 7 files changed, 47 insertions(+), 38 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ca7fbbbde..c8f489147 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,7 +17,7 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "mdb.presetSavedConnections": [ + "mdb.presetConnections": [ { "name": "Preset Database", "connectionString": "mongodb://localhost:27017" diff --git a/package.json b/package.json index e651c6f2f..2e96ffc81 100644 --- a/package.json +++ b/package.json @@ -550,7 +550,7 @@ }, { "command": "mdb.editPresetConnections", - "when": "view == mongoDBConnectionExplorer && (viewItem == connectedPresetConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && viewItem == connectedPresetConnectionTreeItem", "group": "3@2" }, { @@ -590,7 +590,7 @@ }, { "command": "mdb.editPresetConnections", - "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedPresetConnectionTreeItem)", + "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedPresetConnectionTreeItem", "group": "2@2" }, { @@ -1193,7 +1193,7 @@ "default": "", "description": "Specify a shell command that is run to start the browser for authenticating with the OIDC identity provider for the server connection. Leave this empty for default browser." }, - "mdb.presetSavedConnections": { + "mdb.presetConnections": { "scope": "window", "type": "array", "description": "Defines preset saved connections. Can be used to share connection configurations in a workspace or global scope. Do not store sensitive credentials here.", diff --git a/src/connectionController.ts b/src/connectionController.ts index aebaf4402..9251e3338 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -177,9 +177,7 @@ export default class ConnectionController { if (!source) { const mdbConfiguration = vscode.workspace.getConfiguration('mdb'); - const presetConnections = mdbConfiguration?.inspect( - 'presetSavedConnections' - ); + const presetConnections = mdbConfiguration?.inspect('presetConnections'); if (presetConnections?.workspaceValue) { source = 'workspaceSettings'; @@ -189,7 +187,7 @@ export default class ConnectionController { // If no preset settings exist in workspace and global scope, // set a default one inside the workspace and open it. source = 'workspaceSettings'; - await mdbConfiguration.update('presetSavedConnections', [ + await mdbConfiguration.update('presetConnections', [ { name: 'Preset Database', connectionString: 'mongodb://localhost:27017', @@ -197,12 +195,20 @@ export default class ConnectionController { ]); } } - if (originTreeItem?.source === 'globalSettings') { - await vscode.commands.executeCommand('workbench.action.openSettingsJson'); - } else { - await vscode.commands.executeCommand( - 'workbench.action.openWorkspaceSettingsFile' - ); + switch (source) { + case 'globalSettings': + await vscode.commands.executeCommand( + 'workbench.action.openSettingsJson' + ); + break; + case 'workspaceSettings': + case 'user': + await vscode.commands.executeCommand( + 'workbench.action.openWorkspaceSettingsFile' + ); + break; + default: + throw new Error('Unknown preset connection source'); } } diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index f7185d3f7..53f83da70 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -209,7 +209,9 @@ export default class ConnectionTreeItem return Object.values(this._childrenCache); } - private async _buildChildrenCacheForDatabases(dataService: DataService) { + private async _buildChildrenCacheForDatabases( + dataService: DataService + ): Promise> { const databases = await this.listDatabases(); databases.sort((a: string, b: string) => a.localeCompare(b)); @@ -231,7 +233,9 @@ export default class ConnectionTreeItem return newChildrenCache; } - private async _buildChildrenCacheForStreams(dataService: DataService) { + private async _buildChildrenCacheForStreams( + dataService: DataService + ): Promise> { const processors = await this.listStreamProcessors(); processors.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 55ac202c9..b6f0b9505 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -162,7 +162,7 @@ export default class MDBExtensionController implements vscode.Disposable { subscribeToConfigurationChanges(): void { const subscription = vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration('mdb.presetSavedConnections')) { + if (event.affectsConfiguration('mdb.presetConnections')) { void this._connectionController.loadSavedConnections(); } }); diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index dc0731946..e476361eb 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -180,28 +180,27 @@ export class ConnectionStorage { ); } - _loadPresetSavedConnections(): LoadedConnection[] { + _loadPresetConnections(): LoadedConnection[] { const configuration = vscode.workspace.getConfiguration('mdb'); - const presetSavedConnectionsInfo = configuration.inspect< - PresetSavedConnection[] - >('presetSavedConnections'); + const presetConnectionsInfo = + configuration.inspect('presetConnections'); - if (!presetSavedConnectionsInfo) { + if (!presetConnectionsInfo) { return []; } - const combinedPresetSavedConnections: PresetSavedConnectionWithSource[] = [ - ...(presetSavedConnectionsInfo?.globalValue ?? []).map((preset) => ({ + const combinedPresetConnections: PresetSavedConnectionWithSource[] = [ + ...(presetConnectionsInfo?.globalValue ?? []).map((preset) => ({ ...preset, source: 'globalSettings' as const, })), - ...(presetSavedConnectionsInfo?.workspaceValue ?? []).map((preset) => ({ + ...(presetConnectionsInfo?.workspaceValue ?? []).map((preset) => ({ ...preset, source: 'workspaceSettings' as const, })), ]; - return combinedPresetSavedConnections.map((presetConnection) => ({ + return combinedPresetConnections.map((presetConnection) => ({ id: uuidv4(), name: presetConnection.name, connectionOptions: { @@ -250,9 +249,9 @@ export class ConnectionStorage { }) ); - const presetSavedConnections = this._loadPresetSavedConnections(); + const presetConnections = this._loadPresetConnections(); - return [...loadedConnections, ...presetSavedConnections]; + return [...loadedConnections, ...presetConnections]; } async removeConnection(connectionId: string): Promise { diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index a5061cd91..286fb4149 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -349,11 +349,11 @@ suite('Connection Storage Test Suite', function () { ], vscode.WorkspaceConfiguration >; - let inspectPresetSavedConnectionsStub: sinon.SinonStub; + let inspectPresetConnectionsStub: sinon.SinonStub; beforeEach(() => { testSandbox.restore(); - inspectPresetSavedConnectionsStub = testSandbox.stub(); + inspectPresetConnectionsStub = testSandbox.stub(); }); test('loads the preset connections', async () => { @@ -362,12 +362,12 @@ suite('Connection Storage Test Suite', function () { 'getConfiguration' ); getConfigurationStub.returns({ - inspect: inspectPresetSavedConnectionsStub, + inspect: inspectPresetConnectionsStub, get: () => undefined, } as any); - inspectPresetSavedConnectionsStub - .withArgs('presetSavedConnections') + inspectPresetConnectionsStub + .withArgs('presetConnections') .returns(presetConnections); const loadedConnections = await testConnectionStorage.loadConnections(); @@ -391,7 +391,7 @@ suite('Connection Storage Test Suite', function () { const connection = loadedConnections[i]; const expected = expectedConnectionValues[i]; expect(connection.name).equals(expected.name); - expect(connection.connectionOptions.connectionString).contains( + expect(connection.connectionOptions.connectionString).equals( expected.connectionString ); expect(connection.source).equals(expected.source); @@ -407,12 +407,12 @@ suite('Connection Storage Test Suite', function () { 'getConfiguration' ); getConfigurationStub.returns({ - inspect: inspectPresetSavedConnectionsStub, + inspect: inspectPresetConnectionsStub, get: () => undefined, } as any); - inspectPresetSavedConnectionsStub - .withArgs('presetSavedConnections') + inspectPresetConnectionsStub + .withArgs('presetConnections') .returns(presetConnections); const loadedConnections = await testConnectionStorage.loadConnections(); @@ -442,7 +442,7 @@ suite('Connection Storage Test Suite', function () { const connection = loadedConnections[i]; const expected = expectedConnectionValues[i]; expect(connection.name).equals(expected.name); - expect(connection.connectionOptions.connectionString).contains( + expect(connection.connectionOptions.connectionString).equals( expected.connectionString ); expect(connection.source).equals(expected.source); From a72bad8a32b95c4842e724dc34d9b575b6bc0f51 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 20 Jan 2025 13:25:31 +0100 Subject: [PATCH 08/10] test: add clear connection tests and use test strict equality --- src/storage/connectionStorage.ts | 23 +++++++++-------- src/test/suite/connectionController.test.ts | 25 +++++++++++++++++++ .../suite/storage/connectionStorage.test.ts | 10 ++++---- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index e476361eb..853ef12e2 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -200,16 +200,19 @@ export class ConnectionStorage { })), ]; - return combinedPresetConnections.map((presetConnection) => ({ - id: uuidv4(), - name: presetConnection.name, - connectionOptions: { - connectionString: presetConnection.connectionString, - }, - source: presetConnection.source, - storageLocation: StorageLocation.NONE, - secretStorageLocation: SecretStorageLocation.SecretStorage, - })); + return combinedPresetConnections.map( + (presetConnection) => + ({ + id: uuidv4(), + name: presetConnection.name, + connectionOptions: { + connectionString: presetConnection.connectionString, + }, + source: presetConnection.source, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + } satisfies LoadedConnection) + ); } async loadConnections(): Promise { diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 78f7d649d..0f54d5820 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -240,6 +240,31 @@ suite('Connection Controller Test Suite', function () { expect(testConnectionController.getSavedConnections().length).to.equal(0); }); + test('clears connections when loading saved connections', async () => { + // This might happen if i.e. one defines a preset connection and then deletes it. + // In that case we'd have defined this connection but there was never a follow up + // delete event to clear it. So on reload we need to start from a clean slate. + testConnectionController._connections['1234'] = { + id: '1234', + name: 'orphan', + connectionOptions: { + connectionString: 'localhost:3000', + }, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + }; + + // Should persist as this is a saved connection. + await testConnectionController.addNewConnectionStringAndConnect( + TEST_DATABASE_URI + ); + + await testConnectionController.loadSavedConnections(); + + expect(testConnectionController.getSavedConnections().length).to.equal(1); + expect(testConnectionController._connections['1234']).is.undefined; + }); + test('the connection model loads both global and workspace stored connection models', async () => { const expectedDriverUrl = `mongodb://localhost:27088/?appname=mongodb-vscode+${version}`; diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index 286fb4149..2f336547d 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -327,17 +327,18 @@ suite('Connection Storage Test Suite', function () { globalValue: [ { name: 'Global Connection 1', - connectionString: 'mongodb://localhost:27017/', + connectionString: + 'mongodb://localhost:27017/?readPreference=primary&ssl=false', }, ], workspaceValue: [ { name: 'Preset Connection 1', - connectionString: 'mongodb://localhost:27017/', + connectionString: 'mongodb://localhost:27017', }, { name: 'Preset Connection 2', - connectionString: 'mongodb://localhost:27018/', + connectionString: 'mongodb://localhost:27018', }, ], }; @@ -421,8 +422,7 @@ suite('Connection Storage Test Suite', function () { { name: savedConnection.name, source: 'user', - connectionString: - savedConnection.connectionOptions.connectionString, + connectionString: `${savedConnection.connectionOptions.connectionString}/`, }, ...presetConnections.globalValue.map((connection) => ({ ...connection, From 6cf0f1c6028c64275b8b46c4ecd9b2d34bfc632b Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 20 Jan 2025 16:35:50 +0100 Subject: [PATCH 09/10] refactor: changes from review --- .vscode/settings.json | 2 +- package.json | 6 +++--- src/connectionController.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c8f489147..8dc745b7f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,7 +19,7 @@ "typescript.tsc.autoDetect": "off", "mdb.presetConnections": [ { - "name": "Preset Database", + "name": "Preset Connection", "connectionString": "mongodb://localhost:27017" } ] diff --git a/package.json b/package.json index 2e96ffc81..534a89314 100644 --- a/package.json +++ b/package.json @@ -1196,11 +1196,11 @@ "mdb.presetConnections": { "scope": "window", "type": "array", - "description": "Defines preset saved connections. Can be used to share connection configurations in a workspace or global scope. Do not store sensitive credentials here.", + "description": "Defines preset connections. Can be used to share connection configurations in a workspace or global scope. Do not store sensitive credentials here.", "examples": [ [ { - "name": "Preset Database", + "name": "Preset Connection", "connectionString": "mongodb://localhost:27017" } ] @@ -1209,7 +1209,7 @@ "type": "object", "examples": [ { - "name": "Preset Database", + "name": "Preset Connection", "connectionString": "mongodb://localhost:27017" } ], diff --git a/src/connectionController.ts b/src/connectionController.ts index 9251e3338..159ecea8e 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -189,7 +189,7 @@ export default class ConnectionController { source = 'workspaceSettings'; await mdbConfiguration.update('presetConnections', [ { - name: 'Preset Database', + name: 'Preset Connection', connectionString: 'mongodb://localhost:27017', }, ]); From 63c719decf4c6fea3b2da832521c19746cce64d7 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 20 Jan 2025 16:49:09 +0100 Subject: [PATCH 10/10] fix(tree-explorer): fix issues with collapsing active connection --- src/explorer/explorerTreeController.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/explorer/explorerTreeController.ts b/src/explorer/explorerTreeController.ts index ceaf23d35..851fc3ac3 100644 --- a/src/explorer/explorerTreeController.ts +++ b/src/explorer/explorerTreeController.ts @@ -131,11 +131,15 @@ export default class ExplorerTreeController return element; } - private _getConnectionExpandedState(connection: LoadedConnection): { + private _getConnectionExpandedState( + connection: LoadedConnection, + pastConnectionTreeItems: { + [key: string]: ConnectionTreeItem; + } + ): { collapsibleState: vscode.TreeItemCollapsibleState; isExpanded: boolean; } { - const pastConnectionTreeItems = this._connectionTreeItems; const isActiveConnection = connection.id === this._connectionController.getActiveConnectionId(); const isBeingConnectedTo = @@ -181,7 +185,7 @@ export default class ExplorerTreeController // Create new connection tree items, using cached children wherever possible. connections.forEach((connection) => { const { collapsibleState, isExpanded } = - this._getConnectionExpandedState(connection); + this._getConnectionExpandedState(connection, pastConnectionTreeItems); this._connectionTreeItems[connection.id] = new ConnectionTreeItem({ connectionId: connection.id,