Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"Delete": "Delete",
"Delete \"{connectionName}\"?": "Delete \"{connectionName}\"?",
"Delete \"{nodeName}\"?": "Delete \"{nodeName}\"?",
"Delete {count} connections?": "Delete {count} connections?",
"Delete {count} documents?": "Delete {count} documents?",
"Delete collection \"{collectionId}\" and its contents?": "Delete collection \"{collectionId}\" and its contents?",
"Delete database \"{databaseId}\" and its contents?": "Delete database \"{databaseId}\" and its contents?",
Expand Down Expand Up @@ -364,6 +365,8 @@
"Failed to parse secrets for key {0}:": "Failed to parse secrets for key {0}:",
"Failed to parse the response from the language model. LLM output:\n{output}": "Failed to parse the response from the language model. LLM output:\n{output}",
"Failed to process URI: {0}": "Failed to process URI: {0}",
"Failed to remove connection \"{connectionName}\": {error}": "Failed to remove connection \"{connectionName}\": {error}",
"Failed to remove connection \"{connectionName}\".": "Failed to remove connection \"{connectionName}\".",
"Failed to rename the connection.": "Failed to rename the connection.",
"Failed to retrieve Azure accounts: {0}": "Failed to retrieve Azure accounts: {0}",
"Failed to save credentials for \"{cluster}\".": "Failed to save credentials for \"{cluster}\".",
Expand Down Expand Up @@ -528,6 +531,7 @@
"No Azure VMs found with tag \"{tagName}\" in subscription \"{subscriptionName}\".": "No Azure VMs found with tag \"{tagName}\" in subscription \"{subscriptionName}\".",
"No collection selected.": "No collection selected.",
"No commands found in this document.": "No commands found in this document.",
"No connections selected to remove.": "No connections selected to remove.",
"No Connectivity": "No Connectivity",
"No credentials found for id {credentialId}": "No credentials found for id {credentialId}",
"No credentials found for the selected cluster.": "No credentials found for the selected cluster.",
Expand Down Expand Up @@ -613,6 +617,7 @@
"Reload original document from the database": "Reload original document from the database",
"Reload Window": "Reload Window",
"Remind Me Later": "Remind Me Later",
"Removed {successCount} of {total} connections. {failureCount} failed.": "Removed {successCount} of {total} connections. {failureCount} failed.",
"Rename Connection": "Rename Connection",
"Report a Bug": "Report a Bug",
"report an issue": "report an issue",
Expand Down Expand Up @@ -687,6 +692,8 @@
"Successfully created resource group \"{0}\".": "Successfully created resource group \"{0}\".",
"Successfully created storage account \"{0}\".": "Successfully created storage account \"{0}\".",
"Successfully created user assigned identity \"{0}\".": "Successfully created user assigned identity \"{0}\".",
"Successfully removed {count} connections.": "Successfully removed {count} connections.",
"Successfully removed connection \"{connectionName}\".": "Successfully removed connection \"{connectionName}\".",
"Successfully signed in to {0}": "Successfully signed in to {0}",
"Successfully signed in to tenant: {0}": "Successfully signed in to tenant: {0}",
"Suggest a Feature": "Suggest a Feature",
Expand Down
123 changes: 101 additions & 22 deletions src/commands/removeConnection/removeConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,121 @@ import { type DocumentDBClusterItem } from '../../tree/connections-view/Document
import { getConfirmationAsInSettings } from '../../utils/dialogs/getConfirmation';
import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation';

export async function removeAzureConnection(context: IActionContext, node: DocumentDBClusterItem): Promise<void> {
if (!node) {
throw new Error(l10n.t('No node selected.'));
export async function removeConnection(
context: IActionContext,
node?: DocumentDBClusterItem,
nodes?: DocumentDBClusterItem[],
): Promise<void> {
// Determine the list of connections to delete
let connectionsToDelete: DocumentDBClusterItem[];
if (nodes && nodes.length > 0) {
connectionsToDelete = nodes;
} else if (node) {
connectionsToDelete = [node];
} else {
connectionsToDelete = [];
}

await removeConnection(context, node);
}
if (connectionsToDelete.length === 0) {
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the function is called with no connections to delete (both node and nodes are undefined or empty), it returns silently without any user feedback. This could leave users confused if the command is invoked unexpectedly without a selection.

Consider logging a warning or showing a message to inform the user:

if (connectionsToDelete.length === 0) {
    ext.outputChannel.warn(l10n.t('No connections selected to remove.'));
    return;
}
Suggested change
if (connectionsToDelete.length === 0) {
if (connectionsToDelete.length === 0) {
ext.outputChannel.warn(l10n.t('No connections selected to remove.'));

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot agreed, please add this line, remember about running 'npm run prettier-fix' and 'npm run l10n' after making the edits.

ext.outputChannel.warn(l10n.t('No connections selected to remove.'));
return;
}

export async function removeConnection(context: IActionContext, node: DocumentDBClusterItem): Promise<void> {
context.telemetry.properties.experience = node.experience.api;
// Set telemetry for the first node
context.telemetry.properties.experience = connectionsToDelete[0].experience.api;
context.telemetry.measurements.connectionsToDelete = connectionsToDelete.length;

// Confirmation logic - different messages for single vs. multiple deletions
const expectedConfirmationWord =
connectionsToDelete.length === 1 ? 'delete' : connectionsToDelete.length.toString();
const confirmed = await getConfirmationAsInSettings(
l10n.t('Are you sure?'),
l10n.t('Delete "{connectionName}"?', { connectionName: node.cluster.name }) +
'\n' +
l10n.t('This cannot be undone.'),
'delete',
connectionsToDelete.length === 1
? l10n.t('Delete "{connectionName}"?', { connectionName: connectionsToDelete[0].cluster.name }) +
'\n' +
l10n.t('This cannot be undone.')
: l10n.t('Delete {count} connections?', { count: connectionsToDelete.length }) +
'\n' +
l10n.t('This cannot be undone.'),
expectedConfirmationWord,
);

if (!confirmed) {
throw new UserCancelledError();
}

// continue with deletion
// Resilient deletion loop - continue on failure
let successCount = 0;
let failureCount = 0;

await ext.state.showDeleting(node.id, async () => {
if ((node as DocumentDBClusterItem).cluster.emulatorConfiguration?.isEmulator) {
await ConnectionStorageService.delete(ConnectionType.Emulators, node.storageId);
} else {
await ConnectionStorageService.delete(ConnectionType.Clusters, node.storageId);
}
});
for (const connection of connectionsToDelete) {
try {
await ext.state.showDeleting(connection.id, async () => {
if (connection.cluster.emulatorConfiguration?.isEmulator) {
await ConnectionStorageService.delete(ConnectionType.Emulators, connection.storageId);
} else {
await ConnectionStorageService.delete(ConnectionType.Clusters, connection.storageId);
}
});

// delete cached credentials from memory
CredentialCache.deleteCredentials(connection.id);

// Log success
ext.outputChannel.info(
l10n.t('Successfully removed connection "{connectionName}".', {
connectionName: connection.cluster.name,
}),
);
successCount++;
} catch (error) {
// Log error and continue with next connection
ext.outputChannel.error(
l10n.t('Failed to remove connection "{connectionName}": {error}', {
connectionName: connection.cluster.name,
error: error instanceof Error ? error.message : String(error),
}),
);

// delete cached credentials from memory
CredentialCache.deleteCredentials(node.id);
// Note: When multiple deletions fail, we intentionally capture only the last error's
// details in telemetry. The errorCount metric below provides context on total failures.
context.telemetry.properties.error = 'RemoveConnectionError';
context.telemetry.properties.errorMessage = error instanceof Error ? error.message : String(error);
Comment on lines +93 to +94
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When multiple deletions fail, the telemetry properties for error and errorMessage are overwritten in each iteration of the loop. This means only the last error's details are preserved in telemetry, losing information about previous failures.

Consider either:

  1. Recording only the first error (move these lines outside the loop after it completes)
  2. Using array-based telemetry to capture all errors
  3. Using a different telemetry property for multi-connection deletion errors

Example fix for option 1:

let firstError: Error | undefined;
// ... in the catch block:
if (!firstError) {
    firstError = error instanceof Error ? error : new Error(String(error));
}
// ... after the loop:
if (firstError) {
    context.telemetry.properties.error = 'RemoveConnectionError';
    context.telemetry.properties.errorMessage = firstError.message;
}

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, this is totally fine, capturing one is enough, I was aware of it. Please add a comment explaining that we're aware of it. and.. well, add, once when metrics are set, the errorCount as well. This is a derived value, but will add context.


failureCount++;
}
}

// Refresh the tree view
ext.connectionsBranchDataProvider.refresh();

showConfirmationAsInSettings(l10n.t('The selected connection has been removed.'));
// Set telemetry for successfully deleted connections
context.telemetry.measurements.connectionsDeleted = successCount;
context.telemetry.measurements.errorCount = failureCount;

// Show summary message
if (connectionsToDelete.length === 1) {
// For single connection, show success or error message
if (successCount === 1) {
showConfirmationAsInSettings(l10n.t('The selected connection has been removed.'));
} else {
// Error is already logged to outputChannel, so throw error to show notification
throw new Error(
l10n.t('Failed to remove connection "{connectionName}".', {
connectionName: connectionsToDelete[0].cluster.name,
}),
);
}
} else {
// Show summary for multiple deletions
const summaryMessage =
failureCount === 0
? l10n.t('Successfully removed {count} connections.', { count: successCount })
: l10n.t('Removed {successCount} of {total} connections. {failureCount} failed.', {
successCount,
total: connectionsToDelete.length,
failureCount,
});
showConfirmationAsInSettings(summaryMessage);
}
}