diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 9017db4e..533e36f3 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -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?", @@ -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}\".", @@ -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.", @@ -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", @@ -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", diff --git a/src/commands/removeConnection/removeConnection.ts b/src/commands/removeConnection/removeConnection.ts index 493187c3..51e21549 100644 --- a/src/commands/removeConnection/removeConnection.ts +++ b/src/commands/removeConnection/removeConnection.ts @@ -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 { - if (!node) { - throw new Error(l10n.t('No node selected.')); +export async function removeConnection( + context: IActionContext, + node?: DocumentDBClusterItem, + nodes?: DocumentDBClusterItem[], +): Promise { + // 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) { + ext.outputChannel.warn(l10n.t('No connections selected to remove.')); + return; + } -export async function removeConnection(context: IActionContext, node: DocumentDBClusterItem): Promise { - 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); + 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); + } }