Skip to content

Commit 8164973

Browse files
aasimkhan30Aasim KhanCopilot
authored
Fixing recent connections in connection dialog (#21646)
* Fixing recent connections in connection dialog * copilot comments * fixing compile errors * bump version: mssql 1.42.0 -> 1.43.0, sql-database-projects 1.5.9 -> 1.5.10 * undo precommit changes * Implement recent connections limit and update related UI components * Update localization strings for recent connections management * Enhance recent connections management by normalizing display names and ensuring unique entries based on database differences * Add copy connection button to Connections List * Enhance recent connection matching logic and add corresponding tests * Add connection card utility functions and corresponding tests for key generation and redaction * Fix type assertion for baseConnection in ConnectionCardUtils tests Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Aasim Khan <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent b4164b9 commit 8164973

10 files changed

Lines changed: 825 additions & 79 deletions

File tree

extensions/mssql/l10n/bundle.l10n.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@
316316
},
317317
"Default": "Default",
318318
"Delete saved connection": "Delete saved connection",
319-
"Remove recent connection": "Remove recent connection",
319+
"Clear from recent connections list": "Clear from recent connections list",
320320
"Copy connection string to clipboard": "Copy connection string to clipboard",
321321
"Paste connection string from clipboard": "Paste connection string from clipboard",
322322
"Paste": "Paste",

extensions/mssql/src/connectionconfig/connectionDialogWebviewController.ts

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ import { generateConnectionComponents, groupAdvancedOptions } from "./formCompon
7474
import { FormWebviewController } from "../forms/formWebviewController";
7575
import { ConnectionCredentials } from "../models/connectionCredentials";
7676
import { Deferred } from "../protocol";
77-
import { configSelectedAzureSubscriptions } from "../constants/constants";
77+
import { configSelectedAzureSubscriptions, defaultDatabase } from "../constants/constants";
7878
import * as AzureConstants from "../azure/constants";
7979
import { AddFirewallRuleState } from "../sharedInterfaces/addFirewallRule";
8080
import * as Utils from "../models/utils";
@@ -1061,17 +1061,21 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
10611061
savedConnections: IConnectionDialogProfile[];
10621062
recentConnections: IConnectionDialogProfile[];
10631063
}> {
1064+
const recentConnectionsLimit =
1065+
this._mainController.connectionManager.connectionStore.getMaxRecentConnectionsCount();
10641066
const unsortedConnections: IConnectionProfileWithSource[] =
10651067
await this._mainController.connectionManager.connectionStore.readAllConnections(
10661068
true /* includeRecentConnections */,
1069+
recentConnectionsLimit,
10671070
);
10681071

10691072
const savedConnections = unsortedConnections.filter(
10701073
(c) => c.profileSource === CredentialsQuickPickItemType.Profile,
10711074
);
10721075

1073-
const recentConnections = unsortedConnections.filter(
1074-
(c) => c.profileSource === CredentialsQuickPickItemType.Mru,
1076+
const recentConnections = this.normalizeRecentConnectionsForDisplay(
1077+
unsortedConnections.filter((c) => c.profileSource === CredentialsQuickPickItemType.Mru),
1078+
savedConnections,
10751079
);
10761080

10771081
sendActionEvent(
@@ -1131,6 +1135,84 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
11311135
state.savedConnections = loadedConnections.savedConnections;
11321136
}
11331137

1138+
private normalizeRecentConnectionsForDisplay(
1139+
recentConnections: IConnectionProfileWithSource[],
1140+
savedConnections: IConnectionProfileWithSource[],
1141+
): IConnectionProfileWithSource[] {
1142+
return recentConnections.map((recentConnection) => {
1143+
const matchingSavedConnection = savedConnections.find((savedConnection) =>
1144+
this.isOriginalSavedProfile(savedConnection, recentConnection),
1145+
);
1146+
1147+
if (
1148+
matchingSavedConnection &&
1149+
!this.isSameDatabaseName(
1150+
matchingSavedConnection.database,
1151+
recentConnection.database,
1152+
)
1153+
) {
1154+
return {
1155+
...recentConnection,
1156+
profileName: undefined,
1157+
};
1158+
}
1159+
1160+
return recentConnection;
1161+
});
1162+
}
1163+
1164+
private isOriginalSavedProfile(
1165+
savedConnection: IConnectionProfileWithSource,
1166+
recentConnection: IConnectionProfileWithSource,
1167+
): boolean {
1168+
if (savedConnection.id && recentConnection.id) {
1169+
return savedConnection.id === recentConnection.id;
1170+
}
1171+
1172+
if (
1173+
!savedConnection.profileName ||
1174+
!recentConnection.profileName ||
1175+
savedConnection.profileName !== recentConnection.profileName
1176+
) {
1177+
return false;
1178+
}
1179+
1180+
if (savedConnection.connectionString || recentConnection.connectionString) {
1181+
return savedConnection.connectionString === recentConnection.connectionString;
1182+
}
1183+
1184+
if (savedConnection.server !== recentConnection.server) {
1185+
return false;
1186+
}
1187+
1188+
const savedAuthType = savedConnection.authenticationType || AuthenticationType.SqlLogin;
1189+
const recentAuthType = recentConnection.authenticationType || AuthenticationType.SqlLogin;
1190+
1191+
if (savedAuthType !== recentAuthType) {
1192+
return false;
1193+
}
1194+
1195+
if ((savedConnection.user ?? "") !== (recentConnection.user ?? "")) {
1196+
return false;
1197+
}
1198+
1199+
if (savedConnection.accountId || recentConnection.accountId) {
1200+
return areCompatibleEntraAccountIds(
1201+
savedConnection.accountId,
1202+
recentConnection.accountId,
1203+
);
1204+
}
1205+
1206+
return true;
1207+
}
1208+
1209+
private isSameDatabaseName(currentDatabase?: string, expectedDatabase?: string): boolean {
1210+
const normalizedCurrentDatabase = currentDatabase?.trim() || defaultDatabase;
1211+
const normalizedExpectedDatabase = expectedDatabase?.trim() || defaultDatabase;
1212+
1213+
return normalizedCurrentDatabase === normalizedExpectedDatabase;
1214+
}
1215+
11341216
private async validateProfile(connectionProfile?: IConnectionDialogProfile): Promise<string[]> {
11351217
if (!connectionProfile) {
11361218
connectionProfile = this.state.connectionProfile;

extensions/mssql/src/models/connectionStore.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -393,13 +393,18 @@ export class ConnectionStore {
393393
*
394394
* @returns the array of connections, empty if none are found
395395
*/
396-
public getRecentlyUsedConnections(): IConnectionInfo[] {
396+
public getRecentlyUsedConnections(limit?: number): IConnectionInfo[] {
397397
let configValues = this._context.globalState.get<IConnectionInfo[]>(
398398
Constants.configRecentConnections,
399399
);
400400
if (!configValues) {
401401
configValues = [];
402402
}
403+
404+
if (limit && limit > 0) {
405+
return configValues.slice(0, limit);
406+
}
407+
403408
return configValues;
404409
}
405410

@@ -420,7 +425,10 @@ export class ConnectionStore {
420425
// Remove the connection from the list if it already exists
421426
configValues = configValues.filter(
422427
(value) =>
423-
!Utils.isSameProfile(<IConnectionProfile>value, <IConnectionProfile>conn),
428+
!self.isSameRecentConnectionEntry(
429+
value as IConnectionProfile,
430+
conn as IConnectionProfile,
431+
),
424432
);
425433

426434
// Add the connection to the front of the list, taking care to clear out the password field
@@ -493,7 +501,7 @@ export class ConnectionStore {
493501

494502
// Remove the connection from the list if it already exists
495503
configValues = configValues.filter(
496-
(value) => !Utils.isSameProfile(<IConnectionProfile>value, conn),
504+
(value) => !self.isSameRecentConnectionEntry(value as IConnectionProfile, conn),
497505
);
498506

499507
// Remove any saved password
@@ -665,6 +673,7 @@ export class ConnectionStore {
665673

666674
public async readAllConnections(
667675
includeRecentConnections: boolean = false,
676+
recentConnectionsLimit?: number,
668677
): Promise<IConnectionProfileWithSource[]> {
669678
let connResults: IConnectionProfileWithSource[] = [];
670679

@@ -680,22 +689,33 @@ export class ConnectionStore {
680689

681690
// Include recent connections, if specified
682691
if (includeRecentConnections) {
683-
const recentConnections = this.getRecentlyUsedConnections().map((c) => {
684-
const conn = c as IConnectionProfileWithSource;
685-
conn.profileSource = CredentialsQuickPickItemType.Mru;
686-
return conn;
687-
});
692+
const recentConnections = this.getRecentlyUsedConnections(recentConnectionsLimit).map(
693+
(c) => {
694+
const conn = c as IConnectionProfileWithSource;
695+
conn.profileSource = CredentialsQuickPickItemType.Mru;
696+
return conn;
697+
},
698+
);
688699

689700
connResults = connResults.concat(recentConnections);
690701
}
691702

692-
// Deduplicate connections by ID
693-
const uniqueConnections = new Map<string, IConnectionProfileWithSource>();
703+
// Deduplicate connections within the same source while allowing the same connection
704+
// to appear in both the saved and MRU lists.
705+
const uniqueConnections: IConnectionProfileWithSource[] = [];
694706
let dupeCount = 0;
695707

696708
for (const conn of connResults) {
697-
if (!uniqueConnections.has(conn.id)) {
698-
uniqueConnections.set(conn.id, conn);
709+
const isDuplicate = uniqueConnections.some(
710+
(existingConn) =>
711+
existingConn.profileSource === conn.profileSource &&
712+
(conn.profileSource === CredentialsQuickPickItemType.Mru
713+
? this.isSameRecentConnectionEntry(existingConn, conn)
714+
: Utils.isSameProfile(existingConn, conn)),
715+
);
716+
717+
if (!isDuplicate) {
718+
uniqueConnections.push(conn);
699719
} else {
700720
dupeCount++;
701721
this._logger.verbose(
@@ -704,12 +724,16 @@ export class ConnectionStore {
704724
}
705725
}
706726

707-
connResults = Array.from(uniqueConnections.values());
727+
connResults = uniqueConnections;
708728

709729
let logMessage = `readAllConnections(): ${connResults.length} connections found`;
710730

711731
if (includeRecentConnections) {
712732
logMessage += ` (${configConnections.length} from config, ${connResults.length - configConnections.length} from recent)`;
733+
734+
if (typeof recentConnectionsLimit === "number" && recentConnectionsLimit > 0) {
735+
logMessage += `; recent limit ${recentConnectionsLimit}`;
736+
}
713737
} else {
714738
logMessage += "; excluded recent";
715739
}
@@ -738,9 +762,13 @@ export class ConnectionStore {
738762

739763
public async getConnectionQuickpickItems(
740764
includeRecentConnections: boolean = false,
765+
recentConnectionsLimit?: number,
741766
): Promise<IConnectionCredentialsQuickPickItem[]> {
742767
let output: IConnectionCredentialsQuickPickItem[] = [];
743-
const connections = await this.readAllConnections(includeRecentConnections);
768+
const connections = await this.readAllConnections(
769+
includeRecentConnections,
770+
recentConnectionsLimit,
771+
);
744772

745773
output = connections.map((c) => {
746774
return this.createQuickPickItem(c, c.profileSource);
@@ -757,7 +785,7 @@ export class ConnectionStore {
757785
return quickPickItems;
758786
}
759787

760-
private getMaxRecentConnectionsCount(): number {
788+
public getMaxRecentConnectionsCount(): number {
761789
let config = this._vscodeWrapper.getConfiguration(Constants.extensionConfigSectionName);
762790

763791
let maxConnections: number = config[Constants.configMaxRecentConnections];
@@ -766,4 +794,22 @@ export class ConnectionStore {
766794
}
767795
return maxConnections;
768796
}
797+
798+
private isSameRecentConnectionEntry(
799+
currentProfile: IConnectionProfile,
800+
expectedProfile: IConnectionProfile,
801+
): boolean {
802+
const currentRecentProfile = {
803+
...currentProfile,
804+
id: undefined,
805+
profileName: undefined,
806+
} as IConnectionProfile;
807+
const expectedRecentProfile = {
808+
...expectedProfile,
809+
id: undefined,
810+
profileName: undefined,
811+
} as IConnectionProfile;
812+
813+
return Utils.isSameProfile(currentRecentProfile, expectedRecentProfile);
814+
}
769815
}

extensions/mssql/src/webviews/common/locConstants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ export class LocConstants {
446446
}),
447447
default: l10n.t("Default"),
448448
deleteSavedConnection: l10n.t("Delete saved connection"),
449-
removeRecentConnection: l10n.t("Remove recent connection"),
449+
removeRecentConnection: l10n.t("Clear from recent connections list"),
450450
copyConnectionString: l10n.t("Copy connection string to clipboard"),
451451
pasteConnectionString: l10n.t("Paste connection string from clipboard"),
452452
copy: l10n.t("Copy"),
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";
7+
8+
const redactConnectionStringSecrets = (connectionString?: string): string => {
9+
if (!connectionString) {
10+
return "";
11+
}
12+
13+
return connectionString.replace(
14+
/((?:password|pwd|access token)\s*=\s*)(?:'[^']*'|"[^"]*"|\{[^}]*\}|[^;]*)/gi,
15+
"$1<redacted>",
16+
);
17+
};
18+
19+
export const getConnectionCardKey = (connection: IConnectionDialogProfile): string => {
20+
return [
21+
connection.id ?? "",
22+
connection.server ?? "",
23+
connection.database ?? "",
24+
connection.authenticationType ?? "",
25+
connection.profileName ?? "",
26+
connection.user ?? "",
27+
connection.accountId ?? "",
28+
redactConnectionStringSecrets(connection.connectionString),
29+
].join("|");
30+
};
31+
32+
export const getConnectionsListKey = (connections: IConnectionDialogProfile[]): string => {
33+
return connections.map(getConnectionCardKey).join("::");
34+
};

0 commit comments

Comments
 (0)