diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index ee9aae2b7..22bf10345 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -118,11 +118,9 @@ "▶️ Run Command": "▶️ Run Command", "► Task '{taskName}' starting...": "► Task '{taskName}' starting...", "○ Task '{taskName}' initializing...": "○ Task '{taskName}' initializing...", - "⚠️ **Security:** TLS/SSL Disabled": "⚠️ **Security:** TLS/SSL Disabled", "⚠️ existing collection": "⚠️ existing collection", "⚠ TLS/SSL Disabled": "⚠ TLS/SSL Disabled", "⚠️ Warning: This will modify the existing collection. Documents with matching _id values will be handled based on your conflict resolution setting.": "⚠️ Warning: This will modify the existing collection. Documents with matching _id values will be handled based on your conflict resolution setting.", - "✅ **Security:** TLS/SSL Enabled": "✅ **Security:** TLS/SSL Enabled", "✓ Task '{taskName}' completed successfully. {message}": "✓ Task '{taskName}' completed successfully. {message}", "$(add) Create...": "$(add) Create...", "$(arrow-left) Go Back": "$(arrow-left) Go Back", @@ -184,6 +182,7 @@ "Are you sure?": "Are you sure?", "Ask Copilot to generate the query for you": "Ask Copilot to generate the query for you", "Attempting to authenticate with \"{cluster}\"…": "Attempting to authenticate with \"{cluster}\"…", + "Auth": "Auth", "Authenticate to connect with your DocumentDB cluster": "Authenticate to connect with your DocumentDB cluster", "Authenticate to Connect with Your DocumentDB Cluster": "Authenticate to Connect with Your DocumentDB Cluster", "Authenticate using a username and password": "Authenticate using a username and password", @@ -329,6 +328,7 @@ "Credentials updated successfully.": "Credentials updated successfully.", "Data shown was correct": "Data shown was correct", "Data shown was incorrect": "Data shown was incorrect", + "Database": "Database", "database \"{0}\"": "database \"{0}\"", "Database name cannot be longer than 64 characters.": "Database name cannot be longer than 64 characters.", "Database name cannot contain any of the following characters: \"{0}{1}\"": "Database name cannot contain any of the following characters: \"{0}{1}\"", @@ -551,6 +551,7 @@ "Hide Index…": "Hide Index…", "Hiding index…": "Hiding index…", "HIGH PRIORITY": "HIGH PRIORITY", + "Host": "Host", "How do you want to connect?": "How do you want to connect?", "How should conflicts be handled during the copy operation?": "How should conflicts be handled during the copy operation?", "How would you rate Query Insights?": "How would you rate Query Insights?", @@ -853,6 +854,7 @@ "Save to the database": "Save to the database", "Saving \"{path}\" will update the entity \"{name}\" to the cloud.": "Saving \"{path}\" will update the entity \"{name}\" to the cloud.", "Saving credentials for \"{clusterName}\"…": "Saving credentials for \"{clusterName}\"…", + "Security": "Security", "See output for more details.": "See output for more details.", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", @@ -1023,6 +1025,8 @@ "This will also delete {0}.": "This will also delete {0}.", "This will prevent the query planner from using this index.": "This will prevent the query planner from using this index.", "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.": "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.", + "TLS/SSL Disabled": "TLS/SSL Disabled", + "TLS/SSL Enabled": "TLS/SSL Enabled", "To connect to Azure resources, you need to sign in to Azure accounts.": "To connect to Azure resources, you need to sign in to Azure accounts.", "TODO: Share the steps needed to reliably reproduce the problem. Please include actual and expected results.": "TODO: Share the steps needed to reliably reproduce the problem. Please include actual and expected results.", "Too many arguments. Expecting 0 or 1 argument(s) to {constructorCall}": "Too many arguments. Expecting 0 or 1 argument(s) to {constructorCall}", @@ -1073,6 +1077,7 @@ "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.": "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.", "Use anyway": "Use anyway", "Use projection to return only necessary fields. This reduces network transfer and memory usage, especially important for documents with large embedded arrays or binary data.": "Use projection to return only necessary fields. This reduces network transfer and memory usage, especially important for documents with large embedded arrays or binary data.", + "User": "User", "Username and Password": "Username and Password", "Username cannot be empty": "Username cannot be empty", "Username contains characters that cannot be safely encoded.": "Username contains characters that cannot be safely encoded.", diff --git a/src/tree/connections-view/ConnectionsBranchDataProvider.ts b/src/tree/connections-view/ConnectionsBranchDataProvider.ts index 052054c35..359122a9e 100644 --- a/src/tree/connections-view/ConnectionsBranchDataProvider.ts +++ b/src/tree/connections-view/ConnectionsBranchDataProvider.ts @@ -150,6 +150,8 @@ export class ConnectionsBranchDataProvider extends BaseExtendedTreeDataProvider< dbExperience: DocumentDBExperience, connectionString: connection.secrets.connectionString, emulatorConfiguration: connection.properties.emulatorConfiguration, + selectedAuthMethod: connection.properties.selectedAuthMethod, + connectionUser: connection.secrets.nativeAuthConfig?.connectionUser, }; ext.outputChannel.trace( diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 34bcbbf69..9bd7d2f06 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -13,7 +13,13 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { nonNullProp } from '../../utils/nonNull'; -import { authMethodFromString, AuthMethodId, authMethodsFromString } from '../../documentdb/auth/AuthMethod'; +import { + authMethodFromString, + AuthMethodId, + authMethodsFromString, + getAuthMethod, + isSupportedAuthMethod, +} from '../../documentdb/auth/AuthMethod'; import { ClustersClient } from '../../documentdb/ClustersClient'; import { CredentialCache } from '../../documentdb/CredentialCache'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; @@ -30,6 +36,14 @@ import { type TreeCluster } from '../models/BaseClusterModel'; import { type TreeElementWithStorageId } from '../TreeElementWithStorageId'; import { type ConnectionClusterModel } from './models/ConnectionClusterModel'; +/** + * Escapes markdown special characters so user-provided text is always rendered + * as plain text rather than being interpreted as markdown formatting or links. + */ +function escapeMarkdown(text: string): string { + return text.replace(/[\\`*_{}[\]()#+\-.!|~]/g, '\\$&'); +} + export class DocumentDBClusterItem extends ClusterItemBase implements TreeElementWithStorageId { public override readonly cluster: TreeCluster; @@ -309,19 +323,12 @@ export class DocumentDBClusterItem extends ClusterItemBase 0) { + const escapedHosts = hosts.map((host) => escapeMarkdown(host)); + md.appendMarkdown(`**${l10n.t('Host')}:** ${escapedHosts.join(', ')}\n\n`); + } + + // Auth method + const authMethodId = this.cluster.selectedAuthMethod; + if (authMethodId) { + const isSupported = isSupportedAuthMethod(authMethodId); + const authLabel = isSupported ? getAuthMethod(authMethodId).label : authMethodId; + md.appendMarkdown(`**${l10n.t('Auth')}:** ${escapeMarkdown(authLabel)}\n\n`); + + if (isSupported && authMethodId === AuthMethodId.NativeAuth && this.cluster.connectionUser) { + md.appendMarkdown(`**${l10n.t('User')}:** ${escapeMarkdown(this.cluster.connectionUser)}\n\n`); + } + } + + // Emulator security notice + if (this.cluster.emulatorConfiguration?.isEmulator) { + if (this.cluster.emulatorConfiguration.disableEmulatorSecurity) { + md.appendMarkdown(`⚠️ **${l10n.t('Security')}:** ${l10n.t('TLS/SSL Disabled')}\n\n`); + } else { + md.appendMarkdown(`✅ **${l10n.t('Security')}:** ${l10n.t('TLS/SSL Enabled')}\n\n`); + } + } + + return md; + } + + /** + * Extracts the host(s) from the connection string for display in the tooltip. + * Returns an empty array if the connection string is unavailable or unparseable. + */ + private getHosts(): string[] { + if (!this.cluster.connectionString) { + return []; + } + try { + return new DocumentDBConnectionString(this.cluster.connectionString).hosts ?? []; + } catch { + return []; + } + } } diff --git a/src/tree/connections-view/FolderItem.ts b/src/tree/connections-view/FolderItem.ts index 398f3a6bb..795fc1022 100644 --- a/src/tree/connections-view/FolderItem.ts +++ b/src/tree/connections-view/FolderItem.ts @@ -107,6 +107,8 @@ export class FolderItem implements TreeElement, TreeElementWithContextValue { dbExperience: DocumentDBExperience, connectionString: child?.secrets?.connectionString ?? undefined, emulatorConfiguration: child.properties.emulatorConfiguration, + selectedAuthMethod: child.properties.selectedAuthMethod, + connectionUser: child.secrets?.nativeAuthConfig?.connectionUser, }; ext.outputChannel.trace( diff --git a/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts b/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts index d4fe00261..20fb14d4b 100644 --- a/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts +++ b/src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts @@ -69,6 +69,8 @@ export class LocalEmulatorsItem implements TreeElement, TreeElementWithContextVa dbExperience: DocumentDBExperience, connectionString: connection.secrets.connectionString, emulatorConfiguration: emulatorConfiguration, + selectedAuthMethod: connection.properties.selectedAuthMethod, + connectionUser: connection.secrets.nativeAuthConfig?.connectionUser, }; ext.outputChannel.trace( diff --git a/src/tree/connections-view/models/ConnectionClusterModel.ts b/src/tree/connections-view/models/ConnectionClusterModel.ts index 127f7334e..2b4fc24ea 100644 --- a/src/tree/connections-view/models/ConnectionClusterModel.ts +++ b/src/tree/connections-view/models/ConnectionClusterModel.ts @@ -27,4 +27,17 @@ export interface ConnectionClusterModel extends BaseClusterModel { * Present when this connection represents a local emulator instance. */ emulatorConfiguration?: EmulatorConfiguration; + + /** + * The selected authentication method ID (e.g. 'NativeAuth', 'MicrosoftEntraID'). + * Populated from storage when the tree item is built, used for tooltip display. + */ + selectedAuthMethod?: string; + + /** + * The connection username for native (SCRAM) authentication. + * Populated from storage when the tree item is built, used for tooltip display. + * Never contains a password. + */ + connectionUser?: string; } diff --git a/src/tree/documentdb/CollectionItem.ts b/src/tree/documentdb/CollectionItem.ts index 48dc6d35a..c5e79b427 100644 --- a/src/tree/documentdb/CollectionItem.ts +++ b/src/tree/documentdb/CollectionItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createContextValue } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ClustersClient, type CollectionItemModel, type DatabaseItemModel } from '../../documentdb/ClustersClient'; import { type Experience } from '../../DocumentDBExperiences'; @@ -16,6 +17,14 @@ import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { DocumentsItem } from './DocumentsItem'; import { IndexesItem } from './IndexesItem'; +/** + * Escapes markdown special characters so user-provided text is always rendered + * as plain text rather than being interpreted as markdown formatting or links. + */ +function escapeMarkdown(text: string): string { + return text.replace(/[\\`*_{}[\]()#+\-.!|~]/g, '\\$&'); +} + export class CollectionItem implements TreeElement, TreeElementWithExperience, TreeElementWithContextValue { public readonly id: string; public readonly experience: Experience; @@ -98,8 +107,36 @@ export class CollectionItem implements TreeElement, TreeElementWithExperience, T contextValue: this.contextValue, label: this.collectionInfo.name, description, + tooltip: this.buildTooltip(), iconPath: new vscode.ThemeIcon('folder-library'), collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; } + + /** + * Builds a markdown tooltip showing the collection name, type, and document count. + */ + private buildTooltip(): vscode.MarkdownString { + const md = new vscode.MarkdownString(); + md.isTrusted = false; + + md.appendMarkdown(`### ${escapeMarkdown(this.collectionInfo.name)}\n\n`); + + // Type badge (Collection, View, Timeseries) + const collectionType = this.collectionInfo.type ?? 'collection'; + const capitalizedType = collectionType.charAt(0).toUpperCase() + collectionType.slice(1); + md.appendMarkdown(`\`${capitalizedType}\`\n\n`); + + md.appendMarkdown('---\n\n'); + + // Database context + md.appendMarkdown(`**${l10n.t('Database')}:** ${escapeMarkdown(this.databaseInfo.name)}\n\n`); + + // Document count + if (typeof this.documentCount === 'number') { + md.appendMarkdown(`**${l10n.t('Documents')}:** ${formatDocumentCount(this.documentCount)}\n\n`); + } + + return md; + } } diff --git a/src/tree/documentdb/DatabaseItem.ts b/src/tree/documentdb/DatabaseItem.ts index e3b14580e..c6a8eef47 100644 --- a/src/tree/documentdb/DatabaseItem.ts +++ b/src/tree/documentdb/DatabaseItem.ts @@ -14,6 +14,14 @@ import { type TreeElementWithContextValue } from '../TreeElementWithContextValue import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { CollectionItem } from './CollectionItem'; +/** + * Escapes markdown special characters so user-provided text is always rendered + * as plain text rather than being interpreted as markdown formatting or links. + */ +function escapeMarkdown(text: string): string { + return text.replace(/[\\`*_{}[\]()#+\-.!|~]/g, '\\$&'); +} + export class DatabaseItem implements TreeElement, TreeElementWithExperience, TreeElementWithContextValue { public readonly id: string; public readonly experience: Experience; @@ -66,8 +74,23 @@ export class DatabaseItem implements TreeElement, TreeElementWithExperience, Tre id: this.id, contextValue: this.contextValue, label: this.databaseInfo.name, + tooltip: this.buildTooltip(), iconPath: new vscode.ThemeIcon('database'), // TODO: create our own icon here, this one's shape can change collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; } + + /** + * Builds a markdown tooltip showing the database name. + */ + private buildTooltip(): vscode.MarkdownString { + const md = new vscode.MarkdownString(); + md.isTrusted = false; + + md.appendMarkdown(`### ${escapeMarkdown(this.databaseInfo.name)}\n\n`); + + md.appendMarkdown(`\`${l10n.t('Database')}\`\n\n`); + + return md; + } }