From ef8164e5c2a99b8920af20db2c11379a30c2cdef Mon Sep 17 00:00:00 2001 From: Alex Ma Date: Wed, 21 Jun 2023 09:59:58 -0700 Subject: [PATCH] Changed connection title generation to use getConnections (including recent and saved). (#22954) * remove advanced options for table designer and notebook Actions title (unnecessary in usage context) * Revert "remove advanced options for table designer and notebook Actions title (unnecessary in usage context)" This reverts commit e30aee5151319863aebbb4738fb30354c179c2c5. * added changes based on feedback * added null check and updated tests * WIP change to connection title generation * WIP connection management service update * fix to connectionManagementService test * fix editorConnectionTitles * renamed nondefault to distinguishing options * use stored connections for options * removed erroneous connection name set to null * added title by ID search for title generation * Add recent connection title * added fix for stub bug * added child title options appended * WIP rework of getEditorTitle * more work done * WIP changes for 5-2-2023 * moved server info to generate titles. * added reworked title generation * added working active connection title generation and cleanup * added comments to argument * remove unnecessary spaces * added id fix assign * added fromEditor save * Revert "Revert new connection string format (#22997)" This reverts commit 898bb73a343eabef1778b398a48bacbae474042d. * added small fix to tests and exclude empty properties * made small fixes for tests * update expected ID * added support for old password key and removed empty options * added in authenticationType core property * fix for whitespace indentation * added connection save profile to thing * made some small fixes to connection options * added small change to connectionDialogService * added nullcheck for saveProfile * added negation for connection saveProfile * remove duplicate editor title generation * added edit profile handling for titles * Cleanup serverCapabilities property * fixed dependency issues * removed connectionproviderproperties * added fix for treeupdateutils * made update to title generation * added recent connections change * Revert "Cleanup serverCapabilities property" This reverts commit 2c7b94f98cabddb34116dcdd83177614a484c026. * added dashboard text and fix for connection store test * added group name to end also temporary added dashboard changes based on feedback * added in new SQL version * added fix to edit connections * added clarifying information to title generation --------- Co-authored-by: Cheena Malhotra --- extensions/mssql/package.json | 3 +- .../mssql/src/tableDesigner/tableDesigner.ts | 18 +- src/sql/azdata.proposed.d.ts | 9 + .../common/capabilitiesService.ts | 6 + .../test/common/testCapabilitiesService.ts | 1 + .../connection/common/connectionConfig.ts | 10 +- .../connection/common/connectionManagement.ts | 9 + .../connection/common/connectionProfile.ts | 47 +- .../connection/common/connectionStore.ts | 15 +- .../platform/connection/common/interfaces.ts | 2 +- .../common/providerConnectionInfo.ts | 123 +++- .../test/common/connectionConfig.test.ts | 291 +++++++- .../test/common/connectionProfile.test.ts | 5 +- .../common/providerConnectionInfo.test.ts | 48 +- .../common/testConnectionManagementService.ts | 4 + .../browser/mainThreadConnectionManagement.ts | 4 + .../api/common/extHostConnectionManagement.ts | 4 + .../api/common/sqlExtHost.api.impl.ts | 3 + .../api/common/sqlExtHost.protocol.ts | 1 + .../browser/editor/profiler/dashboardInput.ts | 7 +- .../common/editor/query/queryEditorInput.ts | 14 +- .../connection/browser/connectionStatus.ts | 35 +- .../browser/services/breadcrumb.service.ts | 4 +- .../notebook/browser/notebookActions.ts | 5 +- .../objectExplorer/browser/serverTreeView.ts | 19 + .../browser/commonServiceInterface.service.ts | 4 + .../connection/browser/connectionBrowseTab.ts | 7 +- .../browser/connectionDialogService.ts | 3 +- .../browser/connectionManagementService.ts | 179 ++++- .../connectionManagementService.test.ts | 193 +++++- .../objectExplorer/browser/treeUpdateUtils.ts | 55 +- .../test/browser/treeUpdateUtils.test.ts | 645 ++++++++++++++++++ 32 files changed, 1705 insertions(+), 68 deletions(-) create mode 100644 src/sql/workbench/services/objectExplorer/test/browser/treeUpdateUtils.test.ts diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 8254fb23ed..3a365e7d04 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -814,6 +814,7 @@ "connectionProvider": { "providerId": "MSSQL", "displayName": "%mssql.provider.displayName%", + "useFullOptions": true, "isExecutionPlanProvider": true, "supportCopyResultsToClipboard": true, "azureResource": "Sql", @@ -952,7 +953,7 @@ "description": "%mssql.connectionOptions.applicationName.description%", "groupName": "%mssql.connectionOptions.groupName.initialization%", "valueType": "string", - "defaultValue": null, + "defaultValue": "azdata", "objectType": null, "categoryValues": null, "isRequired": false, diff --git a/extensions/mssql/src/tableDesigner/tableDesigner.ts b/extensions/mssql/src/tableDesigner/tableDesigner.ts index b955651ac2..4034e8cf9d 100644 --- a/extensions/mssql/src/tableDesigner/tableDesigner.ts +++ b/extensions/mssql/src/tableDesigner/tableDesigner.ts @@ -26,11 +26,18 @@ export function registerTableDesignerCommands(appContext: AppContext) { if (!connectionString) { throw new Error(FailedToGetConnectionStringError); } + let titleString = `${context.connectionProfile!.serverName} - ${context.connectionProfile!.databaseName} - ${NewTableText}`; + // append distinguishing options to end to let users know exact connection. + let distinguishingOptions = await azdata.connection.getEditorConnectionProfileTitle(context.connectionProfile, true); + if (distinguishingOptions !== '') { + distinguishingOptions = distinguishingOptions.replace('(', '[').replace(')', ']'); + titleString += `${distinguishingOptions}`; + } const tableIcon = context.nodeInfo!.nodeSubType as azdata.designers.TableIcon; const telemetryInfo = await getTelemetryInfo(context, tableIcon); await azdata.designers.openTableDesigner(sqlProviderName, { title: NewTableText, - tooltip: `${context.connectionProfile!.serverName} - ${context.connectionProfile!.databaseName} - ${NewTableText}`, + tooltip: titleString, server: context.connectionProfile!.serverName, database: context.connectionProfile!.databaseName, isNewTable: true, @@ -56,11 +63,18 @@ export function registerTableDesignerCommands(appContext: AppContext) { if (!connectionString) { throw new Error(FailedToGetConnectionStringError); } + let titleString = `${server} - ${database} - ${schema}.${name}`; + // append distinguishing options to end to let users know exact connection. + let distinguishingOptions = await azdata.connection.getEditorConnectionProfileTitle(context.connectionProfile, true); + if (distinguishingOptions !== '') { + distinguishingOptions = distinguishingOptions.replace('(', '[').replace(')', ']'); + titleString += `${distinguishingOptions}`; + } const tableIcon = context.nodeInfo!.nodeSubType as azdata.designers.TableIcon; const telemetryInfo = await getTelemetryInfo(context, tableIcon); await azdata.designers.openTableDesigner(sqlProviderName, { title: `${schema}.${name}`, - tooltip: `${server} - ${database} - ${schema}.${name}`, + tooltip: titleString, server: server, database: database, isNewTable: false, diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 39351fbd1a..ca22454c47 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -508,6 +508,15 @@ declare module 'azdata' { * @returns The new password that is returned from the operation or undefined if unsuccessful. */ export function openChangePasswordDialog(profile: IConnectionProfile): Thenable; + + /** + * Gets the formatted title of the connection profile for display + * @param profile The connection profile we want to get the full display info for. + * @param getOptionsOnly Provide if you only want to get the differing advanced options (for some titles). + * @param includeGroupName Provide if you want to include the groupName as well (in areas that do not display the groupName). + * @returns The title formatted with server info in front, with non default options at the end. + */ + export function getEditorConnectionProfileTitle(profile: IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): Thenable; } /* diff --git a/src/sql/platform/capabilities/common/capabilitiesService.ts b/src/sql/platform/capabilities/common/capabilitiesService.ts index 78775bf205..81c5965493 100644 --- a/src/sql/platform/capabilities/common/capabilitiesService.ts +++ b/src/sql/platform/capabilities/common/capabilitiesService.ts @@ -67,6 +67,12 @@ export interface ConnectionProviderProperties { */ displayName: string; + /** + * Enable to use all connection properties for URI generation (ServiceLayer requires the same options as well.) + * If not specified, only IsIdentity options will be used instead (URI with basic info). + */ + useFullOptions?: boolean; + /** * Alias to be used for the kernel in notebooks */ diff --git a/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts b/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts index ed4ee7c801..a2c3d8d230 100644 --- a/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts +++ b/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts @@ -115,6 +115,7 @@ export class TestCapabilitiesService implements ICapabilitiesService { providerId: mssqlProviderName, displayName: 'MSSQL', connectionOptions: connectionProvider.concat(mssqlAdvancedOptions), + useFullOptions: true, }; let pgSQLCapabilities = { providerId: this.pgsqlProviderName, diff --git a/src/sql/platform/connection/common/connectionConfig.ts b/src/sql/platform/connection/common/connectionConfig.ts index a0dabc30f5..ea8e96f233 100644 --- a/src/sql/platform/connection/common/connectionConfig.ts +++ b/src/sql/platform/connection/common/connectionConfig.ts @@ -363,10 +363,18 @@ export class ConnectionConfig { p.options.database === profile.options.database && p.options.server === profile.options.server && p.options.user === profile.options.user && - p.groupId === newGroupID); + p.options.connectionName === profile.options.connectionName && + p.groupId === newGroupID && + this.checkIfNonDefaultOptionsMatch(p, profile)); return existingProfile === undefined; } + private checkIfNonDefaultOptionsMatch(profileStore: IConnectionProfileStore, profile: ConnectionProfile): boolean { + let tempProfile = ConnectionProfile.createFromStoredProfile(profileStore, this._capabilitiesService); + let result = profile.getNonDefaultOptionsString() === tempProfile.getNonDefaultOptionsString(); + return result; + } + /** * Moves the connection under the target group with the new ID. */ diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index 0061ba5ee6..b9096afdc3 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -381,6 +381,15 @@ export interface IConnectionManagementService { * @returns the new valid password that is entered, or undefined if cancelled or errored. */ openChangePasswordDialog(profile: IConnectionProfile): Promise; + + /** + * Gets the formatted title of the connection profile for display. + * @param profile The connection profile we want to get the full display info for. + * @param getOptionsOnly Provide if you only want to get the differing advanced options (for some titles). + * @param includeGroupName Provide if you want to include the groupName as well (in areas that do not display the groupName). + * @returns The title formatted with server info in front, with non default options at the end. + */ + getEditorConnectionProfileTitle(profile: IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): string; } export enum RunQueryOnConnectionMode { diff --git a/src/sql/platform/connection/common/connectionProfile.ts b/src/sql/platform/connection/common/connectionProfile.ts index eb976d16e9..25fe36f2c0 100644 --- a/src/sql/platform/connection/common/connectionProfile.ts +++ b/src/sql/platform/connection/common/connectionProfile.ts @@ -39,6 +39,9 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa public isDisconnecting: boolean = false; + // title from ProviderConnectionInfo cannot be changed, in order to show different dynamic options appended, we must override the title with our own. + private _title?: string; + public constructor( capabilitiesService: ICapabilitiesService, model: string | azdata.IConnectionProfile | azdata.connection.ConnectionProfile | undefined) { @@ -199,6 +202,21 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa return (this._groupName === ConnectionProfile.RootGroupName); } + public override get title(): string { + if (this._title) { + return this._title; + } + return this.getOriginalTitle(); + } + + public getOriginalTitle(): string { + return super.title; + } + + public override set title(value: string) { + this._title = value; + } + public override clone(): ConnectionProfile { let instance = new ConnectionProfile(this.capabilitiesService, this); return instance; @@ -227,24 +245,39 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa /** * Returns a key derived the connections options (providerName, authenticationType, serverName, databaseName, userName, groupid) + * and all the other properties (except empty ones) if useFullOptions is enabled for the provider. * This key uniquely identifies a connection in a group - * Example: "providerName:MSSQL|authenticationType:|databaseName:database|serverName:server3|userName:user|group:testid" + * Example (original format): "providerName:MSSQL|authenticationType:|databaseName:database|serverName:server3|userName:user|group:testid" + * Example (new format): "providerName:MSSQL|databaseName:database|serverName:server3|userName:user|groupId:testid" + * @param getOriginalOptions will return the original URI format regardless if useFullOptions was set or not. (used for retrieving passwords) */ - public override getOptionsKey(): string { - let id = super.getOptionsKey(); + public override getOptionsKey(getOriginalOptions?: boolean): string { + let id = super.getOptionsKey(getOriginalOptions); let databaseDisplayName: string = this.options['databaseDisplayName']; if (databaseDisplayName) { id += ProviderConnectionInfo.idSeparator + 'databaseDisplayName' + ProviderConnectionInfo.nameValueSeparator + databaseDisplayName; } - return id + ProviderConnectionInfo.idSeparator + 'group' + ProviderConnectionInfo.nameValueSeparator + this.groupId; + let groupProp = 'group' + if (!getOriginalOptions && this.serverCapabilities && this.serverCapabilities.useFullOptions) { + groupProp = 'groupId' + } + + return id + ProviderConnectionInfo.idSeparator + groupProp + ProviderConnectionInfo.nameValueSeparator + this.groupId; } /** - * Returns the unique id for the connection that doesn't include the group name + * Returns the unique id for the connection that doesn't include the group name. + * Used primarily for retrieving shared passwords among different connections in default state. + * @param getOriginalOptions will return the original URI format regardless if useFullOptions was set or not. (used for retrieving passwords) */ - public getConnectionInfoId(): string { - return super.getOptionsKey(); + public getConnectionInfoId(getOriginalOptions = true): string { + let id = super.getOptionsKey(getOriginalOptions); + let databaseDisplayName: string = this.options['databaseDisplayName']; + if (databaseDisplayName && !getOriginalOptions && this.serverCapabilities?.useFullOptions) { + id += ProviderConnectionInfo.idSeparator + 'databaseDisplayName' + ProviderConnectionInfo.nameValueSeparator + databaseDisplayName; + } + return id; } public toIConnectionProfile(): interfaces.IConnectionProfile { diff --git a/src/sql/platform/connection/common/connectionStore.ts b/src/sql/platform/connection/common/connectionStore.ts index 3548bf45ce..38884894b3 100644 --- a/src/sql/platform/connection/common/connectionStore.ts +++ b/src/sql/platform/connection/common/connectionStore.ts @@ -67,6 +67,7 @@ export class ConnectionStore { } cred.push(CRED_ITEMTYPE_PREFIX.concat(itemType)); + // Use basic info for credentials so that passwords can be shared among similar profiles for now. cred.push(CRED_ID_PREFIX.concat(connectionProfileInstance.getConnectionInfoId())); return cred.join(CRED_SEPARATOR); } @@ -248,7 +249,8 @@ export class ConnectionStore { // Remove the connection from the list if it already exists list = list.filter(value => { - let equal = value && value.getConnectionInfoId() === savedProfile.getConnectionInfoId(); + let equal = value && value.connectionName === savedProfile.connectionName; + equal = equal && value.getConnectionInfoId(false) === savedProfile.getConnectionInfoId(false); if (equal && savedProfile.saveProfile) { equal = value.groupId === savedProfile.groupId || ConnectionProfileGroup.sameGroupName(value.groupFullName, savedProfile.groupFullName); @@ -266,7 +268,8 @@ export class ConnectionStore { // Remove the connection from the list if it already exists list = list.filter(value => { - let equal = value && value.getConnectionInfoId() === savedProfile.getConnectionInfoId(); + let equal = value && value.connectionName === savedProfile.connectionName; + equal = equal && value.getConnectionInfoId(false) === savedProfile.getConnectionInfoId(false); if (equal && savedProfile.saveProfile) { equal = value.groupId === savedProfile.groupId || ConnectionProfileGroup.sameGroupName(value.groupFullName, savedProfile.groupFullName); @@ -301,6 +304,8 @@ export class ConnectionStore { private doSavePassword(conn: IConnectionProfile): Promise { if (conn.password) { + // Credentials are currently shared between profiles with the same basic details. + // Credentials are currently not cleared upon deletion of a profile. const credentialId = this.formatCredentialId(conn); return this.credentialService.saveCredential(credentialId, conn.password); } else { @@ -321,6 +326,12 @@ export class ConnectionStore { return this.convertToConnectionGroup(groups, profilesInConfiguration); } + public getAllConnectionsFromConfig(): ConnectionProfile[] { + let profilesInConfiguration: ConnectionProfile[] | undefined; + profilesInConfiguration = this.connectionConfig.getConnections(true); + return profilesInConfiguration; + } + private convertToConnectionGroup(groups: IConnectionProfileGroup[], connections?: ConnectionProfile[], parent?: ConnectionProfileGroup): ConnectionProfileGroup[] { const result: ConnectionProfileGroup[] = []; const children = groups.filter(g => g.parentId === (parent ? parent.id : undefined)); diff --git a/src/sql/platform/connection/common/interfaces.ts b/src/sql/platform/connection/common/interfaces.ts index ae4f2ff52d..a17c7bc388 100644 --- a/src/sql/platform/connection/common/interfaces.ts +++ b/src/sql/platform/connection/common/interfaces.ts @@ -11,7 +11,7 @@ export type ProfileMatcher = (a: IConnectionProfile, b: IConnectionProfile) => b export interface IConnectionProfile extends azdata.IConnectionProfile { serverCapabilities: ConnectionProviderProperties | undefined; - getOptionsKey(): string; + getOptionsKey(getOriginalOptions?: boolean): string; getOptionKeyIdNames(): string[]; matches(profile: azdata.IConnectionProfile): boolean; } diff --git a/src/sql/platform/connection/common/providerConnectionInfo.ts b/src/sql/platform/connection/common/providerConnectionInfo.ts index cf7b726a3b..0e16d69591 100644 --- a/src/sql/platform/connection/common/providerConnectionInfo.ts +++ b/src/sql/platform/connection/common/providerConnectionInfo.ts @@ -134,7 +134,7 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { this.options[name] = value; } - private getServerInfo() { + public getServerInfo() { let title = ''; if (this.serverCapabilities) { title = this.serverName; @@ -161,7 +161,7 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { } } // The provider capabilities are registered at the same time at load time, we can assume all providers are registered as long as the collection is not empty. - else if (Object.keys(this.capabilitiesService.providers).length > 0) { + else if (this.hasLoaded()) { return localize('connection.unsupported', "Unsupported connection"); } else { return localize('loading', "Loading..."); @@ -169,8 +169,16 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { return label; } + public hasLoaded(): boolean { + return Object.keys(this.capabilitiesService.providers).length > 0; + } + public get serverInfo(): string { - return this.getServerInfo(); + let value = this.getServerInfo(); + if (this.serverCapabilities?.useFullOptions) { + value += this.getNonDefaultOptionsString(); + } + return value; } public isPasswordRequired(): boolean { @@ -198,11 +206,14 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { /** * Returns a key derived the connections options (providerName, authenticationType, serverName, databaseName, userName, groupid) + * and all the other properties (except empty ones) if useFullOptions is enabled for the provider. * This key uniquely identifies a connection in a group - * Example: "providerName:MSSQL|authenticationType:|databaseName:database|serverName:server3|userName:user|group:testid" + * Example (original format): "providerName:MSSQL|authenticationType:|databaseName:database|serverName:server3|userName:user|group:testid" + * Example (new format): "providerName:MSSQL|databaseName:database|serverName:server3|userName:user|groupId:testid" + * @param getOriginalOptions will return the original URI format regardless if useFullOptions was set or not. (used for retrieving passwords) */ - public getOptionsKey(): string { - let idNames = this.getOptionKeyIdNames(); + public getOptionsKey(getOriginalOptions?: boolean): string { + let idNames = this.getOptionKeyIdNames(getOriginalOptions); idNames = idNames.filter(x => x !== undefined); //Sort to make sure using names in the same order every time otherwise the ids would be different @@ -211,8 +222,32 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { let idValues: string[] = []; for (let index = 0; index < idNames.length; index++) { let value = this.options[idNames[index]!]; - value = value ? value : ''; - idValues.push(`${idNames[index]}${ProviderConnectionInfo.nameValueSeparator}${value}`); + + // If we're using the new URI format, we do not include any values that are empty or are default. + let useFullOptions = (this.serverCapabilities && this.serverCapabilities.useFullOptions) + let isFullOptions = useFullOptions && !getOriginalOptions; + + if (isFullOptions) { + let finalValue = undefined; + let options = this.serverCapabilities.connectionOptions.filter(value => value.name === idNames[index]!); + if (options.length > 0 && value) { + finalValue = value !== options[0].defaultValue ? value : undefined; + if (options[0].specialValueType === 'appName' && this.providerName === Constants.mssqlProviderName) { + finalValue = (value as string).startsWith('azdata') ? undefined : finalValue + } + } + else if (options.length > 0 && options[0].specialValueType === 'authType') { + // Include default auth type as it is a required part of the option key. + finalValue = ''; + } + value = finalValue; + } + else { + value = value ? value : ''; + } + if ((isFullOptions && value !== undefined) || !isFullOptions) { + idValues.push(`${idNames[index]}${ProviderConnectionInfo.nameValueSeparator}${value}`); + } } return ProviderConnectionInfo.ProviderPropertyName + ProviderConnectionInfo.nameValueSeparator + @@ -222,13 +257,18 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { /** * @returns Array of option key names */ - public getOptionKeyIdNames(): string[] { + public getOptionKeyIdNames(getOriginalOptions?: boolean): string[] { + let useFullOptions = false; let idNames = []; if (this.serverCapabilities) { + useFullOptions = this.serverCapabilities.useFullOptions; idNames = this.serverCapabilities.connectionOptions.map(o => { - if ((o.specialValueType || o.isIdentity) - && o.specialValueType !== ConnectionOptionSpecialType.password - && o.specialValueType !== ConnectionOptionSpecialType.connectionName) { + // All options enabled, use every property besides password. + let newProperty = useFullOptions && o.specialValueType !== ConnectionOptionSpecialType.password && !getOriginalOptions; + // Fallback to original base IsIdentity properties otherwise. + let originalProperty = (o.specialValueType || o.isIdentity) && o.specialValueType !== ConnectionOptionSpecialType.password + && o.specialValueType !== ConnectionOptionSpecialType.connectionName; + if (newProperty || originalProperty) { return o.name; } else { return undefined; @@ -327,4 +367,63 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { public static get displayNameValueSeparator(): string { return '='; } + + + /** + * Get all non specialValueType (or if distinct connections share same connection name, everything but connectionName and password). + * Also allows for getting the non default options for this profile. (this function is used for changing the title). + * @param needSpecial include all the special options key besides connection name or password in case we have multiple + * distinct connections sharing the same connection name (for connection trees mainly). + * @param getNonDefault get only the non default options (for individual connections) to be used for identfying different properties + * among connections sharing the same title. + */ + public getConnectionOptionsList(needSpecial: boolean, getNonDefault: boolean): azdata.ConnectionOption[] { + let connectionOptions: azdata.ConnectionOption[] = []; + + if (this.serverCapabilities) { + this.serverCapabilities.connectionOptions.forEach(element => { + if (((!needSpecial && element.specialValueType !== ConnectionOptionSpecialType.serverName && + element.specialValueType !== ConnectionOptionSpecialType.databaseName && + element.specialValueType !== ConnectionOptionSpecialType.authType && + element.specialValueType !== ConnectionOptionSpecialType.userName) || needSpecial) && + element.specialValueType !== ConnectionOptionSpecialType.connectionName && + element.specialValueType !== ConnectionOptionSpecialType.password) { + if (getNonDefault) { + let value = this.getOptionValue(element.name); + if (value && value !== element.defaultValue) { + connectionOptions.push(element); + } + } + else { + connectionOptions.push(element); + } + } + }); + } + //Need to sort for consistency. + connectionOptions.sort(); + return connectionOptions; + } + + /** + * Append all non default options to tooltip string if useFullOptions is enabled. + */ + public getNonDefaultOptionsString(): string { + let parts: string = ""; + let nonDefaultOptions = this.getConnectionOptionsList(false, true); + nonDefaultOptions.forEach(element => { + let value = this.getOptionValue(element.name); + if (parts.length === 0) { + parts = " ("; + } + let addValue = element.name + ProviderConnectionInfo.displayNameValueSeparator + `${value}`; + parts += parts === " (" ? addValue : (ProviderConnectionInfo.displayIdSeparator + addValue); + }); + if (parts.length > 0) { + parts += ")"; + } + + return parts; + + } } diff --git a/src/sql/platform/connection/test/common/connectionConfig.test.ts b/src/sql/platform/connection/test/common/connectionConfig.test.ts index a20c1adcda..aac1ca793f 100644 --- a/src/sql/platform/connection/test/common/connectionConfig.test.ts +++ b/src/sql/platform/connection/test/common/connectionConfig.test.ts @@ -181,6 +181,30 @@ suite('ConnectionConfig', () => { isRequired: true, specialValueType: ConnectionOptionSpecialType.password, valueType: ServiceOptionType.string + }, + { + name: 'testProperty1', + displayName: undefined!, + description: undefined!, + groupName: undefined!, + categoryValues: undefined!, + defaultValue: "default", + isIdentity: true, + isRequired: true, + specialValueType: undefined!, + valueType: ServiceOptionType.string + }, + { + name: 'testProperty2', + displayName: undefined!, + description: undefined!, + groupName: undefined!, + categoryValues: undefined!, + defaultValue: "10", + isIdentity: true, + isRequired: true, + specialValueType: undefined!, + valueType: ServiceOptionType.number } ] }; @@ -693,6 +717,195 @@ suite('ConnectionConfig', () => { } }); + test('change group for connection should accept similar connection with different options', async () => { + let changingProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { + 'testProperty1': 'nonDefault', + 'testProperty2': '10', + }, + saveProfile: true, + id: 'server3-2', + connectionName: undefined! + }; + let existingProfile = ConnectionProfile.convertToProfileStore(capabilitiesService.object, { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'test', + groupId: 'test', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { 'testProperty2': '15' }, + saveProfile: true, + id: 'server3', + connectionName: undefined! + }); + + let _testConnections = [...deepClone(testConnections), existingProfile, changingProfile]; + + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connections', _testConnections, ConfigurationTarget.USER); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, changingProfile); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + + await config.changeGroupIdForConnection(connectionProfile, 'test'); + + let editedConnections = configurationService.inspect('datasource.connections').userValue!; + + assert.strictEqual(editedConnections.length, _testConnections.length); + let editedConnection = editedConnections.find(con => con.id === 'server3-2'); + assert.ok(editedConnection); + assert.strictEqual(editedConnection!.groupId, 'test'); + }); + + test('change group for connection should not accept similar connection with default options same as another', async () => { + let changingProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { + 'testProperty1': 'nonDefault', + 'testProperty2': '10', + }, + saveProfile: true, + id: 'server3-2', + connectionName: undefined! + }; + let existingProfile = ConnectionProfile.convertToProfileStore(capabilitiesService.object, { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'test', + groupId: 'test', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { 'testProperty1': 'nonDefault' }, + saveProfile: true, + id: 'server3', + connectionName: undefined! + }); + + let _testConnections = [...deepClone(testConnections), existingProfile, changingProfile]; + + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connections', _testConnections, ConfigurationTarget.USER); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, changingProfile); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + + try { + await config.changeGroupIdForConnection(connectionProfile, 'test'); + assert.fail(); + } catch (e) { + let editedConnections = configurationService.inspect('datasource.connections').userValue!; + // two + assert.strictEqual(editedConnections.length, _testConnections.length); + let editedConnection = editedConnections.find(con => con.id === 'server3-2'); + assert.ok(!!editedConnection); + assert.strictEqual(editedConnection!.groupId, 'g3'); + } + }); + + test('change group for connection should accept similar connection with a distinguishing option', async () => { + let changingProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { + 'testProperty1': 'nonDefault', + 'testProperty2': '15', + }, + saveProfile: true, + id: 'server3-2', + connectionName: undefined! + }; + let existingProfile = ConnectionProfile.convertToProfileStore(capabilitiesService.object, { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'test', + groupId: 'test', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { 'testProperty2': '15' }, + saveProfile: true, + id: 'server3', + connectionName: undefined! + }); + + let _testConnections = [...deepClone(testConnections), existingProfile, changingProfile]; + + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connections', _testConnections, ConfigurationTarget.USER); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, changingProfile); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + + await config.changeGroupIdForConnection(connectionProfile, 'test'); + + let editedConnections = configurationService.inspect('datasource.connections').userValue!; + + assert.strictEqual(editedConnections.length, _testConnections.length); + let editedConnection = editedConnections.find(con => con.id === 'server3-2'); + assert.ok(editedConnection); + assert.strictEqual(editedConnection!.groupId, 'test'); + }); + test('change group(parent) for connection', async () => { let newProfile: IConnectionProfile = { serverName: 'server3', @@ -807,8 +1020,8 @@ suite('ConnectionConfig', () => { savePassword: true, groupFullName: 'g3', groupId: 'g3', - getOptionsKey: () => { return 'connectionId'; }, serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, getOptionKeyIdNames: undefined!, matches: undefined!, providerName: 'MSSQL', @@ -826,8 +1039,8 @@ suite('ConnectionConfig', () => { savePassword: true, groupFullName: 'test', groupId: 'test', - getOptionsKey: () => { return 'connectionId'; }, serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, getOptionKeyIdNames: undefined!, matches: undefined!, providerName: 'MSSQL', @@ -870,4 +1083,78 @@ suite('ConnectionConfig', () => { assert(result, 'Matcher did not find a match for identical edit'); }); + + test('isDuplicateEdit should return false if an edit profile has different properties', async () => { + let originalProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'test', + groupId: 'test', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: 'server3-2', + connectionName: undefined! + }; + let changedProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: 'Integrated', + savePassword: true, + groupFullName: 'test', + groupId: 'test', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: 'server3-2', + connectionName: undefined! + }; + let existingProfile = ConnectionProfile.convertToProfileStore(capabilitiesService.object, { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'test', + groupId: 'test', + serverCapabilities: undefined, + getOptionsKey: () => { return 'connectionId'; }, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: 'server3', + connectionName: undefined! + }); + + let _testConnections = [...deepClone(testConnections), existingProfile, originalProfile]; + + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connections', _testConnections, ConfigurationTarget.USER); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, changedProfile); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + + let matcher = (a: IConnectionProfile, b: IConnectionProfile) => a.id === originalProfile.id; + let result = await config.isDuplicateEdit(connectionProfile, matcher); + + assert(!result, 'Matcher matched the profile even when it had a different property'); + }); }); diff --git a/src/sql/platform/connection/test/common/connectionProfile.test.ts b/src/sql/platform/connection/test/common/connectionProfile.test.ts index c5258a29d5..f72448fd3a 100644 --- a/src/sql/platform/connection/test/common/connectionProfile.test.ts +++ b/src/sql/platform/connection/test/common/connectionProfile.test.ts @@ -173,7 +173,8 @@ suite('SQL ConnectionProfileInfo tests', () => { msSQLCapabilities = { providerId: mssqlProviderName, displayName: 'MSSQL', - connectionOptions: connectionProvider + connectionOptions: connectionProvider, + useFullOptions: true }; capabilitiesService = new TestCapabilitiesService(); capabilitiesService.capabilities[mssqlProviderName] = { connection: msSQLCapabilities }; @@ -236,7 +237,7 @@ suite('SQL ConnectionProfileInfo tests', () => { test('getOptionsKey should create a valid unique id', () => { let conn = new ConnectionProfile(capabilitiesService, iConnectionProfile); - let expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|serverName:new server|userName:user|databaseDisplayName:database|group:group id'; + let expectedId = 'providerName:MSSQL|authenticationType:|connectionName:new name|databaseName:database|serverName:new server|userName:user|databaseDisplayName:database|groupId:group id'; let id = conn.getOptionsKey(); assert.strictEqual(id, expectedId); }); diff --git a/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts b/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts index c571a6e37c..a8bb20811e 100644 --- a/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts +++ b/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts @@ -23,8 +23,8 @@ suite('SQL ProviderConnectionInfo tests', () => { authenticationType: '', savePassword: true, groupFullName: 'g2/g2-2', - serverCapabilities: undefined, groupId: undefined, + serverCapabilities: undefined, getOptionsKey: undefined!, getOptionKeyIdNames: undefined!, matches: undefined!, @@ -35,7 +35,6 @@ suite('SQL ProviderConnectionInfo tests', () => { }; setup(() => { - let capabilities: azdata.DataProtocolServerCapabilities[] = []; let connectionProvider: azdata.ConnectionOption[] = [ { name: 'connectionName', @@ -127,8 +126,8 @@ suite('SQL ProviderConnectionInfo tests', () => { providerId: mssqlProviderName, displayName: 'MSSQL', connectionOptions: connectionProvider, + useFullOptions: true }; - capabilities.push(msSQLCapabilities); capabilitiesService = new TestCapabilitiesService(); capabilitiesService.capabilities[mssqlProviderName] = { connection: msSQLCapabilities }; }); @@ -232,15 +231,37 @@ suite('SQL ProviderConnectionInfo tests', () => { }); test('getOptionsKey should create a valid unique id', () => { + // Test the new option key format let conn = new ProviderConnectionInfo(capabilitiesService, connectionProfile); - // **IMPORTANT** This should NEVER change without thorough review and consideration of side effects. This key controls - // things like how passwords are saved, which means if its changed then serious side effects will occur. - let expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|serverName:new server|userName:user'; + let expectedId = 'providerName:MSSQL|authenticationType:|connectionName:name|databaseName:database|serverName:new server|userName:user'; let id = conn.getOptionsKey(); assert.strictEqual(id, expectedId); + + // Test for original options key (used for retrieving passwords and as a fallback for unsupported providers) + // **IMPORTANT** The original format option key should NEVER change without thorough review and consideration of side effects. This version of the key controls + // things like how passwords are saved, which means if its changed then serious side effects will occur. + expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|serverName:new server|userName:user'; + id = conn.getOptionsKey(true); + assert.strictEqual(id, expectedId); }); - test('getOptionsKey should create the same ID regardless of optional options', () => { + test('getOptionsKey should return original formatted ID if useFullOptions is not supported', () => { + // Test the new option key format + let originalCapabilitiesConnection = capabilitiesService.capabilities[mssqlProviderName].connection; + originalCapabilitiesConnection.useFullOptions = false; + let newCapabilitiesService = new TestCapabilitiesService(); + newCapabilitiesService.capabilities[mssqlProviderName] = { connection: originalCapabilitiesConnection } + let conn = new ProviderConnectionInfo(newCapabilitiesService, connectionProfile); + let expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|serverName:new server|userName:user'; + let id = conn.getOptionsKey(); + assert.strictEqual(id, expectedId); + + // Should be the same when getOriginalOptions is true. + id = conn.getOptionsKey(true); + assert.strictEqual(id, expectedId); + }); + + test('getOptionsKey should create different keys based on optional options', () => { const conn1 = new ProviderConnectionInfo(capabilitiesService, connectionProfile); let id1 = conn1.getOptionsKey(); @@ -250,6 +271,19 @@ suite('SQL ProviderConnectionInfo tests', () => { const conn2 = new ProviderConnectionInfo(capabilitiesService, connectionProfile); const id2 = conn2.getOptionsKey(); + assert.notEqual(id1, id2); + }); + + test('getOptionsKey should have the same key if original options is used', () => { + const conn1 = new ProviderConnectionInfo(capabilitiesService, connectionProfile); + let id1 = conn1.getOptionsKey(true); + + connectionProfile.options = { + 'encrypt': true + }; + const conn2 = new ProviderConnectionInfo(capabilitiesService, connectionProfile); + const id2 = conn2.getOptionsKey(true); + assert.strictEqual(id1, id2); }); diff --git a/src/sql/platform/connection/test/common/testConnectionManagementService.ts b/src/sql/platform/connection/test/common/testConnectionManagementService.ts index b91e7850bc..7343deb6fa 100644 --- a/src/sql/platform/connection/test/common/testConnectionManagementService.ts +++ b/src/sql/platform/connection/test/common/testConnectionManagementService.ts @@ -355,6 +355,10 @@ export class TestConnectionManagementService implements IConnectionManagementSer return undefined; } + getEditorConnectionProfileTitle(profile: IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): string { + return undefined!; + } + openCustomErrorDialog(options: azdata.window.IErrorDialogOptions): Promise { return undefined; } diff --git a/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts b/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts index 2e0e371439..5efee87a23 100644 --- a/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts +++ b/src/sql/workbench/api/browser/mainThreadConnectionManagement.ts @@ -185,6 +185,10 @@ export class MainThreadConnectionManagement extends Disposable implements MainTh return this._connectionManagementService.openChangePasswordDialog(convertedProfile); } + public $getEditorConnectionProfileTitle(profile: IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): Thenable { + return Promise.resolve(this._connectionManagementService.getEditorConnectionProfileTitle(profile, getOptionsOnly, includeGroupName)); + } + public async $listDatabases(connectionId: string): Promise { let connectionUri = await this.$getUriForConnection(connectionId); let result = await this._connectionManagementService.listDatabases(connectionUri); diff --git a/src/sql/workbench/api/common/extHostConnectionManagement.ts b/src/sql/workbench/api/common/extHostConnectionManagement.ts index 56f27dbc8f..3cf2a7bf6d 100644 --- a/src/sql/workbench/api/common/extHostConnectionManagement.ts +++ b/src/sql/workbench/api/common/extHostConnectionManagement.ts @@ -78,6 +78,10 @@ export class ExtHostConnectionManagement extends ExtHostConnectionManagementShap return this._proxy.$openChangePasswordDialog(profile); } + public $getEditorConnectionProfileTitle(profile: azdata.IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): Thenable { + return this._proxy.$getEditorConnectionProfileTitle(profile, getOptionsOnly, includeGroupName); + } + public $listDatabases(connectionId: string): Thenable { return this._proxy.$listDatabases(connectionId); } diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index 6aa1f47dc2..ab244e1f58 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -141,6 +141,9 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp openChangePasswordDialog(profile: azdata.IConnectionProfile): Thenable { return extHostConnectionManagement.$openChangePasswordDialog(profile); }, + getEditorConnectionProfileTitle(profile: azdata.IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): Thenable { + return extHostConnectionManagement.$getEditorConnectionProfileTitle(profile, getOptionsOnly, includeGroupName); + }, listDatabases(connectionId: string): Thenable { return extHostConnectionManagement.$listDatabases(connectionId); }, diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 97f533a4de..7ff9ed8653 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -729,6 +729,7 @@ export interface MainThreadConnectionManagementShape extends IDisposable { $getServerInfo(connectedId: string): Thenable; $openConnectionDialog(providers: string[], initialConnectionProfile?: azdata.IConnectionProfile, connectionCompletionOptions?: azdata.IConnectionCompletionOptions): Thenable; $openChangePasswordDialog(profile: azdata.IConnectionProfile): Thenable; + $getEditorConnectionProfileTitle(profile: azdata.IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): Thenable; $listDatabases(connectionId: string): Thenable; $getConnectionString(connectionId: string, includePassword: boolean): Thenable; $getUriForConnection(connectionId: string): Thenable; diff --git a/src/sql/workbench/browser/editor/profiler/dashboardInput.ts b/src/sql/workbench/browser/editor/profiler/dashboardInput.ts index 4dfe33939a..68b18e1081 100644 --- a/src/sql/workbench/browser/editor/profiler/dashboardInput.ts +++ b/src/sql/workbench/browser/editor/profiler/dashboardInput.ts @@ -84,12 +84,15 @@ export class DashboardInput extends EditorInput { return ''; } - let name = this.connectionProfile.connectionName ? this.connectionProfile.connectionName : this.connectionProfile.serverName; - if (this.connectionProfile.databaseName + let name = this.connectionProfile.connectionName ? this.connectionProfile.connectionName : this.connectionProfile.serverName + + if (!this.connectionProfile.connectionName && this.connectionProfile.databaseName && !this.isMasterMssql()) { // Only add DB name if this is a non-default, non-master connection name = name + ':' + this.connectionProfile.databaseName; } + // Append any differing options if needed. + name += this._connectionService.getEditorConnectionProfileTitle(this.connectionProfile, true, true) return name; } diff --git a/src/sql/workbench/common/editor/query/queryEditorInput.ts b/src/sql/workbench/common/editor/query/queryEditorInput.ts index 5d308b7041..85c187e798 100644 --- a/src/sql/workbench/common/editor/query/queryEditorInput.ts +++ b/src/sql/workbench/common/editor/query/queryEditorInput.ts @@ -243,11 +243,17 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab title = this._description + ' '; } if (profile) { - title += `${profile.serverName}`; - if (profile.databaseName) { - title += `.${profile.databaseName}`; + let distinguishedTitle = this.connectionManagementService.getEditorConnectionProfileTitle(profile); + if (distinguishedTitle !== '') { + title += distinguishedTitle; + } + else { + title += `${profile.serverName}`; + if (profile.databaseName) { + title += `.${profile.databaseName}`; + } + title += ` (${profile.userName || profile.authenticationType})`; } - title += ` (${profile.userName || profile.authenticationType})`; } else { title += localize('disconnected', "disconnected"); } diff --git a/src/sql/workbench/contrib/connection/browser/connectionStatus.ts b/src/sql/workbench/contrib/connection/browser/connectionStatus.ts index 6881ceb1f7..a1e3ef1217 100644 --- a/src/sql/workbench/contrib/connection/browser/connectionStatus.ts +++ b/src/sql/workbench/contrib/connection/browser/connectionStatus.ts @@ -68,21 +68,30 @@ export class ConnectionStatusbarItem extends Disposable implements IWorkbenchCon // Set connection info to connection status bar private _setConnectionText(connectionProfile: IConnectionProfile): void { - let text: string = connectionProfile.serverName; - if (text) { - if (connectionProfile.databaseName && connectionProfile.databaseName !== '') { - text = text + ' : ' + connectionProfile.databaseName; - } else { - text = text + ' : ' + ''; + let distinguishedTitle = this.connectionManagementService.getEditorConnectionProfileTitle(connectionProfile); + let text: string = ''; + let tooltip: string = ''; + if (distinguishedTitle === '') { + text = connectionProfile.serverName; + if (text) { + if (connectionProfile.databaseName && connectionProfile.databaseName !== '') { + text = text + ' : ' + connectionProfile.databaseName; + } else { + text = text + ' : ' + ''; + } + } + + + tooltip = 'Server: ' + connectionProfile.serverName + '\r\n' + + 'Database: ' + (connectionProfile.databaseName ? connectionProfile.databaseName : '') + '\r\n'; + + if (connectionProfile.userName && connectionProfile.userName !== '') { + tooltip = tooltip + 'Login: ' + connectionProfile.userName + '\r\n'; } } - - let tooltip: string = - 'Server: ' + connectionProfile.serverName + '\r\n' + - 'Database: ' + (connectionProfile.databaseName ? connectionProfile.databaseName : '') + '\r\n'; - - if (connectionProfile.userName && connectionProfile.userName !== '') { - tooltip = tooltip + 'Login: ' + connectionProfile.userName + '\r\n'; + else { + text = distinguishedTitle; + tooltip = (connectionProfile as any).serverInfo; } this.statusItem.update({ diff --git a/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts b/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts index a3335967dc..4164b90a8f 100644 --- a/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts +++ b/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts @@ -57,7 +57,9 @@ export class BreadcrumbService implements IBreadcrumbService { } private getServerBreadcrumb(profile: ConnectionProfile): MenuItem { - return profile.connectionName ? { label: profile.connectionName, routerLink: ['server-dashboard'] } : { label: profile.serverName, routerLink: ['server-dashboard'] }; + let formattedProfileName = profile.connectionName ? profile.connectionName : profile.serverName; + formattedProfileName += this.commonService.connectionManagementService.getEditorConnectionProfileTitle(profile, true, true); + return { label: formattedProfileName, routerLink: ['server-dashboard'] }; } private getDbBreadcrumb(profile: ConnectionProfile): MenuItem { diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts index 420826d5e3..e9f8b408f3 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts @@ -752,7 +752,10 @@ export class AttachToDropdown extends SelectBox { } else { let connections: string[] = []; if (model.context && model.context.title && (connProviderIds.includes(this.model.context.providerName))) { - connections.push(model.context.title); + let textResult = model.context.title; + let fullTitleText = this._connectionManagementService.getEditorConnectionProfileTitle(model.context); + textResult = fullTitleText.length !== 0 ? fullTitleText : textResult; + connections.push(textResult); } else if (this._configurationService.getValue(saveConnectionNameConfigName) && model.savedConnectionName) { connections.push(model.savedConnectionName); } else { diff --git a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts index dafcb84faa..5245668a4b 100644 --- a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts +++ b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts @@ -125,6 +125,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { // get the full ConnectionProfiles with the server info updated properly const treeInput = TreeUpdateUtils.getTreeInput(this._connectionManagementService)!; await this._tree.setInput(treeInput); + await this.refreshConnectionTreeTitles(); this._treeSelectionHandler.onTreeActionStateChange(false); } else { if (this._connectionManagementService.hasRegisteredServers()) { @@ -272,6 +273,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { if (connectionParentGroup) { connectionParentGroup.addOrReplaceConnection(newConnection); await this._tree.updateChildren(connectionParentGroup); + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(newConnection); await this._tree.expand(newConnection); } @@ -286,6 +288,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { await this._tree.rerender(connectionInTree); await this._tree.revealSelectFocusElement(connectionInTree); await this._tree.updateChildren(connectionInTree); + await this.refreshConnectionTreeTitles(); await this._tree.expand(connectionInTree); } } @@ -298,6 +301,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { if (parentGroup) { parentGroup.removeConnections([e]); await this._tree.updateChildren(parentGroup); + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(parentGroup); } } @@ -315,12 +319,14 @@ export class ServerTreeView extends Disposable implements IServerTreeView { const newProfileParent = this._tree.getElementById(e.profile.groupId); newProfileParent.addOrReplaceConnection(e.profile); await this._tree.updateChildren(newProfileParent); + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(e.profile); await this._tree.expand(e.profile); } else { // If the profile was not moved to a different group then just update the profile in the group. oldProfileParent.replaceConnection(e.profile, e.oldProfileId); await this._tree.updateChildren(oldProfileParent) + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(e.profile); await this._tree.expand(e.profile); } @@ -345,6 +351,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { await this._tree.updateChildren(newParent); await this._tree.expand(newParent); } + await this.refreshConnectionTreeTitles(); const newConnection = this._tree.getElementById(movedConnection.id); if (newConnection) { await this._tree.revealSelectFocusElement(newConnection); @@ -359,6 +366,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { const parent = this._tree.getElementById(e.parentId); parent.children = parent.children.filter(c => c.id !== e.id); await this._tree.updateChildren(parent); + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(parent); } })); @@ -381,6 +389,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { e.parent = parent; e.parentId = parent.id; await this._tree.updateChildren(parent); + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(e); } })); @@ -391,6 +400,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { if (newParent) { newParent.children[newParent.children.findIndex(c => c.id === e.id)] = e; await this._tree.updateChildren(newParent); + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(e); } } @@ -409,6 +419,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { (movedGroup).parent = newParent; (movedGroup).parentId = newParent.id; await this._tree.updateChildren(newParent); + await this.refreshConnectionTreeTitles(); await this._tree.revealSelectFocusElement(movedGroup); // Expanding the previously expanded children of the moved group after the move. this._tree.expandElements(profileExpandedState); @@ -695,6 +706,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { return; } await this._tree.setInput(treeInput!); + await this.refreshConnectionTreeTitles(); if (isHidden(this.messages!)) { this._tree.getFocus(); if (this._tree instanceof AsyncServerTree) { @@ -960,6 +972,13 @@ export class ServerTreeView extends Disposable implements IServerTreeView { return actionContext; } + private async refreshConnectionTreeTitles(): Promise { + let treeInput = this._tree.getInput(); + let treeArray = TreeUpdateUtils.alterTreeChildrenTitles([treeInput], this._connectionManagementService, false); + treeInput = treeArray[0]; + await this._tree!.setInput(treeInput); + } + public collapseAllConnections(): void { const root = TreeUpdateUtils.getTreeInput(this._connectionManagementService)!; const connections = ConnectionProfileGroup.getConnectionsInGroup(root); diff --git a/src/sql/workbench/services/bootstrap/browser/commonServiceInterface.service.ts b/src/sql/workbench/services/bootstrap/browser/commonServiceInterface.service.ts index 045c43f3ee..5275cee1ab 100644 --- a/src/sql/workbench/services/bootstrap/browser/commonServiceInterface.service.ts +++ b/src/sql/workbench/services/bootstrap/browser/commonServiceInterface.service.ts @@ -61,6 +61,10 @@ export class SingleConnectionManagementService { public get connectionInfo(): ConnectionManagementInfo { return this._connectionService.getConnectionInfo(this._uri); } + + public getEditorConnectionProfileTitle(profile: IConnectionProfile, getOptionsOnly?: boolean, includeGroupName?: boolean): string { + return this._connectionService.getEditorConnectionProfileTitle(profile, getOptionsOnly, includeGroupName); + } } export class SingleAdminService { diff --git a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts index 8a5f4caa1d..dc402a61f8 100644 --- a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts +++ b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts @@ -553,7 +553,12 @@ class SavedConnectionNode { } getChildren() { - return this.dataSource.getChildren(TreeUpdateUtils.getTreeInput(this.connectionManagementService)); + let input = TreeUpdateUtils.getTreeInput(this.connectionManagementService); + let newInput = [input]; + if (input instanceof ConnectionProfileGroup) { + newInput = TreeUpdateUtils.alterTreeChildrenTitles([input], this.connectionManagementService); + } + return this.dataSource.getChildren(newInput[0]); } } diff --git a/src/sql/workbench/services/connection/browser/connectionDialogService.ts b/src/sql/workbench/services/connection/browser/connectionDialogService.ts index 341f347d72..834ccf9ba7 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogService.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogService.ts @@ -257,7 +257,8 @@ export class ConnectionDialogService implements IConnectionDialogService { return; } let fromEditor = params && params.connectionType === ConnectionType.editor; - let isTemporaryConnection = params && params.connectionType === ConnectionType.temporary; + let hasSaveProfile = connection && connection.hasOwnProperty('saveProfile'); + let isTemporaryConnection = (params && params.connectionType === ConnectionType.temporary) || (hasSaveProfile && !connection.saveProfile); let uri: string = undefined; if (fromEditor && params && params.input) { uri = params.input.uri; diff --git a/src/sql/workbench/services/connection/browser/connectionManagementService.ts b/src/sql/workbench/services/connection/browser/connectionManagementService.ts index db4bab2053..3179629268 100644 --- a/src/sql/workbench/services/connection/browser/connectionManagementService.ts +++ b/src/sql/workbench/services/connection/browser/connectionManagementService.ts @@ -724,6 +724,183 @@ export class ConnectionManagementService extends Disposable implements IConnecti return result; } + public getEditorConnectionProfileTitle(profile: interfaces.IConnectionProfile, getOptionsOnly?: boolean, includeGroupName: boolean = true): string { + let result = ''; + if (profile) { + let tempProfile = new ConnectionProfile(this._capabilitiesService, profile); + let trimTitle = tempProfile.getOriginalTitle(); + let idToFind = tempProfile.id; + let isChild = false; + let totalConnections: ConnectionProfile[] = []; + let allConnections = this.getConnections(); + + totalConnections = totalConnections.concat(allConnections); + + let initialSearch = totalConnections.filter(inputProfile => { + return inputProfile.id === tempProfile.id; + }); + + let secondarySearch = totalConnections.filter(inputProfile => { + return inputProfile.matches(tempProfile) + }); + + if (initialSearch.length === 0) { + if (secondarySearch.length === 1) { + // Sometimes the connection id will change for an object explorer connection, especially when connecting from dashboard, + // and it will identify as different from the stored one even without changes. Get the info for the stored version as it's the same profile. + idToFind = secondarySearch[0].id; + } + } + + // Handle case where a profile may have been an edited existing connection (the one retrieved may not be up to date) + if (initialSearch.length === 1 && !initialSearch[0].matches(tempProfile)) { + // Remove the old profile with outdated information. + totalConnections = totalConnections.filter(inputProfile => { + return inputProfile.id !== tempProfile.id; + }); + // Replace with up to date version of the profile. + totalConnections = totalConnections.concat(tempProfile); + } + + let newConnectionTitles = []; + + this.generateEditorConnectionTitles(totalConnections); + if (includeGroupName) { + this.appendGroupName(totalConnections); + } + newConnectionTitles = totalConnections; + + let searchResult = newConnectionTitles.filter(inputProfile => inputProfile.id === idToFind); + let finalTitle = searchResult[0]?.title; + if (finalTitle) { + let optionsAppend = finalTitle.substring(trimTitle.length); + if (getOptionsOnly) { + finalTitle = optionsAppend; + } + else if (!getOptionsOnly && isChild) { + finalTitle = tempProfile.getOriginalTitle() + optionsAppend; + } + else { + finalTitle = searchResult[0].getOriginalTitle() + optionsAppend; + } + result = finalTitle; + } + } + return result; + } + + /** + * Change the connection title to display only the unique properties among profiles provided. (used for editors) + */ + private generateEditorConnectionTitles(inputList: ConnectionProfile[]): void { + let profileListMap = new Map(); + // Need to reset title to when it was before (as it may have contained a previously generated title) + for (let i = 0; i < inputList.length; i++) { + inputList[i].title = inputList[i].getOriginalTitle(); + } + + // Map the indices of profiles that share the same server info + for (let i = 0; i < inputList.length; i++) { + // do not add if the profile is still loading as that will result in erroneous entries. + if (inputList[i].serverCapabilities && inputList[i].hasLoaded()) { + let titleKey = inputList[i].getOriginalTitle(); + if (profileListMap.has(titleKey)) { + let profilesForKey = profileListMap.get(titleKey); + profilesForKey.push(i); + profileListMap.set(titleKey, profilesForKey); + } + else { + profileListMap.set(titleKey, [i]); + } + } + } + + profileListMap.forEach(function (indexes, titleString) { + if (profileListMap.get(titleString)?.length > 1) { + let combinedOptions = []; + let needSpecial = false; + if (titleString === inputList[indexes[0]].connectionName) { + // check for potential connections with the same name but technically different connections. + let listOfDuplicates = indexes.filter(item => inputList[item].getOptionsKey() !== inputList[indexes[0]].getOptionsKey()); + if (listOfDuplicates.length > 0) { + // if we do find duplicates, we will need to include the special properties. + needSpecial = true; + } + } + indexes.forEach((indexValue) => { + // Add all possible options across all profiles with the same title to an option list. + let valueOptions = inputList[indexValue].getConnectionOptionsList(needSpecial, false); + combinedOptions = combinedOptions.concat(valueOptions.filter(item => combinedOptions.indexOf(item) < 0)); + }); + + // Generate list of non default option keys for each profile that shares the same server info. + let optionKeyMap = new Map(); + let optionValueOccuranceMap = new Map(); + for (let p = 0; p < indexes.length; p++) { + optionKeyMap.set(inputList[indexes[p]], []); + for (let i = 0; i < combinedOptions.length; i++) { + // See if the option is not default for the inputList profile or is. + if (inputList[indexes[p]].getConnectionOptionsList(needSpecial, true).indexOf(combinedOptions[i]) > -1) { + let optionValue = inputList[indexes[p]].getOptionValue(combinedOptions[i].name); + let currentArray = optionKeyMap.get(inputList[indexes[p]]); + let valueString = combinedOptions[i].name + ConnectionProfile.displayNameValueSeparator + optionValue; + if (!optionValueOccuranceMap.get(valueString)) { + optionValueOccuranceMap.set(valueString, 0); + } + optionValueOccuranceMap.set(valueString, optionValueOccuranceMap.get(valueString) + 1); + currentArray.push(valueString); + optionKeyMap.set(inputList[indexes[p]], currentArray); + } + } + } + + // Filter out options that are found in ALL the entries with the same server info. + optionValueOccuranceMap.forEach(function (count, optionValue) { + optionKeyMap.forEach(function (connOptionValues, profile) { + if (count === optionKeyMap.size) { + optionKeyMap.set(profile, connOptionValues.filter(value => value !== optionValue)); + } + }); + }); + + // Generate the final unique connection string for each profile in the list. + optionKeyMap.forEach(function (connOptionValues, profile) { + let uniqueOptionString = connOptionValues.join(ConnectionProfile.displayIdSeparator); + if (uniqueOptionString.length > 0) { + profile.title = profile.getOriginalTitle() + ' (' + uniqueOptionString + ')'; + } + }); + } + }); + } + + private appendGroupName(inputList: ConnectionProfile[]): void { + let profileListMap = new Map(); + + // Map the indices of profiles that share the same server group + for (let i = 0; i < inputList.length; i++) { + let groupName = inputList[i].groupFullName; + if (profileListMap.has(groupName)) { + let profilesForKey = profileListMap.get(groupName); + profilesForKey.push(i); + profileListMap.set(groupName, profilesForKey); + } + else { + profileListMap.set(groupName, [i]); + } + } + + if (profileListMap.size > 1) { + profileListMap.forEach(function (indexes, groupName) { + for (let t = 0; t < indexes.length; t++) { + if (groupName !== '') { + inputList[indexes[t]].title += nls.localize('connection.connTitleGroupSection', ' (Group: {0})', groupName); + } + } + }); + } + } + private doActionsAfterConnectionComplete(uri: string, options: IConnectionCompletionOptions): void { let connectionManagementInfo = this._connectionStatusManager.findConnection(uri); if (!connectionManagementInfo) { @@ -1298,7 +1475,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti this._connectionGlobalStatus.setStatusToConnected(info.connectionSummary); } - const connectionUniqueId = connection.connectionProfile.getConnectionInfoId(); + const connectionUniqueId = connection.connectionProfile.getOptionsKey(); if (info.isSupportedVersion === false && this._connectionsGotUnsupportedVersionWarning.indexOf(connectionUniqueId) === -1 && this._configurationService.getValue('connection.showUnsupportedServerVersionWarning')) { diff --git a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts index 65fbaed290..68bd5c6511 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts @@ -16,7 +16,7 @@ import * as Constants from 'sql/platform/connection/common/constants'; import * as Utils from 'sql/platform/connection/common/utils'; import { IHandleFirewallRuleResult } from 'sql/workbench/services/resourceProvider/common/resourceProviderService'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; +import { IConnectionProfile, ServiceOptionType } from 'sql/platform/connection/common/interfaces'; import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; import { TestConnectionProvider } from 'sql/platform/connection/test/common/testConnectionProvider'; import { TestResourceProvider } from 'sql/workbench/services/resourceProvider/test/common/testResourceProviderService'; @@ -516,7 +516,7 @@ suite('SQL ConnectionManagementService tests', () => { assert.ok(called, 'expected changeGroupIdForConnectionGroup to be called on ConnectionStore'); }); - test('findExistingConnection should find connection for connectionProfile with same info', async () => { + test('findExistingConnection should find connection for connectionProfile with same basic info', async () => { let profile = Object.assign({}, connectionProfile); let uri1 = 'connection:connectionId'; let options: IConnectionCompletionOptions = { @@ -1015,12 +1015,16 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - connectionStore.setup(x => x.isDuplicateEdit(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - //In a real scenario this would be false as it would match the first instance and not find a duplicate. - return Promise.resolve(false); + let originalProfileKey = ''; + connectionStore.setup(x => x.isDuplicateEdit(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((inputProfile, matcher) => { + let newProfile = ConnectionProfile.fromIConnectionProfile(new TestCapabilitiesService(), inputProfile); + let result = newProfile.getOptionsKey() === originalProfileKey; + return Promise.resolve(result); }); profile.getOptionsKey = () => { return 'test_uri1'; }; await connect(uri1, options, true, profile); + let originalProfile = ConnectionProfile.fromIConnectionProfile(new TestCapabilitiesService(), connectionProfile); + originalProfileKey = originalProfile.getOptionsKey(); let newProfile = Object.assign({}, connectionProfile); newProfile.connectionName = newname; newProfile.getOptionsKey = () => { return 'test_uri1'; }; @@ -1110,7 +1114,6 @@ suite('SQL ConnectionManagementService tests', () => { }); - test('failed firewall rule should open the firewall rule dialog', async () => { handleFirewallRuleResult.canHandleFirewallRule = true; resolveHandleFirewallRuleDialog = true; @@ -2047,6 +2050,184 @@ suite('SQL ConnectionManagementService tests', () => { }); }); +// TODO - need to rework test to match new format. +test.skip('getEditorConnectionProfileTitle should return a correctly formatted title for a connection profile', () => { + let profile: IConnectionProfile = { + connectionName: 'new name', + serverName: 'new server', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: Constants.AuthenticationType.Integrated, + savePassword: true, + groupFullName: 'g2/g2-2', + groupId: 'group id', + serverCapabilities: undefined, + getOptionsKey: () => { return ''; }, + getOptionKeyIdNames: undefined!, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined + }; + + let capabilitiesService = new TestCapabilitiesService(); + const testOption1 = { + name: 'testOption1', + displayName: 'testOption1', + description: 'test description', + groupName: 'test group name', + valueType: ServiceOptionType.string, + specialValueType: undefined, + defaultValue: '', + categoryValues: undefined, + isIdentity: false, + isRequired: false + }; + + const testOption2 = { + name: 'testOption2', + displayName: 'testOption2', + description: 'test description', + groupName: 'test group name', + valueType: ServiceOptionType.number, + specialValueType: undefined, + defaultValue: '10', + categoryValues: undefined, + isIdentity: false, + isRequired: false + }; + + const testOption3 = { + name: 'testOption3', + displayName: 'testOption3', + description: 'test description', + groupName: 'test group name', + valueType: ServiceOptionType.string, + specialValueType: undefined, + defaultValue: 'default', + categoryValues: undefined, + isIdentity: false, + isRequired: false + }; + + profile.options['testOption1'] = 'test value'; + profile.options['testOption2'] = '50'; + profile.options['testOption3'] = 'default'; + + let mainProvider = capabilitiesService.capabilities['MSSQL']; + let mainProperties = mainProvider.connection; + let mainOptions = mainProperties.connectionOptions; + + mainOptions.push(testOption1); + mainOptions.push(testOption2); + mainOptions.push(testOption3); + + mainProperties.connectionOptions = mainOptions; + mainProvider.connection = mainProperties; + + capabilitiesService.capabilities['MSSQL'] = mainProvider; + + const connectionStoreMock = TypeMoq.Mock.ofType(ConnectionStore, TypeMoq.MockBehavior.Loose, new TestStorageService()); + const connectionStatusManagerMock = TypeMoq.Mock.ofType(ConnectionStatusManager, TypeMoq.MockBehavior.Loose); + connectionStatusManagerMock.setup(x => x.getActiveConnectionProfiles(undefined)).returns(() => { + return []; + }); + const testInstantiationService = new TestInstantiationService(); + testInstantiationService.stub(IStorageService, new TestStorageService()); + sinon.stub(testInstantiationService, 'createInstance').withArgs(ConnectionStore).returns(connectionStoreMock.object).withArgs(ConnectionStatusManager).returns(connectionStatusManagerMock.object); + const connectionManagementService = new ConnectionManagementService(undefined, testInstantiationService, undefined, undefined, undefined, capabilitiesService, undefined, undefined, undefined, new TestErrorDiagnosticsService(), undefined, undefined, undefined, undefined, getBasicExtensionService(), undefined, undefined, undefined); + + // We should expect that options by themselves are empty if no other profiles exist. + let result = connectionManagementService.getEditorConnectionProfileTitle(profile, true); + assert.strictEqual(result, '', `Options appeared when they should not have.`); + + // We should expect that the string contains only the server info (basic) if there is no other connection with the same server info. + result = connectionManagementService.getEditorConnectionProfileTitle(profile); + + let generatedProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, profile); + let expectedNonDefaultOption = ' (testOption1=test value; testOption2=50)'; + let profileServerInfo = generatedProfile.serverInfo.substring(0, generatedProfile.serverInfo.indexOf(expectedNonDefaultOption)); + + assert.strictEqual(result, `${profileServerInfo}`, `getEditorConnectionProfileTitle included a connection name when it shouldn't`); + + connectionStoreMock.setup(x => x.getAllConnectionsFromConfig()).returns(() => { + return [generatedProfile]; + }); + + // We should expect that the string only contains the server info (basic) if there is only default options, and another connection with similar title but non default options. + profile.options['testOption1'] = undefined; + profile.options['testOption2'] = undefined; + profile.options['testOption3'] = undefined; + + result = connectionManagementService.getEditorConnectionProfileTitle(profile); + + assert.strictEqual(result, `${profileServerInfo}`, `getEditorConnectionProfileTitle included differing connection options when it shouldn't`); + + //Reset profiles for next test and add secondary profile . + profile.options['testOption1'] = 'test value'; + profile.options['testOption2'] = '50'; + profile.options['testOption3'] = 'default'; + generatedProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, profile); + profile.options['testOption1'] = undefined; + profile.options['testOption2'] = undefined; + profile.options['testOption3'] = undefined; + let emptyGeneratedProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, profile); + + connectionStoreMock.setup(x => x.getAllConnectionsFromConfig()).returns(() => { + return [generatedProfile, emptyGeneratedProfile]; + }); + + // We should expect that the string contains the server info appended with differing options, if there's another connection with similar title that has only default options. + result = connectionManagementService.getEditorConnectionProfileTitle(generatedProfile); + + assert.equal(result, `${generatedProfile.serverInfo}`, `getEditorConnectionProfileTitle did not include differing connection options when it should`); + + connectionStoreMock.setup(x => x.getAllConnectionsFromConfig()).returns(() => { + return [generatedProfile, emptyGeneratedProfile]; + }); + + // We should expect that the string contains only the differing options when we ask for options only + result = connectionManagementService.getEditorConnectionProfileTitle(generatedProfile, true); + + assert.equal(result, expectedNonDefaultOption, `getEditorConnectionProfileTitle did not return differing options only`); + + //Reset profiles for next test and add secondary profile . + profile.options['testOption1'] = 'test value'; + profile.options['testOption2'] = '50'; + profile.options['testOption3'] = 'default'; + profile.connectionName = 'New Connection Name'; + profile.options['connectionName'] = profile.connectionName; + generatedProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, profile); + profile.options['testOption1'] = undefined; + profile.options['testOption2'] = undefined; + profile.options['testOption3'] = undefined; + profile.connectionName = 'new name'; + profile.options['connectionName'] = profile.connectionName; + emptyGeneratedProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, profile); + expectedNonDefaultOption = ' (connectionName=New Connection Name; testOption1=test value; testOption2=50)'; + + connectionStoreMock.setup(x => x.getAllConnectionsFromConfig()).returns(() => { + return [generatedProfile, emptyGeneratedProfile]; + }); + + // We should expect that the string now contains connectionName, when it is different. + result = connectionManagementService.getEditorConnectionProfileTitle(generatedProfile, false); + + assert.equal(result, `${profileServerInfo}${expectedNonDefaultOption}`, `getEditorConnectionProfileTitle did not include connectionName in options when it should`); + + connectionStoreMock.setup(x => x.getAllConnectionsFromConfig()).returns(() => { + return [generatedProfile, emptyGeneratedProfile]; + }); + + // We should expect that the string contains only the differing options (including Connection Name) against server info when we ask for options only. + result = connectionManagementService.getEditorConnectionProfileTitle(generatedProfile, true); + + assert.equal(result, expectedNonDefaultOption, `getEditorConnectionProfileTitle did not include only options with connectionName`); + +}); + export function createConnectionProfile(id: string, password?: string): ConnectionProfile { const capabilitiesService = new TestCapabilitiesService(); return new ConnectionProfile(capabilitiesService, { diff --git a/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts b/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts index 23d1c167f1..6220809a20 100644 --- a/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts +++ b/src/sql/workbench/services/objectExplorer/browser/treeUpdateUtils.ts @@ -47,6 +47,21 @@ export class TreeUpdateUtils { public static isInDragAndDrop: boolean = false; + /** + * Functions to restore/remove the groupId for title generation as they are removed when added to treeInput + */ + private static restoreGroupId(treeInput: ConnectionProfileGroup, originalProfiles: ConnectionProfile[]) { + for (let i = 0; i < treeInput.connections.length; i++) { + treeInput.connections[i].groupId = originalProfiles[i].groupId + } + } + + private static removeGroupId(treeInput: ConnectionProfileGroup) { + for (let i = 0; i < treeInput.connections.length; i++) { + treeInput.connections[i].groupId = undefined; + } + } + /** * Set input for the tree. */ @@ -68,11 +83,21 @@ export class TreeUpdateUtils { if (viewKey === 'recent') { groups = connectionManagementService.getRecentConnections(providers); treeInput.addConnections(groups); + this.restoreGroupId(treeInput, connectionManagementService.getRecentConnections(providers)); + let treeArray = TreeUpdateUtils.alterTreeChildrenTitles([treeInput], connectionManagementService); + this.removeGroupId(treeInput); + treeInput = treeArray[0]; } else if (viewKey === 'active') { groups = connectionManagementService.getActiveConnections(providers); treeInput.addConnections(groups); + this.restoreGroupId(treeInput, connectionManagementService.getActiveConnections(providers)); + let treeArray = TreeUpdateUtils.alterTreeChildrenTitles([treeInput], connectionManagementService); + this.removeGroupId(treeInput); + treeInput = treeArray[0]; } else if (viewKey === 'saved') { treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService, providers); + let treeArray = TreeUpdateUtils.alterTreeChildrenTitles([treeInput], connectionManagementService); + treeInput = treeArray[0]; } const previousTreeInput = tree.getInput(); if (treeInput) { @@ -93,13 +118,29 @@ export class TreeUpdateUtils { } } + /** + * Calls alterConnectionTitles on all levels of the Object Explorer Tree + * so that profiles in connection groups can have distinguishing titles too. + */ + public static alterTreeChildrenTitles(inputGroups: ConnectionProfileGroup[], connectionManagementService: IConnectionManagementService, includeGroupName?: boolean): ConnectionProfileGroup[] { + inputGroups.forEach(group => { + group.children = TreeUpdateUtils.alterTreeChildrenTitles(group.children, connectionManagementService, includeGroupName); + let connections = group.connections; + TreeUpdateUtils.alterConnectionTitles(connections, connectionManagementService, includeGroupName); + group.connections = connections; + }); + return inputGroups; + } + /** * Set input for the registered servers tree. */ public static async registeredServerUpdate(tree: ITree | AsyncServerTree, connectionManagementService: IConnectionManagementService, elementToSelect?: any): Promise { if (tree instanceof AsyncServerTree) { - const treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService); + let treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService); if (treeInput) { + let treeArray = this.alterTreeChildrenTitles([treeInput], connectionManagementService, false); + treeInput = treeArray[0]; await tree.setInput(treeInput); } tree.rerender(); @@ -128,6 +169,8 @@ export class TreeUpdateUtils { let treeInput = TreeUpdateUtils.getTreeInput(connectionManagementService); if (treeInput) { + let treeArray = TreeUpdateUtils.alterTreeChildrenTitles([treeInput], connectionManagementService, false); + treeInput = treeArray[0]; const originalInput = tree.getInput(); if (treeInput !== originalInput) { return tree.setInput(treeInput).then(async () => { @@ -370,4 +413,14 @@ export class TreeUpdateUtils { } return connectionProfile; } + + private static alterConnectionTitles(inputList: ConnectionProfile[], connectionManagementService: IConnectionManagementService, includeGroupName?: boolean): void { + for (let i = 0; i < inputList.length; i++) { + let currentConnection = inputList[i]; + let listOfDuplicates = inputList.filter(connection => connection.getOriginalTitle() === currentConnection.getOriginalTitle()); + if (listOfDuplicates.length > 1) { + inputList[i].title = connectionManagementService.getEditorConnectionProfileTitle(inputList[i], false, includeGroupName); + } + } + } } diff --git a/src/sql/workbench/services/objectExplorer/test/browser/treeUpdateUtils.test.ts b/src/sql/workbench/services/objectExplorer/test/browser/treeUpdateUtils.test.ts new file mode 100644 index 0000000000..fec0c6664f --- /dev/null +++ b/src/sql/workbench/services/objectExplorer/test/browser/treeUpdateUtils.test.ts @@ -0,0 +1,645 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; +import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; +import { mssqlProviderName } from 'sql/platform/connection/common/constants'; +import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; +import * as assert from 'assert'; + +import * as azdata from 'azdata'; + +// TODO - Need to fix these tests to match the refactoring of the Connection Title Generation. +suite.skip('treeUpdateUtils alterConnection', () => { + + let capabilitiesService: TestCapabilitiesService; + + const testOption1 = { + name: 'testOption1', + displayName: 'testOption1', + description: 'test description', + groupName: 'test group name', + valueType: 'string', + specialValueType: undefined, + defaultValue: '', + categoryValues: undefined, + isIdentity: false, + isRequired: false + }; + + const testOption2 = { + name: 'testOption2', + displayName: 'testOption2', + description: 'test description', + groupName: 'test group name', + valueType: 'number', + specialValueType: undefined, + defaultValue: '10', + categoryValues: undefined, + isIdentity: false, + isRequired: false + }; + + const testOption3 = { + name: 'testOption3', + displayName: 'testOption3', + description: 'test description', + groupName: 'test group name', + valueType: 'string', + specialValueType: undefined, + defaultValue: 'default', + categoryValues: undefined, + isIdentity: false, + isRequired: false + }; + + setup(() => { + capabilitiesService = new TestCapabilitiesService(); + let mainProvider = capabilitiesService.capabilities[mssqlProviderName]; + let mainProperties = mainProvider.connection; + let mainOptions = mainProperties.connectionOptions; + + mainOptions.push((testOption1 as azdata.ConnectionOption)); + mainOptions.push((testOption2 as azdata.ConnectionOption)); + mainOptions.push((testOption3 as azdata.ConnectionOption)); + + mainProperties.connectionOptions = mainOptions; + mainProvider.connection = mainProperties; + + capabilitiesService.capabilities['MSSQL'] = mainProvider; + }); + + test('Default properties should not be added to the altered title', async () => { + let profile1: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption3: 'default', testOption2: '10' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile2: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption3: 'nonDefault' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let connectionProfile1 = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2 = new ConnectionProfile(capabilitiesService, profile2); + + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + connectionProfileGroup.addConnections([connectionProfile1, connectionProfile2]); + + let updatedProfileGroup = TreeUpdateUtils.alterTreeChildrenTitles([connectionProfileGroup], undefined); + + let updatedTitleMap = updatedProfileGroup[0].connections.map(profile => profile.title); + + assert.equal(connectionProfile1.title, updatedTitleMap[0]); + assert.equal(connectionProfile1.title + ' (testOption3=nonDefault)', updatedTitleMap[1]); + }); + + test('Similar connections should have different titles based on all differing properties', async () => { + let profile1: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption2: '15', testOption1: 'test string 1', testOption3: 'nonDefault' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile2: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption2: '50', testOption1: 'test string 1', testOption3: 'nonDefault' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile3: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption2: '15', testOption1: 'test string 2', testOption3: 'nonDefault' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile4: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption2: '50', testOption1: 'test string 2', testOption3: 'nonDefault' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let defaultProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption3: 'nonDefault' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let defaultConnectionProfile = new ConnectionProfile(capabilitiesService, defaultProfile); + let connectionProfile1 = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2 = new ConnectionProfile(capabilitiesService, profile2); + let connectionProfile3 = new ConnectionProfile(capabilitiesService, profile3); + let connectionProfile4 = new ConnectionProfile(capabilitiesService, profile4); + + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + let originalTitle = defaultConnectionProfile.title; + connectionProfileGroup.addConnections([defaultConnectionProfile, connectionProfile1, connectionProfile2, connectionProfile3, connectionProfile4]); + + let updatedProfileGroup = TreeUpdateUtils.alterTreeChildrenTitles([connectionProfileGroup], undefined); + + let updatedTitleMap = updatedProfileGroup[0].connections.map(profile => profile.title); + + assert.equal(originalTitle, updatedTitleMap[0]); + assert.equal(originalTitle + ' (testOption1=test string 1; testOption2=15)', updatedTitleMap[1]); + assert.equal(originalTitle + ' (testOption1=test string 1; testOption2=50)', updatedTitleMap[2]); + assert.equal(originalTitle + ' (testOption1=test string 2; testOption2=15)', updatedTitleMap[3]); + assert.equal(originalTitle + ' (testOption1=test string 2; testOption2=50)', updatedTitleMap[4]); + }); + + test('identical connections should have same title if on different levels', async () => { + let profile1: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile2: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3-1', + groupId: 'g3-1', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile3: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3-2', + groupId: 'g3-2', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let connectionProfile1 = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2 = new ConnectionProfile(capabilitiesService, profile2); + let connectionProfile3 = new ConnectionProfile(capabilitiesService, profile3); + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + let childConnectionProfileGroup = new ConnectionProfileGroup('g3-1', undefined, 'g3-1', undefined, undefined); + let grandChildConnectionProfileGroup = new ConnectionProfileGroup('g3-2', undefined, 'g3-2', undefined, undefined); + childConnectionProfileGroup.addConnections([connectionProfile2]); + connectionProfileGroup.addConnections([connectionProfile1]); + grandChildConnectionProfileGroup.addConnections([connectionProfile3]); + childConnectionProfileGroup.addGroups([grandChildConnectionProfileGroup]); + connectionProfileGroup.addGroups([childConnectionProfileGroup]); + + let updatedProfileGroup = TreeUpdateUtils.alterTreeChildrenTitles([connectionProfileGroup], undefined); + + let updatedTitleMap = updatedProfileGroup[0].connections.map(profile => profile.title); + let updatedChildTitleMap = updatedProfileGroup[0].children[0].connections.map(profile => profile.title); + let updatedGrandChildTitleMap = updatedProfileGroup[0].children[0].children[0].connections.map(profile => profile.title); + + // Titles should be the same if they're in different levels. + assert.equal(updatedTitleMap[0], updatedChildTitleMap[0]); + assert.equal(updatedTitleMap[0], updatedGrandChildTitleMap[0]); + assert.equal(updatedChildTitleMap[0], updatedGrandChildTitleMap[0]); + }); + + test('connections should not affect connections on a different level', async () => { + let profile1: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption1: 'value1' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile1a: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption1: 'value2' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile2: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3-1', + groupId: 'g3-1', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let connectionProfile1 = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile1a = new ConnectionProfile(capabilitiesService, profile1a); + let connectionProfile2 = new ConnectionProfile(capabilitiesService, profile2); + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + let childConnectionProfileGroup = new ConnectionProfileGroup('g3-1', undefined, 'g3-1', undefined, undefined); + + childConnectionProfileGroup.addConnections([connectionProfile2]); + connectionProfileGroup.addConnections([connectionProfile1, connectionProfile1a]); + connectionProfileGroup.addGroups([childConnectionProfileGroup]); + + let updatedProfileGroup = TreeUpdateUtils.alterTreeChildrenTitles([connectionProfileGroup], undefined); + + let updatedTitleMap = updatedProfileGroup[0].connections.map(profile => profile.title); + let updatedChildTitleMap = updatedProfileGroup[0].children[0].connections.map(profile => profile.title); + + // Titles should be altered for the first group only. + assert.equal(updatedChildTitleMap[0] + ' (testOption1=value1)', updatedTitleMap[0]); + assert.equal(updatedChildTitleMap[0] + ' (testOption1=value2)', updatedTitleMap[1]); + }); + + test('non default options should only be appended to the connection with non default options', async () => { + let profile1: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile2: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption1: 'value1', testOption2: '15' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let connectionProfile1 = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2 = new ConnectionProfile(capabilitiesService, profile2); + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + + connectionProfileGroup.addConnections([connectionProfile1, connectionProfile2]); + + let updatedProfileGroup = TreeUpdateUtils.alterTreeChildrenTitles([connectionProfileGroup], undefined); + + let updatedTitleMap = updatedProfileGroup[0].connections.map(profile => profile.title); + + //Title for second profile should be the same as the first but with non default options appended. + assert.equal(updatedTitleMap[0] + ' (testOption1=value1; testOption2=15)', updatedTitleMap[1]); + }); + + test('identical profiles added into one group and separate groups should have the same options appended', async () => { + let profile1: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption1: 'value1', testOption2: '15' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile2: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption1: 'value2', testOption2: '30' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let connectionProfile1Base = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2Base = new ConnectionProfile(capabilitiesService, profile2); + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + + connectionProfileGroup.addConnections([connectionProfile1Base, connectionProfile2Base]); + + profile1.groupFullName = 'g3-1'; + profile1.groupId = 'g3-1'; + profile2.groupFullName = 'g3-1'; + profile2.groupId = 'g3-1'; + + let connectionProfile1Child = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2Child = new ConnectionProfile(capabilitiesService, profile2); + + let childConnectionProfileGroup = new ConnectionProfileGroup('g3-1', undefined, 'g3-1', undefined, undefined); + + childConnectionProfileGroup.addConnections([connectionProfile1Child, connectionProfile2Child]); + + profile1.groupFullName = 'g3-2'; + profile1.groupId = 'g3-2'; + profile2.groupFullName = 'g3-2'; + profile2.groupId = 'g3-2'; + + let connectionProfile1Grandchild = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2Grandchild = new ConnectionProfile(capabilitiesService, profile2); + + let grandchildConnectionProfileGroup = new ConnectionProfileGroup('g3-2', undefined, 'g3-2', undefined, undefined); + + grandchildConnectionProfileGroup.addConnections([connectionProfile1Grandchild, connectionProfile2Grandchild]); + + childConnectionProfileGroup.addGroups([grandchildConnectionProfileGroup]); + + connectionProfileGroup.addGroups([childConnectionProfileGroup]); + + let updatedProfileGroup = TreeUpdateUtils.alterTreeChildrenTitles([connectionProfileGroup], undefined); + + let updatedTitleMap = updatedProfileGroup[0].connections.map(profile => profile.title); + let updatedChildTitleMap = updatedProfileGroup[0].children[0].connections.map(profile => profile.title); + let updatedGrandchildTitleMap = updatedProfileGroup[0].children[0].children[0].connections.map(profile => profile.title); + + //Titles for the same profile in different groups should be identical + assert.equal(updatedTitleMap[0], updatedChildTitleMap[0]); + assert.equal(updatedTitleMap[0], updatedGrandchildTitleMap[0]); + assert.equal(updatedTitleMap[1], updatedChildTitleMap[1]); + assert.equal(updatedTitleMap[1], updatedGrandchildTitleMap[1]); + }); + + test('profiles in adjacent groups on the same layer should not affect titles on nearby groups', async () => { + let profile1: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3a', + groupId: 'g3a', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let profile2: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3a', + groupId: 'g3a', + serverCapabilities: undefined, + getOptionsKey: undefined!, + getOptionKeyIdNames: undefined!, + matches: undefined!, + providerName: 'MSSQL', + options: { testOption1: 'value2', testOption2: '30' }, + saveProfile: true, + id: undefined!, + connectionName: undefined! + }; + + let connectionProfile1a = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2a = new ConnectionProfile(capabilitiesService, profile2); + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + + let childConnectionProfileGroup1 = new ConnectionProfileGroup('g3a', undefined, 'g3a', undefined, undefined); + childConnectionProfileGroup1.addConnections([connectionProfile1a, connectionProfile2a]); + + profile1.groupFullName = 'g3b'; + profile1.groupId = 'g3b'; + profile2.groupFullName = 'g3b'; + profile2.groupId = 'g3b'; + + let connectionProfile1b = new ConnectionProfile(capabilitiesService, profile1); + let connectionProfile2b = new ConnectionProfile(capabilitiesService, profile2); + + let childConnectionProfileGroup2 = new ConnectionProfileGroup('g3b', undefined, 'g3b', undefined, undefined); + + childConnectionProfileGroup2.addConnections([connectionProfile1b, connectionProfile2b]); + + connectionProfileGroup.addGroups([childConnectionProfileGroup1]); + + connectionProfileGroup.addGroups([childConnectionProfileGroup2]); + + let updatedProfileGroup = TreeUpdateUtils.alterTreeChildrenTitles([connectionProfileGroup], undefined); + + let updatedChildATitleMap = updatedProfileGroup[0].children[0].connections.map(profile => profile.title); + let updatedChildBTitleMap = updatedProfileGroup[0].children[1].connections.map(profile => profile.title); + + //Check that titles are generated properly for the first group. + assert.equal(updatedChildATitleMap[0] + ' (testOption1=value2; testOption2=30)', updatedChildATitleMap[1]); + + //Titles for the same profile in adjacent groups should be identical + assert.equal(updatedChildATitleMap[0], updatedChildBTitleMap[0]); + assert.equal(updatedChildATitleMap[1], updatedChildBTitleMap[1]); + }); +});