From caadd3197fccbf01b76ce184638ab2c0db28df4b Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Sun, 24 Aug 2025 19:39:57 +0200 Subject: [PATCH 01/88] .nvmrc nodejs version bump to `20.18` --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index cecb93628..10fef252a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.15 +20.18 From d390e9d19fea0172d82bdb40a885d64e4123d09d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 25 Aug 2025 17:23:54 +0200 Subject: [PATCH 02/88] feat: improved dependency caching in Github Actions --- .github/workflows/main.yml | 46 ++++++++++---------------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 879da1ee7..ac02668fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,22 +45,15 @@ jobs: - name: ✅ Checkout Repository uses: actions/checkout@v4 - - name: 🛠 Setup Node.js Environment + - name: 🛠 Setup Node.js Environment (with cache) uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: 'npm' - - - name: 💾 Restore npm Cache - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- + cache: npm + cache-dependency-path: '**/package-lock.json' - name: 📦 Install Dependencies (npm ci) - run: npm ci --verbose + run: npm ci --prefer-offline --no-audit --no-fund --progress=false --verbose - name: 🌐 Check Localization Files run: npm run l10n:check @@ -91,22 +84,15 @@ jobs: - name: ✅ Checkout Repository uses: actions/checkout@v4 - - name: 🛠 Setup Node.js Environment + - name: 🛠 Setup Node.js Environment (with cache) uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: 'npm' - - - name: 💾 Restore npm Cache - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- + cache: npm + cache-dependency-path: '**/package-lock.json' - name: 📦 Install Dependencies (npm ci) - run: npm ci + run: npm ci --prefer-offline --no-audit --no-fund --progress=false - name: 🔄 Run Integration Tests (Headless UI) run: xvfb-run -a npm test @@ -123,7 +109,6 @@ jobs: github.base_ref == 'main' || github.base_ref == 'next' )) - defaults: run: working-directory: '.' @@ -131,22 +116,15 @@ jobs: - name: ✅ Checkout Repository uses: actions/checkout@v4 - - name: 🛠 Setup Node.js Environment + - name: 🛠 Setup Node.js Environment (with cache) uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: 'npm' - - - name: 💾 Restore npm Cache - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- + cache: npm + cache-dependency-path: '**/package-lock.json' - name: 📦 Install Dependencies (npm ci) - run: npm ci + run: npm ci --prefer-offline --no-audit --no-fund --progress=false - name: 🏗 Build Project run: npm run build From 4c7e4e9c3ae838f4c847e7fb57522ad0ad446c34 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 8 Sep 2025 14:25:22 +0200 Subject: [PATCH 03/88] version bump to `v.0.4.2-alpha` --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31e62cf9d..d4363c39d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb", - "version": "0.4.1", + "version": "0.4.2-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb", - "version": "0.4.1", + "version": "0.4.2-alpha", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@azure/arm-compute": "^22.4.0", diff --git a/package.json b/package.json index d04ceb85b..cc15e2f85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb", - "version": "0.4.1", + "version": "0.4.2-alpha", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "publisher": "ms-azuretools", "displayName": "DocumentDB for VS Code", From 5d3c03de7668f12b64cfc0301286e65c8962376d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 19 Sep 2025 12:58:40 +0200 Subject: [PATCH 04/88] wip: Introducing nativeAuth + entraId auth configs for tenantid persistance support --- .../addConnectionFromRegistry.ts | 7 +- .../chooseDataMigrationExtension.ts | 4 +- src/commands/launchShell/launchShell.ts | 14 ++- src/documentdb/ClustersClient.ts | 2 +- src/documentdb/CredentialCache.ts | 85 ++++++++++++++++--- .../documentdb/MongoRUResourceItem.ts | 4 +- .../AzureMongoRUExecuteStep.ts | 4 +- .../utils/ruClusterHelpers.ts | 10 ++- .../documentdb/DocumentDBResourceItem.ts | 2 +- .../discovery-wizard/AzureExecuteStep.ts | 4 +- .../utils/clusterHelpers.ts | 9 +- .../documentdb/VCoreResourceItem.ts | 2 +- .../mongo-ru/RUCoreResourceItem.ts | 16 ++-- src/tree/documentdb/ClusterItemBase.ts | 28 ++++-- 14 files changed, 145 insertions(+), 46 deletions(-) diff --git a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts index 4ac5dd347..62c7ddb10 100644 --- a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts +++ b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts @@ -66,7 +66,8 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C } const parsedCS = new DocumentDBConnectionString(credentials.connectionString); - const username = credentials.connectionUser || parsedCS.username; + const username = + (credentials.nativeAuthConfig?.connectionUser ?? credentials.connectionUser) || parsedCS.username; parsedCS.username = ''; const joinedHosts = [...parsedCS.hosts].sort().join(','); @@ -145,8 +146,8 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C properties: { api: API.DocumentDB, availableAuthMethods: credentials.availableAuthMethods }, secrets: { connectionString: parsedCS.toString(), - userName: credentials.connectionUser, - password: credentials.connectionPassword, + userName: credentials.nativeAuthConfig?.connectionUser ?? credentials.connectionUser, + password: credentials.nativeAuthConfig?.connectionPassword ?? credentials.connectionPassword, }, }; diff --git a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts index 350102d26..d411810b2 100644 --- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts +++ b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts @@ -105,8 +105,8 @@ export async function chooseDataMigrationExtension(context: IActionContext, node } const parsedCS = new DocumentDBConnectionString(credentials.connectionString); - parsedCS.username = credentials?.connectionUser ?? ''; - parsedCS.password = credentials?.connectionPassword ?? ''; + parsedCS.username = CredentialCache.getConnectionUser(node.cluster.id) ?? ''; + parsedCS.password = CredentialCache.getConnectionPassword(node.cluster.id) ?? ''; const options = { connectionString: parsedCS.toString(), diff --git a/src/commands/launchShell/launchShell.ts b/src/commands/launchShell/launchShell.ts index bfb4ea8e1..c8c9e6f61 100644 --- a/src/commands/launchShell/launchShell.ts +++ b/src/commands/launchShell/launchShell.ts @@ -9,6 +9,7 @@ import * as vscode from 'vscode'; import { isWindows } from '../../constants'; import { AuthMethodId } from '../../documentdb/auth/AuthMethod'; import { ClustersClient } from '../../documentdb/ClustersClient'; +import { CredentialCache } from '../../documentdb/CredentialCache'; import { maskSensitiveValuesInTelemetry } from '../../documentdb/utils/connectionStringHelpers'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; import { ext } from '../../extensionVariables'; @@ -44,8 +45,8 @@ export async function launchShell( const clusterCredentials = activeClient.getCredentials(); if (clusterCredentials) { connectionString = clusterCredentials.connectionString; - username = clusterCredentials.connectionUser; - password = clusterCredentials.connectionPassword; + username = CredentialCache.getConnectionUser(node.cluster.id); + password = CredentialCache.getConnectionPassword(node.cluster.id); authMechanism = clusterCredentials.authMechanism; } } else { @@ -68,8 +69,13 @@ export async function launchShell( if (selectedAuthMethod === AuthMethodId.NativeAuth || (nativeAuthIsAvailable && !selectedAuthMethod)) { connectionString = discoveredClusterCredentials.connectionString; - username = discoveredClusterCredentials.connectionUser; - password = discoveredClusterCredentials.connectionPassword; + // Prefer auth config, fallback to legacy fields for backward compatibility + username = + discoveredClusterCredentials.nativeAuthConfig?.connectionUser ?? + discoveredClusterCredentials.connectionUser; + password = + discoveredClusterCredentials.nativeAuthConfig?.connectionPassword ?? + discoveredClusterCredentials.connectionPassword; authMechanism = AuthMethodId.NativeAuth; } else { // Only SCRAM-SHA-256 (username/password) authentication is supported here. diff --git a/src/documentdb/ClustersClient.ts b/src/documentdb/ClustersClient.ts index 41ba1ea66..275aada3d 100644 --- a/src/documentdb/ClustersClient.ts +++ b/src/documentdb/ClustersClient.ts @@ -220,7 +220,7 @@ export class ClustersClient { } getUserName() { - return CredentialCache.getCredentials(this.credentialId)?.connectionUser; + return CredentialCache.getConnectionUser(this.credentialId); } /** diff --git a/src/documentdb/CredentialCache.ts b/src/documentdb/CredentialCache.ts index 5a865ca24..8ae2f00b6 100644 --- a/src/documentdb/CredentialCache.ts +++ b/src/documentdb/CredentialCache.ts @@ -5,25 +5,49 @@ import { CaseInsensitiveMap } from '../utils/CaseInsensitiveMap'; import { type EmulatorConfiguration } from '../utils/emulatorConfiguration'; -import { type AuthMethodId } from './auth/AuthMethod'; +import { AuthMethodId, type AuthMethodId as AuthMethodIdType } from './auth/AuthMethod'; import { addAuthenticationDataToConnectionString } from './utils/connectionStringHelpers'; -export interface ClustersCredentials { +/** + * Configuration for Microsoft Entra ID authentication. + */ +export interface EntraIdAuthConfig { + tenantId: string; + accountId?: string; + subscriptionId?: string; +} + +/** + * Configuration for native authentication (username/password based authentication). + */ +export interface NativeAuthConfig { + connectionUser: string; + connectionPassword?: string; +} + +export interface StoredClusterCredentials { mongoClusterId: string; connectionStringWithPassword?: string; connectionString: string; - connectionUser: string; - connectionPassword?: string; - authMechanism?: AuthMethodId; + authMechanism?: AuthMethodIdType; // Optional, as it's only relevant for local workspace connetions emulatorConfiguration?: EmulatorConfiguration; + + // Authentication method specific configurations + nativeAuthConfig?: NativeAuthConfig; + entraIdConfig?: EntraIdAuthConfig; } +/** + * @deprecated Use StoredClusterCredentials instead. This alias is provided for backward compatibility. + */ +export type ClustersCredentials = StoredClusterCredentials; + export class CredentialCache { // the id of the cluster === the tree item id -> cluster credentials // Some SDKs for azure differ the case on some resources ("DocumentDb" vs "DocumentDB") - private static _store: CaseInsensitiveMap = new CaseInsensitiveMap(); + private static _store: CaseInsensitiveMap = new CaseInsensitiveMap(); public static getConnectionStringWithPassword(mongoClusterId: string): string { return CredentialCache._store.get(mongoClusterId)?.connectionStringWithPassword as string; @@ -37,7 +61,31 @@ export class CredentialCache { return CredentialCache._store.get(mongoClusterId)?.emulatorConfiguration; } - public static getCredentials(mongoClusterId: string): ClustersCredentials | undefined { + public static getEntraIdConfig(mongoClusterId: string): EntraIdAuthConfig | undefined { + return CredentialCache._store.get(mongoClusterId)?.entraIdConfig; + } + + public static getNativeAuthConfig(mongoClusterId: string): NativeAuthConfig | undefined { + return CredentialCache._store.get(mongoClusterId)?.nativeAuthConfig; + } + + /** + * Gets the connection user for native authentication. + * Returns undefined for non-native authentication methods like Entra ID. + */ + public static getConnectionUser(mongoClusterId: string): string | undefined { + return CredentialCache._store.get(mongoClusterId)?.nativeAuthConfig?.connectionUser; + } + + /** + * Gets the connection password for native authentication. + * Returns undefined for non-native authentication methods like Entra ID. + */ + public static getConnectionPassword(mongoClusterId: string): string | undefined { + return CredentialCache._store.get(mongoClusterId)?.nativeAuthConfig?.connectionPassword; + } + + public static getCredentials(mongoClusterId: string): StoredClusterCredentials | undefined { return CredentialCache._store.get(mongoClusterId); } @@ -73,11 +121,14 @@ export class CredentialCache { password, ); - const credentials: ClustersCredentials = { + const credentials: StoredClusterCredentials = { mongoClusterId: mongoClusterId, connectionStringWithPassword: connectionStringWithPassword, connectionString: connectionString, - connectionUser: username, + nativeAuthConfig: { + connectionUser: username, + connectionPassword: password, + }, emulatorConfiguration: emulatorConfiguration, }; @@ -97,14 +148,16 @@ export class CredentialCache { * @param username - The username to be used for authentication (optional for some auth methods). * @param password - The password to be used for authentication (optional for some auth methods). * @param emulatorConfiguration - The emulator configuration object (optional, only relevant for local workspace connections). + * @param entraIdConfig - The Entra ID configuration object (optional, only relevant for Microsoft Entra ID authentication). */ public static setAuthCredentials( mongoClusterId: string, - authMethod: AuthMethodId, + authMethod: AuthMethodIdType, connectionString: string, username: string = '', password: string = '', emulatorConfiguration?: EmulatorConfiguration, + entraIdConfig?: EntraIdAuthConfig, ): void { const connectionStringWithPassword = addAuthenticationDataToConnectionString( connectionString, @@ -112,15 +165,23 @@ export class CredentialCache { password, ); - const credentials: ClustersCredentials = { + const credentials: StoredClusterCredentials = { mongoClusterId: mongoClusterId, connectionStringWithPassword: connectionStringWithPassword, connectionString: connectionString, - connectionUser: username, emulatorConfiguration: emulatorConfiguration, authMechanism: authMethod, + entraIdConfig: entraIdConfig, }; + // Add native auth config only for non-Entra ID authentication methods + if (authMethod !== AuthMethodId.MicrosoftEntraID && (username || password)) { + credentials.nativeAuthConfig = { + connectionUser: username, + connectionPassword: password, + }; + } + CredentialCache._store.set(mongoClusterId, credentials); } } diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts index 828a7047a..29243a720 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts @@ -81,8 +81,8 @@ export class MongoRUResourceItem extends ClusterItemBase { this.id, credentials.selectedAuthMethod ?? credentials.availableAuthMethods[0], credentials.connectionString, - credentials.connectionUser, - credentials.connectionPassword, + credentials.nativeAuthConfig?.connectionUser ?? credentials.connectionUser, + credentials.nativeAuthConfig?.connectionPassword ?? credentials.connectionPassword, ); // Connect using the cached credentials diff --git a/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts b/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts index 63c654ec6..07664a19f 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts @@ -36,8 +36,8 @@ export class AzureMongoRUExecuteStep extends AzureWizardExecuteStep Date: Fri, 19 Sep 2025 15:11:38 +0200 Subject: [PATCH 05/88] wip: updating storage item to support auth method configs --- src/commands/updateCredentials/ExecuteStep.ts | 26 ++++++- .../updateCredentials/updateCredentials.ts | 5 +- src/documentdb/CredentialCache.ts | 75 +++++++++++++++++-- src/documentdb/auth/AuthConfig.ts | 61 +++++++++++++++ src/services/connectionStorageService.ts | 53 +++++++++++++ .../connections-view/DocumentDBClusterItem.ts | 19 ++++- 6 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 src/documentdb/auth/AuthConfig.ts diff --git a/src/commands/updateCredentials/ExecuteStep.ts b/src/commands/updateCredentials/ExecuteStep.ts index 862c70eff..e4567036e 100644 --- a/src/commands/updateCredentials/ExecuteStep.ts +++ b/src/commands/updateCredentials/ExecuteStep.ts @@ -5,6 +5,7 @@ import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import { l10n, window } from 'vscode'; +import { AuthMethodId } from '../../documentdb/auth/AuthMethod'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; import { ConnectionStorageService, ConnectionType } from '../../services/connectionStorageService'; import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation'; @@ -33,14 +34,35 @@ export class ExecuteStep extends AzureWizardExecuteStep cluster credentials // Some SDKs for azure differ the case on some resources ("DocumentDb" vs "DocumentDB") - private static _store: CaseInsensitiveMap = new CaseInsensitiveMap(); + private static _store: CaseInsensitiveMap = new CaseInsensitiveMap(); public static getConnectionStringWithPassword(mongoClusterId: string): string { return CredentialCache._store.get(mongoClusterId)?.connectionStringWithPassword as string; @@ -85,7 +86,7 @@ export class CredentialCache { return CredentialCache._store.get(mongoClusterId)?.nativeAuthConfig?.connectionPassword; } - public static getCredentials(mongoClusterId: string): StoredClusterCredentials | undefined { + public static getCredentials(mongoClusterId: string): CachedClusterCredentials | undefined { return CredentialCache._store.get(mongoClusterId); } @@ -121,7 +122,7 @@ export class CredentialCache { password, ); - const credentials: StoredClusterCredentials = { + const credentials: CachedClusterCredentials = { mongoClusterId: mongoClusterId, connectionStringWithPassword: connectionStringWithPassword, connectionString: connectionString, @@ -165,7 +166,7 @@ export class CredentialCache { password, ); - const credentials: StoredClusterCredentials = { + const credentials: CachedClusterCredentials = { mongoClusterId: mongoClusterId, connectionStringWithPassword: connectionStringWithPassword, connectionString: connectionString, @@ -184,4 +185,64 @@ export class CredentialCache { CredentialCache._store.set(mongoClusterId, credentials); } + + /** + * Bridge method to convert ConnectionItem's structured auth secrets into the runtime cache format. + * This method handles the conversion between persistent storage (ConnectionItem) and memory cache (CachedClusterCredentials). + * + * The conversion handles: + * - Determining auth method from available configurations + * - Converting central auth configs to local cache format + * - Maintaining backward compatibility with legacy username/password + * + * @param connectionItem - The persistent connection item with structured auth secrets + * @param authMethod - Optional explicit auth method; if not provided, will be inferred from available configs + * @param emulatorConfiguration - Optional emulator configuration for local connections + */ + public static setFromConnectionItem( + connectionItem: ConnectionItem, + authMethod?: AuthMethodIdType, + emulatorConfiguration?: EmulatorConfiguration, + ): void { + const { secrets } = connectionItem; + + // Determine auth method if not explicitly provided + let selectedAuthMethod = authMethod; + if (!selectedAuthMethod) { + if (secrets.entraIdAuth) { + selectedAuthMethod = AuthMethodId.MicrosoftEntraID; + } else if (secrets.nativeAuth || secrets.userName || secrets.password) { + selectedAuthMethod = AuthMethodId.NativeAuth; + } else { + // Use the selected method from properties or first available method + selectedAuthMethod = + (connectionItem.properties.selectedAuthMethod as AuthMethodIdType) ?? + (connectionItem.properties.availableAuthMethods[0] as AuthMethodIdType) ?? + AuthMethodId.NativeAuth; + } + } + + // Convert central auth configs to local cache format + let cacheEntraIdConfig: EntraIdAuthConfig | undefined; + if (secrets.entraIdAuth) { + cacheEntraIdConfig = { + tenantId: secrets.entraIdAuth.tenantId ?? '', // Convert optional to required for backward compatibility + }; + } + + // Use structured configs first, fall back to legacy fields + const username = secrets.nativeAuth?.connectionUser ?? secrets.userName ?? ''; + const password = secrets.nativeAuth?.connectionPassword ?? secrets.password ?? ''; + + // Use the existing setAuthCredentials method to ensure consistent behavior + CredentialCache.setAuthCredentials( + connectionItem.id, + selectedAuthMethod, + secrets.connectionString, + username, + password, + emulatorConfiguration, + cacheEntraIdConfig, + ); + } } diff --git a/src/documentdb/auth/AuthConfig.ts b/src/documentdb/auth/AuthConfig.ts new file mode 100644 index 000000000..b27945b06 --- /dev/null +++ b/src/documentdb/auth/AuthConfig.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Configuration for native MongoDB authentication using username/password. + * This represents the traditional authentication method where credentials + * are directly provided for database connection. + */ +export interface NativeAuthConfig { + /** The username for database authentication */ + readonly connectionUser: string; + + /** The password for database authentication */ + readonly connectionPassword: string; +} + +/** + * Configuration for Entra ID (Azure Active Directory) authentication. + * Supports both explicit tenant specification and tenant discovery scenarios. + */ +export interface EntraIdAuthConfig { + /** + * The Azure Active Directory tenant ID. + * When provided, authentication will target this specific tenant. + * When omitted, Azure SDK will attempt tenant discovery based on the user context. + * This flexibility supports both single-tenant and multi-tenant scenarios. + */ + readonly tenantId?: string; + + /** + * Additional Entra ID specific configuration can be added here as needed. + * Examples: clientId, scope, authority, etc. + */ +} + +/** + * Union type representing all supported authentication configurations. + * This type can be extended with additional auth methods in the future + * (e.g., certificate-based auth, OAuth, etc.) without breaking existing code. + */ +export type AuthConfig = NativeAuthConfig | EntraIdAuthConfig; + +/** + * Type guard to check if a configuration is for native authentication. + * @param config The authentication configuration to check + * @returns true if the config is for native auth, false otherwise + */ +export function isNativeAuthConfig(config: AuthConfig): config is NativeAuthConfig { + return 'connectionUser' in config && 'connectionPassword' in config; +} + +/** + * Type guard to check if a configuration is for Entra ID authentication. + * @param config The authentication configuration to check + * @returns true if the config is for Entra ID auth, false otherwise + */ +export function isEntraIdAuthConfig(config: AuthConfig): config is EntraIdAuthConfig { + return 'tenantId' in config || (!('connectionUser' in config) && !('connectionPassword' in config)); +} diff --git a/src/services/connectionStorageService.ts b/src/services/connectionStorageService.ts index 96a87a2a8..91a10f5d0 100644 --- a/src/services/connectionStorageService.ts +++ b/src/services/connectionStorageService.ts @@ -6,6 +6,7 @@ import { apiUtils, callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; +import { type EntraIdAuthConfig, type NativeAuthConfig } from '../documentdb/auth/AuthConfig'; import { AuthMethodId } from '../documentdb/auth/AuthMethod'; import { DocumentDBConnectionString } from '../documentdb/utils/DocumentDBConnectionString'; import { API } from '../DocumentDBExperiences'; @@ -62,19 +63,33 @@ export interface ConnectionItem { secrets: { /** assume that the connection string doesn't contain the username and password */ connectionString: string; + + // Legacy fields for backward compatibility userName?: string; password?: string; + + // Structured auth configurations + nativeAuth?: NativeAuthConfig; + entraIdAuth?: EntraIdAuthConfig; }; } /** * StorageService offers secrets storage as a string[] so we need to ensure * we keep using correct indexes when accessing secrets. + * + * Auth config fields are stored individually as flat string values to avoid + * nested object serialization issues with VS Code SecretStorage. */ const enum SecretIndex { ConnectionString = 0, UserName = 1, Password = 2, + // Native auth config fields + NativeAuthConnectionUser = 3, + NativeAuthConnectionPassword = 4, + // Entra ID auth config fields + EntraIdTenantId = 5, } /** @@ -162,6 +177,19 @@ export class ConnectionStorageService { if (item.secrets.password) { secretsArray[SecretIndex.Password] = item.secrets.password; } + + // Store native auth config fields individually + if (item.secrets.nativeAuth) { + secretsArray[SecretIndex.NativeAuthConnectionUser] = item.secrets.nativeAuth.connectionUser; + if (item.secrets.nativeAuth.connectionPassword) { + secretsArray[SecretIndex.NativeAuthConnectionPassword] = item.secrets.nativeAuth.connectionPassword; + } + } + + // Store Entra ID auth config fields individually + if (item.secrets.entraIdAuth && item.secrets.entraIdAuth.tenantId) { + secretsArray[SecretIndex.EntraIdTenantId] = item.secrets.entraIdAuth.tenantId; + } } return { @@ -179,10 +207,35 @@ export class ConnectionStorageService { } const secretsArray = item.secrets ?? []; + + // Reconstruct native auth config from individual fields + let nativeAuth: NativeAuthConfig | undefined; + const nativeAuthUser = secretsArray[SecretIndex.NativeAuthConnectionUser]; + const nativeAuthPassword = secretsArray[SecretIndex.NativeAuthConnectionPassword]; + + if (nativeAuthUser) { + nativeAuth = { + connectionUser: nativeAuthUser, + connectionPassword: nativeAuthPassword, + }; + } + + // Reconstruct Entra ID auth config from individual fields + let entraIdAuth: EntraIdAuthConfig | undefined; + const entraIdTenantId = secretsArray[SecretIndex.EntraIdTenantId]; + + if (entraIdTenantId) { + entraIdAuth = { + tenantId: entraIdTenantId, + }; + } + const secrets = { connectionString: secretsArray[SecretIndex.ConnectionString] ?? '', password: secretsArray[SecretIndex.Password], userName: secretsArray[SecretIndex.UserName], + nativeAuth, + entraIdAuth, }; return { diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index cda50be24..8d384a8ff 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -15,7 +15,7 @@ import { nonNullProp } from '../../utils/nonNull'; import { authMethodFromString, AuthMethodId, authMethodsFromString } from '../../documentdb/auth/AuthMethod'; import { ClustersClient } from '../../documentdb/ClustersClient'; -import { CredentialCache } from '../../documentdb/CredentialCache'; +import { CredentialCache, type EntraIdAuthConfig } from '../../documentdb/CredentialCache'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; import { Views } from '../../documentdb/Views'; import { type AuthenticateWizardContext } from '../../documentdb/wizards/authenticate/AuthenticateWizardContext'; @@ -53,10 +53,23 @@ export class DocumentDBClusterItem extends ClusterItemBase implements TreeElemen return { connectionString: connectionCredentials.secrets.connectionString, - connectionUser: connectionCredentials.secrets.userName, - connectionPassword: connectionCredentials.secrets.password, availableAuthMethods: authMethodsFromString(connectionCredentials?.properties.availableAuthMethods), selectedAuthMethod: authMethodFromString(connectionCredentials?.properties.selectedAuthMethod), + + // Legacy fields for backward compatibility + connectionUser: + connectionCredentials.secrets.nativeAuth?.connectionUser ?? connectionCredentials.secrets.userName, + connectionPassword: + connectionCredentials.secrets.nativeAuth?.connectionPassword ?? connectionCredentials.secrets.password, + + // Structured auth configs + nativeAuthConfig: connectionCredentials.secrets.nativeAuth, + entraIdConfig: connectionCredentials.secrets.entraIdAuth + ? ({ + tenantId: connectionCredentials.secrets.entraIdAuth.tenantId ?? '', + // Convert other central auth config fields to local cache format as needed + } as EntraIdAuthConfig) + : undefined, }; } From 7dec61725e929d30de8adf89af150e65c9c4596e Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 19 Sep 2025 16:14:52 +0200 Subject: [PATCH 06/88] wip: refactored credential classes for better naming and scoping --- src/documentdb/ClustersClient.ts | 10 +++++----- src/documentdb/CredentialCache.ts | 18 +----------------- .../auth/MicrosoftEntraIDAuthHandler.ts | 4 ++-- src/documentdb/auth/NativeAuthHandler.ts | 4 ++-- .../documentdb/MongoRUResourceItem.ts | 4 ++-- .../utils/ruClusterHelpers.ts | 6 +++--- .../documentdb/DocumentDBResourceItem.ts | 4 ++-- .../discovery-wizard/AzureExecuteStep.ts | 2 +- .../utils/clusterHelpers.ts | 7 ++++--- .../discovery-tree/vm/AzureVMResourceItem.ts | 4 ++-- .../documentdb/VCoreResourceItem.ts | 4 ++-- .../mongo-ru/RUCoreResourceItem.ts | 8 ++++---- .../connections-view/DocumentDBClusterItem.ts | 7 ++++--- src/tree/documentdb/ClusterItemBase.ts | 7 ++++--- 14 files changed, 38 insertions(+), 51 deletions(-) diff --git a/src/documentdb/ClustersClient.ts b/src/documentdb/ClustersClient.ts index 275aada3d..a173f5481 100644 --- a/src/documentdb/ClustersClient.ts +++ b/src/documentdb/ClustersClient.ts @@ -34,7 +34,7 @@ import { type AuthHandler } from './auth/AuthHandler'; import { AuthMethodId } from './auth/AuthMethod'; import { MicrosoftEntraIDAuthHandler } from './auth/MicrosoftEntraIDAuthHandler'; import { NativeAuthHandler } from './auth/NativeAuthHandler'; -import { CredentialCache, type ClustersCredentials } from './CredentialCache'; +import { CredentialCache, type CachedClusterCredentials } from './CredentialCache'; import { getHostsFromConnectionString, hasAzureDomain } from './utils/connectionStringHelpers'; import { getClusterMetadata, type ClusterMetadata } from './utils/getClusterMetadata'; import { toFilterQueryObj } from './utils/toFilterQuery'; @@ -224,21 +224,21 @@ export class ClustersClient { } /** - * @deprecated Use getCredentials() which returns a ClusterCredentials object instead. + * @deprecated Use getCredentials() which returns a CachedClusterCredentials object instead. */ getConnectionString(): string | undefined { return this.getCredentials()?.connectionString; } /** - * @deprecated Use getCredentials() which returns a ClusterCredentials object instead. + * @deprecated Use getCredentials() which returns a CachedClusterCredentials object instead. */ getConnectionStringWithPassword(): string | undefined { return CredentialCache.getConnectionStringWithPassword(this.credentialId); } - public getCredentials(): ClustersCredentials | undefined { - return CredentialCache.getCredentials(this.credentialId) as ClustersCredentials | undefined; + public getCredentials(): CachedClusterCredentials | undefined { + return CredentialCache.getCredentials(this.credentialId) as CachedClusterCredentials | undefined; } async listDatabases(): Promise { diff --git a/src/documentdb/CredentialCache.ts b/src/documentdb/CredentialCache.ts index 58e91fa4e..1ecc2a482 100644 --- a/src/documentdb/CredentialCache.ts +++ b/src/documentdb/CredentialCache.ts @@ -6,26 +6,10 @@ import { type ConnectionItem } from '../services/connectionStorageService'; import { CaseInsensitiveMap } from '../utils/CaseInsensitiveMap'; import { type EmulatorConfiguration } from '../utils/emulatorConfiguration'; +import { type EntraIdAuthConfig, type NativeAuthConfig } from './auth/AuthConfig'; import { AuthMethodId, type AuthMethodId as AuthMethodIdType } from './auth/AuthMethod'; import { addAuthenticationDataToConnectionString } from './utils/connectionStringHelpers'; -/** - * Configuration for Microsoft Entra ID authentication. - */ -export interface EntraIdAuthConfig { - tenantId: string; - accountId?: string; - subscriptionId?: string; -} - -/** - * Configuration for native authentication (username/password based authentication). - */ -export interface NativeAuthConfig { - connectionUser: string; - connectionPassword?: string; -} - export interface CachedClusterCredentials { mongoClusterId: string; connectionStringWithPassword?: string; diff --git a/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts b/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts index 27b9ae6f7..022537516 100644 --- a/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts +++ b/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts @@ -7,7 +7,7 @@ import { getSessionFromVSCode } from '@microsoft/vscode-azext-azureauth/out/src/getSessionFromVSCode'; import * as l10n from '@vscode/l10n'; import { type MongoClientOptions, type OIDCCallbackParams, type OIDCResponse } from 'mongodb'; -import { type ClustersCredentials } from '../CredentialCache'; +import { type CachedClusterCredentials } from '../CredentialCache'; import { DocumentDBConnectionString } from '../utils/DocumentDBConnectionString'; import { type AuthHandler, type AuthHandlerResponse } from './AuthHandler'; @@ -15,7 +15,7 @@ import { type AuthHandler, type AuthHandlerResponse } from './AuthHandler'; * Handler for Microsoft Entra ID authentication via OIDC */ export class MicrosoftEntraIDAuthHandler implements AuthHandler { - constructor(private readonly clusterCredentials: ClustersCredentials) {} + constructor(private readonly clusterCredentials: CachedClusterCredentials) {} public async configureAuth(): Promise { // Get Microsoft Entra ID token diff --git a/src/documentdb/auth/NativeAuthHandler.ts b/src/documentdb/auth/NativeAuthHandler.ts index c9ae7d457..ddab5e92a 100644 --- a/src/documentdb/auth/NativeAuthHandler.ts +++ b/src/documentdb/auth/NativeAuthHandler.ts @@ -5,14 +5,14 @@ import { type MongoClientOptions } from 'mongodb'; import { nonNullValue } from '../../utils/nonNull'; -import { type ClustersCredentials } from '../CredentialCache'; +import { type CachedClusterCredentials } from '../CredentialCache'; import { type AuthHandler, type AuthHandlerResponse } from './AuthHandler'; /** * Handler for native MongoDB authentication using username and password */ export class NativeAuthHandler implements AuthHandler { - constructor(private readonly clusterCredentials: ClustersCredentials) {} + constructor(private readonly clusterCredentials: CachedClusterCredentials) {} public configureAuth(): Promise { const options: MongoClientOptions = {}; diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts index 29243a720..ea67b1bc6 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts @@ -11,7 +11,7 @@ import { ClustersClient } from '../../../../documentdb/ClustersClient'; import { CredentialCache } from '../../../../documentdb/CredentialCache'; import { Views } from '../../../../documentdb/Views'; import { ext } from '../../../../extensionVariables'; -import { ClusterItemBase, type ClusterCredentials } from '../../../../tree/documentdb/ClusterItemBase'; +import { ClusterItemBase, type EphemeralClusterCredentials } from '../../../../tree/documentdb/ClusterItemBase'; import { type ClusterModel } from '../../../../tree/documentdb/ClusterModel'; import { extractCredentialsFromRUAccount } from '../../utils/ruClusterHelpers'; @@ -34,7 +34,7 @@ export class MongoRUResourceItem extends ClusterItemBase { super(cluster); } - public async getCredentials(): Promise { + public async getCredentials(): Promise { return callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { context.telemetry.properties.view = Views.DiscoveryView; context.telemetry.properties.discoveryProvider = 'azure-mongo-ru-discovery'; diff --git a/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts b/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts index 76a438fac..49374713c 100644 --- a/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts +++ b/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts @@ -9,7 +9,7 @@ import * as l10n from '@vscode/l10n'; import { AuthMethodId } from '../../../documentdb/auth/AuthMethod'; import { maskSensitiveValuesInTelemetry } from '../../../documentdb/utils/connectionStringHelpers'; import { DocumentDBConnectionString } from '../../../documentdb/utils/DocumentDBConnectionString'; -import { type ClusterCredentials } from '../../../tree/documentdb/ClusterItemBase'; +import { type EphemeralClusterCredentials } from '../../../tree/documentdb/ClusterItemBase'; import { createCosmosDBManagementClient } from '../../../utils/azureClients'; /** @@ -20,7 +20,7 @@ export async function extractCredentialsFromRUAccount( subscription: AzureSubscription, resourceGroup: string, accountName: string, -): Promise { +): Promise { if (!resourceGroup || !accountName) { throw new Error(l10n.t('Account information is incomplete.')); } @@ -83,7 +83,7 @@ export async function extractCredentialsFromRUAccount( // it here anyway. parsedCS.searchParams.delete('appName'); - const clusterCredentials: ClusterCredentials = { + const clusterCredentials: EphemeralClusterCredentials = { connectionString: parsedCS.toString(), availableAuthMethods: [AuthMethodId.NativeAuth], selectedAuthMethod: AuthMethodId.NativeAuth, diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts index 301a7ded2..b96053d78 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts @@ -22,7 +22,7 @@ import { ChooseAuthMethodStep } from '../../../../documentdb/wizards/authenticat import { ProvidePasswordStep } from '../../../../documentdb/wizards/authenticate/ProvidePasswordStep'; import { ProvideUserNameStep } from '../../../../documentdb/wizards/authenticate/ProvideUsernameStep'; import { ext } from '../../../../extensionVariables'; -import { ClusterItemBase, type ClusterCredentials } from '../../../../tree/documentdb/ClusterItemBase'; +import { ClusterItemBase, type EphemeralClusterCredentials } from '../../../../tree/documentdb/ClusterItemBase'; import { type ClusterModel } from '../../../../tree/documentdb/ClusterModel'; import { nonNullValue } from '../../../../utils/nonNull'; import { extractCredentialsFromCluster, getClusterInformationFromAzure } from '../../utils/clusterHelpers'; @@ -46,7 +46,7 @@ export class DocumentDBResourceItem extends ClusterItemBase { super(cluster); } - public async getCredentials(): Promise { + public async getCredentials(): Promise { return callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { context.telemetry.properties.view = Views.DiscoveryView; context.telemetry.properties.discoveryProvider = 'azure-mongo-vcore-discovery'; diff --git a/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts b/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts index 99d2d961a..c7b98dbe9 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts @@ -53,7 +53,7 @@ export class AzureExecuteStep extends AzureWizardExecuteStep { + public async getCredentials(): Promise { return callWithTelemetryAndErrorHandling('connect', async (context: IActionContext) => { context.telemetry.properties.discoveryProvider = 'azure-vm-discovery'; context.telemetry.properties.view = Views.DiscoveryView; diff --git a/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts b/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts index c2e47aeaf..8fd163bcc 100644 --- a/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts +++ b/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts @@ -27,7 +27,7 @@ import { getClusterInformationFromAzure, } from '../../../plugins/service-azure-mongo-vcore/utils/clusterHelpers'; import { nonNullValue } from '../../../utils/nonNull'; -import { ClusterItemBase, type ClusterCredentials } from '../../documentdb/ClusterItemBase'; +import { ClusterItemBase, type EphemeralClusterCredentials } from '../../documentdb/ClusterItemBase'; import { type ClusterModel } from '../../documentdb/ClusterModel'; export class VCoreResourceItem extends ClusterItemBase { @@ -49,7 +49,7 @@ export class VCoreResourceItem extends ClusterItemBase { super(cluster); } - public async getCredentials(): Promise { + public async getCredentials(): Promise { return callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { context.telemetry.properties.view = Views.AzureResourcesView; context.telemetry.properties.branch = 'documentdb'; diff --git a/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts b/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts index bc6c167d2..efba601c1 100644 --- a/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts +++ b/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts @@ -16,7 +16,7 @@ import { Views } from '../../../documentdb/Views'; import { ext } from '../../../extensionVariables'; import { createCosmosDBManagementClient } from '../../../utils/azureClients'; import { nonNullValue } from '../../../utils/nonNull'; -import { ClusterItemBase, type ClusterCredentials } from '../../documentdb/ClusterItemBase'; +import { ClusterItemBase, type EphemeralClusterCredentials } from '../../documentdb/ClusterItemBase'; import { type ClusterModel } from '../../documentdb/ClusterModel'; export class RUResourceItem extends ClusterItemBase { @@ -38,7 +38,7 @@ export class RUResourceItem extends ClusterItemBase { super(cluster); } - public async getCredentials(): Promise { + public async getCredentials(): Promise { return callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { context.telemetry.properties.view = Views.AzureResourcesView; context.telemetry.properties.branch = 'ru'; @@ -134,7 +134,7 @@ export class RUResourceItem extends ClusterItemBase { subscription: AzureSubscription, resourceGroup: string, clusterName: string, - ): Promise { + ): Promise { // subscription comes from different azure packages in callers; cast here intentionally // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any const managementClient = await createCosmosDBManagementClient(context, subscription as any); @@ -197,7 +197,7 @@ export class RUResourceItem extends ClusterItemBase { // it here anyway. parsedCS.searchParams.delete('appName'); - const clusterCredentials: ClusterCredentials = { + const clusterCredentials: EphemeralClusterCredentials = { connectionString: parsedCS.toString(), availableAuthMethods: [AuthMethodId.NativeAuth], selectedAuthMethod: AuthMethodId.NativeAuth, diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 8d384a8ff..99e6aa1eb 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -13,9 +13,10 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { nonNullProp } from '../../utils/nonNull'; +import { type EntraIdAuthConfig } from '../../documentdb/auth/AuthConfig'; import { authMethodFromString, AuthMethodId, authMethodsFromString } from '../../documentdb/auth/AuthMethod'; import { ClustersClient } from '../../documentdb/ClustersClient'; -import { CredentialCache, type EntraIdAuthConfig } from '../../documentdb/CredentialCache'; +import { CredentialCache } from '../../documentdb/CredentialCache'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; import { Views } from '../../documentdb/Views'; import { type AuthenticateWizardContext } from '../../documentdb/wizards/authenticate/AuthenticateWizardContext'; @@ -25,7 +26,7 @@ import { ProvideUserNameStep } from '../../documentdb/wizards/authenticate/Provi import { SaveCredentialsStep } from '../../documentdb/wizards/authenticate/SaveCredentialsStep'; import { ext } from '../../extensionVariables'; import { ConnectionStorageService, ConnectionType } from '../../services/connectionStorageService'; -import { ClusterItemBase, type ClusterCredentials } from '../documentdb/ClusterItemBase'; +import { ClusterItemBase, type EphemeralClusterCredentials } from '../documentdb/ClusterItemBase'; import { type ClusterModelWithStorage } from '../documentdb/ClusterModel'; import { type TreeElementWithStorageId } from '../TreeElementWithStorageId'; @@ -41,7 +42,7 @@ export class DocumentDBClusterItem extends ClusterItemBase implements TreeElemen return this.cluster.storageId; } - public async getCredentials(): Promise { + public async getCredentials(): Promise { const connectionType = this.cluster.emulatorConfiguration?.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters; diff --git a/src/tree/documentdb/ClusterItemBase.ts b/src/tree/documentdb/ClusterItemBase.ts index abfd90c1e..284976aa8 100644 --- a/src/tree/documentdb/ClusterItemBase.ts +++ b/src/tree/documentdb/ClusterItemBase.ts @@ -8,7 +8,8 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { type Experience } from '../../DocumentDBExperiences'; import { ClustersClient, type DatabaseItemModel } from '../../documentdb/ClustersClient'; -import { CredentialCache, type EntraIdAuthConfig, type NativeAuthConfig } from '../../documentdb/CredentialCache'; +import { CredentialCache } from '../../documentdb/CredentialCache'; +import { type EntraIdAuthConfig, type NativeAuthConfig } from '../../documentdb/auth/AuthConfig'; import { type AuthMethodId } from '../../documentdb/auth/AuthMethod'; import { ext } from '../../extensionVariables'; import { regionToDisplayName } from '../../utils/regionToDisplayName'; @@ -101,9 +102,9 @@ export abstract class ClusterItemBase * Must be implemented by subclasses. * This is relevant for service discovery scenarios * - * @returns A promise that resolves to the credentials if successful; otherwise, undefined. + * @returns A promise that resolves to the EphemeralClusterCredentials if successful; otherwise, undefined. */ - public abstract getCredentials(): Promise; + public abstract getCredentials(): Promise; /** * Authenticates and connects to the cluster to list all available databases. From b35c035a0c156bab93ee02079d3021f87516acdd Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 19 Sep 2025 16:25:15 +0200 Subject: [PATCH 07/88] Removed deprecated properties from EphemeralClusterCredentials --- .../addConnectionFromRegistry.ts | 7 +++---- .../copyConnectionString/copyConnectionString.ts | 2 +- src/commands/launchShell/launchShell.ts | 10 +++------- .../discovery-tree/documentdb/MongoRUResourceItem.ts | 4 ++-- .../discovery-wizard/AzureMongoRUExecuteStep.ts | 4 ++-- .../service-azure-mongo-ru/utils/ruClusterHelpers.ts | 3 --- .../documentdb/DocumentDBResourceItem.ts | 2 +- .../discovery-wizard/AzureExecuteStep.ts | 4 ++-- .../service-azure-mongo-vcore/utils/clusterHelpers.ts | 2 -- .../documentdb/VCoreResourceItem.ts | 2 +- .../mongo-ru/RUCoreResourceItem.ts | 9 +++------ src/tree/connections-view/DocumentDBClusterItem.ts | 6 ------ src/tree/documentdb/ClusterItemBase.ts | 6 ------ 13 files changed, 18 insertions(+), 43 deletions(-) diff --git a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts index 62c7ddb10..3def8d43c 100644 --- a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts +++ b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts @@ -66,8 +66,7 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C } const parsedCS = new DocumentDBConnectionString(credentials.connectionString); - const username = - (credentials.nativeAuthConfig?.connectionUser ?? credentials.connectionUser) || parsedCS.username; + const username = credentials.nativeAuthConfig?.connectionUser || parsedCS.username; parsedCS.username = ''; const joinedHosts = [...parsedCS.hosts].sort().join(','); @@ -146,8 +145,8 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C properties: { api: API.DocumentDB, availableAuthMethods: credentials.availableAuthMethods }, secrets: { connectionString: parsedCS.toString(), - userName: credentials.nativeAuthConfig?.connectionUser ?? credentials.connectionUser, - password: credentials.nativeAuthConfig?.connectionPassword ?? credentials.connectionPassword, + userName: credentials.nativeAuthConfig?.connectionUser, + password: credentials.nativeAuthConfig?.connectionPassword, }, }; diff --git a/src/commands/copyConnectionString/copyConnectionString.ts b/src/commands/copyConnectionString/copyConnectionString.ts index f5b0da958..747c65e9f 100644 --- a/src/commands/copyConnectionString/copyConnectionString.ts +++ b/src/commands/copyConnectionString/copyConnectionString.ts @@ -30,7 +30,7 @@ export async function copyConnectionString(context: IActionContext, node: Cluste } const parsedConnectionString = new DocumentDBConnectionString(credentials.connectionString); - parsedConnectionString.username = credentials.connectionUser ?? ''; + parsedConnectionString.username = credentials.nativeAuthConfig?.connectionUser ?? ''; if (credentials.selectedAuthMethod === AuthMethodId.MicrosoftEntraID) { parsedConnectionString.searchParams.set('authMechanism', 'MONGODB-OIDC'); diff --git a/src/commands/launchShell/launchShell.ts b/src/commands/launchShell/launchShell.ts index c8c9e6f61..7975e2466 100644 --- a/src/commands/launchShell/launchShell.ts +++ b/src/commands/launchShell/launchShell.ts @@ -69,13 +69,9 @@ export async function launchShell( if (selectedAuthMethod === AuthMethodId.NativeAuth || (nativeAuthIsAvailable && !selectedAuthMethod)) { connectionString = discoveredClusterCredentials.connectionString; - // Prefer auth config, fallback to legacy fields for backward compatibility - username = - discoveredClusterCredentials.nativeAuthConfig?.connectionUser ?? - discoveredClusterCredentials.connectionUser; - password = - discoveredClusterCredentials.nativeAuthConfig?.connectionPassword ?? - discoveredClusterCredentials.connectionPassword; + // Use nativeAuthConfig for credential access + username = discoveredClusterCredentials.nativeAuthConfig?.connectionUser; + password = discoveredClusterCredentials.nativeAuthConfig?.connectionPassword; authMechanism = AuthMethodId.NativeAuth; } else { // Only SCRAM-SHA-256 (username/password) authentication is supported here. diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts index ea67b1bc6..c9bb94667 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts @@ -81,8 +81,8 @@ export class MongoRUResourceItem extends ClusterItemBase { this.id, credentials.selectedAuthMethod ?? credentials.availableAuthMethods[0], credentials.connectionString, - credentials.nativeAuthConfig?.connectionUser ?? credentials.connectionUser, - credentials.nativeAuthConfig?.connectionPassword ?? credentials.connectionPassword, + credentials.nativeAuthConfig?.connectionUser ?? '', + credentials.nativeAuthConfig?.connectionPassword ?? '', ); // Connect using the cached credentials diff --git a/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts b/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts index 07664a19f..97136ffe6 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-wizard/AzureMongoRUExecuteStep.ts @@ -36,8 +36,8 @@ export class AzureMongoRUExecuteStep extends AzureWizardExecuteStep Date: Fri, 19 Sep 2025 16:34:24 +0200 Subject: [PATCH 08/88] fix: simplified storage secred index --- src/services/connectionStorageService.ts | 38 +++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/services/connectionStorageService.ts b/src/services/connectionStorageService.ts index 91a10f5d0..5801b13ec 100644 --- a/src/services/connectionStorageService.ts +++ b/src/services/connectionStorageService.ts @@ -83,13 +83,11 @@ export interface ConnectionItem { */ const enum SecretIndex { ConnectionString = 0, - UserName = 1, - Password = 2, - // Native auth config fields - NativeAuthConnectionUser = 3, - NativeAuthConnectionPassword = 4, + // Native auth config fields (consolidated from legacy UserName/Password) + NativeAuthConnectionUser = 1, + NativeAuthConnectionPassword = 2, // Entra ID auth config fields - EntraIdTenantId = 5, + EntraIdTenantId = 3, } /** @@ -171,19 +169,22 @@ export class ConnectionStorageService { const secretsArray: string[] = []; if (item.secrets) { secretsArray[SecretIndex.ConnectionString] = item.secrets.connectionString; - if (item.secrets.userName) { - secretsArray[SecretIndex.UserName] = item.secrets.userName; - } - if (item.secrets.password) { - secretsArray[SecretIndex.Password] = item.secrets.password; - } // Store native auth config fields individually + // Legacy userName/password fields map to the same storage indexes as nativeAuth if (item.secrets.nativeAuth) { secretsArray[SecretIndex.NativeAuthConnectionUser] = item.secrets.nativeAuth.connectionUser; if (item.secrets.nativeAuth.connectionPassword) { secretsArray[SecretIndex.NativeAuthConnectionPassword] = item.secrets.nativeAuth.connectionPassword; } + } else if (item.secrets.userName || item.secrets.password) { + // Fallback: if only legacy fields are provided, store them in nativeAuth indexes + if (item.secrets.userName) { + secretsArray[SecretIndex.NativeAuthConnectionUser] = item.secrets.userName; + } + if (item.secrets.password) { + secretsArray[SecretIndex.NativeAuthConnectionPassword] = item.secrets.password; + } } // Store Entra ID auth config fields individually @@ -232,8 +233,9 @@ export class ConnectionStorageService { const secrets = { connectionString: secretsArray[SecretIndex.ConnectionString] ?? '', - password: secretsArray[SecretIndex.Password], - userName: secretsArray[SecretIndex.UserName], + // Legacy fields populated from nativeAuth data for backward compatibility + password: nativeAuthPassword, + userName: nativeAuthUser, nativeAuth, entraIdAuth, }; @@ -280,8 +282,16 @@ export class ConnectionStorageService { }, secrets: { connectionString: parsedCS.toString(), + // Legacy fields for backward compatibility userName: username, password: password, + // Structured auth config populated from the same data + nativeAuth: username + ? { + connectionUser: username, + connectionPassword: password, + } + : undefined, }, }; } From c2313fd202103cf877432792c7f534116097b78f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 19 Sep 2025 16:47:29 +0200 Subject: [PATCH 09/88] Removed deprecated properties from CachedlClusterCredentials --- .../addConnectionFromRegistry.ts | 8 +++++--- src/commands/newConnection/ExecuteStep.ts | 19 +++++++++++++++---- src/commands/updateCredentials/ExecuteStep.ts | 6 ------ .../updateCredentials/updateCredentials.ts | 5 ++--- src/documentdb/CredentialCache.ts | 8 ++++---- src/services/connectionStorageService.ts | 18 +----------------- .../connections-view/DocumentDBClusterItem.ts | 15 +++++++++++---- 7 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts index 3def8d43c..844cf62b1 100644 --- a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts +++ b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts @@ -77,7 +77,9 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C const existingDuplicateConnection = existingConnections.find((existingConnection) => { const existingCS = new DocumentDBConnectionString(existingConnection.secrets.connectionString); const existingHostsJoined = [...existingCS.hosts].sort().join(','); - return existingConnection.secrets.userName === username && existingHostsJoined === joinedHosts; + // Use nativeAuth for comparison + const existingUsername = existingConnection.secrets.nativeAuth?.connectionUser; + return existingUsername === username && existingHostsJoined === joinedHosts; }); if (existingDuplicateConnection) { @@ -145,8 +147,8 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C properties: { api: API.DocumentDB, availableAuthMethods: credentials.availableAuthMethods }, secrets: { connectionString: parsedCS.toString(), - userName: credentials.nativeAuthConfig?.connectionUser, - password: credentials.nativeAuthConfig?.connectionPassword, + // Populate nativeAuth configuration + nativeAuth: credentials.nativeAuthConfig, }, }; diff --git a/src/commands/newConnection/ExecuteStep.ts b/src/commands/newConnection/ExecuteStep.ts index 91d1ef0b5..1e4b6225d 100644 --- a/src/commands/newConnection/ExecuteStep.ts +++ b/src/commands/newConnection/ExecuteStep.ts @@ -6,6 +6,7 @@ import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; +import { AuthMethodId } from '../../documentdb/auth/AuthMethod'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; import { API } from '../../DocumentDBExperiences'; import { ext } from '../../extensionVariables'; @@ -52,10 +53,10 @@ export class ExecuteStep extends AzureWizardExecuteStep { const existingCS = new DocumentDBConnectionString(existingConnection.secrets.connectionString); const existingHostsJoined = [...existingCS.hosts].sort().join(','); + // Use nativeAuth for comparison + const existingUsername = existingConnection.secrets.nativeAuth?.connectionUser; - return ( - existingConnection.secrets.userName === newUsername && existingHostsJoined === newJoinedHosts - ); + return existingUsername === newUsername && existingHostsJoined === newJoinedHosts; }); if (existingDuplicateConnection) { @@ -129,7 +130,17 @@ export class ExecuteStep extends AzureWizardExecuteStep Date: Sat, 20 Sep 2025 00:15:30 +0200 Subject: [PATCH 10/88] feat: discovery, vcore, tenantid discovery for entraid --- src/documentdb/auth/AuthConfig.ts | 1 + .../documentdb/DocumentDBResourceItem.ts | 4 ++-- .../discovery-wizard/AzureExecuteStep.ts | 2 +- .../service-azure-mongo-vcore/utils/clusterHelpers.ts | 10 +++++++++- .../documentdb/VCoreResourceItem.ts | 4 ++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/documentdb/auth/AuthConfig.ts b/src/documentdb/auth/AuthConfig.ts index b27945b06..c925f9d55 100644 --- a/src/documentdb/auth/AuthConfig.ts +++ b/src/documentdb/auth/AuthConfig.ts @@ -28,6 +28,7 @@ export interface EntraIdAuthConfig { * This flexibility supports both single-tenant and multi-tenant scenarios. */ readonly tenantId?: string; + readonly subscriptionId?: string; /** * Additional Entra ID specific configuration can be added here as needed. diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts index f5d13c169..6798a5611 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts @@ -59,7 +59,7 @@ export class DocumentDBResourceItem extends ClusterItemBase { this.cluster.name, ); - return extractCredentialsFromCluster(context, clusterInformation); + return extractCredentialsFromCluster(context, clusterInformation, this.subscription); }); } @@ -93,7 +93,7 @@ export class DocumentDBResourceItem extends ClusterItemBase { // Get and validate cluster information const clusterInformation = await this.getClusterInformation(context); - const credentials = extractCredentialsFromCluster(context, clusterInformation); + const credentials = extractCredentialsFromCluster(context, clusterInformation, this.subscription); // Prepare wizard context const wizardContext: AuthenticateWizardContext = { diff --git a/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts b/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts index f76e46031..f684056a9 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-wizard/AzureExecuteStep.ts @@ -36,7 +36,7 @@ export class AzureExecuteStep extends AzureWizardExecuteStep !isSupportedAuthMethod(methodId)); context.telemetry.properties.unknownAuthMethods = unknownMethodIds.join(','); + if (credentials.availableAuthMethods.includes(AuthMethodId.MicrosoftEntraID)) { + credentials.entraIdConfig = { + tenantId: subscription.tenantId, + subscriptionId: subscription.subscriptionId, + }; + } + return credentials; } diff --git a/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts b/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts index 7f3476767..18b53484a 100644 --- a/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts +++ b/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts @@ -62,7 +62,7 @@ export class VCoreResourceItem extends ClusterItemBase { this.cluster.name, ); - return extractCredentialsFromCluster(context, clusterInformation); + return extractCredentialsFromCluster(context, clusterInformation, this.subscription); }); } @@ -96,7 +96,7 @@ export class VCoreResourceItem extends ClusterItemBase { // Get and validate cluster information const clusterInformation = await this.getClusterInformation(context); - const credentials = extractCredentialsFromCluster(context, clusterInformation); + const credentials = extractCredentialsFromCluster(context, clusterInformation, this.subscription); // Prepare wizard context const wizardContext: AuthenticateWizardContext = { From 09551a7d41d3ad85bc7a1ed9947c6d924b716023 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 22 Sep 2025 09:50:04 +0200 Subject: [PATCH 11/88] chore: removed obsolete code, removed deprecated fields --- src/documentdb/auth/AuthConfig.ts | 18 ------------------ src/services/connectionStorageService.ts | 3 --- .../connections-view/DocumentDBClusterItem.ts | 8 +++----- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/src/documentdb/auth/AuthConfig.ts b/src/documentdb/auth/AuthConfig.ts index c925f9d55..1c2ca0eb3 100644 --- a/src/documentdb/auth/AuthConfig.ts +++ b/src/documentdb/auth/AuthConfig.ts @@ -42,21 +42,3 @@ export interface EntraIdAuthConfig { * (e.g., certificate-based auth, OAuth, etc.) without breaking existing code. */ export type AuthConfig = NativeAuthConfig | EntraIdAuthConfig; - -/** - * Type guard to check if a configuration is for native authentication. - * @param config The authentication configuration to check - * @returns true if the config is for native auth, false otherwise - */ -export function isNativeAuthConfig(config: AuthConfig): config is NativeAuthConfig { - return 'connectionUser' in config && 'connectionPassword' in config; -} - -/** - * Type guard to check if a configuration is for Entra ID authentication. - * @param config The authentication configuration to check - * @returns true if the config is for Entra ID auth, false otherwise - */ -export function isEntraIdAuthConfig(config: AuthConfig): config is EntraIdAuthConfig { - return 'tenantId' in config || (!('connectionUser' in config) && !('connectionPassword' in config)); -} diff --git a/src/services/connectionStorageService.ts b/src/services/connectionStorageService.ts index d691975db..6a1b6fea3 100644 --- a/src/services/connectionStorageService.ts +++ b/src/services/connectionStorageService.ts @@ -220,9 +220,6 @@ export class ConnectionStorageService { const secrets = { connectionString: secretsArray[SecretIndex.ConnectionString] ?? '', - // Legacy fields populated from nativeAuth data for backward compatibility - password: nativeAuthPassword, - userName: nativeAuthUser, nativeAuth, entraIdAuth, }; diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index 529711c43..dfdb7586e 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -13,7 +13,6 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { nonNullProp } from '../../utils/nonNull'; -import { type EntraIdAuthConfig } from '../../documentdb/auth/AuthConfig'; import { authMethodFromString, AuthMethodId, authMethodsFromString } from '../../documentdb/auth/AuthMethod'; import { ClustersClient } from '../../documentdb/ClustersClient'; import { CredentialCache } from '../../documentdb/CredentialCache'; @@ -60,10 +59,9 @@ export class DocumentDBClusterItem extends ClusterItemBase implements TreeElemen // Structured auth configs nativeAuthConfig: connectionCredentials.secrets.nativeAuth, entraIdConfig: connectionCredentials.secrets.entraIdAuth - ? ({ - tenantId: connectionCredentials.secrets.entraIdAuth.tenantId ?? '', - // Convert other central auth config fields to local cache format as needed - } as EntraIdAuthConfig) + ? { + tenantId: connectionCredentials.secrets.entraIdAuth.tenantId, + } : undefined, }; } From 58f038a0a52e87744bbd306b23703166d84e93b3 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 22 Sep 2025 10:08:08 +0200 Subject: [PATCH 12/88] fix: native auth + connectionPassword is now optional --- src/documentdb/auth/AuthConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documentdb/auth/AuthConfig.ts b/src/documentdb/auth/AuthConfig.ts index 1c2ca0eb3..e6b37c69b 100644 --- a/src/documentdb/auth/AuthConfig.ts +++ b/src/documentdb/auth/AuthConfig.ts @@ -13,7 +13,7 @@ export interface NativeAuthConfig { readonly connectionUser: string; /** The password for database authentication */ - readonly connectionPassword: string; + readonly connectionPassword?: string; } /** From 3234488b42437ea37198d16ad0cce93ab1250de8 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 22 Sep 2025 10:35:16 +0200 Subject: [PATCH 13/88] chore: keep track if potential custom azure clouds --- src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts | 3 +++ src/plugins/service-azure-mongo-vcore/utils/clusterHelpers.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts b/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts index 05f7270ba..104f51fe1 100644 --- a/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts +++ b/src/plugins/service-azure-mongo-ru/utils/ruClusterHelpers.ts @@ -94,5 +94,8 @@ export async function extractCredentialsFromRUAccount( }, }; + // Add telemetry properties from subscription + context.telemetry.properties.isCustomCloud = subscription.isCustomCloud.toString(); + return clusterCredentials; } diff --git a/src/plugins/service-azure-mongo-vcore/utils/clusterHelpers.ts b/src/plugins/service-azure-mongo-vcore/utils/clusterHelpers.ts index 3c3e36640..951633987 100644 --- a/src/plugins/service-azure-mongo-vcore/utils/clusterHelpers.ts +++ b/src/plugins/service-azure-mongo-vcore/utils/clusterHelpers.ts @@ -114,5 +114,8 @@ export function extractCredentialsFromCluster( }; } + // Add telemetry properties from subscription + context.telemetry.properties.isCustomCloud = subscription.isCustomCloud.toString(); + return credentials; } From 504b8591af89411df8436aca5af51de33e3877b6 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 22 Sep 2025 10:55:43 +0200 Subject: [PATCH 14/88] feat: using the tenantId for Entra ID --- src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts b/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts index 022537516..f52c25f4e 100644 --- a/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts +++ b/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts @@ -21,7 +21,7 @@ export class MicrosoftEntraIDAuthHandler implements AuthHandler { // Get Microsoft Entra ID token const session = await getSessionFromVSCode( ['https://ossrdbms-aad.database.windows.net/.default'], - undefined, // currently, we don't see any requirements for support of scoping by tenantIds + this.clusterCredentials.entraIdConfig?.tenantId, { createIfNone: true, }, From b69b55f639e826d22314795c9bc98f64e77bbc8b Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 22 Sep 2025 11:15:09 +0200 Subject: [PATCH 15/88] feat: using the tenantId for Entra ID --- src/documentdb/CredentialCache.ts | 21 +++++++------------ .../documentdb/MongoRUResourceItem.ts | 3 +-- .../documentdb/DocumentDBResourceItem.ts | 10 +++++++-- .../documentdb/VCoreResourceItem.ts | 10 +++++++-- .../mongo-ru/RUCoreResourceItem.ts | 3 +-- .../connections-view/DocumentDBClusterItem.ts | 9 ++++++-- 6 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/documentdb/CredentialCache.ts b/src/documentdb/CredentialCache.ts index 6cf91e24b..54911f7af 100644 --- a/src/documentdb/CredentialCache.ts +++ b/src/documentdb/CredentialCache.ts @@ -130,8 +130,7 @@ export class CredentialCache { * @param mongoClusterId - The credential id. It's supposed to be the same as the tree item id of the mongo cluster item to simplify the lookup. * @param authMethod - The authentication method/mechanism to be used (e.g. SCRAM, X509, Azure/Entra flows). * @param connectionString - The connection string to which optional credentials will be added. - * @param username - The username to be used for authentication (optional for some auth methods). - * @param password - The password to be used for authentication (optional for some auth methods). + * @param nativeAuthConfig - The native authentication configuration (optional, for username/password auth). * @param emulatorConfiguration - The emulator configuration object (optional, only relevant for local workspace connections). * @param entraIdConfig - The Entra ID configuration object (optional, only relevant for Microsoft Entra ID authentication). */ @@ -139,11 +138,13 @@ export class CredentialCache { mongoClusterId: string, authMethod: AuthMethodIdType, connectionString: string, - username: string = '', - password: string = '', + nativeAuthConfig?: NativeAuthConfig, emulatorConfiguration?: EmulatorConfiguration, entraIdConfig?: EntraIdAuthConfig, ): void { + const username = nativeAuthConfig?.connectionUser ?? ''; + const password = nativeAuthConfig?.connectionPassword ?? ''; + const connectionStringWithPassword = addAuthenticationDataToConnectionString( connectionString, username, @@ -157,16 +158,9 @@ export class CredentialCache { emulatorConfiguration: emulatorConfiguration, authMechanism: authMethod, entraIdConfig: entraIdConfig, + nativeAuthConfig: nativeAuthConfig, }; - // Add native auth config only for non-Entra ID authentication methods - if (authMethod !== AuthMethodId.MicrosoftEntraID && (username || password)) { - credentials.nativeAuthConfig = { - connectionUser: username, - connectionPassword: password, - }; - } - CredentialCache._store.set(mongoClusterId, credentials); } @@ -223,8 +217,7 @@ export class CredentialCache { connectionItem.id, selectedAuthMethod, secrets.connectionString, - username, - password, + username || password ? { connectionUser: username, connectionPassword: password } : undefined, emulatorConfiguration, cacheEntraIdConfig, ); diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts index c9bb94667..ebd5eb41c 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts @@ -81,8 +81,7 @@ export class MongoRUResourceItem extends ClusterItemBase { this.id, credentials.selectedAuthMethod ?? credentials.availableAuthMethods[0], credentials.connectionString, - credentials.nativeAuthConfig?.connectionUser ?? '', - credentials.nativeAuthConfig?.connectionPassword ?? '', + credentials.nativeAuthConfig, ); // Connect using the cached credentials diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts index 6798a5611..bef469a1b 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts @@ -122,8 +122,14 @@ export class DocumentDBResourceItem extends ClusterItemBase { 'DocumentDBResourceItem.ts', ), nonNullValue(credentials.connectionString, 'credentials.connectionString', 'DocumentDBResourceItem.ts'), - wizardContext.selectedUserName, - wizardContext.password, + wizardContext.selectedUserName || wizardContext.password + ? { + connectionUser: wizardContext.selectedUserName ?? '', + connectionPassword: wizardContext.password, + } + : undefined, + undefined, + credentials.entraIdConfig, ); switch (wizardContext.selectedAuthMethod) { diff --git a/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts b/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts index 18b53484a..59f012789 100644 --- a/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts +++ b/src/tree/azure-resources-view/documentdb/VCoreResourceItem.ts @@ -125,8 +125,14 @@ export class VCoreResourceItem extends ClusterItemBase { 'VCoreResourceItem.ts', ), nonNullValue(credentials.connectionString, 'credentials.connectionString', 'VCoreResourceItem.ts'), - wizardContext.selectedUserName, - wizardContext.password, + wizardContext.selectedUserName || wizardContext.password + ? { + connectionUser: wizardContext.selectedUserName ?? '', + connectionPassword: wizardContext.password, + } + : undefined, + undefined, + credentials.entraIdConfig, ); switch (wizardContext.selectedAuthMethod) { diff --git a/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts b/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts index 4c0f12484..df387ece9 100644 --- a/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts +++ b/src/tree/azure-resources-view/mongo-ru/RUCoreResourceItem.ts @@ -84,8 +84,7 @@ export class RUResourceItem extends ClusterItemBase { this.id, credentials.selectedAuthMethod!, nonNullValue(credentials.connectionString, 'credentials.connectionString', 'RUCoreResourceItem.ts'), - credentials.nativeAuthConfig?.connectionUser ?? '', - credentials.nativeAuthConfig?.connectionPassword ?? '', + credentials.nativeAuthConfig, ); ext.outputChannel.append( diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index dfdb7586e..8d6f08ff6 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -207,9 +207,14 @@ export class DocumentDBClusterItem extends ClusterItemBase implements TreeElemen this.id, authMethod, connectionString.toString(), - username, - password, + username || password + ? { + connectionUser: username ?? '', + connectionPassword: password, + } + : undefined, this.cluster.emulatorConfiguration, // workspace items can potentially be connecting to an emulator, so we always pass it + connectionCredentials.secrets.entraIdAuth, ); let clustersClient: ClustersClient; From 25f8f7d2341a1034d23d9a96f38142275eea06dd Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 22 Sep 2025 14:14:37 +0200 Subject: [PATCH 16/88] feat: improved entraId data sharing in the extension --- .../addConnectionFromRegistry.ts | 2 +- src/commands/newConnection/ExecuteStep.ts | 15 +++++++++------ .../newConnection/NewConnectionWizardContext.ts | 7 +++++-- .../newConnection/PromptConnectionStringStep.ts | 12 ++++++++++-- .../newConnection/PromptPasswordStep.ts | 10 +++++++--- .../newConnection/PromptUsernameStep.ts | 8 ++++++-- src/commands/updateCredentials/ExecuteStep.ts | 14 ++++++++++---- .../updateCredentials/PromptPasswordStep.ts | 15 +++++++++++---- .../updateCredentials/PromptUserNameStep.ts | 13 ++++++++++--- .../UpdateCredentialsWizardContext.ts | 7 ++++++- .../authenticate/AuthenticateWizardContext.ts | 5 +++++ .../wizards/authenticate/ProvidePasswordStep.ts | 17 ++++++++++++----- .../wizards/authenticate/ProvideUsernameStep.ts | 13 ++++++++++--- .../discovery-wizard/AzureMongoRUExecuteStep.ts | 3 +-- .../discovery-wizard/AzureExecuteStep.ts | 4 ++-- .../utils/clusterHelpers.ts | 2 +- .../documentdb/VCoreResourceItem.ts | 17 +++++++++++------ 17 files changed, 117 insertions(+), 47 deletions(-) diff --git a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts index 844cf62b1..13529e745 100644 --- a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts +++ b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts @@ -147,8 +147,8 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C properties: { api: API.DocumentDB, availableAuthMethods: credentials.availableAuthMethods }, secrets: { connectionString: parsedCS.toString(), - // Populate nativeAuth configuration nativeAuth: credentials.nativeAuthConfig, + entraIdAuth: credentials.entraIdConfig, }, }; diff --git a/src/commands/newConnection/ExecuteStep.ts b/src/commands/newConnection/ExecuteStep.ts index 1e4b6225d..5fc0a58cc 100644 --- a/src/commands/newConnection/ExecuteStep.ts +++ b/src/commands/newConnection/ExecuteStep.ts @@ -37,8 +37,10 @@ export class ExecuteStep extends AzureWizardExecuteStep this.validateInput(context, password), }); context.valuesToMask.push(password); - context.password = password; + // Update both structured config and legacy field + context.nativeAuth = { + connectionUser: context.nativeAuth?.connectionUser ?? '', + connectionPassword: password, + }; } public shouldPrompt(context: NewConnectionWizardContext): boolean { @@ -41,7 +45,7 @@ export class PromptPasswordStep extends AzureWizardPromptStep this.validateInput(context, username), // eslint-disable-next-line @typescript-eslint/require-await asyncValidationTask: async (username?: string) => { @@ -30,7 +30,11 @@ export class PromptUsernameStep extends AzureWizardPromptStep { const passwordTemp = await context.ui.showInputBox({ prompt: l10n.t('Please enter the password for the user "{username}"', { - username: context.username ?? '', + username: context.nativeAuth?.connectionUser ?? context.username ?? '', }), - value: context.password, + value: context.nativeAuth?.connectionPassword ?? context.password, password: true, ignoreFocusOut: true, }); - context.password = passwordTemp.trim(); - context.valuesToMask.push(context.password); + const trimmedPassword = passwordTemp.trim(); + + // Update both structured config and legacy field + context.nativeAuth = { + connectionUser: context.nativeAuth?.connectionUser ?? context.username ?? '', + connectionPassword: trimmedPassword, + }; + context.password = trimmedPassword; + context.valuesToMask.push(trimmedPassword); } public shouldPrompt(context: UpdateCredentialsWizardContext): boolean { diff --git a/src/commands/updateCredentials/PromptUserNameStep.ts b/src/commands/updateCredentials/PromptUserNameStep.ts index 5a02b5b5f..324c7fe71 100644 --- a/src/commands/updateCredentials/PromptUserNameStep.ts +++ b/src/commands/updateCredentials/PromptUserNameStep.ts @@ -13,12 +13,19 @@ export class PromptUserNameStep extends AzureWizardPromptStep { const username = await context.ui.showInputBox({ prompt: l10n.t('Please enter the username'), - value: context.username, + value: context.nativeAuth?.connectionUser ?? context.username, ignoreFocusOut: true, }); - context.username = username.trim(); - context.valuesToMask.push(context.username, username); + const trimmedUsername = username.trim(); + + // Update both structured config and legacy field + context.nativeAuth = { + connectionUser: trimmedUsername, + connectionPassword: context.nativeAuth?.connectionPassword ?? context.password ?? '', + }; + context.username = trimmedUsername; + context.valuesToMask.push(trimmedUsername, username); } public shouldPrompt(context: UpdateCredentialsWizardContext): boolean { diff --git a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts index ca6d98541..98e141894 100644 --- a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts +++ b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type EntraIdAuthConfig, type NativeAuthConfig } from '../../documentdb/auth/AuthConfig'; import { type AuthMethodId } from '../../documentdb/auth/AuthMethod'; export interface UpdateCredentialsWizardContext extends IActionContext { @@ -13,7 +14,11 @@ export interface UpdateCredentialsWizardContext extends IActionContext { availableAuthenticationMethods: AuthMethodId[]; - // user input + // structured authentication configs + nativeAuth?: NativeAuthConfig; + entraIdAuth?: EntraIdAuthConfig; + + // legacy fields for backward compatibility (deprecated) username?: string; password?: string; selectedAuthenticationMethod?: AuthMethodId; diff --git a/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts b/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts index fff9e836d..8b31058b9 100644 --- a/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts +++ b/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts @@ -5,6 +5,7 @@ import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type EntraIdAuthConfig, type NativeAuthConfig } from '../../auth/AuthConfig'; import { type AuthMethodId } from '../../auth/AuthMethod'; export interface AuthenticateWizardContext extends IActionContext { @@ -20,6 +21,10 @@ export interface AuthenticateWizardContext extends IActionContext { /** These values will be populated by the wizard. */ + // structured authentication configs + nativeAuth?: NativeAuthConfig; + entraIdAuth?: EntraIdAuthConfig; + /** States whether the username was set during the wizard flow. */ isUserNameUpdated?: boolean; selectedUserName?: string; diff --git a/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts b/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts index 366b82291..ec5ff3993 100644 --- a/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts +++ b/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts @@ -13,19 +13,26 @@ export class ProvidePasswordStep extends AzureWizardPromptStep Date: Tue, 23 Sep 2025 06:59:07 +0200 Subject: [PATCH 17/88] removed deprecated properties --- src/commands/updateCredentials/ExecuteStep.ts | 14 ++++---------- .../updateCredentials/PromptPasswordStep.ts | 9 ++++----- .../updateCredentials/PromptUserNameStep.ts | 7 +++---- .../UpdateCredentialsWizardContext.ts | 3 --- .../updateCredentials/updateCredentials.ts | 3 +-- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/commands/updateCredentials/ExecuteStep.ts b/src/commands/updateCredentials/ExecuteStep.ts index dd3f6f2d4..a9472978a 100644 --- a/src/commands/updateCredentials/ExecuteStep.ts +++ b/src/commands/updateCredentials/ExecuteStep.ts @@ -43,17 +43,11 @@ export class ExecuteStep extends AzureWizardExecuteStep { const passwordTemp = await context.ui.showInputBox({ prompt: l10n.t('Please enter the password for the user "{username}"', { - username: context.nativeAuth?.connectionUser ?? context.username ?? '', + username: context.nativeAuth?.connectionUser ?? '', }), - value: context.nativeAuth?.connectionPassword ?? context.password, + value: context.nativeAuth?.connectionPassword, password: true, ignoreFocusOut: true, }); const trimmedPassword = passwordTemp.trim(); - // Update both structured config and legacy field + // Update structured config context.nativeAuth = { - connectionUser: context.nativeAuth?.connectionUser ?? context.username ?? '', + connectionUser: context.nativeAuth?.connectionUser ?? '', connectionPassword: trimmedPassword, }; - context.password = trimmedPassword; context.valuesToMask.push(trimmedPassword); } diff --git a/src/commands/updateCredentials/PromptUserNameStep.ts b/src/commands/updateCredentials/PromptUserNameStep.ts index 324c7fe71..2707358e7 100644 --- a/src/commands/updateCredentials/PromptUserNameStep.ts +++ b/src/commands/updateCredentials/PromptUserNameStep.ts @@ -13,18 +13,17 @@ export class PromptUserNameStep extends AzureWizardPromptStep { const username = await context.ui.showInputBox({ prompt: l10n.t('Please enter the username'), - value: context.nativeAuth?.connectionUser ?? context.username, + value: context.nativeAuth?.connectionUser ?? '', ignoreFocusOut: true, }); const trimmedUsername = username.trim(); - // Update both structured config and legacy field + // Update structured config context.nativeAuth = { connectionUser: trimmedUsername, - connectionPassword: context.nativeAuth?.connectionPassword ?? context.password ?? '', + connectionPassword: context.nativeAuth?.connectionPassword, }; - context.username = trimmedUsername; context.valuesToMask.push(trimmedUsername, username); } diff --git a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts index 98e141894..9bfc05ffb 100644 --- a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts +++ b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts @@ -18,8 +18,5 @@ export interface UpdateCredentialsWizardContext extends IActionContext { nativeAuth?: NativeAuthConfig; entraIdAuth?: EntraIdAuthConfig; - // legacy fields for backward compatibility (deprecated) - username?: string; - password?: string; selectedAuthenticationMethod?: AuthMethodId; } diff --git a/src/commands/updateCredentials/updateCredentials.ts b/src/commands/updateCredentials/updateCredentials.ts index 20065a057..c8d3b3b9e 100644 --- a/src/commands/updateCredentials/updateCredentials.ts +++ b/src/commands/updateCredentials/updateCredentials.ts @@ -52,8 +52,7 @@ export async function updateCredentials(context: IActionContext, node: DocumentD const wizardContext: UpdateCredentialsWizardContext = { ...context, - username: connectionCredentials?.secrets.nativeAuth?.connectionUser, - password: connectionCredentials?.secrets.nativeAuth?.connectionPassword, + nativeAuth: connectionCredentials?.secrets.nativeAuth, availableAuthenticationMethods: authMethodsFromString(supportedAuthMethods), selectedAuthenticationMethod: authMethodFromString(connectionCredentials?.properties.selectedAuthMethod), isEmulator: Boolean(node.cluster.emulatorConfiguration?.isEmulator), From 1de02a9d1b0db0958a8d3db8a67acc9ea1feda08 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 07:22:46 +0200 Subject: [PATCH 18/88] chore: refactoring - unified auth config names in the codebase --- .../addConnectionFromRegistry.ts | 8 +++--- src/commands/newConnection/ExecuteStep.ts | 14 +++++------ .../NewConnectionWizardContext.ts | 4 +-- .../PromptConnectionStringStep.ts | 4 +-- .../newConnection/PromptPasswordStep.ts | 8 +++--- .../newConnection/PromptUsernameStep.ts | 6 ++--- src/commands/updateCredentials/ExecuteStep.ts | 10 ++++---- .../updateCredentials/PromptPasswordStep.ts | 8 +++--- .../updateCredentials/PromptUserNameStep.ts | 6 ++--- .../UpdateCredentialsWizardContext.ts | 6 ++--- .../updateCredentials/updateCredentials.ts | 2 +- src/documentdb/CredentialCache.ts | 14 +++++------ .../authenticate/AuthenticateWizardContext.ts | 6 ++--- .../authenticate/ProvidePasswordStep.ts | 10 ++++---- .../authenticate/ProvideUsernameStep.ts | 6 ++--- .../AzureMongoRUExecuteStep.ts | 2 +- .../documentdb/DocumentDBResourceItem.ts | 2 +- .../discovery-wizard/AzureExecuteStep.ts | 4 +-- .../utils/clusterHelpers.ts | 2 +- src/services/connectionStorageService.ts | 25 ++++++++++--------- .../documentdb/VCoreResourceItem.ts | 8 +++--- .../connections-view/DocumentDBClusterItem.ts | 20 +++++++-------- src/tree/documentdb/ClusterItemBase.ts | 2 +- 23 files changed, 89 insertions(+), 88 deletions(-) diff --git a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts index 13529e745..8ae66db20 100644 --- a/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts +++ b/src/commands/addConnectionFromRegistry/addConnectionFromRegistry.ts @@ -77,8 +77,8 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C const existingDuplicateConnection = existingConnections.find((existingConnection) => { const existingCS = new DocumentDBConnectionString(existingConnection.secrets.connectionString); const existingHostsJoined = [...existingCS.hosts].sort().join(','); - // Use nativeAuth for comparison - const existingUsername = existingConnection.secrets.nativeAuth?.connectionUser; + // Use nativeAuthConfig for comparison + const existingUsername = existingConnection.secrets.nativeAuthConfig?.connectionUser; return existingUsername === username && existingHostsJoined === joinedHosts; }); @@ -147,8 +147,8 @@ export async function addConnectionFromRegistry(context: IActionContext, node: C properties: { api: API.DocumentDB, availableAuthMethods: credentials.availableAuthMethods }, secrets: { connectionString: parsedCS.toString(), - nativeAuth: credentials.nativeAuthConfig, - entraIdAuth: credentials.entraIdConfig, + nativeAuthConfig: credentials.nativeAuthConfig, + entraIdAuthConfig: credentials.entraIdAuthConfig, }, }; diff --git a/src/commands/newConnection/ExecuteStep.ts b/src/commands/newConnection/ExecuteStep.ts index 5fc0a58cc..66d4ae13a 100644 --- a/src/commands/newConnection/ExecuteStep.ts +++ b/src/commands/newConnection/ExecuteStep.ts @@ -38,8 +38,8 @@ export class ExecuteStep extends AzureWizardExecuteStep { const existingCS = new DocumentDBConnectionString(existingConnection.secrets.connectionString); const existingHostsJoined = [...existingCS.hosts].sort().join(','); - // Use nativeAuth for comparison - const existingUsername = existingConnection.secrets.nativeAuth?.connectionUser; + // Use nativeAuthConfig for comparison + const existingUsername = existingConnection.secrets.nativeAuthConfig?.connectionUser; return existingUsername === newUsername && existingHostsJoined === newJoinedHosts; }); @@ -134,15 +134,15 @@ export class ExecuteStep extends AzureWizardExecuteStep this.validateInput(context, password), }); context.valuesToMask.push(password); // Update both structured config and legacy field - context.nativeAuth = { - connectionUser: context.nativeAuth?.connectionUser ?? '', + context.nativeAuthConfig = { + connectionUser: context.nativeAuthConfig?.connectionUser ?? '', connectionPassword: password, }; } @@ -45,7 +45,7 @@ export class PromptPasswordStep extends AzureWizardPromptStep this.validateInput(context, username), // eslint-disable-next-line @typescript-eslint/require-await asyncValidationTask: async (username?: string) => { @@ -31,9 +31,9 @@ export class PromptUsernameStep extends AzureWizardPromptStep { const passwordTemp = await context.ui.showInputBox({ prompt: l10n.t('Please enter the password for the user "{username}"', { - username: context.nativeAuth?.connectionUser ?? '', + username: context.nativeAuthConfig?.connectionUser ?? '', }), - value: context.nativeAuth?.connectionPassword, + value: context.nativeAuthConfig?.connectionPassword, password: true, ignoreFocusOut: true, }); @@ -22,8 +22,8 @@ export class PromptPasswordStep extends AzureWizardPromptStep { const username = await context.ui.showInputBox({ prompt: l10n.t('Please enter the username'), - value: context.nativeAuth?.connectionUser ?? '', + value: context.nativeAuthConfig?.connectionUser ?? '', ignoreFocusOut: true, }); const trimmedUsername = username.trim(); // Update structured config - context.nativeAuth = { + context.nativeAuthConfig = { connectionUser: trimmedUsername, - connectionPassword: context.nativeAuth?.connectionPassword, + connectionPassword: context.nativeAuthConfig?.connectionPassword, }; context.valuesToMask.push(trimmedUsername, username); } diff --git a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts index 9bfc05ffb..66589d8ad 100644 --- a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts +++ b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts @@ -14,9 +14,9 @@ export interface UpdateCredentialsWizardContext extends IActionContext { availableAuthenticationMethods: AuthMethodId[]; - // structured authentication configs - nativeAuth?: NativeAuthConfig; - entraIdAuth?: EntraIdAuthConfig; + // structured authentication configurations + nativeAuthConfig?: NativeAuthConfig; + entraIdAuthConfig?: EntraIdAuthConfig; selectedAuthenticationMethod?: AuthMethodId; } diff --git a/src/commands/updateCredentials/updateCredentials.ts b/src/commands/updateCredentials/updateCredentials.ts index c8d3b3b9e..1fee7770f 100644 --- a/src/commands/updateCredentials/updateCredentials.ts +++ b/src/commands/updateCredentials/updateCredentials.ts @@ -52,7 +52,7 @@ export async function updateCredentials(context: IActionContext, node: DocumentD const wizardContext: UpdateCredentialsWizardContext = { ...context, - nativeAuth: connectionCredentials?.secrets.nativeAuth, + nativeAuthConfig: connectionCredentials?.secrets.nativeAuthConfig, availableAuthenticationMethods: authMethodsFromString(supportedAuthMethods), selectedAuthenticationMethod: authMethodFromString(connectionCredentials?.properties.selectedAuthMethod), isEmulator: Boolean(node.cluster.emulatorConfiguration?.isEmulator), diff --git a/src/documentdb/CredentialCache.ts b/src/documentdb/CredentialCache.ts index 54911f7af..a60460a4c 100644 --- a/src/documentdb/CredentialCache.ts +++ b/src/documentdb/CredentialCache.ts @@ -187,9 +187,9 @@ export class CredentialCache { // Determine auth method if not explicitly provided let selectedAuthMethod = authMethod; if (!selectedAuthMethod) { - if (secrets.entraIdAuth) { + if (secrets.entraIdAuthConfig) { selectedAuthMethod = AuthMethodId.MicrosoftEntraID; - } else if (secrets.nativeAuth) { + } else if (secrets.nativeAuthConfig) { selectedAuthMethod = AuthMethodId.NativeAuth; } else { // Use the selected method from properties or first available method @@ -202,15 +202,15 @@ export class CredentialCache { // Convert central auth configs to local cache format let cacheEntraIdConfig: EntraIdAuthConfig | undefined; - if (secrets.entraIdAuth) { + if (secrets.entraIdAuthConfig) { cacheEntraIdConfig = { - tenantId: secrets.entraIdAuth.tenantId ?? '', // Convert optional to required for backward compatibility + tenantId: secrets.entraIdAuthConfig.tenantId ?? '', // Convert optional to required for backward compatibility }; } - // Use structured configs - const username = secrets.nativeAuth?.connectionUser ?? ''; - const password = secrets.nativeAuth?.connectionPassword ?? ''; + // Use structured configurations + const username = secrets.nativeAuthConfig?.connectionUser ?? ''; + const password = secrets.nativeAuthConfig?.connectionPassword ?? ''; // Use the existing setAuthCredentials method to ensure consistent behavior CredentialCache.setAuthCredentials( diff --git a/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts b/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts index 8b31058b9..bceecaf2e 100644 --- a/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts +++ b/src/documentdb/wizards/authenticate/AuthenticateWizardContext.ts @@ -21,9 +21,9 @@ export interface AuthenticateWizardContext extends IActionContext { /** These values will be populated by the wizard. */ - // structured authentication configs - nativeAuth?: NativeAuthConfig; - entraIdAuth?: EntraIdAuthConfig; + // structured authentication configurations + nativeAuthConfig?: NativeAuthConfig; + entraIdAuthConfig?: EntraIdAuthConfig; /** States whether the username was set during the wizard flow. */ isUserNameUpdated?: boolean; diff --git a/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts b/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts index ec5ff3993..b5e405fa6 100644 --- a/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts +++ b/src/documentdb/wizards/authenticate/ProvidePasswordStep.ts @@ -13,13 +13,13 @@ export class ProvidePasswordStep extends AzureWizardPromptStep Date: Tue, 23 Sep 2025 07:37:01 +0200 Subject: [PATCH 19/88] feat: entraid, persisiting subscriptionId --- src/services/connectionStorageService.ts | 26 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/services/connectionStorageService.ts b/src/services/connectionStorageService.ts index 4a9d4c8a9..ff9101af5 100644 --- a/src/services/connectionStorageService.ts +++ b/src/services/connectionStorageService.ts @@ -84,6 +84,7 @@ const enum SecretIndex { NativeAuthConnectionPassword = 2, // Entra ID auth config fields EntraIdTenantId = 3, + EntraIdSubscriptionId = 4, } /** @@ -176,8 +177,13 @@ export class ConnectionStorageService { } // Store Entra ID auth config fields individually - if (item.secrets.entraIdAuthConfig && item.secrets.entraIdAuthConfig.tenantId) { - secretsArray[SecretIndex.EntraIdTenantId] = item.secrets.entraIdAuthConfig.tenantId; + if (item.secrets.entraIdAuthConfig) { + if (item.secrets.entraIdAuthConfig.tenantId) { + secretsArray[SecretIndex.EntraIdTenantId] = item.secrets.entraIdAuthConfig.tenantId; + } + if (item.secrets.entraIdAuthConfig.subscriptionId) { + secretsArray[SecretIndex.EntraIdSubscriptionId] = item.secrets.entraIdAuthConfig.subscriptionId; + } } } @@ -198,31 +204,33 @@ export class ConnectionStorageService { const secretsArray = item.secrets ?? []; // Reconstruct native auth config from individual fields - let nativeAuth: NativeAuthConfig | undefined; + let nativeAuthConfig: NativeAuthConfig | undefined; const nativeAuthUser = secretsArray[SecretIndex.NativeAuthConnectionUser]; const nativeAuthPassword = secretsArray[SecretIndex.NativeAuthConnectionPassword]; if (nativeAuthUser) { - nativeAuth = { + nativeAuthConfig = { connectionUser: nativeAuthUser, connectionPassword: nativeAuthPassword, }; } // Reconstruct Entra ID auth config from individual fields - let entraIdAuth: EntraIdAuthConfig | undefined; + let entraIdAuthConfig: EntraIdAuthConfig | undefined; const entraIdTenantId = secretsArray[SecretIndex.EntraIdTenantId]; + const entraIdSubscriptionId = secretsArray[SecretIndex.EntraIdSubscriptionId]; - if (entraIdTenantId) { - entraIdAuth = { + if (entraIdTenantId || entraIdSubscriptionId) { + entraIdAuthConfig = { tenantId: entraIdTenantId, + subscriptionId: entraIdSubscriptionId, }; } const secrets = { connectionString: secretsArray[SecretIndex.ConnectionString] ?? '', - nativeAuth, - entraIdAuth, + nativeAuthConfig: nativeAuthConfig, + entraIdAuthConfig: entraIdAuthConfig, }; return { From 9ad4bd0812df7848200897559082e2ac48bc7254 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 09:18:40 +0200 Subject: [PATCH 20/88] chore; comments. Update src/documentdb/auth/AuthConfig.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/documentdb/auth/AuthConfig.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/documentdb/auth/AuthConfig.ts b/src/documentdb/auth/AuthConfig.ts index e6b37c69b..d7efebb5a 100644 --- a/src/documentdb/auth/AuthConfig.ts +++ b/src/documentdb/auth/AuthConfig.ts @@ -28,6 +28,13 @@ export interface EntraIdAuthConfig { * This flexibility supports both single-tenant and multi-tenant scenarios. */ readonly tenantId?: string; + /** + * The Azure subscription ID associated with the authentication context. + * This is typically required when performing operations that are scoped to a specific Azure subscription, + * such as resource management or billing. While `tenantId` identifies the Azure Active Directory tenant, + * `subscriptionId` specifies the particular subscription within that tenant. + * This field is optional and may not be needed for all authentication scenarios. + */ readonly subscriptionId?: string; /** From eee4e687cb751d6ac8983283e5ac6ffdbdd5401d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 09:18:57 +0200 Subject: [PATCH 21/88] chore. Update src/documentdb/CredentialCache.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/documentdb/CredentialCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documentdb/CredentialCache.ts b/src/documentdb/CredentialCache.ts index a60460a4c..1d5199b0c 100644 --- a/src/documentdb/CredentialCache.ts +++ b/src/documentdb/CredentialCache.ts @@ -204,7 +204,7 @@ export class CredentialCache { let cacheEntraIdConfig: EntraIdAuthConfig | undefined; if (secrets.entraIdAuthConfig) { cacheEntraIdConfig = { - tenantId: secrets.entraIdAuthConfig.tenantId ?? '', // Convert optional to required for backward compatibility + tenantId: secrets.entraIdAuthConfig.tenantId, // Preserve optional nature for backward compatibility }; } From 91be0d867f93d120e7647e714e93c55a8b6f838a Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 09:19:37 +0200 Subject: [PATCH 22/88] Update src/tree/connections-view/DocumentDBClusterItem.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/tree/connections-view/DocumentDBClusterItem.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tree/connections-view/DocumentDBClusterItem.ts b/src/tree/connections-view/DocumentDBClusterItem.ts index a9bd6a916..b44d92a13 100644 --- a/src/tree/connections-view/DocumentDBClusterItem.ts +++ b/src/tree/connections-view/DocumentDBClusterItem.ts @@ -207,9 +207,9 @@ export class DocumentDBClusterItem extends ClusterItemBase implements TreeElemen this.id, authMethod, connectionString.toString(), - username || password + username && password ? { - connectionUser: username ?? '', + connectionUser: username, connectionPassword: password, } : undefined, From 1b0a583d3137b9467b5e319a937e981dea45317d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 10:03:18 +0200 Subject: [PATCH 23/88] feat: showing tenant names in the subscription list --- l10n/bundle.l10n.json | 3 +++ .../AzureMongoRUServiceRootItem.ts | 8 ++++++++ .../AzureMongoRUSubscriptionItem.ts | 18 +++++++++++++++++- .../discovery-tree/AzureServiceRootItem.ts | 8 ++++++++ .../discovery-tree/AzureSubscriptionItem.ts | 18 +++++++++++++++++- .../discovery-tree/AzureServiceRootItem.ts | 8 ++++++++ .../discovery-tree/AzureSubscriptionItem.ts | 19 +++++++++++++++++-- 7 files changed, 78 insertions(+), 4 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index a55a2f9d0..dda070615 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -380,6 +380,7 @@ "Starting executable: \"{command}\"": "Starting executable: \"{command}\"", "Starts with mongodb:// or mongodb+srv://": "Starts with mongodb:// or mongodb+srv://", "subscription": "subscription", + "Subscription ID: {0}": "Subscription ID: {0}", "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}\".", @@ -389,6 +390,8 @@ "Tag can only contain alphanumeric characters, underscores, periods, and hyphens.": "Tag can only contain alphanumeric characters, underscores, periods, and hyphens.", "Tag cannot be empty.": "Tag cannot be empty.", "Tag cannot be longer than 256 characters.": "Tag cannot be longer than 256 characters.", + "Tenant ID: {0}": "Tenant ID: {0}", + "Tenant Name: {0}": "Tenant Name: {0}", "The \"{databaseId}\" database has been deleted.": "The \"{databaseId}\" database has been deleted.", "The \"{name}\" database has been created.": "The \"{name}\" database has been created.", "The \"{newCollectionName}\" collection has been created.": "The \"{newCollectionName}\" collection has been created.", diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts index 5a60b20cf..105fa5163 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts @@ -61,6 +61,13 @@ export class AzureMongoRUServiceRootItem return []; } + // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' + // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however + // this lead to incorrect responses from getSubscriptions. We didn't investigate + const tenantPromise = this.azureSubscriptionProvider.getTenants().catch(() => undefined); + const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); + const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + return ( subscriptions // sort by name @@ -71,6 +78,7 @@ export class AzureMongoRUServiceRootItem subscription: sub, subscriptionName: sub.name, subscriptionId: sub.subscriptionId, + tenant: knownTenants?.find((tenant) => tenant.tenantId === sub.tenantId), }); }) ); diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts index a6cdd22db..95555c80d 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; import { getResourceGroupFromId, uiUtils } from '@microsoft/vscode-azext-azureutils'; import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; @@ -20,6 +21,7 @@ export interface AzureSubscriptionModel { subscriptionName: string; subscription: AzureSubscription; subscriptionId: string; + tenant?: AzureTenant; } export class AzureMongoRUSubscriptionItem implements TreeElement, TreeElementWithContextValue { @@ -61,11 +63,25 @@ export class AzureMongoRUSubscriptionItem implements TreeElement, TreeElementWit } public getTreeItem(): vscode.TreeItem { + const tooltipParts: string[] = [vscode.l10n.t('Subscription ID: {0}', this.subscription.subscriptionId), '']; + + const tenantName = this.subscription.tenant?.displayName; + if (tenantName) { + tooltipParts.push(vscode.l10n.t('Tenant Name: {0}', tenantName)); + } + + const tenantId = this.subscription.subscription.tenantId; + if (tenantId) { + tooltipParts.push(vscode.l10n.t('Tenant ID: {0}', tenantId)); + } + + const tooltip: string = tooltipParts.join('\n'); + return { id: this.id, contextValue: this.contextValue, label: this.subscription.subscriptionName, - tooltip: `Subscription ID: ${this.subscription.subscriptionId}`, + tooltip, iconPath: vscode.Uri.joinPath( ext.context.extensionUri, 'resources', diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts index c150280cd..bf58463ef 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts @@ -60,6 +60,13 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext return []; } + // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' + // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however + // this lead to incorrect responses from getSubscriptions. We didn't investigate + const tenantPromise = this.azureSubscriptionProvider.getTenants().catch(() => undefined); + const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); + const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + return ( subscriptions // sort by name @@ -70,6 +77,7 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext subscription: sub, subscriptionName: sub.name, subscriptionId: sub.subscriptionId, + tenant: knownTenants?.find((tenant) => tenant.tenantId === sub.tenantId), }); }) ); diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts index e6687fc4b..1a0bc27d7 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureSubscriptionItem.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; import { getResourceGroupFromId, uiUtils } from '@microsoft/vscode-azext-azureutils'; import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; @@ -20,6 +21,7 @@ export interface AzureSubscriptionModel { subscriptionName: string; subscription: AzureSubscription; subscriptionId: string; + tenant?: AzureTenant; } export class AzureSubscriptionItem implements TreeElement, TreeElementWithContextValue { @@ -61,11 +63,25 @@ export class AzureSubscriptionItem implements TreeElement, TreeElementWithContex } public getTreeItem(): vscode.TreeItem { + const tooltipParts: string[] = [vscode.l10n.t('Subscription ID: {0}', this.subscription.subscriptionId), '']; + + const tenantName = this.subscription.tenant?.displayName; + if (tenantName) { + tooltipParts.push(vscode.l10n.t('Tenant Name: {0}', tenantName)); + } + + const tenantId = this.subscription.subscription.tenantId; + if (tenantId) { + tooltipParts.push(vscode.l10n.t('Tenant ID: {0}', tenantId)); + } + + const tooltip: string = tooltipParts.join('\n'); + return { id: this.id, contextValue: this.contextValue, label: this.subscription.subscriptionName, - tooltip: `Subscription ID: ${this.subscription.subscriptionId}`, + tooltip, iconPath: vscode.Uri.joinPath( ext.context.extensionUri, 'resources', diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts index d4bb0eb26..7253e6dd1 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts @@ -60,6 +60,13 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext return []; } + // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' + // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however + // this lead to incorrect responses from getSubscriptions. We didn't investigate + const tenantPromise = this.azureSubscriptionProvider.getTenants().catch(() => undefined); + const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); + const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + return ( subscriptions // sort by name @@ -70,6 +77,7 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext subscription: sub, subscriptionName: sub.name, subscriptionId: sub.subscriptionId, + tenant: knownTenants?.find((tenant) => tenant.tenantId === sub.tenantId), }); }) ); diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts index 96c1f67c6..57a037572 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureSubscriptionItem.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; import { getResourceGroupFromId, uiUtils } from '@microsoft/vscode-azext-azureutils'; import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; @@ -20,6 +21,7 @@ export interface AzureSubscriptionModel { subscriptionName: string; subscription: AzureSubscription; subscriptionId: string; + tenant?: AzureTenant; } export class AzureSubscriptionItem implements TreeElement, TreeElementWithContextValue { @@ -48,7 +50,6 @@ export class AzureSubscriptionItem implements TreeElement, TreeElementWithContex const vmItems: AzureVMResourceItem[] = []; for (const vm of vms) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (vm.tags && vm.tags[tagName] !== undefined && vm.id && vm.name) { let publicIpAddress: string | undefined; let fqdn: string | undefined; @@ -118,11 +119,25 @@ export class AzureSubscriptionItem implements TreeElement, TreeElementWithContex } public getTreeItem(): vscode.TreeItem { + const tooltipParts: string[] = [vscode.l10n.t('Subscription ID: {0}', this.subscription.subscriptionId), '']; + + const tenantName = this.subscription.tenant?.displayName; + if (tenantName) { + tooltipParts.push(vscode.l10n.t('Tenant Name: {0}', tenantName)); + } + + const tenantId = this.subscription.subscription.tenantId; + if (tenantId) { + tooltipParts.push(vscode.l10n.t('Tenant ID: {0}', tenantId)); + } + + const tooltip: string = tooltipParts.join('\n'); + return { id: this.id, contextValue: this.contextValue, label: this.subscription.subscriptionName, - tooltip: `Subscription ID: ${this.subscription.subscriptionId}`, + tooltip, iconPath: vscode.Uri.joinPath( ext.context.extensionUri, 'resources', From a9ca7cf5d500e992aab89665665903cd1f457245 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 10:03:18 +0200 Subject: [PATCH 24/88] feat: showing tenant names in the subscription list # Conflicts: # src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts # src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts # src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts --- .../azure/wizard/SelectSubscriptionStep.ts | 41 +++++++-- .../AzureMongoRUDiscoveryProvider.ts | 2 +- .../AzureMongoRUServiceRootItem.ts | 14 ++- .../AzureDiscoveryProvider.ts | 2 +- .../discovery-tree/AzureServiceRootItem.ts | 14 ++- .../AzureVMDiscoveryProvider.ts | 2 +- .../discovery-tree/AzureServiceRootItem.ts | 14 ++- .../SelectSubscriptionStep.ts | 87 ------------------- 8 files changed, 71 insertions(+), 105 deletions(-) delete mode 100644 src/plugins/service-azure-vm/discovery-wizard/SelectSubscriptionStep.ts diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 54bbe9a9d..885631d8d 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -55,23 +55,46 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep undefined); + const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); + const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + + // Build tenant display name lookup for better UX + const tenantDisplayNames = new Map(); + + if (knownTenants) { + for (const tenant of knownTenants) { + if (tenant.tenantId && tenant.displayName) { + tenantDisplayNames.set(tenant.tenantId, tenant.displayName); + } + } + } + const promptItems: (QuickPickItem & { id: string })[] = subscriptions - .map((subscription) => ({ - id: subscription.subscriptionId, - label: subscription.name, - description: subscription.subscriptionId, - iconPath: this.iconPath, + .map((subscription) => { + const tenantName = tenantDisplayNames.get(subscription.tenantId); + const description = tenantName + ? `${subscription.subscriptionId} (${tenantName})` + : subscription.subscriptionId; - alwaysShow: true, - })) - // Sort alphabetically + return { + id: subscription.subscriptionId, + label: subscription.name, + description, + iconPath: this.iconPath, + alwaysShow: true, + }; + }) .sort((a, b) => a.label.localeCompare(b.label)); const selectedItem = await context.ui.showQuickPick([...promptItems], { stepName: 'selectSubscription', placeHolder: l10n.t('Choose a subscription…'), loadingPlaceHolder: l10n.t('Loading subscriptions…'), - enableGrouping: true, + enableGrouping: false, matchOnDescription: true, suppressPersistence: true, }); diff --git a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts index 34211b5bd..b14584c42 100644 --- a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts @@ -12,7 +12,7 @@ import { type TreeElement } from '../../tree/TreeElement'; import { AzureSubscriptionProviderWithFilters } from '../api-shared/azure/AzureSubscriptionProviderWithFilters'; import { configureAzureSubscriptionFilter } from '../api-shared/azure/subscriptionFiltering'; import { AzureContextProperties } from '../api-shared/azure/wizard/AzureContextProperties'; -import { SelectSubscriptionStep } from '../service-azure-vm/discovery-wizard/SelectSubscriptionStep'; +import { SelectSubscriptionStep } from '../api-shared/azure/wizard/SelectSubscriptionStep'; import { AzureMongoRUServiceRootItem } from './discovery-tree/AzureMongoRUServiceRootItem'; import { AzureMongoRUExecuteStep } from './discovery-wizard/AzureMongoRUExecuteStep'; import { SelectRUClusterStep } from './discovery-wizard/SelectRUClusterStep'; diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts index 105fa5163..821e7db2d 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { type AzureTenant, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../extensionVariables'; @@ -68,6 +68,16 @@ export class AzureMongoRUServiceRootItem const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + // Build tenant lookup for better performance + const tenantMap = new Map(); + if (knownTenants) { + for (const tenant of knownTenants) { + if (tenant.tenantId) { + tenantMap.set(tenant.tenantId, tenant); + } + } + } + return ( subscriptions // sort by name @@ -78,7 +88,7 @@ export class AzureMongoRUServiceRootItem subscription: sub, subscriptionName: sub.name, subscriptionId: sub.subscriptionId, - tenant: knownTenants?.find((tenant) => tenant.tenantId === sub.tenantId), + tenant: tenantMap.get(sub.tenantId), }); }) ); diff --git a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts index a58633d62..b2665d478 100644 --- a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts @@ -12,7 +12,7 @@ import { type TreeElement } from '../../tree/TreeElement'; import { AzureSubscriptionProviderWithFilters } from '../api-shared/azure/AzureSubscriptionProviderWithFilters'; import { configureAzureSubscriptionFilter } from '../api-shared/azure/subscriptionFiltering'; import { AzureContextProperties } from '../api-shared/azure/wizard/AzureContextProperties'; -import { SelectSubscriptionStep } from '../service-azure-vm/discovery-wizard/SelectSubscriptionStep'; +import { SelectSubscriptionStep } from '../api-shared/azure/wizard/SelectSubscriptionStep'; import { AzureServiceRootItem } from './discovery-tree/AzureServiceRootItem'; import { AzureExecuteStep } from './discovery-wizard/AzureExecuteStep'; import { SelectClusterStep } from './discovery-wizard/SelectClusterStep'; diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts index bf58463ef..ea48e32dc 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { type AzureTenant, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../extensionVariables'; @@ -67,6 +67,16 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + // Build tenant lookup for better performance + const tenantMap = new Map(); + if (knownTenants) { + for (const tenant of knownTenants) { + if (tenant.tenantId) { + tenantMap.set(tenant.tenantId, tenant); + } + } + } + return ( subscriptions // sort by name @@ -77,7 +87,7 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext subscription: sub, subscriptionName: sub.name, subscriptionId: sub.subscriptionId, - tenant: knownTenants?.find((tenant) => tenant.tenantId === sub.tenantId), + tenant: tenantMap.get(sub.tenantId), }); }) ); diff --git a/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts b/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts index d9b1a3f6a..b8e10e34e 100644 --- a/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts +++ b/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts @@ -10,11 +10,11 @@ import { ext } from '../../extensionVariables'; import { type DiscoveryProvider } from '../../services/discoveryServices'; import { type TreeElement } from '../../tree/TreeElement'; import { AzureSubscriptionProviderWithFilters } from '../api-shared/azure/AzureSubscriptionProviderWithFilters'; +import { SelectSubscriptionStep } from '../api-shared/azure/wizard/SelectSubscriptionStep'; import { AzureServiceRootItem } from './discovery-tree/AzureServiceRootItem'; import { configureVmFilter } from './discovery-tree/configureVmFilterWizard'; import { AzureVMExecuteStep } from './discovery-wizard/AzureVMExecuteStep'; import { SelectPortStep } from './discovery-wizard/SelectPortStep'; -import { SelectSubscriptionStep } from './discovery-wizard/SelectSubscriptionStep'; import { SelectTagStep } from './discovery-wizard/SelectTagStep'; import { SelectVMStep } from './discovery-wizard/SelectVMStep'; diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts index 7253e6dd1..fdaf17e15 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { type AzureTenant, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../extensionVariables'; @@ -67,6 +67,16 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + // Build tenant lookup for better performance + const tenantMap = new Map(); + if (knownTenants) { + for (const tenant of knownTenants) { + if (tenant.tenantId) { + tenantMap.set(tenant.tenantId, tenant); + } + } + } + return ( subscriptions // sort by name @@ -77,7 +87,7 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext subscription: sub, subscriptionName: sub.name, subscriptionId: sub.subscriptionId, - tenant: knownTenants?.find((tenant) => tenant.tenantId === sub.tenantId), + tenant: tenantMap.get(sub.tenantId), }); }) ); diff --git a/src/plugins/service-azure-vm/discovery-wizard/SelectSubscriptionStep.ts b/src/plugins/service-azure-vm/discovery-wizard/SelectSubscriptionStep.ts deleted file mode 100644 index 96eacec1a..000000000 --- a/src/plugins/service-azure-vm/discovery-wizard/SelectSubscriptionStep.ts +++ /dev/null @@ -1,87 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; -import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import { Uri, window, type MessageItem, type QuickPickItem } from 'vscode'; -import { type NewConnectionWizardContext } from '../../../commands/newConnection/NewConnectionWizardContext'; -import { ext } from '../../../extensionVariables'; -import { AzureContextProperties } from '../../api-shared/azure/wizard/AzureContextProperties'; - -export class SelectSubscriptionStep extends AzureWizardPromptStep { - iconPath = Uri.joinPath( - ext.context.extensionUri, - 'resources', - 'from_node_modules', - '@microsoft', - 'vscode-azext-azureutils', - 'resources', - 'azureSubscription.svg', - ); - - public async prompt(context: NewConnectionWizardContext): Promise { - if ( - context.properties[AzureContextProperties.AzureSubscriptionProvider] === undefined || - !( - context.properties[AzureContextProperties.AzureSubscriptionProvider] instanceof - VSCodeAzureSubscriptionProvider - ) - ) { - throw new Error('ServiceDiscoveryProvider is not set or is not of the correct type.'); - } - - const subscriptionProvider = context.properties[ - AzureContextProperties.AzureSubscriptionProvider - ] as VSCodeAzureSubscriptionProvider; - - /** - * This is an important step to ensure that the user is signed in to Azure before listing subscriptions. - */ - if (!(await subscriptionProvider.isSignedIn())) { - const signIn: MessageItem = { title: l10n.t('Sign In') }; - void window - .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) - .then((input) => { - if (input === signIn) { - void subscriptionProvider.signIn(); - } - }); - - throw new UserCancelledError(l10n.t('User is not signed in to Azure.')); - } - - const subscriptions = await subscriptionProvider.getSubscriptions(false); - - const promptItems: (QuickPickItem & { id: string })[] = subscriptions - .map((subscription) => ({ - id: subscription.subscriptionId, - label: subscription.name, - description: subscription.subscriptionId, - iconPath: this.iconPath, - - alwaysShow: true, - })) - // Sort alphabetically - .sort((a, b) => a.label.localeCompare(b.label)); - - const selectedItem = await context.ui.showQuickPick([...promptItems], { - stepName: 'selectSubscription', - placeHolder: l10n.t('Choose a subscription…'), - loadingPlaceHolder: l10n.t('Loading subscriptions…'), - enableGrouping: true, - matchOnDescription: true, - suppressPersistence: true, - }); - - context.properties[AzureContextProperties.SelectedSubscription] = subscriptions.find( - (subscription) => subscription.subscriptionId === selectedItem.id, - ); - } - - public shouldPrompt(): boolean { - return true; - } -} From 9c20fada337c0afee08ba770c8ab0992718eff73 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 14:16:08 +0200 Subject: [PATCH 25/88] wip: discovery, manage credentials, prepared framework --- package.json | 46 ++++++++++++++---- .../manageCredentials.ts | 48 +++++++++++++++++++ src/documentdb/ClustersExtension.ts | 6 +++ .../discovery-tree/AzureServiceRootItem.ts | 2 +- src/services/discoveryServices.ts | 2 + 5 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 src/commands/discoveryService.manageCredentials/manageCredentials.ts diff --git a/package.json b/package.json index cc15e2f85..eacd34fa5 100644 --- a/package.json +++ b/package.json @@ -327,6 +327,13 @@ "title": "Save To DocumentDB Connections", "icon": "$(save)" }, + { + "//": "[DiscoveryView] Content Provider: Manage Credentials", + "category": "DocumentDB", + "command": "vscode-documentdb.command.discoveryView.manageCredentials", + "title": "Manage Credentials…", + "icon": "$(organization)" + }, { "//": "[DiscoveryView] Filter Provider Content", "category": "DocumentDB", @@ -537,6 +544,11 @@ "when": "view == connectionsView && viewItem =~ /\\btreeitem_documentdbcluster\\b/i", "group": "0@5" }, + { + "command": "vscode-documentdb.command.discoveryView.addConnectionToConnectionsView", + "when": "view == discoveryView && viewItem =~ /\\btreeitem_documentdbcluster\\b/i", + "group": "0@1" + }, { "command": "vscode-documentdb.command.discoveryView.removeRegistry", "when": "view == discoveryView && viewItem =~ /\\brootItem\\b/i", @@ -548,29 +560,39 @@ "group": "0@1" }, { - "command": "vscode-documentdb.command.discoveryView.addConnectionToConnectionsView", - "when": "view == discoveryView && viewItem =~ /\\btreeitem_documentdbcluster\\b/i", - "group": "inline" + "command": "vscode-documentdb.command.discoveryView.manageCredentials", + "when": "view == discoveryView && viewItem =~ /\\benableManageCredentialsCommand\\b/i", + "group": "inline@1" }, { - "command": "vscode-documentdb.command.discoveryView.learnMoreAboutProvider", - "when": "view == discoveryView && viewItem =~ /\\benableLearnMoreCommand\\b/i", - "group": "1@3" + "command": "vscode-documentdb.command.discoveryView.manageCredentials", + "when": "view == discoveryView && viewItem =~ /\\benableManageCredentialsCommand\\b/i", + "group": "1@2" }, { - "command": "vscode-documentdb.command.discoveryView.learnMoreAboutProvider", - "when": "view == discoveryView && viewItem =~ /\\benableLearnMoreCommand\\b/i", + "command": "vscode-documentdb.command.discoveryView.addConnectionToConnectionsView", + "when": "view == discoveryView && viewItem =~ /\\btreeitem_documentdbcluster\\b/i", "group": "inline" }, { "command": "vscode-documentdb.command.discoveryView.filterProviderContent", "when": "view == discoveryView && viewItem =~ /\\benableFilterCommand\\b/i", - "group": "1@2" + "group": "1@3" }, { "command": "vscode-documentdb.command.discoveryView.filterProviderContent", "when": "view == discoveryView && viewItem =~ /\\benableFilterCommand\\b/i", - "group": "inline" + "group": "inline@2" + }, + { + "command": "vscode-documentdb.command.discoveryView.learnMoreAboutProvider", + "when": "view == discoveryView && viewItem =~ /\\benableLearnMoreCommand\\b/i", + "group": "1@4" + }, + { + "command": "vscode-documentdb.command.discoveryView.learnMoreAboutProvider", + "when": "view == discoveryView && viewItem =~ /\\benableLearnMoreCommand\\b/i", + "group": "inline@3" }, { "command": "vscode-documentdb.command.azureResourcesView.addConnectionToConnectionsView", @@ -716,6 +738,10 @@ "command": "vscode-documentdb.command.discoveryView.addConnectionToConnectionsView", "when": "never" }, + { + "command": "vscode-documentdb.command.discoveryView.manageCredentials", + "when": "never" + }, { "command": "vscode-documentdb.command.discoveryView.filterProviderContent", "when": "never" diff --git a/src/commands/discoveryService.manageCredentials/manageCredentials.ts b/src/commands/discoveryService.manageCredentials/manageCredentials.ts new file mode 100644 index 000000000..751af34ac --- /dev/null +++ b/src/commands/discoveryService.manageCredentials/manageCredentials.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { l10n } from 'vscode'; +import { Views } from '../../documentdb/Views'; +import { ext } from '../../extensionVariables'; +import { DiscoveryService } from '../../services/discoveryServices'; +import { type TreeElement } from '../../tree/TreeElement'; + +export async function manageCredentials(context: IActionContext, node: TreeElement): Promise { + if (!node) { + throw new Error(l10n.t('No node selected.')); + } + /** + * We can extract the provider id from the node instead of hardcoding it + * by accessing the node.id and looking from the start for the id in the following format + * + * node.id = '${Views.DiscoveryView}//potential/elements/thisNodesId' + * + * first, we'll verify that the id is in the format expected, if not, we'll return with an error + */ + + const idSections = node.id.split('/'); + const isValidFormat = + idSections.length >= 2 && idSections[0] === String(Views.DiscoveryView) && idSections[1].length > 0; + + if (!isValidFormat) { + ext.outputChannel.error('Internal error: Node id is not in the expected format.'); + return; + } + + const providerId = idSections[1]; + const provider = DiscoveryService.getProvider(providerId); + + if (!provider?.configureCredentials) { + ext.outputChannel.error(`No management function provided by the provider with the id "${providerId}".`); + return; + } + + // Call the filter function provided by the provider + await provider.configureCredentials(context, node as TreeElement); + + // Refresh the discovery branch data provider to show the updated list + ext.discoveryBranchDataProvider.refresh(node as TreeElement); +} diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 248b7a2e6..c963827c6 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -27,6 +27,7 @@ import { createAzureDatabase } from '../commands/createDatabase/createDatabase'; import { createMongoDocument } from '../commands/createDocument/createDocument'; import { deleteCollection } from '../commands/deleteCollection/deleteCollection'; import { deleteAzureDatabase } from '../commands/deleteDatabase/deleteDatabase'; +import { manageCredentials } from '../commands/discoveryService.manageCredentials/manageCredentials'; import { exportEntireCollection, exportQueryResults } from '../commands/exportDocuments/exportDocuments'; import { filterProviderContent } from '../commands/filterProviderContent/filterProviderContent'; import { importDocuments } from '../commands/importDocuments/importDocuments'; @@ -205,6 +206,11 @@ export class ClustersExtension implements vscode.Disposable { filterProviderContent, ); + registerCommandWithTreeNodeUnwrapping( + 'vscode-documentdb.command.discoveryView.manageCredentials', + manageCredentials, + ); + registerCommandWithTreeNodeUnwrapping( 'vscode-documentdb.command.discoveryView.learnMoreAboutProvider', learnMoreAboutServiceProvider, diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts index ea48e32dc..2e95ce46d 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts @@ -19,7 +19,7 @@ import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { public readonly id: string; public contextValue: string = - 'enableRefreshCommand;enableFilterCommand;enableLearnMoreCommand;discoveryAzureServiceRootItem'; + 'enableRefreshCommand;enableManageCredentialsCommand;enableFilterCommand;enableLearnMoreCommand;discoveryAzureServiceRootItem'; constructor( private readonly azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, diff --git a/src/services/discoveryServices.ts b/src/services/discoveryServices.ts index 0716fb0f1..4a354523d 100644 --- a/src/services/discoveryServices.ts +++ b/src/services/discoveryServices.ts @@ -55,6 +55,8 @@ export interface DiscoveryProvider extends ProviderDescription { getLearnMoreUrl?(): string | undefined; configureTreeItemFilter?(context: IActionContext, node: TreeElement): Promise; + + configureCredentials?(context: IActionContext, node: TreeElement): Promise; } /** From cdaf8fc99b7f0f4fadaa2121a017594a84c3cd01 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 18:13:52 +0200 Subject: [PATCH 26/88] wip: multiuser+multitenant support in azure discovery --- .../AzureSubscriptionProviderWithFilters.ts | 24 ++- .../CredentialsManagementWizardContext.ts | 26 +++ .../credentialsManagement/ExecuteStep.ts | 62 +++++++ .../SelectAccountStep.ts | 145 +++++++++++++++ .../SelectTenantsStep.ts | 146 +++++++++++++++ .../configureAzureCredentials.ts | 170 ++++++++++++++++++ .../azure/credentialsManagement/index.ts | 14 ++ .../api-shared/azure/subscriptionFiltering.ts | 22 +++ .../azure/wizard/AzureContextProperties.ts | 2 + .../AzureDiscoveryProvider.ts | 9 + 10 files changed, 614 insertions(+), 6 deletions(-) create mode 100644 src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts create mode 100644 src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts create mode 100644 src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts create mode 100644 src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts create mode 100644 src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts create mode 100644 src/plugins/api-shared/azure/credentialsManagement/index.ts diff --git a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts index 02f87e830..289436e45 100644 --- a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts +++ b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts @@ -15,7 +15,7 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio super(logger); } - private async getTenantAndSubscriptionFilters(): Promise { + private getTenantAndSubscriptionFilters(): string[] { // Try the Azure Resource Groups config first const config = vscode.workspace.getConfiguration('azureResourceGroups'); let fullSubscriptionIds = config.get('selectedSubscriptions', []); @@ -32,12 +32,24 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio /** * Override the getTenantFilters method to provide custom tenant filtering - * Uses the same logic as in the original implementation but with fallback storage support + * Uses both subscription-based filtering and explicit tenant filtering */ protected override async getTenantFilters(): Promise { - const fullSubscriptionIds = await this.getTenantAndSubscriptionFilters(); - // Extract the tenant IDs from the full IDs (tenantId/subscriptionId) - return fullSubscriptionIds.map((id) => id.split('/')[0]); + // Get tenant filters from subscription selections + const fullSubscriptionIds = this.getTenantAndSubscriptionFilters(); + const subscriptionBasedTenants = fullSubscriptionIds.map((id) => id.split('/')[0]); + + // Get explicit tenant filters + const { getSelectedTenantIds } = await import('./subscriptionFiltering'); + const selectedTenantIds = getSelectedTenantIds(); + const explicitTenants = selectedTenantIds.map((id) => id.split('/')[0]); + + // Combine both sources, with explicit tenant filtering taking precedence + if (explicitTenants.length > 0) { + return [...new Set(explicitTenants)]; // Remove duplicates + } + + return [...new Set(subscriptionBasedTenants)]; // Fallback to subscription-based filtering } /** @@ -45,7 +57,7 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio * Uses the same logic as in the original implementation but with fallback storage support */ protected override async getSubscriptionFilters(): Promise { - const fullSubscriptionIds = await this.getTenantAndSubscriptionFilters(); + const fullSubscriptionIds = this.getTenantAndSubscriptionFilters(); // Extract the subscription IDs from the full IDs (tenantId/subscriptionId) return fullSubscriptionIds.map((id) => id.split('/')[1]); } diff --git a/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts new file mode 100644 index 000000000..1eed9e141 --- /dev/null +++ b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import type * as vscode from 'vscode'; +import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; + +export interface CredentialsManagementWizardContext extends IActionContext { + // Required context + azureSubscriptionProvider: AzureSubscriptionProviderWithFilters; + + // Selected account information + selectedAccount?: vscode.AuthenticationSessionAccountInformation; + selectedTenants?: AzureTenant[]; + + // Available options + availableAccounts?: vscode.AuthenticationSessionAccountInformation[]; + availableTenants?: Map; // accountId -> tenants + + // State tracking + shouldRestartWizard?: boolean; + newAccountSignedIn?: boolean; +} diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts new file mode 100644 index 000000000..65c49ca21 --- /dev/null +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import { ext } from '../../../../extensionVariables'; +import { nonNullValue } from '../../../../utils/nonNull'; +import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; + +export class ExecuteStep extends AzureWizardExecuteStep { + public priority: number = 100; + + public async execute(context: CredentialsManagementWizardContext): Promise { + const selectedAccount = nonNullValue(context.selectedAccount, 'context.selectedAccount', 'ExecuteStep.ts'); + + const selectedTenants = context.selectedTenants || []; + + ext.outputChannel.appendLine( + l10n.t('Saving Azure credentials configuration for account: {0}', selectedAccount.label), + ); + + // Create tenant/account selection identifiers in the format: "tenantId/accountId" + const tenantAccountIds = selectedTenants.map((tenant) => `${tenant.tenantId || ''}/${selectedAccount.id}`); + + // Save the selections to global state + const { setSelectedTenantIds } = await import('../subscriptionFiltering'); + await setSelectedTenantIds(tenantAccountIds); + + ext.outputChannel.appendLine( + l10n.t( + 'Successfully configured Azure tenant filtering. Selected {0} tenant(s) for account {1}', + selectedTenants.length, + selectedAccount.label, + ), + ); + + if (selectedTenants.length > 0) { + const tenantNames = selectedTenants.map( + (tenant) => tenant.displayName || tenant.tenantId || l10n.t('Unknown tenant'), + ); + ext.outputChannel.appendLine(l10n.t('Selected tenants: {0}', tenantNames.join(', '))); + } else { + ext.outputChannel.appendLine( + l10n.t( + 'No tenants selected. Azure discovery will be filtered to exclude all results for this account.', + ), + ); + } + + // Refresh the discovery tree to apply the new filtering + ext.outputChannel.appendLine(l10n.t('Refreshing Azure discovery tree...')); + ext.discoveryBranchDataProvider.refresh(); + + ext.outputChannel.appendLine(l10n.t('Azure credentials configuration completed successfully.')); + } + + public shouldExecute(context: CredentialsManagementWizardContext): boolean { + return !!context.selectedAccount && !context.shouldRestartWizard; + } +} diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts new file mode 100644 index 000000000..b4b6d12f9 --- /dev/null +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ext } from '../../../../extensionVariables'; +import { nonNullValue } from '../../../../utils/nonNull'; +import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; + +interface AccountQuickPickItem extends vscode.QuickPickItem { + account?: vscode.AuthenticationSessionAccountInformation; + isSignInOption?: boolean; + isLearnMoreOption?: boolean; +} + +export class SelectAccountStep extends AzureWizardPromptStep { + public async prompt(context: CredentialsManagementWizardContext): Promise { + // Get all authenticated accounts + const accounts = await this.getAvailableAccounts(context); + context.availableAccounts = accounts; + + // Create quick pick items + const accountItems: AccountQuickPickItem[] = this.createAccountPickItems(accounts); + + // Add separator and additional options + const separatorItems: AccountQuickPickItem[] = [ + { label: '', kind: vscode.QuickPickItemKind.Separator }, + { + label: l10n.t('$(plus) Sign in with a different account'), + detail: l10n.t('Add a new Azure account to VS Code'), + isSignInOption: true, + }, + { + label: l10n.t('$(question) Learn More'), + detail: l10n.t('Learn more about Azure authentication in VS Code'), + isLearnMoreOption: true, + }, + ]; + + const allItems = [...accountItems, ...separatorItems]; + + const selectedItem = await context.ui.showQuickPick(allItems, { + stepName: 'selectAccount', + placeHolder: l10n.t('Select an Azure account'), + matchOnDescription: true, + suppressPersistence: true, + loadingPlaceHolder: 'Loading...', + }); + + if (selectedItem.isSignInOption) { + await this.handleSignIn(context); + // Set flag to restart wizard after sign-in + context.shouldRestartWizard = true; + context.newAccountSignedIn = true; + throw new UserCancelledError(l10n.t('Restarting wizard after sign-in')); + } else if (selectedItem.isLearnMoreOption) { + await this.handleLearnMore(); + throw new UserCancelledError(l10n.t('User selected learn more')); + } else { + context.selectedAccount = nonNullValue( + selectedItem.account, + 'selectedItem.account', + 'SelectAccountStep.ts', + ); + } + } + + public shouldPrompt(context: CredentialsManagementWizardContext): boolean { + return !context.selectedAccount && !context.shouldRestartWizard; + } + + private async getAvailableAccounts( + context: CredentialsManagementWizardContext, + ): Promise { + try { + // Get all tenants which include the accounts + const tenants = await context.azureSubscriptionProvider.getTenants(); + + // Extract unique accounts from tenants + const accounts = tenants.map((tenant) => tenant.account); + const uniqueAccounts = accounts.filter( + (account, index, self) => index === self.findIndex((a) => a.id === account.id), + ); + + return uniqueAccounts.sort((a, b) => a.label.localeCompare(b.label)); + } catch (error) { + ext.outputChannel.appendLine( + l10n.t( + 'Failed to retrieve Azure accounts: {0}', + error instanceof Error ? error.message : String(error), + ), + ); + return []; + } + } + + private createAccountPickItems(accounts: vscode.AuthenticationSessionAccountInformation[]): AccountQuickPickItem[] { + if (accounts.length === 0) { + return [ + { + label: l10n.t('No Azure accounts found'), + detail: l10n.t('Sign in to Azure to continue'), + picked: true, + isSignInOption: true, + }, + ]; + } + + return accounts.map((account) => ({ + label: account.label, + description: account.id, + iconPath: new vscode.ThemeIcon('account'), + account, + })); + } + + private async handleSignIn(context: CredentialsManagementWizardContext): Promise { + try { + ext.outputChannel.appendLine(l10n.t('Starting Azure sign-in process...')); + const success = await context.azureSubscriptionProvider.signIn(); + + if (success) { + ext.outputChannel.appendLine(l10n.t('Azure sign-in completed successfully')); + // Refresh discovery tree to reflect new authentication + ext.discoveryBranchDataProvider.refresh(); + } else { + ext.outputChannel.appendLine(l10n.t('Azure sign-in was cancelled or failed')); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + ext.outputChannel.appendLine(l10n.t('Azure sign-in failed: {0}', errorMessage)); + void vscode.window.showErrorMessage(l10n.t('Failed to sign in to Azure: {0}', errorMessage)); + throw error; + } + } + + private async handleLearnMore(): Promise { + const learnMoreUrl = + 'https://docs.microsoft.com/en-us/azure/developer/javascript/tutorial-vscode-azure-cli-node-01'; + await vscode.env.openExternal(vscode.Uri.parse(learnMoreUrl)); + } +} diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts new file mode 100644 index 000000000..96a081e86 --- /dev/null +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; +import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ext } from '../../../../extensionVariables'; +import { nonNullValue } from '../../../../utils/nonNull'; +import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; + +interface TenantQuickPickItem extends vscode.QuickPickItem { + tenant?: AzureTenant; + isSelectAllOption?: boolean; + isClearAllOption?: boolean; +} + +export class SelectTenantsStep extends AzureWizardPromptStep { + public async prompt(context: CredentialsManagementWizardContext): Promise { + // Get available tenants for the selected account + const tenants = await this.getAvailableTenantsForAccount(context); + + // Initialize availableTenants map if not exists + if (!context.availableTenants) { + context.availableTenants = new Map(); + } + + // Store tenants for this account + const selectedAccount = nonNullValue( + context.selectedAccount, + 'context.selectedAccount', + 'SelectTenantsStep.ts', + ); + context.availableTenants.set(selectedAccount.id, tenants); + + if (tenants.length === 0) { + void vscode.window.showWarningMessage( + l10n.t( + 'No tenants found for the selected account. Please try signing in again or selecting a different account.', + ), + ); + return; + } + + // Get currently selected tenant IDs from storage + const { getSelectedTenantIds } = await import('../subscriptionFiltering'); + const currentlySelectedTenants = getSelectedTenantIds(); + const currentlySelectedTenantIds = new Set(currentlySelectedTenants.map((id) => id.split('/')[0])); + + // Create quick pick items with checkboxes + const tenantItems: TenantQuickPickItem[] = this.createTenantPickItems(tenants, currentlySelectedTenantIds); + + // Add control options + const controlItems: TenantQuickPickItem[] = [ + { label: '', kind: vscode.QuickPickItemKind.Separator }, + { + label: l10n.t('$(check-all) Select All'), + detail: l10n.t('Select all tenants for this account'), + isSelectAllOption: true, + }, + { + label: l10n.t('$(clear-all) Clear All'), + detail: l10n.t('Clear all selected tenants for this account'), + isClearAllOption: true, + }, + ]; + + const allItems = [...tenantItems, ...controlItems]; + + const selectedItems = await context.ui.showQuickPick(allItems, { + stepName: 'selectTenants', + placeHolder: l10n.t('Select tenants to include in discovery (multiple selection)'), + canPickMany: true, + matchOnDescription: true, + suppressPersistence: true, + loadingPlaceHolder: 'Loading...', + }); + + // Handle control options + if (selectedItems.some((item) => item.isSelectAllOption)) { + context.selectedTenants = tenants; + } else if (selectedItems.some((item) => item.isClearAllOption)) { + context.selectedTenants = []; + } else { + // Filter out control items and extract tenants + const tenantSelections = selectedItems.filter((item) => item.tenant); + context.selectedTenants = tenantSelections.map((item) => + nonNullValue(item.tenant, 'item.tenant', 'SelectTenantsStep.ts'), + ); + } + } + + public shouldPrompt(context: CredentialsManagementWizardContext): boolean { + return !!context.selectedAccount && !context.shouldRestartWizard; + } + + private async getAvailableTenantsForAccount(context: CredentialsManagementWizardContext): Promise { + try { + const selectedAccount = nonNullValue( + context.selectedAccount, + 'context.selectedAccount', + 'SelectTenantsStep.ts', + ); + + // Get tenants for the specific account + const tenants = await context.azureSubscriptionProvider.getTenants(selectedAccount); + + return tenants.sort((a, b) => { + // Sort by display name if available, otherwise by tenant ID + const aName = a.displayName || a.tenantId || ''; + const bName = b.displayName || b.tenantId || ''; + return aName.localeCompare(bName); + }); + } catch (error) { + ext.outputChannel.appendLine( + l10n.t( + 'Failed to retrieve tenants for account: {0}', + error instanceof Error ? error.message : String(error), + ), + ); + return []; + } + } + + private createTenantPickItems( + tenants: AzureTenant[], + currentlySelectedTenantIds: Set, + ): TenantQuickPickItem[] { + return tenants.map((tenant) => { + const tenantId = tenant.tenantId || ''; + const displayName = tenant.displayName || tenantId; + const isSelected = currentlySelectedTenantIds.has(tenantId); + + return { + label: displayName, + description: tenantId, + detail: tenant.domains?.[0] || undefined, // Show primary domain if available + iconPath: new vscode.ThemeIcon('organization'), + picked: isSelected, + tenant, + }; + }); + } +} diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts new file mode 100644 index 000000000..b04fb89e9 --- /dev/null +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ext } from '../../../../extensionVariables'; +import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; +import { AzureContextProperties } from '../wizard/AzureContextProperties'; +import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; +import { ExecuteStep } from './ExecuteStep'; +import { SelectAccountStep } from './SelectAccountStep'; +import { SelectTenantsStep } from './SelectTenantsStep'; + +/** + * Configures Azure credentials by allowing the user to select accounts and tenants + * for filtering Azure discovery results. This replaces the TODO in AzureDiscoveryProvider. + * + * @param context - The action context + * @param azureSubscriptionProvider - The Azure subscription provider with filtering capabilities + */ +export async function configureAzureCredentials( + context: IActionContext, + azureSubscriptionProvider: AzureSubscriptionProviderWithFilters, +): Promise { + const maxRetries = 3; + let attempt = 0; + + while (attempt < maxRetries) { + try { + attempt++; + ext.outputChannel.appendLine( + l10n.t('Starting Azure credentials configuration wizard (attempt {0}/{1})', attempt, maxRetries), + ); + + // Create wizard context + const wizardContext: CredentialsManagementWizardContext = { + ...context, + [AzureContextProperties.SelectedAccount]: undefined, + [AzureContextProperties.SelectedTenants]: undefined, + azureSubscriptionProvider, + shouldRestartWizard: false, + newAccountSignedIn: false, + }; + + // Create and configure the wizard + const wizard = new AzureWizard(wizardContext, { + title: l10n.t('Configure Azure Credentials'), + promptSteps: [new SelectAccountStep(), new SelectTenantsStep()], + executeSteps: [new ExecuteStep()], + }); + + // Execute the wizard + await wizard.prompt(); + await wizard.execute(); + + // Success - exit the retry loop + ext.outputChannel.appendLine(l10n.t('Azure credentials configuration completed successfully.')); + break; + } catch (error) { + if (error instanceof UserCancelledError) { + // User cancelled or no restart needed + ext.outputChannel.appendLine(l10n.t('Azure credentials configuration was cancelled by user.')); + return; + } + + // Other errors + const errorMessage = error instanceof Error ? error.message : String(error); + ext.outputChannel.appendLine( + l10n.t( + 'Azure credentials configuration failed (attempt {0}/{1}): {2}', + attempt, + maxRetries, + errorMessage, + ), + ); + + if (attempt >= maxRetries) { + // Final attempt failed + void vscode.window.showErrorMessage( + l10n.t('Failed to configure Azure credentials after {0} attempts: {1}', maxRetries, errorMessage), + ); + throw error; + } + + // Wait before retrying + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } +} + +/** + * Displays current Azure credentials configuration status + * + * @param _context - The action context (not used but required for command pattern) + */ +export async function showAzureCredentialsStatus(_context: IActionContext): Promise { + try { + const { getSelectedTenantIds } = await import('../subscriptionFiltering'); + const selectedTenantIds = getSelectedTenantIds(); + + if (selectedTenantIds.length === 0) { + void vscode.window.showInformationMessage( + l10n.t('No Azure tenant filters are currently configured. All tenants will be included in discovery.'), + ); + return; + } + + // Group by account + const accountTenantMap = new Map(); + for (const tenantAccountId of selectedTenantIds) { + const [tenantId, accountId] = tenantAccountId.split('/'); + if (!accountTenantMap.has(accountId)) { + accountTenantMap.set(accountId, []); + } + accountTenantMap.get(accountId)?.push(tenantId); + } + + const statusMessages: string[] = []; + for (const [accountId, tenantIds] of accountTenantMap) { + statusMessages.push(l10n.t('Account {0}: {1} tenant(s) selected', accountId, tenantIds.length)); + } + + const fullMessage = l10n.t( + 'Azure tenant filtering is active:\n\n{0}\n\nUse "Configure Azure Credentials" to modify these settings.', + statusMessages.join('\n'), + ); + + void vscode.window.showInformationMessage(fullMessage); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + void vscode.window.showErrorMessage(l10n.t('Failed to retrieve Azure credentials status: {0}', errorMessage)); + } +} + +/** + * Clears all Azure credentials configuration + * + * @param _context - The action context (not used but required for command pattern) + */ +export async function clearAzureCredentialsConfiguration(_context: IActionContext): Promise { + try { + const confirmResult = await vscode.window.showWarningMessage( + l10n.t( + 'This will clear all Azure tenant filtering configuration. Azure discovery will include all tenants. Continue?', + ), + { modal: true }, + l10n.t('Clear Configuration'), + ); + + if (confirmResult) { + const { setSelectedTenantIds } = await import('../subscriptionFiltering'); + await setSelectedTenantIds([]); + + ext.outputChannel.appendLine(l10n.t('Azure credentials configuration cleared.')); + ext.discoveryBranchDataProvider.refresh(); + + void vscode.window.showInformationMessage( + l10n.t('Azure credentials configuration has been cleared. Discovery will now include all tenants.'), + ); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + void vscode.window.showErrorMessage( + l10n.t('Failed to clear Azure credentials configuration: {0}', errorMessage), + ); + } +} diff --git a/src/plugins/api-shared/azure/credentialsManagement/index.ts b/src/plugins/api-shared/azure/credentialsManagement/index.ts new file mode 100644 index 000000000..9ea08cf54 --- /dev/null +++ b/src/plugins/api-shared/azure/credentialsManagement/index.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { + clearAzureCredentialsConfiguration, + configureAzureCredentials, + showAzureCredentialsStatus, +} from './configureAzureCredentials'; +export type { CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; +export { ExecuteStep } from './ExecuteStep'; +export { SelectAccountStep } from './SelectAccountStep'; +export { SelectTenantsStep } from './SelectTenantsStep'; diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index 29c79fba1..3cd82dcd7 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -74,6 +74,27 @@ export async function setSelectedSubscriptionIds(subscriptionIds: string[]): Pro } } +/** + * Returns the currently selected tenant IDs from the stored configuration. + * The IDs are stored in the format 'tenantId/accountId'. + * For example: 'tenantId1/accountId1', 'tenantId2/accountId2'. + * The function returns an array of tenant/account ID combinations. + * + * @returns An array of selected tenant IDs with account information. + */ +export function getSelectedTenantIds(): string[] { + return ext.context.globalState.get('azure-discovery.selectedTenants', []); +} + +/** + * Updates the selected tenant IDs in the stored configuration. + * These have to contain the full tenant/account ID combination. + * For example: 'tenantId1/accountId1', 'tenantId2/accountId2'. + */ +export async function setSelectedTenantIds(tenantIds: string[]): Promise { + await ext.context.globalState.update('azure-discovery.selectedTenants', tenantIds); +} + /** * Identifies subscriptions with duplicate names. */ @@ -102,6 +123,7 @@ export async function configureAzureSubscriptionFilter( /** * Ensure the user is signed in to Azure */ + if (!(await azureSubscriptionProvider.isSignedIn())) { const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; void vscode.window diff --git a/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts b/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts index 6ec698d0f..e829f24d7 100644 --- a/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts +++ b/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts @@ -7,4 +7,6 @@ export enum AzureContextProperties { AzureSubscriptionProvider = 'azureSubscriptionProvider', SelectedSubscription = 'selectedSubscription', SelectedCluster = 'selectedCluster', + SelectedAccount = 'selectedAccount', + SelectedTenants = 'selectedTenants', } diff --git a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts index b2665d478..7d4314ae5 100644 --- a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts @@ -64,4 +64,13 @@ export class AzureDiscoveryProvider extends Disposable implements DiscoveryProvi ext.discoveryBranchDataProvider.refresh(node); } } + + async configureCredentials(context: IActionContext, node: TreeElement): Promise { + if (node instanceof AzureServiceRootItem) { + // Use the new Azure credentials configuration wizard + const { configureAzureCredentials } = await import('../api-shared/azure/credentialsManagement'); + await configureAzureCredentials(context, this.azureSubscriptionProvider); + ext.discoveryBranchDataProvider.refresh(node); + } + } } From acd79166fa69a40b3864efabcf91f3d78a752a24 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 23 Sep 2025 18:47:33 +0200 Subject: [PATCH 27/88] wip: multiuser+multitenant support in azure discovery --- .../AzureSubscriptionProviderWithFilters.ts | 10 ++-- .../CredentialsManagementWizardContext.ts | 1 + .../credentialsManagement/ExecuteStep.ts | 8 ++- .../SelectTenantsStep.ts | 6 ++- .../configureAzureCredentials.ts | 14 +++-- .../api-shared/azure/subscriptionFiltering.ts | 53 +++++++++++++++---- 6 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts index 289436e45..e8cd36226 100644 --- a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts +++ b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts @@ -39,10 +39,14 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio const fullSubscriptionIds = this.getTenantAndSubscriptionFilters(); const subscriptionBasedTenants = fullSubscriptionIds.map((id) => id.split('/')[0]); - // Get explicit tenant filters + // Get all available tenants to pass to getSelectedTenantIds + const allTenants = await this.getTenants(); + const allTenantKeys = allTenants.map((tenant) => `${tenant.tenantId}/${tenant.account.id}`); + + // Get explicit tenant filters using the new signature const { getSelectedTenantIds } = await import('./subscriptionFiltering'); - const selectedTenantIds = getSelectedTenantIds(); - const explicitTenants = selectedTenantIds.map((id) => id.split('/')[0]); + const selectedTenantKeys = getSelectedTenantIds(allTenantKeys); + const explicitTenants = selectedTenantKeys.map((id) => id.split('/')[0]); // Combine both sources, with explicit tenant filtering taking precedence if (explicitTenants.length > 0) { diff --git a/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts index 1eed9e141..1372ed7c9 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts @@ -19,6 +19,7 @@ export interface CredentialsManagementWizardContext extends IActionContext { // Available options availableAccounts?: vscode.AuthenticationSessionAccountInformation[]; availableTenants?: Map; // accountId -> tenants + allTenants?: AzureTenant[]; // All available tenants for the selected account // State tracking shouldRestartWizard?: boolean; diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index 65c49ca21..5c1abbee6 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -24,9 +24,13 @@ export class ExecuteStep extends AzureWizardExecuteStep `${tenant.tenantId || ''}/${selectedAccount.id}`); - // Save the selections to global state + // Get all available tenants for this account to calculate the full set + const allTenantsForAccount = nonNullValue(context.allTenants, 'context.allTenants', 'ExecuteStep.ts'); + const allTenantKeys = allTenantsForAccount.map((tenant) => `${tenant.tenantId}/${selectedAccount.id}`); + + // Save the selections to global state using the new signature const { setSelectedTenantIds } = await import('../subscriptionFiltering'); - await setSelectedTenantIds(tenantAccountIds); + await setSelectedTenantIds(tenantAccountIds, allTenantKeys); ext.outputChannel.appendLine( l10n.t( diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts index 96a081e86..3b39a6bf7 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -35,6 +35,9 @@ export class SelectTenantsStep extends AzureWizardPromptStep `${tenant.tenantId}/${selectedAccount.id}`); + const currentlySelectedTenants = getSelectedTenantIds(allTenantKeys); const currentlySelectedTenantIds = new Set(currentlySelectedTenants.map((id) => id.split('/')[0])); // Create quick pick items with checkboxes diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index b04fb89e9..a97330317 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -7,7 +7,7 @@ import { AzureWizard, UserCancelledError, type IActionContext } from '@microsoft import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; -import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; +import { AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; import { AzureContextProperties } from '../wizard/AzureContextProperties'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; import { ExecuteStep } from './ExecuteStep'; @@ -98,8 +98,15 @@ export async function configureAzureCredentials( */ export async function showAzureCredentialsStatus(_context: IActionContext): Promise { try { + // For status display, we need to get all available tenants to use the filtering function + const azureSubscriptionProvider = new AzureSubscriptionProviderWithFilters(); + const allTenants = await azureSubscriptionProvider.getTenants(); + const allTenantKeys = allTenants.map((tenant) => `${tenant.tenantId}/${tenant.account.id}`); + const { getSelectedTenantIds } = await import('../subscriptionFiltering'); - const selectedTenantIds = getSelectedTenantIds(); + const selectedTenantIds = getSelectedTenantIds(allTenantKeys); + + azureSubscriptionProvider.dispose(); // Clean up if (selectedTenantIds.length === 0) { void vscode.window.showInformationMessage( @@ -152,7 +159,8 @@ export async function clearAzureCredentialsConfiguration(_context: IActionContex if (confirmResult) { const { setSelectedTenantIds } = await import('../subscriptionFiltering'); - await setSelectedTenantIds([]); + // For clearing, we pass an empty array for both selected and all tenants + await setSelectedTenantIds([], []); ext.outputChannel.appendLine(l10n.t('Azure credentials configuration cleared.')); ext.discoveryBranchDataProvider.refresh(); diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index 3cd82dcd7..9589b5369 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -76,23 +76,54 @@ export async function setSelectedSubscriptionIds(subscriptionIds: string[]): Pro /** * Returns the currently selected tenant IDs from the stored configuration. - * The IDs are stored in the format 'tenantId/accountId'. - * For example: 'tenantId1/accountId1', 'tenantId2/accountId2'. - * The function returns an array of tenant/account ID combinations. + * This function syncs with the Azure Resource Groups extension which stores + * unselected tenants in workspace configuration. * - * @returns An array of selected tenant IDs with account information. + * The Azure Resource Groups extension uses inverse logic - it stores tenants that + * are NOT selected, so we need to return all tenants EXCEPT the unselected ones. + * + * @param allTenants All available tenant/account combinations in 'tenantId/accountId' format + * @returns An array of selected tenant IDs with account information in 'tenantId/accountId' format */ -export function getSelectedTenantIds(): string[] { - return ext.context.globalState.get('azure-discovery.selectedTenants', []); +export function getSelectedTenantIds(allTenants: string[]): string[] { + // Try the Azure Resource Groups config first + const config = vscode.workspace.getConfiguration('azureResourceGroups'); + const unselectedTenants = config.get('unselectedTenants', []); + + // If nothing found there, try our fallback storage + if (unselectedTenants.length === 0) { + const fallbackUnselected = ext.context.globalState.get('azure-discovery.unselectedTenants', []); + return allTenants.filter((tenant) => !fallbackUnselected.includes(tenant)); + } + + // Sync to our fallback storage if primary storage had data + void ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); + + // Return all tenants except the unselected ones + return allTenants.filter((tenant) => !unselectedTenants.includes(tenant)); } /** - * Updates the selected tenant IDs in the stored configuration. - * These have to contain the full tenant/account ID combination. - * For example: 'tenantId1/accountId1', 'tenantId2/accountId2'. + * Updates the selected tenant IDs by updating the unselected tenants list. + * This syncs with the Azure Resource Groups extension which stores unselected tenants. + * + * @param selectedTenantIds Array of selected tenant IDs in 'tenantId/accountId' format + * @param allTenants All available tenant/account combinations in 'tenantId/accountId' format */ -export async function setSelectedTenantIds(tenantIds: string[]): Promise { - await ext.context.globalState.update('azure-discovery.selectedTenants', tenantIds); +export async function setSelectedTenantIds(selectedTenantIds: string[], allTenants: string[]): Promise { + // Calculate unselected tenants (inverse logic to match Azure Resource Groups) + const unselectedTenants = allTenants.filter((tenant) => !selectedTenantIds.includes(tenant)); + + try { + const config = vscode.workspace.getConfiguration('azureResourceGroups'); + await config.update('unselectedTenants', unselectedTenants, vscode.ConfigurationTarget.Global); + } catch (error) { + // Log the error if the Azure Resource Groups config update fails + console.error('Unable to update Azure Resource Groups tenant configuration, using fallback storage.', error); + } finally { + // Always update our fallback storage regardless of primary storage success + await ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); + } } /** From b0a259fd3600947fa585fbba8df19f7ed212ee90 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 10:49:02 +0200 Subject: [PATCH 28/88] wip: multiuser+multitenant support in azure discovery --- .../AzureSubscriptionProviderWithFilters.ts | 43 ++++++++-------- .../credentialsManagement/ExecuteStep.ts | 20 ++++++-- .../SelectTenantsStep.ts | 13 +++-- .../configureAzureCredentials.ts | 32 +++++++++--- .../api-shared/azure/subscriptionFiltering.ts | 51 +++++++------------ 5 files changed, 90 insertions(+), 69 deletions(-) diff --git a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts index e8cd36226..d146e4e99 100644 --- a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts +++ b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts @@ -3,7 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSCodeAzureSubscriptionProvider, type SubscriptionId, type TenantId } from '@microsoft/vscode-azext-azureauth'; +import { + VSCodeAzureSubscriptionProvider, + type AzureSubscription, + type GetSubscriptionsFilter, + type SubscriptionId, +} from '@microsoft/vscode-azext-azureauth'; import * as vscode from 'vscode'; import { ext } from '../../../extensionVariables'; @@ -31,29 +36,25 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio } /** - * Override the getTenantFilters method to provide custom tenant filtering - * Uses both subscription-based filtering and explicit tenant filtering + * Gets subscriptions from the Azure subscription provider and applies tenant filtering. + * When getSubscriptions(true) is called, all filtering is handled by the base provider. + * When getSubscriptions(false) is called, we manually apply tenant filtering client-side. + * + * @param filter Whether to apply subscription filtering or a custom filter + * @returns Filtered list of subscriptions */ - protected override async getTenantFilters(): Promise { - // Get tenant filters from subscription selections - const fullSubscriptionIds = this.getTenantAndSubscriptionFilters(); - const subscriptionBasedTenants = fullSubscriptionIds.map((id) => id.split('/')[0]); - - // Get all available tenants to pass to getSelectedTenantIds - const allTenants = await this.getTenants(); - const allTenantKeys = allTenants.map((tenant) => `${tenant.tenantId}/${tenant.account.id}`); - - // Get explicit tenant filters using the new signature - const { getSelectedTenantIds } = await import('./subscriptionFiltering'); - const selectedTenantKeys = getSelectedTenantIds(allTenantKeys); - const explicitTenants = selectedTenantKeys.map((id) => id.split('/')[0]); + public override async getSubscriptions(filter?: boolean | GetSubscriptionsFilter): Promise { + // Get subscriptions from the base provider with the original filter parameter + const subscriptions = await super.getSubscriptions(filter); - // Combine both sources, with explicit tenant filtering taking precedence - if (explicitTenants.length > 0) { - return [...new Set(explicitTenants)]; // Remove duplicates + // If filter is explicitly false, apply tenant filtering manually + // When filter is true/undefined, the base provider already handles all filtering + if (filter === false) { + const { getTenantFilteredSubscriptions } = await import('./subscriptionFiltering'); + return getTenantFilteredSubscriptions(subscriptions); } - return [...new Set(subscriptionBasedTenants)]; // Fallback to subscription-based filtering + return subscriptions; } /** @@ -63,6 +64,6 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio protected override async getSubscriptionFilters(): Promise { const fullSubscriptionIds = this.getTenantAndSubscriptionFilters(); // Extract the subscription IDs from the full IDs (tenantId/subscriptionId) - return fullSubscriptionIds.map((id) => id.split('/')[1]); + return Promise.resolve(fullSubscriptionIds.map((id) => id.split('/')[1])); } } diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index 5c1abbee6..675dec860 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -5,6 +5,7 @@ import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { nonNullValue } from '../../../../utils/nonNull'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; @@ -28,9 +29,22 @@ export class ExecuteStep extends AzureWizardExecuteStep `${tenant.tenantId}/${selectedAccount.id}`); - // Save the selections to global state using the new signature - const { setSelectedTenantIds } = await import('../subscriptionFiltering'); - await setSelectedTenantIds(tenantAccountIds, allTenantKeys); + // Calculate unselected tenants (inverse logic to match Azure Resource Groups) + const unselectedTenants = allTenantKeys.filter((tenant) => !tenantAccountIds.includes(tenant)); + + // Save unselected tenants to workspace configuration (with fallback to globalState) + try { + const config = vscode.workspace.getConfiguration('azureResourceGroups'); + await config.update('unselectedTenants', unselectedTenants, vscode.ConfigurationTarget.Global); + } catch (error) { + console.error( + 'Unable to update Azure Resource Groups tenant configuration, using fallback storage.', + error, + ); + } finally { + // Always update our fallback storage regardless of primary storage success + await ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); + } ext.outputChannel.appendLine( l10n.t( diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts index 3b39a6bf7..f25cc3654 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -48,10 +48,15 @@ export class SelectTenantsStep extends AzureWizardPromptStep `${tenant.tenantId}/${selectedAccount.id}`); - const currentlySelectedTenants = getSelectedTenantIds(allTenantKeys); - const currentlySelectedTenantIds = new Set(currentlySelectedTenants.map((id) => id.split('/')[0])); + const { isTenantFilteredOut } = await import('../subscriptionFiltering'); + const currentlySelectedTenantIds = new Set(); + + // Check which tenants are currently selected (not filtered out) + for (const tenant of tenants) { + if (tenant.tenantId && !isTenantFilteredOut(tenant.tenantId, selectedAccount.id)) { + currentlySelectedTenantIds.add(tenant.tenantId); + } + } // Create quick pick items with checkboxes const tenantItems: TenantQuickPickItem[] = this.createTenantPickItems(tenants, currentlySelectedTenantIds); diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index a97330317..53e353393 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -101,14 +101,20 @@ export async function showAzureCredentialsStatus(_context: IActionContext): Prom // For status display, we need to get all available tenants to use the filtering function const azureSubscriptionProvider = new AzureSubscriptionProviderWithFilters(); const allTenants = await azureSubscriptionProvider.getTenants(); - const allTenantKeys = allTenants.map((tenant) => `${tenant.tenantId}/${tenant.account.id}`); - const { getSelectedTenantIds } = await import('../subscriptionFiltering'); - const selectedTenantIds = getSelectedTenantIds(allTenantKeys); + const { isTenantFilteredOut } = await import('../subscriptionFiltering'); azureSubscriptionProvider.dispose(); // Clean up - if (selectedTenantIds.length === 0) { + // Check which tenants are currently selected (not filtered out) + const selectedTenantKeys: string[] = []; + for (const tenant of allTenants) { + if (tenant.tenantId && !isTenantFilteredOut(tenant.tenantId, tenant.account.id)) { + selectedTenantKeys.push(`${tenant.tenantId}/${tenant.account.id}`); + } + } + + if (selectedTenantKeys.length === 0) { void vscode.window.showInformationMessage( l10n.t('No Azure tenant filters are currently configured. All tenants will be included in discovery.'), ); @@ -117,7 +123,7 @@ export async function showAzureCredentialsStatus(_context: IActionContext): Prom // Group by account const accountTenantMap = new Map(); - for (const tenantAccountId of selectedTenantIds) { + for (const tenantAccountId of selectedTenantKeys) { const [tenantId, accountId] = tenantAccountId.split('/'); if (!accountTenantMap.has(accountId)) { accountTenantMap.set(accountId, []); @@ -158,9 +164,19 @@ export async function clearAzureCredentialsConfiguration(_context: IActionContex ); if (confirmResult) { - const { setSelectedTenantIds } = await import('../subscriptionFiltering'); - // For clearing, we pass an empty array for both selected and all tenants - await setSelectedTenantIds([], []); + // Clear tenant filtering by setting empty unselected tenants array + try { + const config = vscode.workspace.getConfiguration('azureResourceGroups'); + await config.update('unselectedTenants', [], vscode.ConfigurationTarget.Global); + } catch (error) { + console.error( + 'Unable to update Azure Resource Groups tenant configuration, using fallback storage.', + error, + ); + } finally { + // Always update our fallback storage regardless of primary storage success + await ext.context.globalState.update('azure-discovery.unselectedTenants', []); + } ext.outputChannel.appendLine(l10n.t('Azure credentials configuration cleared.')); ext.discoveryBranchDataProvider.refresh(); diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index 9589b5369..7c3cb78a8 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -12,7 +12,7 @@ import { ext } from '../../../extensionVariables'; /** * Subscription filtering functionality is provided by the `VSCodeAzureSubscriptionProvider` * from the `vscode-azuretools` library: - * https://github.com/tnaum-ms/vscode-azuretools/blob/main/auth/src/VSCodeAzureSubscriptionProvider.ts + * https://github.com/microsoft/vscode-azuretools/blob/main/auth/src/VSCodeAzureSubscriptionProvider.ts * * Although the provider supports filtering subscriptions internally, it does not include built-in * UI or configuration logic to manage these filters directly. @@ -75,17 +75,13 @@ export async function setSelectedSubscriptionIds(subscriptionIds: string[]): Pro } /** - * Returns the currently selected tenant IDs from the stored configuration. - * This function syncs with the Azure Resource Groups extension which stores - * unselected tenants in workspace configuration. + * Checks if a tenant is filtered out based on stored tenant filters. * - * The Azure Resource Groups extension uses inverse logic - it stores tenants that - * are NOT selected, so we need to return all tenants EXCEPT the unselected ones. - * - * @param allTenants All available tenant/account combinations in 'tenantId/accountId' format - * @returns An array of selected tenant IDs with account information in 'tenantId/accountId' format + * @param tenantId The tenant ID to check + * @param accountId The account ID associated with the tenant + * @returns True if the tenant is filtered out (unchecked), false otherwise */ -export function getSelectedTenantIds(allTenants: string[]): string[] { +export function isTenantFilteredOut(tenantId: string, accountId: string): boolean { // Try the Azure Resource Groups config first const config = vscode.workspace.getConfiguration('azureResourceGroups'); const unselectedTenants = config.get('unselectedTenants', []); @@ -93,37 +89,26 @@ export function getSelectedTenantIds(allTenants: string[]): string[] { // If nothing found there, try our fallback storage if (unselectedTenants.length === 0) { const fallbackUnselected = ext.context.globalState.get('azure-discovery.unselectedTenants', []); - return allTenants.filter((tenant) => !fallbackUnselected.includes(tenant)); + return fallbackUnselected.includes(`${tenantId}/${accountId}`); } - // Sync to our fallback storage if primary storage had data - void ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); - - // Return all tenants except the unselected ones - return allTenants.filter((tenant) => !unselectedTenants.includes(tenant)); + return unselectedTenants.includes(`${tenantId}/${accountId}`); } /** - * Updates the selected tenant IDs by updating the unselected tenants list. - * This syncs with the Azure Resource Groups extension which stores unselected tenants. + * Filters subscriptions based on tenant selection settings. + * Returns only subscriptions from selected tenants. * - * @param selectedTenantIds Array of selected tenant IDs in 'tenantId/accountId' format - * @param allTenants All available tenant/account combinations in 'tenantId/accountId' format + * @param subscriptions All subscriptions returned from the API + * @returns Filtered subscriptions from selected tenants only */ -export async function setSelectedTenantIds(selectedTenantIds: string[], allTenants: string[]): Promise { - // Calculate unselected tenants (inverse logic to match Azure Resource Groups) - const unselectedTenants = allTenants.filter((tenant) => !selectedTenantIds.includes(tenant)); +export function getTenantFilteredSubscriptions(subscriptions: AzureSubscription[]): AzureSubscription[] { + const filteredSubscriptions = subscriptions.filter( + (subscription) => !isTenantFilteredOut(subscription.tenantId, subscription.account.id), + ); - try { - const config = vscode.workspace.getConfiguration('azureResourceGroups'); - await config.update('unselectedTenants', unselectedTenants, vscode.ConfigurationTarget.Global); - } catch (error) { - // Log the error if the Azure Resource Groups config update fails - console.error('Unable to update Azure Resource Groups tenant configuration, using fallback storage.', error); - } finally { - // Always update our fallback storage regardless of primary storage success - await ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); - } + // If filtering would result in an empty list, return all subscriptions as a fallback + return filteredSubscriptions.length > 0 ? filteredSubscriptions : subscriptions; } /** From 960130b8f7137d9d88cde19baaf69b1f91e4fad9 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 11:02:13 +0200 Subject: [PATCH 29/88] wip: multiuser+multitenant support in azure discovery --- .../credentialsManagement/ExecuteStep.ts | 20 +- .../api-shared/azure/subscriptionFiltering.ts | 175 +++++++++++++++--- 2 files changed, 156 insertions(+), 39 deletions(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index 675dec860..91309a690 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -5,7 +5,6 @@ import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; -import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { nonNullValue } from '../../../../utils/nonNull'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; @@ -29,22 +28,9 @@ export class ExecuteStep extends AzureWizardExecuteStep `${tenant.tenantId}/${selectedAccount.id}`); - // Calculate unselected tenants (inverse logic to match Azure Resource Groups) - const unselectedTenants = allTenantKeys.filter((tenant) => !tenantAccountIds.includes(tenant)); - - // Save unselected tenants to workspace configuration (with fallback to globalState) - try { - const config = vscode.workspace.getConfiguration('azureResourceGroups'); - await config.update('unselectedTenants', unselectedTenants, vscode.ConfigurationTarget.Global); - } catch (error) { - console.error( - 'Unable to update Azure Resource Groups tenant configuration, using fallback storage.', - error, - ); - } finally { - // Always update our fallback storage regardless of primary storage success - await ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); - } + // Save the tenant selections using the centralized function + const { setSelectedTenantIds } = await import('../subscriptionFiltering'); + await setSelectedTenantIds(tenantAccountIds, allTenantKeys); ext.outputChannel.appendLine( l10n.t( diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index 7c3cb78a8..39fd24cc7 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -39,21 +39,24 @@ import { ext } from '../../../extensionVariables'; * @returns An array of selected subscription IDs. */ export function getSelectedSubscriptionIds(): string[] { - // Try the Azure Resource Groups config first - const config = vscode.workspace.getConfiguration('azureResourceGroups'); - const fullSubscriptionIds = config.get('selectedSubscriptions', []); - - // If nothing found there, try our fallback storage - if (fullSubscriptionIds.length === 0) { - const fallbackIds = ext.context.globalState.get('azure-discovery.selectedSubscriptions', []); - return fallbackIds.map((id) => id.split('/')[1]); + // Try the Azure Resource Groups config first (primary storage) + const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); + const primarySubscriptionIds = azureResourcesConfig.get('selectedSubscriptions', []); + + // If nothing found in primary storage, try our fallback storage + if (primarySubscriptionIds.length === 0) { + const fallbackSubscriptionIds = ext.context.globalState.get( + 'azure-discovery.selectedSubscriptions', + [], + ); + return fallbackSubscriptionIds.map((id) => id.split('/')[1]); } // Sync to our fallback storage if primary storage had data // This ensures we maintain a copy if Azure Resources extension is later removed - void ext.context.globalState.update('azure-discovery.selectedSubscriptions', fullSubscriptionIds); + void ext.context.globalState.update('azure-discovery.selectedSubscriptions', primarySubscriptionIds); - return fullSubscriptionIds.map((id) => id.split('/')[1]); + return primarySubscriptionIds.map((id) => id.split('/')[1]); } /** @@ -63,10 +66,10 @@ export function getSelectedSubscriptionIds(): string[] { */ export async function setSelectedSubscriptionIds(subscriptionIds: string[]): Promise { try { - const config = vscode.workspace.getConfiguration('azureResourceGroups'); - await config.update('selectedSubscriptions', subscriptionIds, vscode.ConfigurationTarget.Global); + const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); + await azureResourcesConfig.update('selectedSubscriptions', subscriptionIds, vscode.ConfigurationTarget.Global); } catch (error) { - // Log the error if the Azure Resource Groups config update fails + // Log the error if the primary storage (Azure Resource Groups config) update fails console.error('Unable to update Azure Resource Groups configuration, using fallback storage.', error); } finally { // Always update our fallback storage regardless of primary storage success @@ -82,17 +85,20 @@ export async function setSelectedSubscriptionIds(subscriptionIds: string[]): Pro * @returns True if the tenant is filtered out (unchecked), false otherwise */ export function isTenantFilteredOut(tenantId: string, accountId: string): boolean { - // Try the Azure Resource Groups config first - const config = vscode.workspace.getConfiguration('azureResourceGroups'); - const unselectedTenants = config.get('unselectedTenants', []); - - // If nothing found there, try our fallback storage - if (unselectedTenants.length === 0) { - const fallbackUnselected = ext.context.globalState.get('azure-discovery.unselectedTenants', []); - return fallbackUnselected.includes(`${tenantId}/${accountId}`); + // Try the Azure Resource Groups config first (primary storage) + const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); + const primaryUnselectedTenants = azureResourcesConfig.get('unselectedTenants', []); + + // If nothing found in primary storage, try our fallback storage + if (primaryUnselectedTenants.length === 0) { + const fallbackUnselectedTenants = ext.context.globalState.get( + 'azure-discovery.unselectedTenants', + [], + ); + return fallbackUnselectedTenants.includes(`${tenantId}/${accountId}`); } - return unselectedTenants.includes(`${tenantId}/${accountId}`); + return primaryUnselectedTenants.includes(`${tenantId}/${accountId}`); } /** @@ -111,6 +117,131 @@ export function getTenantFilteredSubscriptions(subscriptions: AzureSubscription[ return filteredSubscriptions.length > 0 ? filteredSubscriptions : subscriptions; } +/** + * Adds a tenant to the unselected tenants list. + * This will filter out the tenant from discovery. + * + * @param tenantId The tenant ID to add to unselected list + * @param accountId The account ID associated with the tenant + */ +export async function addUnselectedTenant(tenantId: string, accountId: string): Promise { + const tenantKey = `${tenantId}/${accountId}`; + + // Get current unselected tenants from both storage locations + const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); + const primaryUnselectedTenants = azureResourcesConfig.get('unselectedTenants', []); + const fallbackUnselectedTenants = ext.context.globalState.get('azure-discovery.unselectedTenants', []); + + // Use primary storage if available, otherwise fallback storage + const currentUnselectedTenants = + primaryUnselectedTenants.length > 0 ? primaryUnselectedTenants : fallbackUnselectedTenants; + + // Add if not already present + if (!currentUnselectedTenants.includes(tenantKey)) { + const updatedUnselectedTenants = [...currentUnselectedTenants, tenantKey]; + + try { + await azureResourcesConfig.update( + 'unselectedTenants', + updatedUnselectedTenants, + vscode.ConfigurationTarget.Global, + ); + } catch (error) { + console.error( + 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', + error, + ); + } finally { + // Always update our fallback storage regardless of primary storage success + await ext.context.globalState.update('azure-discovery.unselectedTenants', updatedUnselectedTenants); + } + } +} + +/** + * Removes a tenant from the unselected tenants list. + * This will make the tenant available for discovery. + * + * @param tenantId The tenant ID to remove from unselected list + * @param accountId The account ID associated with the tenant + */ +export async function removeUnselectedTenant(tenantId: string, accountId: string): Promise { + const tenantKey = `${tenantId}/${accountId}`; + + // Get current unselected tenants from both storage locations + const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); + const primaryUnselectedTenants = azureResourcesConfig.get('unselectedTenants', []); + const fallbackUnselectedTenants = ext.context.globalState.get('azure-discovery.unselectedTenants', []); + + // Use primary storage if available, otherwise fallback storage + const currentUnselectedTenants = + primaryUnselectedTenants.length > 0 ? primaryUnselectedTenants : fallbackUnselectedTenants; + + // Remove if present + const updatedUnselectedTenants = currentUnselectedTenants.filter((tenant) => tenant !== tenantKey); + + try { + await azureResourcesConfig.update( + 'unselectedTenants', + updatedUnselectedTenants, + vscode.ConfigurationTarget.Global, + ); + } catch (error) { + console.error( + 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', + error, + ); + } finally { + // Always update our fallback storage regardless of primary storage success + await ext.context.globalState.update('azure-discovery.unselectedTenants', updatedUnselectedTenants); + } +} + +/** + * Updates the unselected tenants list based on selected tenant IDs. + * This syncs with the Azure Resource Groups extension which stores unselected tenants. + * + * @param selectedTenantKeys Array of selected tenant IDs in 'tenantId/accountId' format + * @param allTenantKeys All available tenant/account combinations in 'tenantId/accountId' format + */ +export async function setSelectedTenantIds(selectedTenantKeys: string[], allTenantKeys: string[]): Promise { + // Calculate unselected tenants (inverse logic to match Azure Resource Groups) + const unselectedTenants = allTenantKeys.filter((tenant) => !selectedTenantKeys.includes(tenant)); + + try { + const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); + await azureResourcesConfig.update('unselectedTenants', unselectedTenants, vscode.ConfigurationTarget.Global); + } catch (error) { + // Log the error if the primary storage (Azure Resource Groups config) update fails + console.error( + 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', + error, + ); + } finally { + // Always update our fallback storage regardless of primary storage success + await ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); + } +} + +/** + * Clears all tenant filtering configuration. + * This will make all tenants available for discovery. + */ +export async function clearTenantFiltering(): Promise { + try { + const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); + await azureResourcesConfig.update('unselectedTenants', [], vscode.ConfigurationTarget.Global); + } catch (error) { + console.error( + 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', + error, + ); + } finally { + // Always update our fallback storage regardless of primary storage success + await ext.context.globalState.update('azure-discovery.unselectedTenants', []); + } +} + /** * Identifies subscriptions with duplicate names. */ From 7172389665a57f3e387d37714aeb2d31cce211ef Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 11:04:50 +0200 Subject: [PATCH 30/88] wip: multiuser+multitenant support in azure discovery --- .../credentialsManagement/ExecuteStep.ts | 24 +++++++++++------ .../api-shared/azure/subscriptionFiltering.ts | 26 ------------------- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index 91309a690..b3f4ccbd5 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -21,16 +21,24 @@ export class ExecuteStep extends AzureWizardExecuteStep `${tenant.tenantId || ''}/${selectedAccount.id}`); - - // Get all available tenants for this account to calculate the full set + // Get all available tenants for this account const allTenantsForAccount = nonNullValue(context.allTenants, 'context.allTenants', 'ExecuteStep.ts'); - const allTenantKeys = allTenantsForAccount.map((tenant) => `${tenant.tenantId}/${selectedAccount.id}`); + const selectedTenantIds = new Set(selectedTenants.map((tenant) => tenant.tenantId || '')); + + // Use the individual add/remove functions to update tenant selections + const { addUnselectedTenant, removeUnselectedTenant } = await import('../subscriptionFiltering'); - // Save the tenant selections using the centralized function - const { setSelectedTenantIds } = await import('../subscriptionFiltering'); - await setSelectedTenantIds(tenantAccountIds, allTenantKeys); + // Process each tenant - add to unselected if not selected, remove from unselected if selected + for (const tenant of allTenantsForAccount) { + const tenantId = tenant.tenantId || ''; + if (selectedTenantIds.has(tenantId)) { + // Tenant is selected, so remove it from unselected list (make it available) + await removeUnselectedTenant(tenantId, selectedAccount.id); + } else { + // Tenant is not selected, so add it to unselected list (filter it out) + await addUnselectedTenant(tenantId, selectedAccount.id); + } + } ext.outputChannel.appendLine( l10n.t( diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index 39fd24cc7..ebdac04b9 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -197,32 +197,6 @@ export async function removeUnselectedTenant(tenantId: string, accountId: string } } -/** - * Updates the unselected tenants list based on selected tenant IDs. - * This syncs with the Azure Resource Groups extension which stores unselected tenants. - * - * @param selectedTenantKeys Array of selected tenant IDs in 'tenantId/accountId' format - * @param allTenantKeys All available tenant/account combinations in 'tenantId/accountId' format - */ -export async function setSelectedTenantIds(selectedTenantKeys: string[], allTenantKeys: string[]): Promise { - // Calculate unselected tenants (inverse logic to match Azure Resource Groups) - const unselectedTenants = allTenantKeys.filter((tenant) => !selectedTenantKeys.includes(tenant)); - - try { - const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); - await azureResourcesConfig.update('unselectedTenants', unselectedTenants, vscode.ConfigurationTarget.Global); - } catch (error) { - // Log the error if the primary storage (Azure Resource Groups config) update fails - console.error( - 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', - error, - ); - } finally { - // Always update our fallback storage regardless of primary storage success - await ext.context.globalState.update('azure-discovery.unselectedTenants', unselectedTenants); - } -} - /** * Clears all tenant filtering configuration. * This will make all tenants available for discovery. From 960adaa4440ecea03fa204bfd6439e205da8aacf Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 11:56:58 +0200 Subject: [PATCH 31/88] improved account selection --- l10n/bundle.l10n.json | 40 +++++++ .../CredentialsManagementWizardContext.ts | 1 - .../SelectAccountStep.ts | 110 +++++++----------- .../configureAzureCredentials.ts | 9 +- 4 files changed, 90 insertions(+), 70 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index dda070615..6493fab7a 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -19,6 +19,8 @@ "⚠ TLS/SSL Disabled": "⚠ TLS/SSL Disabled", "✅ **Security:** TLS/SSL Enabled": "✅ **Security:** TLS/SSL Enabled", "$(add) Create...": "$(add) Create...", + "$(check-all) Select All": "$(check-all) Select All", + "$(clear-all) Clear All": "$(clear-all) Clear All", "$(info) Some storage accounts were filtered because of their sku. Learn more...": "$(info) Some storage accounts were filtered because of their sku. Learn more...", "$(keyboard) Manually enter error": "$(keyboard) Manually enter error", "$(plus) Create new {0}...": "$(plus) Create new {0}...", @@ -35,6 +37,7 @@ "A connection with the same username and host already exists.": "A connection with the same username and host already exists.", "A new connection will be added to your Connections View.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the exension settings.": "A new connection will be added to your Connections View.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the exension settings.", "A value is required to proceed.": "A value is required to proceed.", + "Account {0}: {1} tenant(s) selected": "Account {0}: {1} tenant(s) selected", "Account information is incomplete.": "Account information is incomplete.", "Add new document": "Add new document", "Advanced": "Advanced", @@ -60,8 +63,17 @@ "Azure Cosmos DB for MongoDB (RU)": "Azure Cosmos DB for MongoDB (RU)", "Azure Cosmos DB for MongoDB (RU) Emulator": "Azure Cosmos DB for MongoDB (RU) Emulator", "Azure Cosmos DB for MongoDB (vCore)": "Azure Cosmos DB for MongoDB (vCore)", + "Azure credentials configuration cleared.": "Azure credentials configuration cleared.", + "Azure credentials configuration completed successfully.": "Azure credentials configuration completed successfully.", + "Azure credentials configuration failed (attempt {0}/{1}): {2}": "Azure credentials configuration failed (attempt {0}/{1}): {2}", + "Azure credentials configuration has been cleared. Discovery will now include all tenants.": "Azure credentials configuration has been cleared. Discovery will now include all tenants.", + "Azure credentials configuration was cancelled by user.": "Azure credentials configuration was cancelled by user.", "Azure Service Discovery": "Azure Service Discovery", "Azure Service Discovery for MongoDB RU": "Azure Service Discovery for MongoDB RU", + "Azure sign-in completed successfully": "Azure sign-in completed successfully", + "Azure sign-in failed: {0}": "Azure sign-in failed: {0}", + "Azure sign-in was cancelled or failed": "Azure sign-in was cancelled or failed", + "Azure tenant filtering is active:\n\n{0}\n\nUse \"Configure Azure Credentials\" to modify these settings.": "Azure tenant filtering is active:\n\n{0}\n\nUse \"Configure Azure Credentials\" to modify these settings.", "Azure VM Service Discovery": "Azure VM Service Discovery", "Azure VM: Attempting to authenticate with \"{vmName}\"…": "Azure VM: Attempting to authenticate with \"{vmName}\"…", "Azure VM: Connected to \"{vmName}\" as \"{username}\".": "Azure VM: Connected to \"{vmName}\" as \"{username}\".", @@ -80,6 +92,8 @@ "Choose the migration action…": "Choose the migration action…", "Choose your provider…": "Choose your provider…", "Choose your Service Provider": "Choose your Service Provider", + "Clear all selected tenants for this account": "Clear all selected tenants for this account", + "Clear Configuration": "Clear Configuration", "Click here to retry": "Click here to retry", "Click here to update credentials": "Click here to update credentials", "Click to view resource": "Click to view resource", @@ -90,6 +104,7 @@ "Collection name cannot contain the null character.": "Collection name cannot contain the null character.", "Collection name is required.": "Collection name is required.", "Collection names should begin with an underscore or a letter character.": "Collection names should begin with an underscore or a letter character.", + "Configure Azure Credentials": "Configure Azure Credentials", "Configure Azure VM Discovery Filters": "Configure Azure VM Discovery Filters", "Configure TLS/SSL Security": "Configure TLS/SSL Security", "Connect to a database": "Connect to a database", @@ -139,6 +154,7 @@ "Document must be an object.": "Document must be an object.", "Document must be an object. Skipping…": "Document must be an object. Skipping…", "DocumentDB and MongoDB Accounts": "DocumentDB and MongoDB Accounts", + "DocumentDB for VS Code is not signed in to Azure": "DocumentDB for VS Code is not signed in to Azure", "DocumentDB Local": "DocumentDB Local", "Documents": "Documents", "Does this occur consistently? ": "Does this occur consistently? ", @@ -193,6 +209,8 @@ "Exporting…": "Exporting…", "Extension dependency with id \"{0}\" must be updated.": "Extension dependency with id \"{0}\" must be updated.", "Failed to access Azure Databases VS Code Extension storage for migration: {error}": "Failed to access Azure Databases VS Code Extension storage for migration: {error}", + "Failed to clear Azure credentials configuration: {0}": "Failed to clear Azure credentials configuration: {0}", + "Failed to configure Azure credentials after {0} attempts: {1}": "Failed to configure Azure credentials after {0} attempts: {1}", "Failed to connect to \"{cluster}\"": "Failed to connect to \"{cluster}\"", "Failed to connect to VM \"{vmName}\"": "Failed to connect to VM \"{vmName}\"", "Failed to create Azure management clients: {0}": "Failed to create Azure management clients: {0}", @@ -211,8 +229,12 @@ "Failed to parse secrets for key {0}:": "Failed to parse secrets for key {0}:", "Failed to process URI: {0}": "Failed to process URI: {0}", "Failed to rename the connection.": "Failed to rename the connection.", + "Failed to retrieve Azure accounts: {0}": "Failed to retrieve Azure accounts: {0}", + "Failed to retrieve Azure credentials status: {0}": "Failed to retrieve Azure credentials status: {0}", + "Failed to retrieve tenants for account: {0}": "Failed to retrieve tenants for account: {0}", "Failed to save credentials for \"{cluster}\".": "Failed to save credentials for \"{cluster}\".", "Failed to save credentials.": "Failed to save credentials.", + "Failed to sign in to Azure: {0}": "Failed to sign in to Azure: {0}", "Failed to store secrets for key {0}:": "Failed to store secrets for key {0}:", "Failed to update the connection.": "Failed to update the connection.", "Failed with code \"{0}\".": "Failed with code \"{0}\".", @@ -241,6 +263,7 @@ "Importing…": "Importing…", "Indexes": "Indexes", "Info from the webview: ": "Info from the webview: ", + "Initializing Credentials Management…": "Initializing Credentials Management…", "Inserted {0} document(s). See output for more details.": "Inserted {0} document(s). See output for more details.", "Install Azure Account Extension...": "Install Azure Account Extension...", "Internal error: connectionString must be defined.": "Internal error: connectionString must be defined.", @@ -294,6 +317,7 @@ "No authentication method selected.": "No authentication method selected.", "No authentication methods available for \"{cluster}\".": "No authentication methods available for \"{cluster}\".", "No Azure subscription found for this tree item.": "No Azure subscription found for this tree item.", + "No Azure tenant filters are currently configured. All tenants will be included in discovery.": "No Azure tenant filters are currently configured. All tenants will be included in discovery.", "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.", @@ -308,6 +332,8 @@ "No scope was provided for the role assignment.": "No scope was provided for the role assignment.", "No session found for id {sessionId}": "No session found for id {sessionId}", "No subscriptions found": "No subscriptions found", + "No tenants found for the selected account. Please try signing in again or selecting a different account.": "No tenants found for the selected account. Please try signing in again or selecting a different account.", + "No tenants selected. Azure discovery will be filtered to exclude all results for this account.": "No tenants selected. Azure discovery will be filtered to exclude all results for this account.", "Not connected to any MongoDB database.": "Not connected to any MongoDB database.", "Note: This confirmation type can be configured in the extension settings.": "Note: This confirmation type can be configured in the extension settings.", "Note: You can disable these URL handling confirmations in the extension settings.": "Note: You can disable these URL handling confirmations in the extension settings.", @@ -335,6 +361,7 @@ "Provider \"{0}\" does not have resource type \"{1}\".": "Provider \"{0}\" does not have resource type \"{1}\".", "Refresh": "Refresh", "Refresh current view": "Refresh current view", + "Refreshing Azure discovery tree...": "Refreshing Azure discovery tree...", "Registering Providers...": "Registering Providers...", "Reload original document from the database": "Reload original document from the database", "Reload Window": "Reload Window", @@ -342,6 +369,7 @@ "Rename Connection": "Rename Connection", "Report an issue": "Report an issue", "Resource group \"{0}\" already exists in subscription \"{1}\".": "Resource group \"{0}\" already exists in subscription \"{1}\".", + "Restarting wizard after account sign-in...": "Restarting wizard after account sign-in...", "Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".", "Revisit connection details and try again.": "Revisit connection details and try again.", "Role assignment \"{0}\" created for the {2} resource \"{1}\".": "Role assignment \"{0}\" created for the {2} resource \"{1}\".", @@ -353,34 +381,44 @@ "Save document to the database": "Save document to the database", "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 Azure credentials configuration for account: {0}": "Saving Azure credentials configuration for account: {0}", "Saving credentials for \"{clusterName}\"…": "Saving credentials for \"{clusterName}\"…", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", "Select a location for new resources.": "Select a location for new resources.", "Select a workspace folder": "Select a workspace folder", + "Select all tenants for this account": "Select all tenants for this account", "Select an authentication method": "Select an authentication method", "Select an authentication method for \"{resourceName}\"": "Select an authentication method for \"{resourceName}\"", + "Select an Azure account to choose which tenants to use": "Select an Azure account to choose which tenants to use", "Select Existing": "Select Existing", "Select resource": "Select resource", "Select subscription": "Select subscription", "Select Subscriptions": "Select Subscriptions", "Select Subscriptions to Display": "Select Subscriptions to Display", "Select Subscriptions...": "Select Subscriptions...", + "Select tenants to include in discovery (multiple selection)": "Select tenants to include in discovery (multiple selection)", "Select the error you would like to report": "Select the error you would like to report", "Select the local connection type…": "Select the local connection type…", + "Selected tenants: {0}": "Selected tenants: {0}", "Service Discovery": "Service Discovery", "Sign In": "Sign In", + "Sign in to Azure to continue…": "Sign in to Azure to continue…", "Sign in to Azure...": "Sign in to Azure...", + "Sign in with a different account…": "Sign in with a different account…", "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.": "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.", "Skip for now": "Skip for now", "Small breadcrumb example with buttons": "Small breadcrumb example with buttons", "Some items could not be displayed": "Some items could not be displayed", "Specified character lengths should be 1 character or greater.": "Specified character lengths should be 1 character or greater.", "Started executable: \"{command}\". Connecting to host…": "Started executable: \"{command}\". Connecting to host…", + "Starting Azure credentials configuration wizard (attempt {0}/{1})": "Starting Azure credentials configuration wizard (attempt {0}/{1})", + "Starting Azure sign-in process...": "Starting Azure sign-in process...", "Starting executable: \"{command}\"": "Starting executable: \"{command}\"", "Starts with mongodb:// or mongodb+srv://": "Starts with mongodb:// or mongodb+srv://", "subscription": "subscription", "Subscription ID: {0}": "Subscription ID: {0}", + "Successfully configured Azure tenant filtering. Selected {0} tenant(s) for account {1}": "Successfully configured Azure tenant filtering. Selected {0} tenant(s) for account {1}", "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}\".", @@ -434,6 +472,7 @@ "This functionality requires updating the Azure Account extension to at least version \"{0}\".": "This functionality requires updating the Azure Account extension to at least version \"{0}\".", "This operation is not supported.": "This operation is not supported.", "This table view presents data at the root level by default.": "This table view presents data at the root level by default.", + "This will clear all Azure tenant filtering configuration. Azure discovery will include all tenants. Continue?": "This will clear all Azure tenant filtering configuration. Azure discovery will include all tenants. Continue?", "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.", "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}", @@ -448,6 +487,7 @@ "Unexpected status code: {0}": "Unexpected status code: {0}", "Unknown error": "Unknown error", "Unknown Error": "Unknown Error", + "Unknown tenant": "Unknown tenant", "Unrecognized node type encountered. Could not parse {constructorCall} as part of {functionCall}": "Unrecognized node type encountered. Could not parse {constructorCall} as part of {functionCall}", "Unrecognized node type encountered. We could not parse {text}": "Unrecognized node type encountered. We could not parse {text}", "Unrecognized token. Token text: {text}": "Unrecognized token. Token text: {text}", diff --git a/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts index 1372ed7c9..be238960f 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts @@ -23,5 +23,4 @@ export interface CredentialsManagementWizardContext extends IActionContext { // State tracking shouldRestartWizard?: boolean; - newAccountSignedIn?: boolean; } diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index b4b6d12f9..b9a4bfb49 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; @@ -18,54 +18,58 @@ interface AccountQuickPickItem extends vscode.QuickPickItem { export class SelectAccountStep extends AzureWizardPromptStep { public async prompt(context: CredentialsManagementWizardContext): Promise { - // Get all authenticated accounts - const accounts = await this.getAvailableAccounts(context); - context.availableAccounts = accounts; - - // Create quick pick items - const accountItems: AccountQuickPickItem[] = this.createAccountPickItems(accounts); - - // Add separator and additional options - const separatorItems: AccountQuickPickItem[] = [ - { label: '', kind: vscode.QuickPickItemKind.Separator }, - { - label: l10n.t('$(plus) Sign in with a different account'), - detail: l10n.t('Add a new Azure account to VS Code'), - isSignInOption: true, - }, - { - label: l10n.t('$(question) Learn More'), - detail: l10n.t('Learn more about Azure authentication in VS Code'), - isLearnMoreOption: true, - }, - ]; - - const allItems = [...accountItems, ...separatorItems]; - - const selectedItem = await context.ui.showQuickPick(allItems, { + // Create a promise that will resolve to the quick pick items + const quickPickItemsPromise = this.getAvailableAccounts(context).then((accounts) => { + context.availableAccounts = accounts; + + const accountItems: AccountQuickPickItem[] = accounts.map((account) => ({ + label: account.label, + detail: account.id, + iconPath: new vscode.ThemeIcon('account'), + account, + })); + + // Handle empty accounts case + if (accountItems.length === 0) { + return [ + { + label: l10n.t('Sign in to Azure to continue…'), + detail: l10n.t('DocumentDB for VS Code is not signed in to Azure'), + iconPath: new vscode.ThemeIcon('sign-in'), + isSignInOption: true, + }, + ]; + } + + // Only show "sign in with different account" when there are existing accounts + return [ + ...accountItems, + { label: '', kind: vscode.QuickPickItemKind.Separator }, + { + label: l10n.t('Sign in with a different account…'), + iconPath: new vscode.ThemeIcon('sign-in'), + isSignInOption: true, + }, + ]; + }); + + const selectedItem = await context.ui.showQuickPick(quickPickItemsPromise, { stepName: 'selectAccount', - placeHolder: l10n.t('Select an Azure account'), + placeHolder: l10n.t('Select an Azure account to choose which tenants to use'), matchOnDescription: true, suppressPersistence: true, - loadingPlaceHolder: 'Loading...', + loadingPlaceHolder: l10n.t('Initializing Credentials Management…'), }); if (selectedItem.isSignInOption) { await this.handleSignIn(context); + // Set flag to restart wizard after sign-in context.shouldRestartWizard = true; - context.newAccountSignedIn = true; - throw new UserCancelledError(l10n.t('Restarting wizard after sign-in')); - } else if (selectedItem.isLearnMoreOption) { - await this.handleLearnMore(); - throw new UserCancelledError(l10n.t('User selected learn more')); - } else { - context.selectedAccount = nonNullValue( - selectedItem.account, - 'selectedItem.account', - 'SelectAccountStep.ts', - ); + return; // Exit this step, other steps won't run due to shouldPrompt() checks } + + context.selectedAccount = nonNullValue(selectedItem.account, 'selectedItem.account', 'SelectAccountStep.ts'); } public shouldPrompt(context: CredentialsManagementWizardContext): boolean { @@ -97,26 +101,6 @@ export class SelectAccountStep extends AzureWizardPromptStep ({ - label: account.label, - description: account.id, - iconPath: new vscode.ThemeIcon('account'), - account, - })); - } - private async handleSignIn(context: CredentialsManagementWizardContext): Promise { try { ext.outputChannel.appendLine(l10n.t('Starting Azure sign-in process...')); @@ -124,8 +108,6 @@ export class SelectAccountStep extends AzureWizardPromptStep { - const learnMoreUrl = - 'https://docs.microsoft.com/en-us/azure/developer/javascript/tutorial-vscode-azure-cli-node-01'; - await vscode.env.openExternal(vscode.Uri.parse(learnMoreUrl)); - } } diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index 53e353393..7f597aad6 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -16,7 +16,7 @@ import { SelectTenantsStep } from './SelectTenantsStep'; /** * Configures Azure credentials by allowing the user to select accounts and tenants - * for filtering Azure discovery results. This replaces the TODO in AzureDiscoveryProvider. + * for filtering Azure discovery results. * * @param context - The action context * @param azureSubscriptionProvider - The Azure subscription provider with filtering capabilities @@ -42,7 +42,6 @@ export async function configureAzureCredentials( [AzureContextProperties.SelectedTenants]: undefined, azureSubscriptionProvider, shouldRestartWizard: false, - newAccountSignedIn: false, }; // Create and configure the wizard @@ -56,6 +55,12 @@ export async function configureAzureCredentials( await wizard.prompt(); await wizard.execute(); + // Check if we need to restart the wizard (e.g., after sign-in) + if (wizardContext.shouldRestartWizard) { + ext.outputChannel.appendLine(l10n.t('Restarting wizard after account sign-in...')); + continue; + } + // Success - exit the retry loop ext.outputChannel.appendLine(l10n.t('Azure credentials configuration completed successfully.')); break; From d899a17602e0202d3c117ddb1b07b2fd57ca2bd7 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 12:09:43 +0200 Subject: [PATCH 32/88] fix: simplified select account step. --- .../SelectAccountStep.ts | 5 +- .../configureAzureCredentials.ts | 154 ++---------------- .../azure/credentialsManagement/index.ts | 6 +- 3 files changed, 18 insertions(+), 147 deletions(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index b9a4bfb49..4c881b3bb 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -62,10 +62,11 @@ export class SelectAccountStep extends AzureWizardPromptStep { - const maxRetries = 3; - let attempt = 0; + let wizardContext: CredentialsManagementWizardContext; - while (attempt < maxRetries) { + do { try { - attempt++; - ext.outputChannel.appendLine( - l10n.t('Starting Azure credentials configuration wizard (attempt {0}/{1})', attempt, maxRetries), - ); + ext.outputChannel.appendLine(l10n.t('Starting Azure credentials configuration wizard')); // Create wizard context - const wizardContext: CredentialsManagementWizardContext = { + wizardContext = { ...context, [AzureContextProperties.SelectedAccount]: undefined, [AzureContextProperties.SelectedTenants]: undefined, @@ -46,7 +42,7 @@ export async function configureAzureCredentials( // Create and configure the wizard const wizard = new AzureWizard(wizardContext, { - title: l10n.t('Configure Azure Credentials'), + title: l10n.t('Manage Azure Credentials'), promptSteps: [new SelectAccountStep(), new SelectTenantsStep()], executeSteps: [new ExecuteStep()], }); @@ -55,145 +51,23 @@ export async function configureAzureCredentials( await wizard.prompt(); await wizard.execute(); - // Check if we need to restart the wizard (e.g., after sign-in) if (wizardContext.shouldRestartWizard) { ext.outputChannel.appendLine(l10n.t('Restarting wizard after account sign-in...')); - continue; + } else { + ext.outputChannel.appendLine(l10n.t('Azure credentials configuration completed successfully.')); } - - // Success - exit the retry loop - ext.outputChannel.appendLine(l10n.t('Azure credentials configuration completed successfully.')); - break; } catch (error) { if (error instanceof UserCancelledError) { - // User cancelled or no restart needed + // User cancelled ext.outputChannel.appendLine(l10n.t('Azure credentials configuration was cancelled by user.')); return; } - // Other errors + // Any other error - don't retry, just throw const errorMessage = error instanceof Error ? error.message : String(error); - ext.outputChannel.appendLine( - l10n.t( - 'Azure credentials configuration failed (attempt {0}/{1}): {2}', - attempt, - maxRetries, - errorMessage, - ), - ); - - if (attempt >= maxRetries) { - // Final attempt failed - void vscode.window.showErrorMessage( - l10n.t('Failed to configure Azure credentials after {0} attempts: {1}', maxRetries, errorMessage), - ); - throw error; - } - - // Wait before retrying - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } -} - -/** - * Displays current Azure credentials configuration status - * - * @param _context - The action context (not used but required for command pattern) - */ -export async function showAzureCredentialsStatus(_context: IActionContext): Promise { - try { - // For status display, we need to get all available tenants to use the filtering function - const azureSubscriptionProvider = new AzureSubscriptionProviderWithFilters(); - const allTenants = await azureSubscriptionProvider.getTenants(); - - const { isTenantFilteredOut } = await import('../subscriptionFiltering'); - - azureSubscriptionProvider.dispose(); // Clean up - - // Check which tenants are currently selected (not filtered out) - const selectedTenantKeys: string[] = []; - for (const tenant of allTenants) { - if (tenant.tenantId && !isTenantFilteredOut(tenant.tenantId, tenant.account.id)) { - selectedTenantKeys.push(`${tenant.tenantId}/${tenant.account.id}`); - } - } - - if (selectedTenantKeys.length === 0) { - void vscode.window.showInformationMessage( - l10n.t('No Azure tenant filters are currently configured. All tenants will be included in discovery.'), - ); - return; - } - - // Group by account - const accountTenantMap = new Map(); - for (const tenantAccountId of selectedTenantKeys) { - const [tenantId, accountId] = tenantAccountId.split('/'); - if (!accountTenantMap.has(accountId)) { - accountTenantMap.set(accountId, []); - } - accountTenantMap.get(accountId)?.push(tenantId); - } - - const statusMessages: string[] = []; - for (const [accountId, tenantIds] of accountTenantMap) { - statusMessages.push(l10n.t('Account {0}: {1} tenant(s) selected', accountId, tenantIds.length)); - } - - const fullMessage = l10n.t( - 'Azure tenant filtering is active:\n\n{0}\n\nUse "Configure Azure Credentials" to modify these settings.', - statusMessages.join('\n'), - ); - - void vscode.window.showInformationMessage(fullMessage); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - void vscode.window.showErrorMessage(l10n.t('Failed to retrieve Azure credentials status: {0}', errorMessage)); - } -} - -/** - * Clears all Azure credentials configuration - * - * @param _context - The action context (not used but required for command pattern) - */ -export async function clearAzureCredentialsConfiguration(_context: IActionContext): Promise { - try { - const confirmResult = await vscode.window.showWarningMessage( - l10n.t( - 'This will clear all Azure tenant filtering configuration. Azure discovery will include all tenants. Continue?', - ), - { modal: true }, - l10n.t('Clear Configuration'), - ); - - if (confirmResult) { - // Clear tenant filtering by setting empty unselected tenants array - try { - const config = vscode.workspace.getConfiguration('azureResourceGroups'); - await config.update('unselectedTenants', [], vscode.ConfigurationTarget.Global); - } catch (error) { - console.error( - 'Unable to update Azure Resource Groups tenant configuration, using fallback storage.', - error, - ); - } finally { - // Always update our fallback storage regardless of primary storage success - await ext.context.globalState.update('azure-discovery.unselectedTenants', []); - } - - ext.outputChannel.appendLine(l10n.t('Azure credentials configuration cleared.')); - ext.discoveryBranchDataProvider.refresh(); - - void vscode.window.showInformationMessage( - l10n.t('Azure credentials configuration has been cleared. Discovery will now include all tenants.'), - ); + ext.outputChannel.appendLine(l10n.t('Azure credentials configuration failed: {0}', errorMessage)); + void vscode.window.showErrorMessage(l10n.t('Failed to configure Azure credentials: {0}', errorMessage)); + throw error; } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - void vscode.window.showErrorMessage( - l10n.t('Failed to clear Azure credentials configuration: {0}', errorMessage), - ); - } + } while (wizardContext.shouldRestartWizard); } diff --git a/src/plugins/api-shared/azure/credentialsManagement/index.ts b/src/plugins/api-shared/azure/credentialsManagement/index.ts index 9ea08cf54..ad3fc618a 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/index.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/index.ts @@ -3,11 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export { - clearAzureCredentialsConfiguration, - configureAzureCredentials, - showAzureCredentialsStatus, -} from './configureAzureCredentials'; +export { configureAzureCredentials } from './configureAzureCredentials'; export type { CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; export { ExecuteStep } from './ExecuteStep'; export { SelectAccountStep } from './SelectAccountStep'; From 5c9de0a729b80532f253c63c77b8e68fef28227c Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 13:33:04 +0200 Subject: [PATCH 33/88] fix: simplified select tenant step. --- .../SelectTenantsStep.ts | 135 +++++++----------- 1 file changed, 52 insertions(+), 83 deletions(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts index f25cc3654..061b8f596 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -13,92 +13,81 @@ import { type CredentialsManagementWizardContext } from './CredentialsManagement interface TenantQuickPickItem extends vscode.QuickPickItem { tenant?: AzureTenant; - isSelectAllOption?: boolean; - isClearAllOption?: boolean; } export class SelectTenantsStep extends AzureWizardPromptStep { public async prompt(context: CredentialsManagementWizardContext): Promise { - // Get available tenants for the selected account - const tenants = await this.getAvailableTenantsForAccount(context); - - // Initialize availableTenants map if not exists - if (!context.availableTenants) { - context.availableTenants = new Map(); - } - - // Store tenants for this account const selectedAccount = nonNullValue( context.selectedAccount, 'context.selectedAccount', 'SelectTenantsStep.ts', ); - context.availableTenants.set(selectedAccount.id, tenants); - - // Store all tenants for the selected account in context for ExecuteStep - context.allTenants = tenants; - if (tenants.length === 0) { - void vscode.window.showWarningMessage( - l10n.t( - 'No tenants found for the selected account. Please try signing in again or selecting a different account.', - ), - ); - return; - } - - // Get currently selected tenant IDs from storage + // Get the filtering function const { isTenantFilteredOut } = await import('../subscriptionFiltering'); - const currentlySelectedTenantIds = new Set(); - // Check which tenants are currently selected (not filtered out) - for (const tenant of tenants) { - if (tenant.tenantId && !isTenantFilteredOut(tenant.tenantId, selectedAccount.id)) { - currentlySelectedTenantIds.add(tenant.tenantId); + // Create a promise that will resolve to the quick pick items + const quickPickItemsPromise = this.getAvailableTenantsForAccount(context).then((tenants) => { + // Initialize availableTenants map if not exists + if (!context.availableTenants) { + context.availableTenants = new Map(); } - } - // Create quick pick items with checkboxes - const tenantItems: TenantQuickPickItem[] = this.createTenantPickItems(tenants, currentlySelectedTenantIds); + // Store tenants for this account + context.availableTenants.set(selectedAccount.id, tenants); - // Add control options - const controlItems: TenantQuickPickItem[] = [ - { label: '', kind: vscode.QuickPickItemKind.Separator }, - { - label: l10n.t('$(check-all) Select All'), - detail: l10n.t('Select all tenants for this account'), - isSelectAllOption: true, - }, - { - label: l10n.t('$(clear-all) Clear All'), - detail: l10n.t('Clear all selected tenants for this account'), - isClearAllOption: true, - }, - ]; + // Store all tenants for the selected account in context for ExecuteStep + context.allTenants = tenants; + + if (tenants.length === 0) { + void vscode.window.showWarningMessage( + l10n.t( + 'No tenants found for the selected account. Please try signing in again or selecting a different account.', + ), + ); + return []; + } + + // Create quick pick items + const tenantItems: TenantQuickPickItem[] = tenants.map((tenant) => { + const tenantId = tenant.tenantId || ''; + const displayName = tenant.displayName || tenantId; + + return { + label: displayName, + detail: tenantId, + description: tenant.defaultDomain ?? undefined, + iconPath: new vscode.ThemeIcon('organization'), + tenant, + }; + }); - const allItems = [...tenantItems, ...controlItems]; + return tenantItems; + }); - const selectedItems = await context.ui.showQuickPick(allItems, { + const selectedItems = await context.ui.showQuickPick(quickPickItemsPromise, { stepName: 'selectTenants', - placeHolder: l10n.t('Select tenants to include in discovery (multiple selection)'), + placeHolder: l10n.t('Select tenants to use (multiple selection)'), canPickMany: true, matchOnDescription: true, suppressPersistence: true, - loadingPlaceHolder: 'Loading...', + loadingPlaceHolder: l10n.t('Loading tenants…'), + isPickSelected: (pick) => { + const tenantPick = pick as TenantQuickPickItem; + + // Check if this tenant is currently selected (not filtered out) + if (tenantPick.tenant?.tenantId) { + return !isTenantFilteredOut(tenantPick.tenant.tenantId, selectedAccount.id); + } + + return false; + }, }); - // Handle control options - if (selectedItems.some((item) => item.isSelectAllOption)) { - context.selectedTenants = tenants; - } else if (selectedItems.some((item) => item.isClearAllOption)) { - context.selectedTenants = []; - } else { - // Filter out control items and extract tenants - const tenantSelections = selectedItems.filter((item) => item.tenant); - context.selectedTenants = tenantSelections.map((item) => - nonNullValue(item.tenant, 'item.tenant', 'SelectTenantsStep.ts'), - ); - } + // Extract selected tenants + context.selectedTenants = selectedItems.map((item) => + nonNullValue(item.tenant, 'item.tenant', 'SelectTenantsStep.ts'), + ); } public shouldPrompt(context: CredentialsManagementWizardContext): boolean { @@ -132,24 +121,4 @@ export class SelectTenantsStep extends AzureWizardPromptStep, - ): TenantQuickPickItem[] { - return tenants.map((tenant) => { - const tenantId = tenant.tenantId || ''; - const displayName = tenant.displayName || tenantId; - const isSelected = currentlySelectedTenantIds.has(tenantId); - - return { - label: displayName, - description: tenantId, - detail: tenant.domains?.[0] || undefined, // Show primary domain if available - iconPath: new vscode.ThemeIcon('organization'), - picked: isSelected, - tenant, - }; - }); - } } From 5cb323f0b827a88e58e2103ef0c49abd013dfc08 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 13:37:50 +0200 Subject: [PATCH 34/88] simplified context config for credentials management --- l10n/bundle.l10n.json | 24 +++++-------------- .../configureAzureCredentials.ts | 7 ++---- .../azure/wizard/AzureContextProperties.ts | 2 -- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 6493fab7a..378710268 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -19,8 +19,6 @@ "⚠ TLS/SSL Disabled": "⚠ TLS/SSL Disabled", "✅ **Security:** TLS/SSL Enabled": "✅ **Security:** TLS/SSL Enabled", "$(add) Create...": "$(add) Create...", - "$(check-all) Select All": "$(check-all) Select All", - "$(clear-all) Clear All": "$(clear-all) Clear All", "$(info) Some storage accounts were filtered because of their sku. Learn more...": "$(info) Some storage accounts were filtered because of their sku. Learn more...", "$(keyboard) Manually enter error": "$(keyboard) Manually enter error", "$(plus) Create new {0}...": "$(plus) Create new {0}...", @@ -37,7 +35,6 @@ "A connection with the same username and host already exists.": "A connection with the same username and host already exists.", "A new connection will be added to your Connections View.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the exension settings.": "A new connection will be added to your Connections View.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the exension settings.", "A value is required to proceed.": "A value is required to proceed.", - "Account {0}: {1} tenant(s) selected": "Account {0}: {1} tenant(s) selected", "Account information is incomplete.": "Account information is incomplete.", "Add new document": "Add new document", "Advanced": "Advanced", @@ -63,17 +60,14 @@ "Azure Cosmos DB for MongoDB (RU)": "Azure Cosmos DB for MongoDB (RU)", "Azure Cosmos DB for MongoDB (RU) Emulator": "Azure Cosmos DB for MongoDB (RU) Emulator", "Azure Cosmos DB for MongoDB (vCore)": "Azure Cosmos DB for MongoDB (vCore)", - "Azure credentials configuration cleared.": "Azure credentials configuration cleared.", "Azure credentials configuration completed successfully.": "Azure credentials configuration completed successfully.", - "Azure credentials configuration failed (attempt {0}/{1}): {2}": "Azure credentials configuration failed (attempt {0}/{1}): {2}", - "Azure credentials configuration has been cleared. Discovery will now include all tenants.": "Azure credentials configuration has been cleared. Discovery will now include all tenants.", + "Azure credentials configuration failed: {0}": "Azure credentials configuration failed: {0}", "Azure credentials configuration was cancelled by user.": "Azure credentials configuration was cancelled by user.", "Azure Service Discovery": "Azure Service Discovery", "Azure Service Discovery for MongoDB RU": "Azure Service Discovery for MongoDB RU", "Azure sign-in completed successfully": "Azure sign-in completed successfully", "Azure sign-in failed: {0}": "Azure sign-in failed: {0}", "Azure sign-in was cancelled or failed": "Azure sign-in was cancelled or failed", - "Azure tenant filtering is active:\n\n{0}\n\nUse \"Configure Azure Credentials\" to modify these settings.": "Azure tenant filtering is active:\n\n{0}\n\nUse \"Configure Azure Credentials\" to modify these settings.", "Azure VM Service Discovery": "Azure VM Service Discovery", "Azure VM: Attempting to authenticate with \"{vmName}\"…": "Azure VM: Attempting to authenticate with \"{vmName}\"…", "Azure VM: Connected to \"{vmName}\" as \"{username}\".": "Azure VM: Connected to \"{vmName}\" as \"{username}\".", @@ -92,8 +86,6 @@ "Choose the migration action…": "Choose the migration action…", "Choose your provider…": "Choose your provider…", "Choose your Service Provider": "Choose your Service Provider", - "Clear all selected tenants for this account": "Clear all selected tenants for this account", - "Clear Configuration": "Clear Configuration", "Click here to retry": "Click here to retry", "Click here to update credentials": "Click here to update credentials", "Click to view resource": "Click to view resource", @@ -104,7 +96,6 @@ "Collection name cannot contain the null character.": "Collection name cannot contain the null character.", "Collection name is required.": "Collection name is required.", "Collection names should begin with an underscore or a letter character.": "Collection names should begin with an underscore or a letter character.", - "Configure Azure Credentials": "Configure Azure Credentials", "Configure Azure VM Discovery Filters": "Configure Azure VM Discovery Filters", "Configure TLS/SSL Security": "Configure TLS/SSL Security", "Connect to a database": "Connect to a database", @@ -209,8 +200,7 @@ "Exporting…": "Exporting…", "Extension dependency with id \"{0}\" must be updated.": "Extension dependency with id \"{0}\" must be updated.", "Failed to access Azure Databases VS Code Extension storage for migration: {error}": "Failed to access Azure Databases VS Code Extension storage for migration: {error}", - "Failed to clear Azure credentials configuration: {0}": "Failed to clear Azure credentials configuration: {0}", - "Failed to configure Azure credentials after {0} attempts: {1}": "Failed to configure Azure credentials after {0} attempts: {1}", + "Failed to configure Azure credentials: {0}": "Failed to configure Azure credentials: {0}", "Failed to connect to \"{cluster}\"": "Failed to connect to \"{cluster}\"", "Failed to connect to VM \"{vmName}\"": "Failed to connect to VM \"{vmName}\"", "Failed to create Azure management clients: {0}": "Failed to create Azure management clients: {0}", @@ -230,7 +220,6 @@ "Failed to process URI: {0}": "Failed to process URI: {0}", "Failed to rename the connection.": "Failed to rename the connection.", "Failed to retrieve Azure accounts: {0}": "Failed to retrieve Azure accounts: {0}", - "Failed to retrieve Azure credentials status: {0}": "Failed to retrieve Azure credentials status: {0}", "Failed to retrieve tenants for account: {0}": "Failed to retrieve tenants for account: {0}", "Failed to save credentials for \"{cluster}\".": "Failed to save credentials for \"{cluster}\".", "Failed to save credentials.": "Failed to save credentials.", @@ -300,9 +289,11 @@ "Loading resources...": "Loading resources...", "Loading RU clusters…": "Loading RU clusters…", "Loading subscriptions…": "Loading subscriptions…", + "Loading tenants…": "Loading tenants…", "Loading Virtual Machines…": "Loading Virtual Machines…", "Loading...": "Loading...", "Location": "Location", + "Manage Azure Credentials": "Manage Azure Credentials", "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.": "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.", "Mongo Shell connected.": "Mongo Shell connected.", "Mongo Shell Error: {error}": "Mongo Shell Error: {error}", @@ -317,7 +308,6 @@ "No authentication method selected.": "No authentication method selected.", "No authentication methods available for \"{cluster}\".": "No authentication methods available for \"{cluster}\".", "No Azure subscription found for this tree item.": "No Azure subscription found for this tree item.", - "No Azure tenant filters are currently configured. All tenants will be included in discovery.": "No Azure tenant filters are currently configured. All tenants will be included in discovery.", "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.", @@ -387,7 +377,6 @@ "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", "Select a location for new resources.": "Select a location for new resources.", "Select a workspace folder": "Select a workspace folder", - "Select all tenants for this account": "Select all tenants for this account", "Select an authentication method": "Select an authentication method", "Select an authentication method for \"{resourceName}\"": "Select an authentication method for \"{resourceName}\"", "Select an Azure account to choose which tenants to use": "Select an Azure account to choose which tenants to use", @@ -397,7 +386,7 @@ "Select Subscriptions": "Select Subscriptions", "Select Subscriptions to Display": "Select Subscriptions to Display", "Select Subscriptions...": "Select Subscriptions...", - "Select tenants to include in discovery (multiple selection)": "Select tenants to include in discovery (multiple selection)", + "Select tenants to use (multiple selection)": "Select tenants to use (multiple selection)", "Select the error you would like to report": "Select the error you would like to report", "Select the local connection type…": "Select the local connection type…", "Selected tenants: {0}": "Selected tenants: {0}", @@ -412,7 +401,7 @@ "Some items could not be displayed": "Some items could not be displayed", "Specified character lengths should be 1 character or greater.": "Specified character lengths should be 1 character or greater.", "Started executable: \"{command}\". Connecting to host…": "Started executable: \"{command}\". Connecting to host…", - "Starting Azure credentials configuration wizard (attempt {0}/{1})": "Starting Azure credentials configuration wizard (attempt {0}/{1})", + "Starting Azure credentials configuration wizard": "Starting Azure credentials configuration wizard", "Starting Azure sign-in process...": "Starting Azure sign-in process...", "Starting executable: \"{command}\"": "Starting executable: \"{command}\"", "Starts with mongodb:// or mongodb+srv://": "Starts with mongodb:// or mongodb+srv://", @@ -472,7 +461,6 @@ "This functionality requires updating the Azure Account extension to at least version \"{0}\".": "This functionality requires updating the Azure Account extension to at least version \"{0}\".", "This operation is not supported.": "This operation is not supported.", "This table view presents data at the root level by default.": "This table view presents data at the root level by default.", - "This will clear all Azure tenant filtering configuration. Azure discovery will include all tenants. Continue?": "This will clear all Azure tenant filtering configuration. Azure discovery will include all tenants. Continue?", "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.", "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}", diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index 766aac7a6..9910c19b9 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -8,7 +8,6 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; -import { AzureContextProperties } from '../wizard/AzureContextProperties'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; import { ExecuteStep } from './ExecuteStep'; import { SelectAccountStep } from './SelectAccountStep'; @@ -34,8 +33,8 @@ export async function configureAzureCredentials( // Create wizard context wizardContext = { ...context, - [AzureContextProperties.SelectedAccount]: undefined, - [AzureContextProperties.SelectedTenants]: undefined, + selectedAccount: undefined, + selectedTenants: undefined, azureSubscriptionProvider, shouldRestartWizard: false, }; @@ -53,8 +52,6 @@ export async function configureAzureCredentials( if (wizardContext.shouldRestartWizard) { ext.outputChannel.appendLine(l10n.t('Restarting wizard after account sign-in...')); - } else { - ext.outputChannel.appendLine(l10n.t('Azure credentials configuration completed successfully.')); } } catch (error) { if (error instanceof UserCancelledError) { diff --git a/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts b/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts index e829f24d7..6ec698d0f 100644 --- a/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts +++ b/src/plugins/api-shared/azure/wizard/AzureContextProperties.ts @@ -7,6 +7,4 @@ export enum AzureContextProperties { AzureSubscriptionProvider = 'azureSubscriptionProvider', SelectedSubscription = 'selectedSubscription', SelectedCluster = 'selectedCluster', - SelectedAccount = 'selectedAccount', - SelectedTenants = 'selectedTenants', } From e202ad7c0fea423c37e285da258056fe295e0766 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 18:40:43 +0200 Subject: [PATCH 35/88] fix: not attempting to sync unselectedTenants with Azure Resources (it's not accessible) --- .../AzureSubscriptionProviderWithFilters.ts | 16 +-- .../api-shared/azure/subscriptionFiltering.ts | 101 +++++------------- 2 files changed, 30 insertions(+), 87 deletions(-) diff --git a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts index d146e4e99..62d357dc0 100644 --- a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts +++ b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts @@ -11,6 +11,7 @@ import { } from '@microsoft/vscode-azext-azureauth'; import * as vscode from 'vscode'; import { ext } from '../../../extensionVariables'; +import { getTenantFilteredSubscriptions } from './subscriptionFiltering'; /** * Extends VSCodeAzureSubscriptionProvider to customize tenant and subscription filters @@ -37,24 +38,17 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio /** * Gets subscriptions from the Azure subscription provider and applies tenant filtering. - * When getSubscriptions(true) is called, all filtering is handled by the base provider. - * When getSubscriptions(false) is called, we manually apply tenant filtering client-side. + * Tenant filtering is always applied regardless of the subscription filter parameter. * * @param filter Whether to apply subscription filtering or a custom filter - * @returns Filtered list of subscriptions + * @returns Filtered list of subscriptions with tenant filtering applied */ public override async getSubscriptions(filter?: boolean | GetSubscriptionsFilter): Promise { // Get subscriptions from the base provider with the original filter parameter const subscriptions = await super.getSubscriptions(filter); - // If filter is explicitly false, apply tenant filtering manually - // When filter is true/undefined, the base provider already handles all filtering - if (filter === false) { - const { getTenantFilteredSubscriptions } = await import('./subscriptionFiltering'); - return getTenantFilteredSubscriptions(subscriptions); - } - - return subscriptions; + // Always apply tenant filtering regardless of the filter parameter + return getTenantFilteredSubscriptions(subscriptions); } /** diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index ebdac04b9..a377ab143 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -80,25 +80,19 @@ export async function setSelectedSubscriptionIds(subscriptionIds: string[]): Pro /** * Checks if a tenant is filtered out based on stored tenant filters. * + * Note: The Azure Resource Groups extension stores unselected tenants in their own + * extension's globalState using context.globalState.get('unselectedTenants'). + * Since each extension has its own isolated globalState, we cannot access their data. + * We replicate their behavior using our own storage so that if Azure Resource Groups + * ever exposes their unselected tenants list publicly, we can set up synchronization. + * * @param tenantId The tenant ID to check * @param accountId The account ID associated with the tenant * @returns True if the tenant is filtered out (unchecked), false otherwise */ export function isTenantFilteredOut(tenantId: string, accountId: string): boolean { - // Try the Azure Resource Groups config first (primary storage) - const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); - const primaryUnselectedTenants = azureResourcesConfig.get('unselectedTenants', []); - - // If nothing found in primary storage, try our fallback storage - if (primaryUnselectedTenants.length === 0) { - const fallbackUnselectedTenants = ext.context.globalState.get( - 'azure-discovery.unselectedTenants', - [], - ); - return fallbackUnselectedTenants.includes(`${tenantId}/${accountId}`); - } - - return primaryUnselectedTenants.includes(`${tenantId}/${accountId}`); + const unselectedTenants = ext.context.globalState.get('azure-discovery.unselectedTenants', []); + return unselectedTenants.includes(`${tenantId}/${accountId}`); } /** @@ -121,40 +115,21 @@ export function getTenantFilteredSubscriptions(subscriptions: AzureSubscription[ * Adds a tenant to the unselected tenants list. * This will filter out the tenant from discovery. * + * Note: We use our own extension's globalState for tenant filtering since the Azure Resource Groups + * extension's unselected tenants list is not publicly accessible (stored in their own globalState). + * This replicates their behavior so that if they ever expose their data, we can sync with it. + * * @param tenantId The tenant ID to add to unselected list * @param accountId The account ID associated with the tenant */ export async function addUnselectedTenant(tenantId: string, accountId: string): Promise { const tenantKey = `${tenantId}/${accountId}`; - - // Get current unselected tenants from both storage locations - const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); - const primaryUnselectedTenants = azureResourcesConfig.get('unselectedTenants', []); - const fallbackUnselectedTenants = ext.context.globalState.get('azure-discovery.unselectedTenants', []); - - // Use primary storage if available, otherwise fallback storage - const currentUnselectedTenants = - primaryUnselectedTenants.length > 0 ? primaryUnselectedTenants : fallbackUnselectedTenants; + const currentUnselectedTenants = ext.context.globalState.get('azure-discovery.unselectedTenants', []); // Add if not already present if (!currentUnselectedTenants.includes(tenantKey)) { const updatedUnselectedTenants = [...currentUnselectedTenants, tenantKey]; - - try { - await azureResourcesConfig.update( - 'unselectedTenants', - updatedUnselectedTenants, - vscode.ConfigurationTarget.Global, - ); - } catch (error) { - console.error( - 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', - error, - ); - } finally { - // Always update our fallback storage regardless of primary storage success - await ext.context.globalState.update('azure-discovery.unselectedTenants', updatedUnselectedTenants); - } + await ext.context.globalState.update('azure-discovery.unselectedTenants', updatedUnselectedTenants); } } @@ -162,58 +137,32 @@ export async function addUnselectedTenant(tenantId: string, accountId: string): * Removes a tenant from the unselected tenants list. * This will make the tenant available for discovery. * + * Note: We use our own extension's globalState for tenant filtering since the Azure Resource Groups + * extension's unselected tenants list is not publicly accessible (stored in their own globalState). + * This replicates their behavior so that if they ever expose their data, we can sync with it. + * * @param tenantId The tenant ID to remove from unselected list * @param accountId The account ID associated with the tenant */ export async function removeUnselectedTenant(tenantId: string, accountId: string): Promise { const tenantKey = `${tenantId}/${accountId}`; - - // Get current unselected tenants from both storage locations - const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); - const primaryUnselectedTenants = azureResourcesConfig.get('unselectedTenants', []); - const fallbackUnselectedTenants = ext.context.globalState.get('azure-discovery.unselectedTenants', []); - - // Use primary storage if available, otherwise fallback storage - const currentUnselectedTenants = - primaryUnselectedTenants.length > 0 ? primaryUnselectedTenants : fallbackUnselectedTenants; + const currentUnselectedTenants = ext.context.globalState.get('azure-discovery.unselectedTenants', []); // Remove if present const updatedUnselectedTenants = currentUnselectedTenants.filter((tenant) => tenant !== tenantKey); - - try { - await azureResourcesConfig.update( - 'unselectedTenants', - updatedUnselectedTenants, - vscode.ConfigurationTarget.Global, - ); - } catch (error) { - console.error( - 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', - error, - ); - } finally { - // Always update our fallback storage regardless of primary storage success - await ext.context.globalState.update('azure-discovery.unselectedTenants', updatedUnselectedTenants); - } + await ext.context.globalState.update('azure-discovery.unselectedTenants', updatedUnselectedTenants); } /** * Clears all tenant filtering configuration. * This will make all tenants available for discovery. + * + * Note: We use our own extension's globalState for tenant filtering since the Azure Resource Groups + * extension's unselected tenants list is not publicly accessible (stored in their own globalState). + * This replicates their behavior so that if they ever expose their data, we can sync with it. */ export async function clearTenantFiltering(): Promise { - try { - const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); - await azureResourcesConfig.update('unselectedTenants', [], vscode.ConfigurationTarget.Global); - } catch (error) { - console.error( - 'Unable to update primary storage (Azure Resource Groups tenant configuration), using fallback storage.', - error, - ); - } finally { - // Always update our fallback storage regardless of primary storage success - await ext.context.globalState.update('azure-discovery.unselectedTenants', []); - } + await ext.context.globalState.update('azure-discovery.unselectedTenants', []); } /** From ec5fce5ae0578d97e033c2e1d3d1104b09d3493d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 19:01:57 +0200 Subject: [PATCH 36/88] fix: improved loading behavior on subscriptions --- .../SelectTenantsStep.ts | 6 +- .../api-shared/azure/subscriptionFiltering.ts | 61 +++++++++----- .../azure/wizard/SelectSubscriptionStep.ts | 82 +++++++++++-------- 3 files changed, 93 insertions(+), 56 deletions(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts index 061b8f596..c0eb29433 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -9,6 +9,7 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { nonNullValue } from '../../../../utils/nonNull'; +import { isTenantFilteredOut } from '../subscriptionFiltering'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; interface TenantQuickPickItem extends vscode.QuickPickItem { @@ -23,9 +24,6 @@ export class SelectTenantsStep extends AzureWizardPromptStep { // Initialize availableTenants map if not exists @@ -67,7 +65,7 @@ export class SelectTenantsStep extends AzureWizardPromptStep undefined); + const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); + const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + + // Build tenant display name lookup for better UX + const tenantDisplayNames = new Map(); + if (knownTenants) { + for (const tenant of knownTenants) { + if (tenant.tenantId && tenant.displayName) { + tenantDisplayNames.set(tenant.tenantId, tenant.displayName); + } + } + } + return allSubscriptions - .map( - (subscription) => - >{ - label: duplicates.includes(subscription) - ? subscription.name + ` (${subscription.account?.label})` - : subscription.name, - description: subscription.subscriptionId, - data: subscription, - group: subscription.account.label, - iconPath: vscode.Uri.joinPath( - ext.context.extensionUri, - 'resources', - 'from_node_modules', - '@microsoft', - 'vscode-azext-azureutils', - 'resources', - 'azureSubscription.svg', - ), - }, - ) + .map((subscription) => { + const tenantName = tenantDisplayNames.get(subscription.tenantId); + + // Build description with tenant information + const description = tenantName + ? `${subscription.subscriptionId} (${tenantName})` + : subscription.subscriptionId; + + return >{ + label: duplicates.includes(subscription) + ? subscription.name + ` (${subscription.account?.label})` + : subscription.name, + description, + data: subscription, + group: subscription.account.label, + iconPath: vscode.Uri.joinPath( + ext.context.extensionUri, + 'resources', + 'from_node_modules', + '@microsoft', + 'vscode-azext-azureutils', + 'resources', + 'azureSubscription.svg', + ), + }; + }) .sort((a, b) => a.label.localeCompare(b.label)); }; diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 885631d8d..8d1743e30 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -9,6 +9,7 @@ import * as l10n from '@vscode/l10n'; import { Uri, window, type MessageItem, type QuickPickItem } from 'vscode'; import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; +import { getDuplicateSubscriptions } from '../subscriptionFiltering'; import { AzureContextProperties } from './AzureContextProperties'; export class SelectSubscriptionStep extends AzureWizardPromptStep { @@ -53,44 +54,60 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep>; - // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' - // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however - // this lead to incorrect responses from getSubscriptions. We didn't investigate - const tenantPromise = subscriptionProvider.getTenants().catch(() => undefined); - const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); - const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); + // Create async function to provide better loading UX and debugging experience + const getSubscriptionQuickPickItems = async (): Promise<(QuickPickItem & { id: string })[]> => { + subscriptions = await subscriptionProvider.getSubscriptions(false); - // Build tenant display name lookup for better UX - const tenantDisplayNames = new Map(); + // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' + // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however + // this lead to incorrect responses from getSubscriptions. We didn't investigate + const tenantPromise = subscriptionProvider.getTenants().catch(() => undefined); + const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); + const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); - if (knownTenants) { - for (const tenant of knownTenants) { - if (tenant.tenantId && tenant.displayName) { - tenantDisplayNames.set(tenant.tenantId, tenant.displayName); + // Build tenant display name lookup for better UX + const tenantDisplayNames = new Map(); + + if (knownTenants) { + for (const tenant of knownTenants) { + if (tenant.tenantId && tenant.displayName) { + tenantDisplayNames.set(tenant.tenantId, tenant.displayName); + } } } - } - const promptItems: (QuickPickItem & { id: string })[] = subscriptions - .map((subscription) => { - const tenantName = tenantDisplayNames.get(subscription.tenantId); - const description = tenantName - ? `${subscription.subscriptionId} (${tenantName})` - : subscription.subscriptionId; - - return { - id: subscription.subscriptionId, - label: subscription.name, - description, - iconPath: this.iconPath, - alwaysShow: true, - }; - }) - .sort((a, b) => a.label.localeCompare(b.label)); - - const selectedItem = await context.ui.showQuickPick([...promptItems], { + // Use duplicate detection logic from subscriptionFiltering + const duplicates = getDuplicateSubscriptions(subscriptions); + + return subscriptions + .map((subscription) => { + const tenantName = tenantDisplayNames.get(subscription.tenantId); + + // Handle duplicate subscription names by adding account label + const label = duplicates.includes(subscription) + ? `${subscription.name} (${subscription.account?.label})` + : subscription.name; + + // Build description with tenant information + const description = tenantName + ? `${subscription.subscriptionId} (${tenantName})` + : subscription.subscriptionId; + + return { + id: subscription.subscriptionId, + label, + description, + iconPath: this.iconPath, + alwaysShow: true, + }; + }) + .sort((a, b) => a.label.localeCompare(b.label)); + }; + + const selectedItem = await context.ui.showQuickPick(getSubscriptionQuickPickItems(), { stepName: 'selectSubscription', placeHolder: l10n.t('Choose a subscription…'), loadingPlaceHolder: l10n.t('Loading subscriptions…'), @@ -99,6 +116,7 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep subscription.subscriptionId === selectedItem.id, ); From 15fec873aec140dc8554285d58a8a27256c17732 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 19:18:47 +0200 Subject: [PATCH 37/88] fix: updated quickpicks to show loading animations correctly --- l10n/bundle.l10n.json | 2 +- .../SelectAccountStep.ts | 9 +- .../SelectTenantsStep.ts | 10 +- .../discovery-wizard/SelectRUClusterStep.ts | 47 ++++-- .../discovery-wizard/SelectClusterStep.ts | 50 +++--- .../discovery-wizard/SelectVMStep.ts | 148 +++++++++--------- 6 files changed, 150 insertions(+), 116 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 378710268..62790987f 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -386,7 +386,7 @@ "Select Subscriptions": "Select Subscriptions", "Select Subscriptions to Display": "Select Subscriptions to Display", "Select Subscriptions...": "Select Subscriptions...", - "Select tenants to use (multiple selection)": "Select tenants to use (multiple selection)", + "Select tenants to use": "Select tenants to use", "Select the error you would like to report": "Select the error you would like to report", "Select the local connection type…": "Select the local connection type…", "Selected tenants: {0}": "Selected tenants: {0}", diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index 4c881b3bb..9a9b0dc5e 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -18,8 +18,9 @@ interface AccountQuickPickItem extends vscode.QuickPickItem { export class SelectAccountStep extends AzureWizardPromptStep { public async prompt(context: CredentialsManagementWizardContext): Promise { - // Create a promise that will resolve to the quick pick items - const quickPickItemsPromise = this.getAvailableAccounts(context).then((accounts) => { + // Create async function to provide better loading UX and debugging experience + const getAccountQuickPickItems = async (): Promise => { + const accounts = await this.getAvailableAccounts(context); context.availableAccounts = accounts; const accountItems: AccountQuickPickItem[] = accounts.map((account) => ({ @@ -51,9 +52,9 @@ export class SelectAccountStep extends AzureWizardPromptStep { + // Create async function to provide better loading UX and debugging experience + const getTenantQuickPickItems = async (): Promise => { + const tenants = await this.getAvailableTenantsForAccount(context); + // Initialize availableTenants map if not exists if (!context.availableTenants) { context.availableTenants = new Map(); @@ -61,9 +63,9 @@ export class SelectTenantsStep extends AzureWizardPromptStep => { + const managementClient = await createCosmosDBManagementClient( + context, + context.properties[AzureContextProperties.SelectedSubscription] as unknown as AzureSubscription, + ); - const allAccounts = await uiUtils.listAllIterator(managementClient.databaseAccounts.list()); - const accounts = allAccounts.filter((account) => account.kind === 'MongoDB'); + const allAccounts = await uiUtils.listAllIterator(managementClient.databaseAccounts.list()); + const accounts = allAccounts.filter((account) => account.kind === 'MongoDB'); + + const promptItems: (QuickPickItem & { id: string })[] = accounts + .filter((account) => account.name) // Filter out accounts without a name + .map((account) => ({ + id: account.id!, + label: account.name!, + description: account.id, + iconPath: this.iconPath, - const promptItems: (QuickPickItem & { id: string })[] = accounts - .filter((account) => account.name) // Filter out accounts without a name - .map((account) => ({ - id: account.id!, - label: account.name!, - description: account.id, - iconPath: this.iconPath, + alwaysShow: true, + })) + .sort((a, b) => a.label.localeCompare(b.label)); - alwaysShow: true, - })) - .sort((a, b) => a.label.localeCompare(b.label)); + return promptItems; + }; - const selectedItem = await context.ui.showQuickPick([...promptItems], { + const selectedItem = await context.ui.showQuickPick(getRUClusterQuickPickItems(), { stepName: 'selectRUCluster', placeHolder: l10n.t('Choose a RU cluster…'), loadingPlaceHolder: l10n.t('Loading RU clusters…'), @@ -59,6 +64,14 @@ export class SelectRUClusterStep extends AzureWizardPromptStep account.kind === 'MongoDB'); + context.properties[AzureContextProperties.SelectedCluster] = accounts.find( (account) => account.id === selectedItem.id, ); diff --git a/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts b/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts index bc0d0b736..d63e31c6a 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts @@ -33,28 +33,33 @@ export class SelectClusterStep extends AzureWizardPromptStep => { + const client = await createResourceManagementClient( + context, + context.properties[AzureContextProperties.SelectedSubscription] as unknown as AzureSubscription, + ); - const accounts = await uiUtils.listAllIterator( - client.resources.list({ filter: "resourceType eq 'Microsoft.DocumentDB/mongoClusters'" }), - ); + const accounts = await uiUtils.listAllIterator( + client.resources.list({ filter: "resourceType eq 'Microsoft.DocumentDB/mongoClusters'" }), + ); + + const promptItems: (QuickPickItem & { id: string })[] = accounts + .filter((account) => account.name) // Filter out accounts without a name + .map((account) => ({ + id: account.id!, + label: account.name!, + description: account.id, + iconPath: this.iconPath, - const promptItems: (QuickPickItem & { id: string })[] = accounts - .filter((account) => account.name) // Filter out accounts without a name - .map((account) => ({ - id: account.id!, - label: account.name!, - description: account.id, - iconPath: this.iconPath, + alwaysShow: true, + })) + .sort((a, b) => a.label.localeCompare(b.label)); - alwaysShow: true, - })) - .sort((a, b) => a.label.localeCompare(b.label)); + return promptItems; + }; - const selectedItem = await context.ui.showQuickPick([...promptItems], { + const selectedItem = await context.ui.showQuickPick(getClusterQuickPickItems(), { stepName: 'selectCluster', placeHolder: l10n.t('Choose a cluster…'), loadingPlaceHolder: l10n.t('Loading clusters…'), @@ -63,6 +68,15 @@ export class SelectClusterStep extends AzureWizardPromptStep account.id === selectedItem.id, ); diff --git a/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts b/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts index cd69a8761..f18d2a7d4 100644 --- a/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts +++ b/src/plugins/service-azure-vm/discovery-wizard/SelectVMStep.ts @@ -47,90 +47,94 @@ export class SelectVMStep extends AzureWizardPromptStep[] = []; - - for (const vm of allVms) { - if (vm.tags && vm.tags[tagName] !== undefined) { - let publicIpAddress: string | undefined; - let fqdn: string | undefined; - - if (vm.networkProfile?.networkInterfaces) { - for (const nicRef of vm.networkProfile.networkInterfaces) { - if (nicRef.id) { - const nicName = nicRef.id.substring(nicRef.id.lastIndexOf('/') + 1); - const rgName = getResourceGroupFromId(nicRef.id); - const nic = await networkClient.networkInterfaces.get(rgName, nicName); - if (nic.ipConfigurations) { - for (const ipConfig of nic.ipConfigurations) { - if (ipConfig.publicIPAddress?.id) { - const pipName = ipConfig.publicIPAddress.id.substring( - ipConfig.publicIPAddress.id.lastIndexOf('/') + 1, - ); - const pipRg = getResourceGroupFromId(ipConfig.publicIPAddress.id); - const publicIp = await networkClient.publicIPAddresses.get(pipRg, pipName); - if (publicIp.ipAddress) { - publicIpAddress = publicIp.ipAddress; + // Create async function to provide better loading UX and debugging experience + const getVMQuickPickItems = async (): Promise< + IAzureQuickPickItem[] + > => { + // Using ComputeManagementClient to list VMs + const allVms = await uiUtils.listAllIterator(computeClient.virtualMachines.listAll()); + + const taggedVms: IAzureQuickPickItem[] = []; + + for (const vm of allVms) { + if (vm.tags && vm.tags[tagName] !== undefined) { + let publicIpAddress: string | undefined; + let fqdn: string | undefined; + + if (vm.networkProfile?.networkInterfaces) { + for (const nicRef of vm.networkProfile.networkInterfaces) { + if (nicRef.id) { + const nicName = nicRef.id.substring(nicRef.id.lastIndexOf('/') + 1); + const rgName = getResourceGroupFromId(nicRef.id); + const nic = await networkClient.networkInterfaces.get(rgName, nicName); + if (nic.ipConfigurations) { + for (const ipConfig of nic.ipConfigurations) { + if (ipConfig.publicIPAddress?.id) { + const pipName = ipConfig.publicIPAddress.id.substring( + ipConfig.publicIPAddress.id.lastIndexOf('/') + 1, + ); + const pipRg = getResourceGroupFromId(ipConfig.publicIPAddress.id); + const publicIp = await networkClient.publicIPAddresses.get(pipRg, pipName); + if (publicIp.ipAddress) { + publicIpAddress = publicIp.ipAddress; + } + if (publicIp.dnsSettings?.fqdn) { + fqdn = publicIp.dnsSettings.fqdn; + } + // Stop if we found a public IP for this VM + if (publicIpAddress) break; } - if (publicIp.dnsSettings?.fqdn) { - fqdn = publicIp.dnsSettings.fqdn; - } - // Stop if we found a public IP for this VM - if (publicIpAddress) break; } } } + if (publicIpAddress) break; // Stop checking NICs if IP found } - if (publicIpAddress) break; // Stop checking NICs if IP found } - } - const label = vm.name!; - let description = ''; - let detail = `VM Size: ${vm.hardwareProfile?.vmSize}`; // Add VM Size to detail + const label = vm.name!; + let description = ''; + let detail = `VM Size: ${vm.hardwareProfile?.vmSize}`; // Add VM Size to detail - if (publicIpAddress || fqdn) { - description = fqdn ? fqdn : publicIpAddress!; - detail += fqdn ? ` (IP: ${publicIpAddress || 'N/A'})` : ''; - } else { - description = l10n.t('No public connectivity'); - detail += l10n.t(', No public IP or FQDN found.'); - } + if (publicIpAddress || fqdn) { + description = fqdn ? fqdn : publicIpAddress!; + detail += fqdn ? ` (IP: ${publicIpAddress || 'N/A'})` : ''; + } else { + description = l10n.t('No public connectivity'); + detail += l10n.t(', No public IP or FQDN found.'); + } - taggedVms.push({ - label, - description, - detail, - data: { ...vm, publicIpAddress, fqdn }, - iconPath: this.iconPath, - alwaysShow: true, - }); + taggedVms.push({ + label, + description, + detail, + data: { ...vm, publicIpAddress, fqdn }, + iconPath: this.iconPath, + alwaysShow: true, + }); + } } - } - if (taggedVms.length === 0) { - context.errorHandling.suppressReportIssue = true; // No need to report an issue if no VMs are found - throw new Error( - l10n.t(`No Azure VMs found with tag "{tagName}" in subscription "{subscriptionName}".`, { - tagName, - subscriptionName: subscription.name, - }), - ); - } + if (taggedVms.length === 0) { + context.errorHandling.suppressReportIssue = true; // No need to report an issue if no VMs are found + throw new Error( + l10n.t(`No Azure VMs found with tag "{tagName}" in subscription "{subscriptionName}".`, { + tagName, + subscriptionName: subscription.name, + }), + ); + } - const selectedVMItem = await context.ui.showQuickPick( - taggedVms.sort((a, b) => a.label.localeCompare(b.label)), - { - stepName: 'selectVM', - placeHolder: l10n.t('Choose a Virtual Machine…'), - loadingPlaceHolder: l10n.t('Loading Virtual Machines…'), - enableGrouping: true, - matchOnDescription: true, - suppressPersistence: true, - }, - ); + return taggedVms.sort((a, b) => a.label.localeCompare(b.label)); + }; + + const selectedVMItem = await context.ui.showQuickPick(getVMQuickPickItems(), { + stepName: 'selectVM', + placeHolder: l10n.t('Choose a Virtual Machine…'), + loadingPlaceHolder: l10n.t('Loading Virtual Machines…'), + enableGrouping: true, + matchOnDescription: true, + suppressPersistence: true, + }); context.properties[AzureVMContextProperties.SelectedVM] = selectedVMItem.data; } From d53abee7f1c21fb0a5fefc85943ab39d415f2710 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 19:54:19 +0200 Subject: [PATCH 38/88] feat: added improved quick pick loading state to existing quickpick instances --- l10n/bundle.l10n.json | 1 + .../chooseDataMigrationExtension.ts | 59 ++++++++++++------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 62790987f..a88409ef8 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -286,6 +286,7 @@ "Loading Content": "Loading Content", "Loading document {num} of {countUri}": "Loading document {num} of {countUri}", "Loading documents…": "Loading documents…", + "Loading migration actions…": "Loading migration actions…", "Loading resources...": "Loading resources...", "Loading RU clusters…": "Loading RU clusters…", "Loading subscriptions…": "Loading subscriptions…", diff --git a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts index d411810b2..469ebecf7 100644 --- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts +++ b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts @@ -122,33 +122,48 @@ export async function chooseDataMigrationExtension(context: IActionContext, node // No actions available, execute default action await selectedProvider.executeAction(options); } else { - // Extend actions with Learn More option if provider has a learn more URL - const extendedActions: (QuickPickItem & { - id: string; - learnMoreUrl?: string; - requiresAuthentication?: boolean; - })[] = [...availableActions]; - - const learnMoreUrl = selectedProvider.getLearnMoreUrl?.(); - - if (learnMoreUrl) { - extendedActions.push( - { id: 'separator', label: '', kind: QuickPickItemKind.Separator }, - { - id: 'learnMore', - label: l10n.t('Learn more…'), - detail: l10n.t('Learn more about {0}.', selectedProvider.label), - learnMoreUrl, - alwaysShow: true, - }, - ); - } + // Create async function to provide better loading UX and debugging experience + const getActionQuickPickItems = async (): Promise< + (QuickPickItem & { + id: string; + learnMoreUrl?: string; + requiresAuthentication?: boolean; + })[] + > => { + // Get available actions from the provider + const actions = await selectedProvider.getAvailableActions(options); + + // Extend actions with Learn More option if provider has a learn more URL + const extendedActions: (QuickPickItem & { + id: string; + learnMoreUrl?: string; + requiresAuthentication?: boolean; + })[] = [...actions]; + + const learnMoreUrl = selectedProvider.getLearnMoreUrl?.(); + + if (learnMoreUrl) { + extendedActions.push( + { id: 'separator', label: '', kind: QuickPickItemKind.Separator }, + { + id: 'learnMore', + label: l10n.t('Learn more…'), + detail: l10n.t('Learn more about {0}.', selectedProvider.label), + learnMoreUrl, + alwaysShow: true, + }, + ); + } + + return extendedActions; + }; // Show action picker to user - const selectedAction = await context.ui.showQuickPick(extendedActions, { + const selectedAction = await context.ui.showQuickPick(getActionQuickPickItems(), { placeHolder: l10n.t('Choose the migration action…'), stepName: 'selectMigrationAction', suppressPersistence: true, + loadingPlaceHolder: l10n.t('Loading migration actions…'), }); if (selectedAction.id === 'learnMore') { From 7a9bb7a93d78959a2f3bfe4e3e1edc8a66252d2c Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 21:54:25 +0200 Subject: [PATCH 39/88] feat: added credentials management to the discovery wizard (when no subs are returned) --- .../azure/wizard/SelectSubscriptionStep.ts | 56 +++++++++++++++++++ .../AzureMongoRUDiscoveryProvider.ts | 16 ++++++ .../AzureDiscoveryProvider.ts | 13 ++++- .../AzureVMDiscoveryProvider.ts | 16 ++++++ src/services/discoveryServices.ts | 9 ++- 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 8d1743e30..60f41a84a 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -9,6 +9,7 @@ import * as l10n from '@vscode/l10n'; import { Uri, window, type MessageItem, type QuickPickItem } from 'vscode'; import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; +import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; import { getDuplicateSubscriptions } from '../subscriptionFiltering'; import { AzureContextProperties } from './AzureContextProperties'; @@ -79,6 +80,18 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { + const configure = l10n.t('Configure'); + const cancel = l10n.t('Cancel'); + + const result = await window.showInformationMessage( + l10n.t('No Azure Subscriptions Found'), + { + modal: true, + detail: l10n.t( + 'To connect to Azure resources, you need to configure your Azure credentials and tenant access.\n\n' + + 'Would you like to configure your Azure credentials now?', + ), + }, + { title: configure, isCloseAffordance: false }, + { title: cancel, isCloseAffordance: true }, // Default button + ); + + return result?.title === configure ? 'configure' : 'cancel'; + } + + private async configureCredentialsFromWizard( + context: NewConnectionWizardContext, + subscriptionProvider: VSCodeAzureSubscriptionProvider, + ): Promise { + // Call the credentials management function directly using the subscription provider from context + // The subscription provider in the wizard context is actually AzureSubscriptionProviderWithFilters + const { configureAzureCredentials } = await import('../credentialsManagement'); + await configureAzureCredentials(context, subscriptionProvider as AzureSubscriptionProviderWithFilters); + } + + private async showRetryInstructions(): Promise { + await window.showInformationMessage( + l10n.t('Azure credentials configured successfully'), + { + modal: true, + detail: l10n.t( + 'Your Azure credentials have been updated. Please try the service discovery again to see your available subscriptions.', + ), + }, + l10n.t('OK'), + ); + } } diff --git a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts index b14584c42..6f1b5a1be 100644 --- a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts @@ -58,4 +58,20 @@ export class AzureMongoRUDiscoveryProvider extends Disposable implements Discove ext.discoveryBranchDataProvider.refresh(node); } } + + async configureCredentials(context: IActionContext, node?: TreeElement): Promise { + if (!node || node instanceof AzureMongoRUServiceRootItem) { + // Use the new Azure credentials configuration wizard + const { configureAzureCredentials } = await import('../api-shared/azure/credentialsManagement'); + await configureAzureCredentials(context, this.azureSubscriptionProvider); + + if (node) { + // Tree context: refresh specific node + ext.discoveryBranchDataProvider.refresh(node); + } else { + // Wizard context: refresh entire discovery tree + ext.discoveryBranchDataProvider.refresh(); + } + } + } } diff --git a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts index 7d4314ae5..eb4f2cafb 100644 --- a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts @@ -65,12 +65,19 @@ export class AzureDiscoveryProvider extends Disposable implements DiscoveryProvi } } - async configureCredentials(context: IActionContext, node: TreeElement): Promise { - if (node instanceof AzureServiceRootItem) { + async configureCredentials(context: IActionContext, node?: TreeElement): Promise { + if (!node || node instanceof AzureServiceRootItem) { // Use the new Azure credentials configuration wizard const { configureAzureCredentials } = await import('../api-shared/azure/credentialsManagement'); await configureAzureCredentials(context, this.azureSubscriptionProvider); - ext.discoveryBranchDataProvider.refresh(node); + + if (node) { + // Tree context: refresh specific node + ext.discoveryBranchDataProvider.refresh(node); + } else { + // Wizard context: refresh entire discovery tree + ext.discoveryBranchDataProvider.refresh(); + } } } } diff --git a/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts b/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts index b8e10e34e..50330afda 100644 --- a/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts +++ b/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts @@ -70,4 +70,20 @@ export class AzureVMDiscoveryProvider extends Disposable implements DiscoveryPro ext.discoveryBranchDataProvider.refresh(node); } } + + async configureCredentials(context: IActionContext, node?: TreeElement): Promise { + if (!node || node instanceof AzureServiceRootItem) { + // Use the new Azure credentials configuration wizard + const { configureAzureCredentials } = await import('../api-shared/azure/credentialsManagement'); + await configureAzureCredentials(context, this.azureSubscriptionProvider); + + if (node) { + // Tree context: refresh specific node + ext.discoveryBranchDataProvider.refresh(node); + } else { + // Wizard context: refresh entire discovery tree + ext.discoveryBranchDataProvider.refresh(); + } + } + } } diff --git a/src/services/discoveryServices.ts b/src/services/discoveryServices.ts index 4a354523d..6d82e6f1d 100644 --- a/src/services/discoveryServices.ts +++ b/src/services/discoveryServices.ts @@ -56,7 +56,14 @@ export interface DiscoveryProvider extends ProviderDescription { configureTreeItemFilter?(context: IActionContext, node: TreeElement): Promise; - configureCredentials?(context: IActionContext, node: TreeElement): Promise; + /** + * Configures credentials for the discovery provider. + * + * @param context - The action context + * @param node - Optional tree node. When provided, refreshes the specific node. + * When undefined, refreshes the entire discovery tree (wizard context). + */ + configureCredentials?(context: IActionContext, node?: TreeElement): Promise; } /** From fcc1e3ea13fc919324b933faff2cd83d245cde1a Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 22:08:30 +0200 Subject: [PATCH 40/88] updated user facing language in dialogs --- l10n/bundle.l10n.json | 5 +++++ .../api-shared/azure/wizard/SelectSubscriptionStep.ts | 8 +++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index a88409ef8..434aadbc0 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -124,6 +124,7 @@ "Creating resource group \"{0}\" in location \"{1}\"...": "Creating resource group \"{0}\" in location \"{1}\"...", "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...": "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...", "Creating user assigned identity \"{0}\" in location \"{1}\"\"...": "Creating user assigned identity \"{0}\" in location \"{1}\"\"...", + "Credential Management Finished": "Credential Management Finished", "Credentials updated successfully.": "Credentials updated successfully.", "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}\"", @@ -309,6 +310,7 @@ "No authentication method selected.": "No authentication method selected.", "No authentication methods available for \"{cluster}\".": "No authentication methods available for \"{cluster}\".", "No Azure subscription found for this tree item.": "No Azure subscription found for this tree item.", + "No Azure Subscriptions Found": "No Azure Subscriptions Found", "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.", @@ -328,6 +330,7 @@ "Not connected to any MongoDB database.": "Not connected to any MongoDB database.", "Note: This confirmation type can be configured in the extension settings.": "Note: This confirmation type can be configured in the extension settings.", "Note: You can disable these URL handling confirmations in the extension settings.": "Note: You can disable these URL handling confirmations in the extension settings.", + "OK": "OK", "Open Collection": "Open Collection", "Open installation page": "Open installation page", "Opening DocumentDB connection…": "Opening DocumentDB connection…", @@ -429,6 +432,7 @@ "The connection string is required.": "The connection string is required.", "The connection will now be opened in the Connections View.": "The connection will now be opened in the Connections View.", "The connection with the name \"{0}\" already exists.": "The connection with the name \"{0}\" already exists.", + "The credential management flow has completed. Please try Service Discovery again to see your available subscriptions.": "The credential management flow has completed. Please try Service Discovery again to see your available subscriptions.", "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.": "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.", "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".": "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".", "The default port: {defaultPort}": "The default port: {defaultPort}", @@ -515,6 +519,7 @@ "Write error: {0}": "Write error: {0}", "Yes": "Yes", "Yes, continue": "Yes, continue", + "Yes, Manage Credentials": "Yes, Manage Credentials", "Yes, open Collection View": "Yes, open Collection View", "Yes, open connection": "Yes, open connection", "Yes, save my credentials": "Yes, save my credentials", diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 60f41a84a..076c38190 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -140,8 +140,7 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { - const configure = l10n.t('Configure'); - const cancel = l10n.t('Cancel'); + const configure = l10n.t('Yes, Manage Credentials'); const result = await window.showInformationMessage( l10n.t('No Azure Subscriptions Found'), @@ -153,7 +152,6 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { await window.showInformationMessage( - l10n.t('Azure credentials configured successfully'), + l10n.t('Credential Management Finished'), { modal: true, detail: l10n.t( - 'Your Azure credentials have been updated. Please try the service discovery again to see your available subscriptions.', + 'The credential management flow has completed. Please try Service Discovery again to see your available subscriptions.', ), }, l10n.t('OK'), From e1e1d3ad40f5356cc855651b79cbbe5417986651 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 24 Sep 2025 22:12:36 +0200 Subject: [PATCH 41/88] feat: enabled cred management to all azure service discovery providers --- .../discovery-tree/AzureMongoRUServiceRootItem.ts | 3 ++- .../service-azure-vm/discovery-tree/AzureServiceRootItem.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts index 821e7db2d..6fcde565e 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts @@ -20,7 +20,8 @@ export class AzureMongoRUServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { public readonly id: string; - public contextValue: string = 'enableRefreshCommand;enableFilterCommand;enableLearnMoreCommand;azureMongoRUService'; + public contextValue: string = + 'enableRefreshCommand;enableManageCredentialsCommand;enableFilterCommand;enableLearnMoreCommand;azureMongoRUService'; constructor( private readonly azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts index fdaf17e15..5a7f10f1c 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts @@ -19,7 +19,7 @@ import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { public readonly id: string; public contextValue: string = - 'enableRefreshCommand;enableFilterCommand;enableLearnMoreCommand;discoveryAzureVMRootItem'; + 'enableRefreshCommand;enableManageCredentialsCommand;enableFilterCommand;enableLearnMoreCommand;discoveryAzureVMRootItem'; constructor( private readonly azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, From 9314c57c6e844354df6fa02c75cf8cb2b8059ca3 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 25 Sep 2025 11:31:43 +0200 Subject: [PATCH 42/88] feat: account/tenant config available in the wizard-discovery flow --- l10n/bundle.l10n.json | 13 +++--- .../SelectTenantsStep.ts | 2 +- .../azure/wizard/SelectSubscriptionStep.ts | 41 ++++++++++++++----- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 434aadbc0..3d4ce3d94 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -80,7 +80,7 @@ "Check document syntax": "Check document syntax", "Choose a cluster…": "Choose a cluster…", "Choose a RU cluster…": "Choose a RU cluster…", - "Choose a subscription…": "Choose a subscription…", + "Choose a Subscription…": "Choose a Subscription…", "Choose a Virtual Machine…": "Choose a Virtual Machine…", "Choose the data migration provider…": "Choose the data migration provider…", "Choose the migration action…": "Choose the migration action…", @@ -124,7 +124,7 @@ "Creating resource group \"{0}\" in location \"{1}\"...": "Creating resource group \"{0}\" in location \"{1}\"...", "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...": "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...", "Creating user assigned identity \"{0}\" in location \"{1}\"\"...": "Creating user assigned identity \"{0}\" in location \"{1}\"\"...", - "Credential Management Finished": "Credential Management Finished", + "Credential Management Completed": "Credential Management Completed", "Credentials updated successfully.": "Credentials updated successfully.", "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}\"", @@ -154,6 +154,7 @@ "Don't upload": "Don't upload", "Don't warn again": "Don't warn again", "e.g., DocumentDB, Environment, Project": "e.g., DocumentDB, Environment, Project", + "Edit account and tenant filters…": "Edit account and tenant filters…", "Edit selected document": "Edit selected document", "Element with id of {rootId} not found.": "Element with id of {rootId} not found.", "Enable TLS/SSL (Default)": "Enable TLS/SSL (Default)", @@ -290,8 +291,8 @@ "Loading migration actions…": "Loading migration actions…", "Loading resources...": "Loading resources...", "Loading RU clusters…": "Loading RU clusters…", - "Loading subscriptions…": "Loading subscriptions…", - "Loading tenants…": "Loading tenants…", + "Loading Subscriptions…": "Loading Subscriptions…", + "Loading Tenants…": "Loading Tenants…", "Loading Virtual Machines…": "Loading Virtual Machines…", "Loading...": "Loading...", "Location": "Location", @@ -432,7 +433,7 @@ "The connection string is required.": "The connection string is required.", "The connection will now be opened in the Connections View.": "The connection will now be opened in the Connections View.", "The connection with the name \"{0}\" already exists.": "The connection with the name \"{0}\" already exists.", - "The credential management flow has completed. Please try Service Discovery again to see your available subscriptions.": "The credential management flow has completed. Please try Service Discovery again to see your available subscriptions.", + "The credential management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.": "The credential management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.", "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.": "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.", "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".": "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".", "The default port: {defaultPort}": "The default port: {defaultPort}", @@ -518,8 +519,8 @@ "Would you like to open the Collection View?": "Would you like to open the Collection View?", "Write error: {0}": "Write error: {0}", "Yes": "Yes", + "Yes, Configure Credentials": "Yes, Configure Credentials", "Yes, continue": "Yes, continue", - "Yes, Manage Credentials": "Yes, Manage Credentials", "Yes, open Collection View": "Yes, open Collection View", "Yes, open connection": "Yes, open connection", "Yes, save my credentials": "Yes, save my credentials", diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts index d1210f370..988c47f93 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -71,7 +71,7 @@ export class SelectTenantsStep extends AzureWizardPromptStep { const tenantPick = pick as TenantQuickPickItem; diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 076c38190..cc6212c71 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -6,7 +6,7 @@ import { VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; -import { Uri, window, type MessageItem, type QuickPickItem } from 'vscode'; +import { QuickPickItemKind, ThemeIcon, Uri, window, type MessageItem, type QuickPickItem } from 'vscode'; import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; @@ -83,7 +83,7 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { const tenantName = tenantDisplayNames.get(subscription.tenantId); @@ -118,17 +118,38 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep a.label.localeCompare(b.label)); + + // Add edit entry at the top + return [ + { + id: 'editAccountsAndTenants', + label: l10n.t('Edit account and tenant filters…'), + iconPath: new ThemeIcon('settings-gear'), + alwaysShow: true, + }, + { id: 'separator', label: '', kind: QuickPickItemKind.Separator }, + ...subscriptionItems, + ]; }; const selectedItem = await context.ui.showQuickPick(getSubscriptionQuickPickItems(), { stepName: 'selectSubscription', - placeHolder: l10n.t('Choose a subscription…'), - loadingPlaceHolder: l10n.t('Loading subscriptions…'), + placeHolder: l10n.t('Choose a Subscription…'), + loadingPlaceHolder: l10n.t('Loading Subscriptions…'), enableGrouping: false, matchOnDescription: true, suppressPersistence: true, }); + // Handle edit accounts and tenants selection + if (selectedItem.id === 'editAccountsAndTenants') { + await this.configureCredentialsFromWizard(context, subscriptionProvider); + await this.showRetryInstructions(); + + // Exit wizard - user needs to restart service discovery + throw new UserCancelledError('Account and tenant filters updated'); + } + // Use the subscriptions we already loaded (no second API call needed) context.properties[AzureContextProperties.SelectedSubscription] = subscriptions.find( (subscription) => subscription.subscriptionId === selectedItem.id, @@ -139,8 +160,8 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { - const configure = l10n.t('Yes, Manage Credentials'); + private async askToConfigureCredentials(): Promise<'configure' | 'cancel'> { + const configure = l10n.t('Yes, Configure Credentials'); const result = await window.showInformationMessage( l10n.t('No Azure Subscriptions Found'), @@ -151,7 +172,7 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { await window.showInformationMessage( - l10n.t('Credential Management Finished'), + l10n.t('Credential Management Completed'), { modal: true, detail: l10n.t( - 'The credential management flow has completed. Please try Service Discovery again to see your available subscriptions.', + 'The credential management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.', ), }, l10n.t('OK'), From 97b2d619f208df381481841c9d31529779925bcb Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 25 Sep 2025 11:46:44 +0200 Subject: [PATCH 43/88] updated wording / icons of account/tenant mangement --- l10n/bundle.l10n.json | 2 +- package.json | 2 +- src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 3d4ce3d94..707be36df 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -154,7 +154,6 @@ "Don't upload": "Don't upload", "Don't warn again": "Don't warn again", "e.g., DocumentDB, Environment, Project": "e.g., DocumentDB, Environment, Project", - "Edit account and tenant filters…": "Edit account and tenant filters…", "Edit selected document": "Edit selected document", "Element with id of {rootId} not found.": "Element with id of {rootId} not found.", "Enable TLS/SSL (Default)": "Enable TLS/SSL (Default)", @@ -399,6 +398,7 @@ "Sign In": "Sign In", "Sign in to Azure to continue…": "Sign in to Azure to continue…", "Sign in to Azure...": "Sign in to Azure...", + "Sign in to other Azure accounts or tenants to access more subscriptions": "Sign in to other Azure accounts or tenants to access more subscriptions", "Sign in with a different account…": "Sign in with a different account…", "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.": "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.", "Skip for now": "Skip for now", diff --git a/package.json b/package.json index eacd34fa5..eab1f8ea0 100644 --- a/package.json +++ b/package.json @@ -332,7 +332,7 @@ "category": "DocumentDB", "command": "vscode-documentdb.command.discoveryView.manageCredentials", "title": "Manage Credentials…", - "icon": "$(organization)" + "icon": "$(key)" }, { "//": "[DiscoveryView] Filter Provider Content", diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index cc6212c71..7de4979da 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -123,8 +123,8 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep Date: Thu, 25 Sep 2025 12:13:46 +0200 Subject: [PATCH 44/88] fix: avoiding potential runtime errors --- .../api-shared/azure/credentialsManagement/SelectTenantsStep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts index 988c47f93..674084e9f 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -76,7 +76,7 @@ export class SelectTenantsStep extends AzureWizardPromptStep Date: Thu, 25 Sep 2025 14:26:33 +0200 Subject: [PATCH 45/88] telemetry --- .../manageCredentials.ts | 21 +++++-- .../credentialsManagement/ExecuteStep.ts | 9 +++ .../SelectAccountStep.ts | 12 ++++ .../SelectTenantsStep.ts | 12 ++++ .../configureAzureCredentials.ts | 63 ++++++++++++++++--- .../api-shared/azure/subscriptionFiltering.ts | 26 ++++++++ .../azure/wizard/SelectSubscriptionStep.ts | 10 ++- .../AzureMongoRUDiscoveryProvider.ts | 7 ++- .../AzureMongoRUSubscriptionItem.ts | 5 ++ .../documentdb/MongoRUResourceItem.ts | 13 ++++ .../AzureDiscoveryProvider.ts | 7 ++- .../documentdb/DocumentDBResourceItem.ts | 15 +++++ .../AzureVMDiscoveryProvider.ts | 7 ++- 13 files changed, 192 insertions(+), 15 deletions(-) diff --git a/src/commands/discoveryService.manageCredentials/manageCredentials.ts b/src/commands/discoveryService.manageCredentials/manageCredentials.ts index 751af34ac..178a4da31 100644 --- a/src/commands/discoveryService.manageCredentials/manageCredentials.ts +++ b/src/commands/discoveryService.manageCredentials/manageCredentials.ts @@ -28,21 +28,34 @@ export async function manageCredentials(context: IActionContext, node: TreeEleme idSections.length >= 2 && idSections[0] === String(Views.DiscoveryView) && idSections[1].length > 0; if (!isValidFormat) { + context.telemetry.properties.result = 'Failed'; + context.telemetry.properties.errorReason = 'invalidNodeIdFormat'; ext.outputChannel.error('Internal error: Node id is not in the expected format.'); return; } const providerId = idSections[1]; + context.telemetry.properties.discoveryProviderId = providerId; const provider = DiscoveryService.getProvider(providerId); if (!provider?.configureCredentials) { + context.telemetry.properties.result = 'Failed'; + context.telemetry.properties.errorReason = 'noConfigureCredentialsFunction'; ext.outputChannel.error(`No management function provided by the provider with the id "${providerId}".`); return; } - // Call the filter function provided by the provider - await provider.configureCredentials(context, node as TreeElement); + try { + // Call the filter function provided by the provider + await provider.configureCredentials(context, node as TreeElement); - // Refresh the discovery branch data provider to show the updated list - ext.discoveryBranchDataProvider.refresh(node as TreeElement); + // Refresh the discovery branch data provider to show the updated list + ext.discoveryBranchDataProvider.refresh(node as TreeElement); + + context.telemetry.properties.result = 'Succeeded'; + } catch (error) { + context.telemetry.properties.result = 'Failed'; + context.telemetry.properties.errorReason = 'configureCredentialsThrew'; + throw error; + } } diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index b3f4ccbd5..54c9eb8b1 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -13,6 +13,7 @@ export class ExecuteStep extends AzureWizardExecuteStep { + const executeStartTime = Date.now(); const selectedAccount = nonNullValue(context.selectedAccount, 'context.selectedAccount', 'ExecuteStep.ts'); const selectedTenants = context.selectedTenants || []; @@ -25,6 +26,11 @@ export class ExecuteStep extends AzureWizardExecuteStep tenant.tenantId || '')); + // Add telemetry for execution + context.telemetry.measurements.tenantFilteringCount = allTenantsForAccount.length; + context.telemetry.measurements.selectedFinalTenantsCount = selectedTenants.length; + context.telemetry.properties.filteringActionType = 'tenantFiltering'; + // Use the individual add/remove functions to update tenant selections const { addUnselectedTenant, removeUnselectedTenant } = await import('../subscriptionFiltering'); @@ -66,6 +72,9 @@ export class ExecuteStep extends AzureWizardExecuteStep { // Create async function to provide better loading UX and debugging experience const getAccountQuickPickItems = async (): Promise => { + const loadStartTime = Date.now(); + const accounts = await this.getAvailableAccounts(context); context.availableAccounts = accounts; + // Add telemetry for account availability + context.telemetry.measurements.availableAccountsCount = accounts.length; + context.telemetry.measurements.accountsLoadingTimeMs = Date.now() - loadStartTime; + const accountItems: AccountQuickPickItem[] = accounts.map((account) => ({ label: account.label, detail: account.id, @@ -32,6 +38,7 @@ export class SelectAccountStep extends AzureWizardPromptStep => { + const loadStartTime = Date.now(); const tenants = await this.getAvailableTenantsForAccount(context); + // Add telemetry for tenant loading + context.telemetry.measurements.availableTenantsCount = tenants.length; + context.telemetry.measurements.tenantLoadingTimeMs = Date.now() - loadStartTime; + // Initialize availableTenants map if not exists if (!context.availableTenants) { context.availableTenants = new Map(); @@ -88,6 +93,13 @@ export class SelectTenantsStep extends AzureWizardPromptStep nonNullValue(item.tenant, 'item.tenant', 'SelectTenantsStep.ts'), ); + + // Add telemetry for tenant selection + const totalTenants = context.allTenants?.length ?? 0; + context.telemetry.measurements.selectedTenantsCount = selectedItems.length; + context.telemetry.measurements.unselectedTenantsCount = totalTenants - selectedItems.length; + context.telemetry.properties.allTenantsSelected = (selectedItems.length === totalTenants).toString(); + context.telemetry.properties.noTenantsSelected = (selectedItems.length === 0).toString(); } public shouldPrompt(context: CredentialsManagementWizardContext): boolean { diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index 9910c19b9..16bb90697 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -3,10 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizard, UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { + AzureWizard, + callWithTelemetryAndErrorHandling, + UserCancelledError, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; +import { isTreeElementWithContextValue } from '../../../../tree/TreeElementWithContextValue'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; import { ExecuteStep } from './ExecuteStep'; @@ -14,16 +20,15 @@ import { SelectAccountStep } from './SelectAccountStep'; import { SelectTenantsStep } from './SelectTenantsStep'; /** - * Configures Azure credentials by allowing the user to select accounts and tenants - * for filtering Azure discovery results. - * - * @param context - The action context - * @param azureSubscriptionProvider - The Azure subscription provider with filtering capabilities + * Internal implementation of Azure credentials configuration. */ -export async function configureAzureCredentials( +async function configureAzureCredentialsInternal( context: IActionContext, azureSubscriptionProvider: AzureSubscriptionProviderWithFilters, ): Promise { + const startTime = Date.now(); + context.telemetry.properties.credentialsManagementAction = 'configure'; + let wizardContext: CredentialsManagementWizardContext; do { @@ -51,20 +56,64 @@ export async function configureAzureCredentials( await wizard.execute(); if (wizardContext.shouldRestartWizard) { + context.telemetry.properties.wizardRestarted = 'true'; ext.outputChannel.appendLine(l10n.t('Restarting wizard after account sign-in...')); } } catch (error) { + context.telemetry.measurements.credentialsManagementDurationMs = Date.now() - startTime; + if (error instanceof UserCancelledError) { // User cancelled + context.telemetry.properties.credentialsManagementResult = 'Canceled'; ext.outputChannel.appendLine(l10n.t('Azure credentials configuration was cancelled by user.')); return; } // Any other error - don't retry, just throw + context.telemetry.properties.credentialsManagementResult = 'Failed'; + context.telemetry.properties.credentialsManagementError = + error instanceof Error ? error.name : 'UnknownError'; const errorMessage = error instanceof Error ? error.message : String(error); ext.outputChannel.appendLine(l10n.t('Azure credentials configuration failed: {0}', errorMessage)); void vscode.window.showErrorMessage(l10n.t('Failed to configure Azure credentials: {0}', errorMessage)); throw error; } } while (wizardContext.shouldRestartWizard); + + // Success telemetry + context.telemetry.measurements.credentialsManagementDurationMs = Date.now() - startTime; + context.telemetry.properties.credentialsManagementResult = 'Succeeded'; +} + +/** + * Configures Azure credentials by allowing the user to select accounts and tenants + * for filtering Azure discovery results. + * + * @param context - The action context + * @param azureSubscriptionProvider - The Azure subscription provider with filtering capabilities + * @param node - Optional tree node from which the configuration was initiated + */ +export async function configureAzureCredentials( + context: IActionContext, + azureSubscriptionProvider: AzureSubscriptionProviderWithFilters, + node?: unknown, +): Promise { + return await callWithTelemetryAndErrorHandling( + 'serviceDiscovery.configureAzureCredentials', + async (telemetryContext: IActionContext) => { + // Track node context information + telemetryContext.telemetry.properties.nodeProvided = node ? 'true' : 'false'; + if (node && isTreeElementWithContextValue(node)) { + telemetryContext.telemetry.properties.nodeContextValue = node.contextValue; + } + + // Pass through other telemetry properties from the calling context + if (context.telemetry.properties.discoveryProviderId) { + telemetryContext.telemetry.properties.discoveryProviderId = + context.telemetry.properties.discoveryProviderId; + } + + await configureAzureCredentialsInternal(telemetryContext, azureSubscriptionProvider); + }, + ); } diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index f98ace336..f7479bb88 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -190,11 +190,16 @@ export async function configureAzureSubscriptionFilter( context: IActionContext, azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, ): Promise { + const startTime = Date.now(); + context.telemetry.properties.subscriptionFilteringAction = 'configure'; + /** * Ensure the user is signed in to Azure */ if (!(await azureSubscriptionProvider.isSignedIn())) { + context.telemetry.properties.subscriptionFilteringResult = 'Failed'; + context.telemetry.properties.subscriptionFilteringError = 'NotSignedIn'; const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; void vscode.window .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) @@ -213,9 +218,15 @@ export async function configureAzureSubscriptionFilter( // it's an async function so that the wizard when shown can show the 'loading' state const subscriptionQuickPickItems: () => Promise[]> = async () => { + const subscriptionLoadStartTime = Date.now(); const allSubscriptions = await azureSubscriptionProvider.getSubscriptions(false); // Get all unfiltered subscriptions const duplicates = getDuplicateSubscriptions(allSubscriptions); + // Add telemetry for subscription loading + context.telemetry.measurements.totalSubscriptionsAvailable = allSubscriptions.length; + context.telemetry.measurements.duplicateSubscriptionsCount = duplicates.length; + context.telemetry.measurements.subscriptionLoadingTimeMs = Date.now() - subscriptionLoadStartTime; + // Get tenant information for better UX (similar to SelectSubscriptionStep) const tenantPromise = azureSubscriptionProvider.getTenants().catch(() => undefined); const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); @@ -231,6 +242,9 @@ export async function configureAzureSubscriptionFilter( } } + // Add telemetry for tenant information + context.telemetry.measurements.tenantsWithSubscriptionsCount = tenantDisplayNames.size; + return allSubscriptions .map((subscription) => { const tenantName = tenantDisplayNames.get(subscription.tenantId); @@ -276,5 +290,17 @@ export async function configureAzureSubscriptionFilter( // Update the setting with the new selection const newSelectedIds = picks.map((pick) => `${pick.data.tenantId}/${pick.data.subscriptionId}`); await setSelectedSubscriptionIds(newSelectedIds); + + // Add telemetry for subscription selection + const totalAvailable = context.telemetry.measurements.totalSubscriptionsAvailable || 0; + context.telemetry.measurements.subscriptionsSelected = picks.length; + context.telemetry.measurements.subscriptionsFiltered = totalAvailable - picks.length; + context.telemetry.properties.allSubscriptionsSelected = (picks.length === totalAvailable).toString(); + context.telemetry.properties.subscriptionFilteringResult = 'Succeeded'; + } else { + context.telemetry.properties.subscriptionFilteringResult = 'Canceled'; } + + // Add completion timing + context.telemetry.measurements.subscriptionFilteringDurationMs = Date.now() - startTime; } diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 7de4979da..7cc716a0b 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -182,10 +182,18 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { + // Add telemetry for credential configuration activation + context.telemetry.properties.credentialConfigActivated = 'true'; + context.telemetry.properties.nodeProvided = 'false'; + // Call the credentials management function directly using the subscription provider from context // The subscription provider in the wizard context is actually AzureSubscriptionProviderWithFilters const { configureAzureCredentials } = await import('../credentialsManagement'); - await configureAzureCredentials(context, subscriptionProvider as AzureSubscriptionProviderWithFilters); + await configureAzureCredentials( + context, + subscriptionProvider as AzureSubscriptionProviderWithFilters, + undefined, + ); } private async showRetryInstructions(): Promise { diff --git a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts index 6f1b5a1be..0623754fc 100644 --- a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts @@ -60,10 +60,15 @@ export class AzureMongoRUDiscoveryProvider extends Disposable implements Discove } async configureCredentials(context: IActionContext, node?: TreeElement): Promise { + // Add telemetry for credential configuration activation + context.telemetry.properties.credentialConfigActivated = 'true'; + context.telemetry.properties.discoveryProviderId = this.id; + context.telemetry.properties.nodeProvided = node ? 'true' : 'false'; + if (!node || node instanceof AzureMongoRUServiceRootItem) { // Use the new Azure credentials configuration wizard const { configureAzureCredentials } = await import('../api-shared/azure/credentialsManagement'); - await configureAzureCredentials(context, this.azureSubscriptionProvider); + await configureAzureCredentials(context, this.azureSubscriptionProvider, node); if (node) { // Tree context: refresh specific node diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts index 95555c80d..a2268772a 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUSubscriptionItem.ts @@ -39,12 +39,17 @@ export class AzureMongoRUSubscriptionItem implements TreeElement, TreeElementWit return await callWithTelemetryAndErrorHandling( 'azure-mongo-ru-discovery.getChildren', async (context: IActionContext) => { + const startTime = Date.now(); context.telemetry.properties.discoveryProvider = 'azure-mongo-ru-discovery'; const managementClient = await createCosmosDBManagementClient(context, this.subscription.subscription); const allAccounts = await uiUtils.listAllIterator(managementClient.databaseAccounts.list()); const accounts = allAccounts.filter((account) => account.kind === 'MongoDB'); + // Add enhanced telemetry for discovery + context.telemetry.measurements.discoveryResourcesCount = accounts.length; + context.telemetry.measurements.discoveryLoadTimeMs = Date.now() - startTime; + return accounts .sort((a, b) => (a.name || '').localeCompare(b.name || '')) .map((account) => { diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts index ebd5eb41c..9ef5e5c97 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/documentdb/MongoRUResourceItem.ts @@ -38,6 +38,7 @@ export class MongoRUResourceItem extends ClusterItemBase { return callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { context.telemetry.properties.view = Views.DiscoveryView; context.telemetry.properties.discoveryProvider = 'azure-mongo-ru-discovery'; + context.telemetry.properties.resourceType = 'mongoRU'; const credentials = await extractCredentialsFromRUAccount( context, @@ -56,8 +57,11 @@ export class MongoRUResourceItem extends ClusterItemBase { */ protected async authenticateAndConnect(): Promise { const result = await callWithTelemetryAndErrorHandling('connect', async (context: IActionContext) => { + const connectionStartTime = Date.now(); context.telemetry.properties.view = Views.DiscoveryView; context.telemetry.properties.discoveryProvider = 'azure-mongo-ru-discovery'; + context.telemetry.properties.connectionInitiatedFrom = 'discoveryView'; + context.telemetry.properties.resourceType = 'mongoRU'; ext.outputChannel.appendLine( l10n.t('Attempting to authenticate with "{cluster}"…', { @@ -93,8 +97,17 @@ export class MongoRUResourceItem extends ClusterItemBase { }), ); + // Add success telemetry + context.telemetry.measurements.connectionEstablishmentTimeMs = Date.now() - connectionStartTime; + context.telemetry.properties.connectionResult = 'success'; + return clustersClient; } catch (error) { + // Add error telemetry + context.telemetry.measurements.connectionEstablishmentTimeMs = Date.now() - connectionStartTime; + context.telemetry.properties.connectionResult = 'failed'; + context.telemetry.properties.connectionErrorType = error instanceof Error ? error.name : 'UnknownError'; + ext.outputChannel.appendLine(l10n.t('Error: {error}', { error: (error as Error).message })); void vscode.window.showErrorMessage( diff --git a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts index eb4f2cafb..27b7c5a21 100644 --- a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts @@ -66,10 +66,15 @@ export class AzureDiscoveryProvider extends Disposable implements DiscoveryProvi } async configureCredentials(context: IActionContext, node?: TreeElement): Promise { + // Add telemetry for credential configuration activation + context.telemetry.properties.credentialConfigActivated = 'true'; + context.telemetry.properties.discoveryProviderId = this.id; + context.telemetry.properties.nodeProvided = node ? 'true' : 'false'; + if (!node || node instanceof AzureServiceRootItem) { // Use the new Azure credentials configuration wizard const { configureAzureCredentials } = await import('../api-shared/azure/credentialsManagement'); - await configureAzureCredentials(context, this.azureSubscriptionProvider); + await configureAzureCredentials(context, this.azureSubscriptionProvider, node); if (node) { // Tree context: refresh specific node diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts index 18c9f63e2..d87de9248 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/documentdb/DocumentDBResourceItem.ts @@ -50,6 +50,7 @@ export class DocumentDBResourceItem extends ClusterItemBase { return callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { context.telemetry.properties.view = Views.DiscoveryView; context.telemetry.properties.discoveryProvider = 'azure-mongo-vcore-discovery'; + context.telemetry.properties.resourceType = 'mongoVCore'; // Retrieve and validate cluster information (throws if invalid) const clusterInformation = await getClusterInformationFromAzure( @@ -82,8 +83,11 @@ export class DocumentDBResourceItem extends ClusterItemBase { */ protected async authenticateAndConnect(): Promise { const result = await callWithTelemetryAndErrorHandling('connect', async (context: IActionContext) => { + const connectionStartTime = Date.now(); context.telemetry.properties.view = Views.DiscoveryView; context.telemetry.properties.discoveryProvider = 'azure-mongo-vcore-discovery'; + context.telemetry.properties.connectionInitiatedFrom = 'discoveryView'; + context.telemetry.properties.resourceType = 'mongoVCore'; ext.outputChannel.appendLine( l10n.t('Attempting to authenticate with "{cluster}"…', { @@ -153,8 +157,17 @@ export class DocumentDBResourceItem extends ClusterItemBase { }), ); + // Add success telemetry + context.telemetry.measurements.connectionEstablishmentTimeMs = Date.now() - connectionStartTime; + context.telemetry.properties.connectionResult = 'success'; + return clustersClient; } catch (error) { + // Add error telemetry + context.telemetry.measurements.connectionEstablishmentTimeMs = Date.now() - connectionStartTime; + context.telemetry.properties.connectionResult = 'failed'; + context.telemetry.properties.connectionErrorType = error instanceof Error ? error.name : 'UnknownError'; + ext.outputChannel.appendLine(l10n.t('Error: {error}', { error: (error as Error).message })); void vscode.window.showErrorMessage( @@ -196,6 +209,8 @@ export class DocumentDBResourceItem extends ClusterItemBase { await callWithTelemetryAndErrorHandling('connect.promptForCredentials', async (context: IActionContext) => { context.telemetry.properties.view = Views.DiscoveryView; context.telemetry.properties.discoveryProvider = 'azure-mongo-vcore-discovery'; + context.telemetry.properties.credentialsRequired = 'true'; + context.telemetry.properties.credentialPromptReason = 'firstTime'; context.errorHandling.rethrow = true; context.errorHandling.suppressDisplay = false; diff --git a/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts b/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts index 50330afda..43e50c2c7 100644 --- a/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts +++ b/src/plugins/service-azure-vm/AzureVMDiscoveryProvider.ts @@ -72,10 +72,15 @@ export class AzureVMDiscoveryProvider extends Disposable implements DiscoveryPro } async configureCredentials(context: IActionContext, node?: TreeElement): Promise { + // Add telemetry for credential configuration activation + context.telemetry.properties.credentialConfigActivated = 'true'; + context.telemetry.properties.discoveryProviderId = this.id; + context.telemetry.properties.nodeProvided = node ? 'true' : 'false'; + if (!node || node instanceof AzureServiceRootItem) { // Use the new Azure credentials configuration wizard const { configureAzureCredentials } = await import('../api-shared/azure/credentialsManagement'); - await configureAzureCredentials(context, this.azureSubscriptionProvider); + await configureAzureCredentials(context, this.azureSubscriptionProvider, node); if (node) { // Tree context: refresh specific node From e26c684f550d9db57e98421efd9f01d0b06d3d31 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 25 Sep 2025 15:25:58 +0200 Subject: [PATCH 46/88] feat: UX improvement: warn user about potentially hidden subscriptions --- l10n/bundle.l10n.json | 3 ++ .../manageCredentials.ts | 32 ++++++++++++++++++- .../configureAzureCredentials.ts | 6 ++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 707be36df..8596e7d9e 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -125,6 +125,8 @@ "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...": "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...", "Creating user assigned identity \"{0}\" in location \"{1}\"\"...": "Creating user assigned identity \"{0}\" in location \"{1}\"\"...", "Credential Management Completed": "Credential Management Completed", + "Credential update completed.": "Credential update completed.", + "Credential update completed. If you don't see expected entries, use the optional \"Filter Entries…\" option to adjust your filters.": "Credential update completed. If you don't see expected entries, use the optional \"Filter Entries…\" option to adjust your filters.", "Credentials updated successfully.": "Credentials updated successfully.", "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}\"", @@ -228,6 +230,7 @@ "Failed to store secrets for key {0}:": "Failed to store secrets for key {0}:", "Failed to update the connection.": "Failed to update the connection.", "Failed with code \"{0}\".": "Failed with code \"{0}\".", + "Filter Entries Now": "Filter Entries Now", "Find Query": "Find Query", "Finished importing": "Finished importing", "Go back.": "Go back.", diff --git a/src/commands/discoveryService.manageCredentials/manageCredentials.ts b/src/commands/discoveryService.manageCredentials/manageCredentials.ts index 178a4da31..07676b7e2 100644 --- a/src/commands/discoveryService.manageCredentials/manageCredentials.ts +++ b/src/commands/discoveryService.manageCredentials/manageCredentials.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import { l10n } from 'vscode'; +import { commands, l10n, window } from 'vscode'; import { Views } from '../../documentdb/Views'; import { ext } from '../../extensionVariables'; import { DiscoveryService } from '../../services/discoveryServices'; @@ -53,6 +53,36 @@ export async function manageCredentials(context: IActionContext, node: TreeEleme ext.discoveryBranchDataProvider.refresh(node as TreeElement); context.telemetry.properties.result = 'Succeeded'; + + // Only show notification if credentials were actually managed successfully (user didn't cancel) + // TODO: this is not the best way to do this, but this feature has to ship. Refactor to expose results as a result object + if (context.telemetry.properties.credentialsManagementResult === 'Succeeded') { + // Show informational message about potential entry filtering conflicts + // This is only shown when credentials are managed from explicit commands, not from wizards, + // because that's where users interact with settings explicitly and the message makes sense. + // Adding it to every call to the management wizard might put too high mental load on the user. + if (provider?.configureTreeItemFilter) { + const filterAction = l10n.t('Filter Entries Now'); + const cancelAction = l10n.t('Cancel'); + const selectedAction = await window.showInformationMessage( + l10n.t( + 'Credential update completed. If you don\'t see expected entries, use the optional "Filter Entries…" option to adjust your filters.', + ), + filterAction, + cancelAction, + ); + + if (selectedAction === filterAction) { + await commands.executeCommand( + 'vscode-documentdb.command.discoveryView.filterProviderContent', + node, + ); + } + // If selectedAction === cancelAction or undefined (ESC pressed), we do nothing + } else { + void window.showInformationMessage(l10n.t('Credential update completed.')); + } + } } catch (error) { context.telemetry.properties.result = 'Failed'; context.telemetry.properties.errorReason = 'configureCredentialsThrew'; diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index 16bb90697..31dd85215 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -114,6 +114,12 @@ export async function configureAzureCredentials( } await configureAzureCredentialsInternal(telemetryContext, azureSubscriptionProvider); + + // Copy the credentials management result to the outer context so providers can access it + if (telemetryContext.telemetry.properties.credentialsManagementResult) { + context.telemetry.properties.credentialsManagementResult = + telemetryContext.telemetry.properties.credentialsManagementResult; + } }, ); } From 5927c10528f31cf00878e0dd2acb61504537394b Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 25 Sep 2025 15:30:11 +0200 Subject: [PATCH 47/88] chore: copilot instructions tweak --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a8fd8e834..0e5e2ade5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -68,7 +68,7 @@ This document provides comprehensive guidelines and context for GitHub Copilot t - Use l10n for any user-facing strings with `vscode.l10n.t()`. - Use `npm run prettier-fix` to format your code before committing. - Use `npm run lint` to check for linting errors. -- Use `npm run build` to ensure the project builds successfully (do not use `npm run compile`). +- Use `npm run build` to ensure the project builds successfully. - Use `npm run l10n` to update localization files in case you change any user-facing strings. - Ensure TypeScript compilation passes without errors. From 07c2c032d143355b5d6ed9daadca7a5254084520 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 25 Sep 2025 18:31:08 +0200 Subject: [PATCH 48/88] chore: refactor and extract tenant filtering --- .../AzureSubscriptionProviderWithFilters.ts | 13 +- .../api-shared/azure/subscriptionFiltering.ts | 7 +- .../azure/wizard/SelectSubscriptionStep.ts | 5 +- .../wizard/SelectTenantAndSubscriptionStep.ts | 122 ++++++++++++++++++ .../AzureMongoRUServiceRootItem.ts | 4 +- .../discovery-tree/AzureServiceRootItem.ts | 4 +- .../discovery-tree/AzureServiceRootItem.ts | 4 +- .../discovery-tree/configureVmFilterWizard.ts | 6 +- 8 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts diff --git a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts index 62d357dc0..22a3703a4 100644 --- a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts +++ b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts @@ -11,7 +11,6 @@ import { } from '@microsoft/vscode-azext-azureauth'; import * as vscode from 'vscode'; import { ext } from '../../../extensionVariables'; -import { getTenantFilteredSubscriptions } from './subscriptionFiltering'; /** * Extends VSCodeAzureSubscriptionProvider to customize tenant and subscription filters @@ -37,18 +36,14 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio } /** - * Gets subscriptions from the Azure subscription provider and applies tenant filtering. - * Tenant filtering is always applied regardless of the subscription filter parameter. + * Gets subscriptions from the Azure subscription provider. + * Note: Callers must explicitly call getTenantFilteredSubscriptions() if tenant filtering is needed. * * @param filter Whether to apply subscription filtering or a custom filter - * @returns Filtered list of subscriptions with tenant filtering applied + * @returns List of subscriptions from the base provider (without tenant filtering) */ public override async getSubscriptions(filter?: boolean | GetSubscriptionsFilter): Promise { - // Get subscriptions from the base provider with the original filter parameter - const subscriptions = await super.getSubscriptions(filter); - - // Always apply tenant filtering regardless of the filter parameter - return getTenantFilteredSubscriptions(subscriptions); + return await super.getSubscriptions(filter); } /** diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering.ts index f7479bb88..4c21c0f07 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering.ts @@ -220,10 +220,11 @@ export async function configureAzureSubscriptionFilter( const subscriptionQuickPickItems: () => Promise[]> = async () => { const subscriptionLoadStartTime = Date.now(); const allSubscriptions = await azureSubscriptionProvider.getSubscriptions(false); // Get all unfiltered subscriptions - const duplicates = getDuplicateSubscriptions(allSubscriptions); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); // Apply tenant filtering + const duplicates = getDuplicateSubscriptions(subscriptions); // Add telemetry for subscription loading - context.telemetry.measurements.totalSubscriptionsAvailable = allSubscriptions.length; + context.telemetry.measurements.totalSubscriptionsAvailable = subscriptions.length; context.telemetry.measurements.duplicateSubscriptionsCount = duplicates.length; context.telemetry.measurements.subscriptionLoadingTimeMs = Date.now() - subscriptionLoadStartTime; @@ -245,7 +246,7 @@ export async function configureAzureSubscriptionFilter( // Add telemetry for tenant information context.telemetry.measurements.tenantsWithSubscriptionsCount = tenantDisplayNames.size; - return allSubscriptions + return subscriptions .map((subscription) => { const tenantName = tenantDisplayNames.get(subscription.tenantId); diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 7cc716a0b..d729adaa6 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -10,7 +10,7 @@ import { QuickPickItemKind, ThemeIcon, Uri, window, type MessageItem, type Quick import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; -import { getDuplicateSubscriptions } from '../subscriptionFiltering'; +import { getDuplicateSubscriptions, getTenantFilteredSubscriptions } from '../subscriptionFiltering'; import { AzureContextProperties } from './AzureContextProperties'; export class SelectSubscriptionStep extends AzureWizardPromptStep { @@ -60,7 +60,8 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep => { - subscriptions = await subscriptionProvider.getSubscriptions(false); + const allSubscriptions = await subscriptionProvider.getSubscriptions(false); + subscriptions = getTenantFilteredSubscriptions(allSubscriptions); // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however diff --git a/src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts new file mode 100644 index 000000000..afce58d4a --- /dev/null +++ b/src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * REFERENCE IMPLEMENTATION - Mock FilteringInitializeStep + * + * This file demonstrates advanced UX patterns for Azure service discovery: + * - Loading states with loadingPlaceHolder + * - Exception-based flow control for seamless wizard transitions + * - Dynamic subwizard creation based on initialization results + * - Smart routing (single tenant → direct subscription, multiple → tenant selection) + * + * Key UX Features: + * - 5-second initialization with loading animation + * - Automatic progression without user interaction + * - Context-aware subwizard selection + * - Clean exception-driven flow control + * + * This implementation is kept as a reference for future filtering initialization steps. + */ + +import { AzureWizardPromptStep, type IWizardOptions } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import { type QuickPickItem } from 'vscode'; +import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; +import { SelectClusterStep } from '../../../service-azure-mongo-vcore/discovery-wizard/SelectClusterStep'; +import { SelectSubscriptionStep } from './SelectSubscriptionStep'; + +/** + * Custom error to signal that initialization has completed and wizard should proceed to subwizard + */ +class InitializationCompleteError extends Error { + constructor(message: string = 'Initialization completed successfully') { + super(message); + this.name = 'InitializationCompleteError'; + } +} + +/** + * Mock step to demonstrate the FilteringInitializeStep UX pattern with fake delay + */ +export class SelectTenantAndSubscriptionStep extends AzureWizardPromptStep { + public async prompt(context: NewConnectionWizardContext): Promise { + try { + // Use QuickPick with loading state for unified UX demonstration + await context.ui.showQuickPick(this.mockInitializeFilteringData(context), { + placeHolder: l10n.t('Initializing filtering options...'), + loadingPlaceHolder: l10n.t('Loading tenants and subscription data...'), + suppressPersistence: true, + }); + } catch (error) { + // Initialization completed - this is expected behavior + // The exception signals that initialization is done and we should proceed to subwizard + if (error instanceof InitializationCompleteError) { + // Mock: Set fake selected subscription for the rest of the wizard to work + context.properties.selectedSubscription = { + subscriptionId: 'mock-subscription-id', + displayName: 'Mock Subscription', + }; + return; // Proceed to getSubWizard + } + // Re-throw any other errors + throw error; + } + } + + private async mockInitializeFilteringData(context: NewConnectionWizardContext): Promise { + // Mock: Add fake 5-second delay to simulate tenant/subscription loading + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Mock: Simulate tenant discovery logic + const mockTenantCount = Math.floor(Math.random() * 3) + 1; // 1-3 tenants + + context.telemetry.properties.mockTenantCount = mockTenantCount.toString(); + + if (mockTenantCount === 1) { + // Single tenant: simulate auto-selection and subscription pre-loading + context.telemetry.properties.mockFlow = 'singleTenant'; + // Simulate additional subscription loading delay + await new Promise((resolve) => setTimeout(resolve, 2000)); + } else { + // Multi-tenant: simulate tenant discovery + context.telemetry.properties.mockFlow = 'multiTenant'; + } + + // Throw exception to signal initialization completion and auto-proceed to subwizard + throw new InitializationCompleteError('Tenant and subscription initialization completed'); + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async getSubWizard( + context: NewConnectionWizardContext, + ): Promise> { + const mockTenantCount = parseInt((context.telemetry.properties.mockTenantCount as string) || '1'); + + if (mockTenantCount > 1) { + // Multi-tenant: show both tenant and subscription selection + return { + title: l10n.t('Filter Tenants & Subscriptions (Mock)'), + promptSteps: [ + // Mock tenant selection step (using existing subscription step as placeholder) + new SelectSubscriptionStep(), + new SelectClusterStep(), + ], + executeSteps: [], + }; + } else { + // Single tenant: skip directly to cluster selection + return { + title: l10n.t('Select Cluster (Mock - Single Tenant)'), + promptSteps: [new SelectClusterStep()], + executeSteps: [], + }; + } + } + + public shouldPrompt(): boolean { + return true; + } +} diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts index 6fcde565e..71859108f 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts @@ -14,6 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering'; import { AzureMongoRUSubscriptionItem } from './AzureMongoRUSubscriptionItem'; export class AzureMongoRUServiceRootItem @@ -57,7 +58,8 @@ export class AzureMongoRUServiceRootItem ]; } - const subscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); if (!subscriptions || subscriptions.length === 0) { return []; } diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts index 2e95ce46d..6b737c614 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts @@ -14,6 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { @@ -55,7 +56,8 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext ]; } - const subscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); if (!subscriptions || subscriptions.length === 0) { return []; } diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts index 5a7f10f1c..5130a0d7d 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts @@ -14,6 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { @@ -55,7 +56,8 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext ]; } - const subscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); if (!subscriptions || subscriptions.length === 0) { return []; } diff --git a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts index d9697fbb5..02c376c92 100644 --- a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts +++ b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts @@ -17,6 +17,7 @@ import { ext } from '../../../extensionVariables'; import { getDuplicateSubscriptions, getSelectedSubscriptionIds, + getTenantFilteredSubscriptions, setSelectedSubscriptionIds, } from '../../api-shared/azure/subscriptionFiltering'; @@ -48,9 +49,10 @@ class SubscriptionFilterStep extends AzureWizardPromptStep[] > = async () => { const allSubscriptions = await azureSubscriptionProvider.getSubscriptions(false); // Get all unfiltered subscriptions - const duplicates = getDuplicateSubscriptions(allSubscriptions); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); // Apply tenant filtering + const duplicates = getDuplicateSubscriptions(subscriptions); - return allSubscriptions + return subscriptions .map( (subscription) => >{ From 3355a1e2dc9d89b2c8fdc428e87fa58c34ac28bc Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 25 Sep 2025 19:00:37 +0200 Subject: [PATCH 49/88] feat: in wizard-discovery, do not filter by tenant --- .../api-shared/azure/wizard/SelectSubscriptionStep.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index d729adaa6..034144977 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { VSCodeAzureSubscriptionProvider, type AzureSubscription } from '@microsoft/vscode-azext-azureauth'; import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import { QuickPickItemKind, ThemeIcon, Uri, window, type MessageItem, type QuickPickItem } from 'vscode'; import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; -import { getDuplicateSubscriptions, getTenantFilteredSubscriptions } from '../subscriptionFiltering'; +import { getDuplicateSubscriptions } from '../subscriptionFiltering'; import { AzureContextProperties } from './AzureContextProperties'; export class SelectSubscriptionStep extends AzureWizardPromptStep { @@ -56,12 +56,12 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep>; + let subscriptions!: Awaited; // Create async function to provide better loading UX and debugging experience const getSubscriptionQuickPickItems = async (): Promise<(QuickPickItem & { id: string })[]> => { - const allSubscriptions = await subscriptionProvider.getSubscriptions(false); - subscriptions = getTenantFilteredSubscriptions(allSubscriptions); + // Note: No tenant filtering here, because this flow should allow the user to access everything with no filtering. + subscriptions = await subscriptionProvider.getSubscriptions(false); // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however From 586bff97bf693855773f41e76474e3bd2106795d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 26 Sep 2025 15:59:34 +0200 Subject: [PATCH 50/88] wip: tenant/subscription filtering --- .../SelectTenantsStep.ts | 2 +- .../FilterSubscriptionSubStep.ts | 116 ++++++++++++++++ .../FilterTenantSubStep.ts | 89 ++++++++++++ .../FilteringExecuteStep.ts | 126 +++++++++++++++++ .../FilteringWizardContext.ts | 23 ++++ .../InitializeFilteringStep.ts | 128 ++++++++++++++++++ .../configureAzureSubscriptionFilterWizard.ts | 70 ++++++++++ .../azure/subscriptionFiltering/index.ts | 12 ++ .../subscriptionFiltering.ts | 125 +---------------- .../azure/wizard/SelectSubscriptionStep.ts | 2 +- .../AzureMongoRUServiceRootItem.ts | 2 +- .../discovery-tree/AzureServiceRootItem.ts | 2 +- .../discovery-tree/AzureServiceRootItem.ts | 2 +- .../discovery-tree/configureVmFilterWizard.ts | 2 +- 14 files changed, 577 insertions(+), 124 deletions(-) create mode 100644 src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts create mode 100644 src/plugins/api-shared/azure/subscriptionFiltering/FilterTenantSubStep.ts create mode 100644 src/plugins/api-shared/azure/subscriptionFiltering/FilteringExecuteStep.ts create mode 100644 src/plugins/api-shared/azure/subscriptionFiltering/FilteringWizardContext.ts create mode 100644 src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts create mode 100644 src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilterWizard.ts create mode 100644 src/plugins/api-shared/azure/subscriptionFiltering/index.ts rename src/plugins/api-shared/azure/{ => subscriptionFiltering}/subscriptionFiltering.ts (61%) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts index 261f261b4..6c5756694 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts @@ -9,7 +9,7 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { nonNullValue } from '../../../../utils/nonNull'; -import { isTenantFilteredOut } from '../subscriptionFiltering'; +import { isTenantFilteredOut } from '../subscriptionFiltering/subscriptionFiltering'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; interface TenantQuickPickItem extends vscode.QuickPickItem { diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts new file mode 100644 index 000000000..9c404ace5 --- /dev/null +++ b/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureSubscription } from '@microsoft/vscode-azext-azureauth'; +import { AzureWizardPromptStep, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ext } from '../../../../extensionVariables'; +import { type FilteringWizardContext } from './FilteringWizardContext'; +import { getDuplicateSubscriptions, getSelectedSubscriptionIds } from './subscriptionFiltering'; + +export class FilterSubscriptionSubStep extends AzureWizardPromptStep { + public async prompt(context: FilteringWizardContext): Promise { + const selectedSubscriptionIds = getSelectedSubscriptionIds(); + const allSubscriptions = context.allSubscriptions || []; + const selectedTenants = context.selectedTenants || []; + + // Filter subscriptions to only show those from selected tenants + const selectedTenantIds = new Set(selectedTenants.map((tenant) => tenant.tenantId)); + const availableSubscriptions = allSubscriptions.filter((subscription) => { + // If no tenants selected (single tenant flow), show all subscriptions + if (selectedTenantIds.size === 0) { + return true; + } + // Otherwise, only show subscriptions from selected tenants + return selectedTenantIds.has(subscription.tenantId); + }); + + // Add telemetry for subscription filtering + context.telemetry.measurements.subscriptionsAfterTenantFiltering = availableSubscriptions.length; + + if (availableSubscriptions.length === 0) { + void vscode.window.showWarningMessage( + l10n.t( + 'No subscriptions found for the selected tenants. Please adjust your tenant selection or check your Azure permissions.', + ), + ); + return; + } + + // Build tenant display name lookup from preloaded tenant data + const tenantDisplayNames = new Map(); + const availableTenants = context.availableTenants || []; + for (const tenant of availableTenants) { + if (tenant.tenantId && tenant.displayName) { + tenantDisplayNames.set(tenant.tenantId, tenant.displayName); + } + } + + // Use duplicate detection logic + const duplicates = getDuplicateSubscriptions(availableSubscriptions); + + // Create subscription quick pick items (data is preloaded, no async needed) + const subscriptionItems: IAzureQuickPickItem[] = availableSubscriptions + .map((subscription) => { + const tenantName = tenantDisplayNames.get(subscription.tenantId); + + // Build description with tenant information + const description = tenantName + ? `${subscription.subscriptionId} (${tenantName})` + : subscription.subscriptionId; + + return >{ + label: duplicates.includes(subscription) + ? subscription.name + ` (${subscription.account?.label})` + : subscription.name, + description, + data: subscription, + group: subscription.account.label, + iconPath: vscode.Uri.joinPath( + ext.context.extensionUri, + 'resources', + 'from_node_modules', + '@microsoft', + 'vscode-azext-azureutils', + 'resources', + 'azureSubscription.svg', + ), + }; + }) + .sort((a, b) => a.label.localeCompare(b.label)); + + const selectedItems = await context.ui.showQuickPick(subscriptionItems, { + stepName: 'filterSubscriptions', + canPickMany: true, + placeHolder: l10n.t('Select subscriptions to include in service discovery'), + isPickSelected: (item: IAzureQuickPickItem) => + selectedSubscriptionIds.includes(item.data.subscriptionId), + }); + + const selectedSubscriptions = selectedItems.map((item) => item.data); + + // Add telemetry for subscription selection + const selectedCount = selectedItems.length; + const unselectedCount = subscriptionItems.length - selectedCount; + + context.telemetry.measurements.subscriptionsSelected = selectedCount; + context.telemetry.measurements.subscriptionsUnselected = unselectedCount; + + // Store the selected subscriptions in context for the execute step + context.selectedSubscriptions = selectedSubscriptions; + + // Show warning if nothing selected + if (selectedSubscriptions.length === 0) { + void vscode.window.showWarningMessage( + l10n.t('No subscriptions selected. Service discovery will not show any resources.'), + ); + } + } + + public shouldPrompt(): boolean { + return true; + } +} diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/FilterTenantSubStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/FilterTenantSubStep.ts new file mode 100644 index 000000000..ccf644cce --- /dev/null +++ b/src/plugins/api-shared/azure/subscriptionFiltering/FilterTenantSubStep.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; +import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { nonNullValue } from '../../../../utils/nonNull'; +import { type FilteringWizardContext } from './FilteringWizardContext'; + +interface TenantQuickPickItem extends vscode.QuickPickItem { + tenant?: AzureTenant; +} + +export class FilterTenantSubStep extends AzureWizardPromptStep { + public async prompt(context: FilteringWizardContext): Promise { + const tenants = context.availableTenants || []; + + // Add telemetry for tenant filtering + context.telemetry.measurements.availableTenantsForFilteringCount = tenants.length; + + if (tenants.length === 0) { + void vscode.window.showWarningMessage( + l10n.t('No tenants found. Please try signing in again or check your Azure permissions.'), + ); + return; + } + + // Create quick pick items for tenants (data is preloaded, no async needed) + const tenantItems: TenantQuickPickItem[] = tenants.map((tenant) => { + return { + label: tenant.displayName ?? tenant.tenantId ?? '', + detail: tenant.tenantId, + description: tenant.defaultDomain, + group: tenant.account.label, + iconPath: new vscode.ThemeIcon('organization'), + tenant, + }; + }); + + const selectedItems = await context.ui.showQuickPick(tenantItems, { + stepName: 'filterTenants', + placeHolder: l10n.t('Select tenants to include in subscription discovery'), + canPickMany: true, + enableGrouping: true, + matchOnDescription: true, + suppressPersistence: true, + loadingPlaceHolder: l10n.t('Loading Tenant Filter Options…'), + isPickSelected: (pick) => { + const tenantPick = pick as TenantQuickPickItem; + + if (!tenantPick.tenant?.tenantId) { + return true; // Default to selected if no tenant ID + } + + // Use the preinitialized selectedTenants from context (handles both initial and going back scenarios) + if (context.selectedTenants && context.selectedTenants.length > 0) { + return context.selectedTenants.some( + (selectedTenant) => selectedTenant.tenantId === tenantPick.tenant?.tenantId, + ); + } + + // Fallback to true if no selectedTenants (shouldn't happen with proper initialization) + return true; + }, + }); + + // Extract selected tenants + context.selectedTenants = selectedItems.map((item) => + nonNullValue(item.tenant, 'item.tenant', 'FilterTenantSubStep.ts'), + ); + + // Add telemetry for tenant selection + const totalTenants = context.availableTenants?.length ?? 0; + context.telemetry.measurements.selectedTenantsForFilteringCount = selectedItems.length; + context.telemetry.measurements.unselectedTenantsForFilteringCount = totalTenants - selectedItems.length; + context.telemetry.properties.allTenantsSelectedForFiltering = ( + selectedItems.length === totalTenants + ).toString(); + context.telemetry.properties.noTenantsSelectedForFiltering = (selectedItems.length === 0).toString(); + } + + public shouldPrompt(): boolean { + // The decision has been made in the init step when the subwizard was constructed + return true; + } +} diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/FilteringExecuteStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/FilteringExecuteStep.ts new file mode 100644 index 000000000..1e02a4136 --- /dev/null +++ b/src/plugins/api-shared/azure/subscriptionFiltering/FilteringExecuteStep.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import { ext } from '../../../../extensionVariables'; +import { type FilteringWizardContext } from './FilteringWizardContext'; +import { addUnselectedTenant, removeUnselectedTenant, setSelectedSubscriptionIds } from './subscriptionFiltering'; + +export class FilteringExecuteStep extends AzureWizardExecuteStep { + public priority: number = 100; + + public async execute(context: FilteringWizardContext): Promise { + const executeStartTime = Date.now(); + + ext.outputChannel.appendLine(l10n.t('Applying Azure discovery filters...')); + + // Apply tenant filtering if tenants were selected + if (context.selectedTenants && context.availableTenants && context.availableTenants.length > 0) { + await this.applyTenantFiltering(context); + } + + // Apply subscription filtering if subscriptions were selected + if (context.selectedSubscriptions && context.selectedSubscriptions.length > 0) { + await this.applySubscriptionFiltering(context); + } + + ext.outputChannel.appendLine(l10n.t('Refreshing Azure discovery tree...')); + ext.discoveryBranchDataProvider.refresh(); + + ext.outputChannel.appendLine(l10n.t('Azure discovery filters applied successfully.')); + + // Add completion telemetry + context.telemetry.measurements.filteringExecutionTimeMs = Date.now() - executeStartTime; + context.telemetry.properties.filteringExecutionResult = 'Succeeded'; + } + + private async applyTenantFiltering(context: FilteringWizardContext): Promise { + const selectedTenants = context.selectedTenants || []; + const allTenants = context.availableTenants || []; + + ext.outputChannel.appendLine(l10n.t('Configuring tenant filtering...')); + + // Get all unique account IDs from subscriptions to apply tenant filtering per account + const accountIds = new Set(); + if (context.allSubscriptions) { + for (const subscription of context.allSubscriptions) { + if (subscription.account?.id) { + accountIds.add(subscription.account.id); + } + } + } + + const selectedTenantIds = new Set(selectedTenants.map((tenant) => tenant.tenantId || '')); + + // Add telemetry for tenant filtering + context.telemetry.measurements.tenantFilteringCount = allTenants.length; + context.telemetry.measurements.selectedFinalTenantsCount = selectedTenants.length; + context.telemetry.properties.filteringActionType = 'tenantFiltering'; + + // Apply tenant filtering for each account + for (const accountId of accountIds) { + // Process each tenant - add to unselected if not selected, remove from unselected if selected + for (const tenant of allTenants) { + const tenantId = tenant.tenantId || ''; + if (selectedTenantIds.has(tenantId)) { + // Tenant is selected, so remove it from unselected list (make it available) + await removeUnselectedTenant(tenantId, accountId); + } else { + // Tenant is not selected, so add it to unselected list (filter it out) + await addUnselectedTenant(tenantId, accountId); + } + } + } + + ext.outputChannel.appendLine( + l10n.t('Successfully configured tenant filtering. Selected {0} tenant(s)', selectedTenants.length), + ); + + if (selectedTenants.length > 0) { + const tenantNames = selectedTenants.map( + (tenant) => tenant.displayName || tenant.tenantId || l10n.t('Unknown tenant'), + ); + ext.outputChannel.appendLine(l10n.t('Selected tenants: {0}', tenantNames.join(', '))); + } else { + ext.outputChannel.appendLine( + l10n.t('No tenants selected. Azure discovery will be filtered to exclude all tenant results.'), + ); + } + } + + private async applySubscriptionFiltering(context: FilteringWizardContext): Promise { + const selectedSubscriptions = context.selectedSubscriptions || []; + + ext.outputChannel.appendLine(l10n.t('Configuring subscription filtering...')); + + // Convert subscriptions to the format expected by setSelectedSubscriptionIds + const selectedIds = selectedSubscriptions.map( + (subscription) => `${subscription.tenantId}/${subscription.subscriptionId}`, + ); + + // Store the selected subscription IDs + await setSelectedSubscriptionIds(selectedIds); + + ext.outputChannel.appendLine( + l10n.t( + 'Successfully configured subscription filtering. Selected {0} subscription(s)', + selectedSubscriptions.length, + ), + ); + + if (selectedSubscriptions.length > 0) { + const subscriptionNames = selectedSubscriptions.map( + (subscription) => subscription.name || subscription.subscriptionId, + ); + ext.outputChannel.appendLine(l10n.t('Selected subscriptions: {0}', subscriptionNames.join(', '))); + } + } + + public shouldExecute(context: FilteringWizardContext): boolean { + // Execute if we have either tenant or subscription filtering to apply + return !!(context.selectedTenants || context.selectedSubscriptions); + } +} diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/FilteringWizardContext.ts b/src/plugins/api-shared/azure/subscriptionFiltering/FilteringWizardContext.ts new file mode 100644 index 000000000..c3df6810f --- /dev/null +++ b/src/plugins/api-shared/azure/subscriptionFiltering/FilteringWizardContext.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + type AzureSubscription, + type AzureTenant, + type VSCodeAzureSubscriptionProvider, +} from '@microsoft/vscode-azext-azureauth'; +import { type IActionContext } from '@microsoft/vscode-azext-utils'; + +export interface FilteringWizardContext extends IActionContext { + azureSubscriptionProvider: VSCodeAzureSubscriptionProvider; + + // Initialized data + availableTenants?: AzureTenant[]; + allSubscriptions?: AzureSubscription[]; + + // User selections + selectedTenants?: AzureTenant[]; + selectedSubscriptions?: AzureSubscription[]; +} diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts new file mode 100644 index 000000000..8a394e57d --- /dev/null +++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; +import { AzureWizardPromptStep, type IWizardOptions } from '@microsoft/vscode-azext-utils'; +import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; +import * as l10n from '@vscode/l10n'; +import { type QuickPickItem } from 'vscode'; +import { type FilteringWizardContext } from './FilteringWizardContext'; +import { FilterSubscriptionSubStep } from './FilterSubscriptionSubStep'; +import { FilterTenantSubStep } from './FilterTenantSubStep'; +import { isTenantFilteredOut } from './subscriptionFiltering'; + +/** + * Custom error to signal that initialization has completed and wizard should proceed to subwizard + */ +class InitializationCompleteError extends Error { + constructor(message: string = 'Filtering initialization completed successfully') { + super(message); + this.name = 'InitializationCompleteError'; + } +} + +/** + * Initialize filtering data and determine the appropriate subwizard flow based on tenant count + */ +export class InitializeFilteringStep extends AzureWizardPromptStep { + public async prompt(context: FilteringWizardContext): Promise { + try { + // Use QuickPick with loading state for unified UX + await context.ui.showQuickPick(this.initializeFilteringData(context), { + loadingPlaceHolder: l10n.t('Loading tenants and subscription data...'), + suppressPersistence: true, + }); + } catch (error) { + if (error instanceof InitializationCompleteError) { + // Initialization completed - this is expected behavior + // The exception signals that initialization is done and we should proceed to subwizard + // Note: This was the only way to make the quick pick terminate. We're using it + // to maintain a UX-unified behavior to control the visibility of the tenant-selection step. + // Wizard steps upport "shouldPrompt" function and that'd be the preferred path, however + // while "shouldPrompt" is processed, no UI is being shown. This is a bad UX. + return; // Proceed to getSubWizard + } + // Re-throw any other errors + throw error; + } + } + + private async initializeFilteringData(context: FilteringWizardContext): Promise { + const azureSubscriptionProvider = context.azureSubscriptionProvider; + + const tenantLoadStartTime = Date.now(); + context.availableTenants = await azureSubscriptionProvider.getTenants(); + context.telemetry.measurements.tenantLoadTimeMs = Date.now() - tenantLoadStartTime; + context.telemetry.measurements.tenantsCount = context.availableTenants.length; + + const subscriptionLoadStartTime = Date.now(); + context.allSubscriptions = await azureSubscriptionProvider.getSubscriptions(false); + context.telemetry.measurements.subscriptionLoadTimeMs = Date.now() - subscriptionLoadStartTime; + context.telemetry.measurements.allSubscriptionsCount = context.allSubscriptions.length; + + // Initialize selectedTenants based on current filtering state (only if not already set from going back) + if (!context.selectedTenants) { + context.selectedTenants = this.getSelectedTenantsFromSettings(context.availableTenants); + } + + // Determine the flow based on tenant count, but let's look at the actual subscriptions, + // so that in case of a tenant without subscriptions, we don't bother the user with these. + const uniqueTenants = this.getUniqueTenants(context.allSubscriptions); + context.telemetry.properties.tenantCountFromSubscriptions = uniqueTenants.length.toString(); + + if (uniqueTenants.length > 1) { + context.telemetry.properties.filteringFlow = 'multiTenant'; + } else { + context.telemetry.properties.filteringFlow = 'singleTenant'; + } + + // Throw exception to signal initialization completion and auto-proceed to subwizard + throw new InitializationCompleteError('Tenant and subscription initialization completed'); + } + + private getUniqueTenants(subscriptions: AzureSubscription[]): string[] { + const tenantIds = new Set(); + for (const subscription of subscriptions) { + if (subscription.tenantId) { + tenantIds.add(subscription.tenantId); + } + } + return Array.from(tenantIds); + } + + private getSelectedTenantsFromSettings(availableTenants: AzureTenant[]): AzureTenant[] { + // Initialize selectedTenants based on current filtering state + // Include tenants that are NOT filtered out (i.e., currently selected) + return availableTenants.filter((tenant) => { + if (tenant.tenantId && tenant.account?.id) { + // Tenant is selected if it's NOT filtered out + return !isTenantFilteredOut(tenant.tenantId, tenant.account.id); + } + // Default to selected if no tenant ID or account ID + return true; + }); + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async getSubWizard(context: FilteringWizardContext): Promise> { + if (context.telemetry.properties.filteringFlow === 'multiTenant') { + // Multi-tenant: show both tenant and subscription filtering + return { + title: l10n.t('Configure Tenant & Subscription Filters'), + promptSteps: [new FilterTenantSubStep(), new FilterSubscriptionSubStep()], + }; + } else { + // Single tenant: skip directly to subscription filtering + return { + title: l10n.t('Configure Subscription Filter'), + promptSteps: [new FilterSubscriptionSubStep()], + }; + } + } + + public shouldPrompt(): boolean { + return true; + } +} diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilterWizard.ts b/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilterWizard.ts new file mode 100644 index 000000000..eee7df285 --- /dev/null +++ b/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilterWizard.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { AzureWizard, type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ext } from '../../../../extensionVariables'; +import { FilteringExecuteStep } from './FilteringExecuteStep'; +import { type FilteringWizardContext } from './FilteringWizardContext'; +import { InitializeFilteringStep } from './InitializeFilteringStep'; + +/** + * Configures the Azure subscription filter using the wizard pattern. + */ +export async function configureAzureSubscriptionFilterWizard( + context: IActionContext, + azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, +): Promise { + const startTime = Date.now(); + context.telemetry.properties.subscriptionFilteringAction = 'configureWizard'; + + /** + * Ensure the user is signed in to Azure + */ + if (!(await azureSubscriptionProvider.isSignedIn())) { + context.telemetry.properties.subscriptionFilteringResult = 'Failed'; + context.telemetry.properties.subscriptionFilteringError = 'NotSignedIn'; + const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; + void vscode.window + .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) + .then(async (input) => { + if (input === signIn) { + await azureSubscriptionProvider.signIn(); + ext.discoveryBranchDataProvider.refresh(); + } + }); + + // Return so that the signIn flow can be completed before continuing + return; + } + + // Create wizard context + const wizardContext: FilteringWizardContext = { + ...context, + azureSubscriptionProvider, + }; + + // Create and run wizard + const wizard = new AzureWizard(wizardContext, { + title: l10n.t('Configure Azure Discovery Filters'), + promptSteps: [new InitializeFilteringStep()], + executeSteps: [new FilteringExecuteStep()], + }); + + try { + await wizard.prompt(); + context.telemetry.properties.subscriptionFilteringResult = 'Succeeded'; + } catch (error) { + context.telemetry.properties.subscriptionFilteringResult = 'Failed'; + context.telemetry.properties.subscriptionFilteringError = + error instanceof Error ? error.message : String(error); + throw error; + } finally { + // Add completion timing + context.telemetry.measurements.subscriptionFilteringDurationMs = Date.now() - startTime; + } +} diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/index.ts b/src/plugins/api-shared/azure/subscriptionFiltering/index.ts new file mode 100644 index 000000000..cc49a50f9 --- /dev/null +++ b/src/plugins/api-shared/azure/subscriptionFiltering/index.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { configureAzureSubscriptionFilterWizard } from './configureAzureSubscriptionFilterWizard'; +export * from './FilteringExecuteStep'; +export * from './FilteringWizardContext'; +export * from './FilterSubscriptionSubStep'; +export * from './FilterTenantSubStep'; +export * from './InitializeFilteringStep'; +export * from './subscriptionFiltering'; diff --git a/src/plugins/api-shared/azure/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFiltering.ts similarity index 61% rename from src/plugins/api-shared/azure/subscriptionFiltering.ts rename to src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFiltering.ts index 4c21c0f07..c60a73dfb 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFiltering.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { type AzureSubscription, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; -import { type IActionContext, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; +import { type IActionContext } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; -import { l10n } from 'vscode'; -import { ext } from '../../../extensionVariables'; +import { ext } from '../../../../extensionVariables'; /** * Subscription filtering functionality is provided by the `VSCodeAzureSubscriptionProvider` @@ -184,124 +183,14 @@ export function getDuplicateSubscriptions(subscriptions: AzureSubscription[]): A } /** - * Configures the Azure subscription filter. + * Configures the Azure subscription filter using the new wizard pattern. + * This is a wrapper function that maintains backward compatibility. */ export async function configureAzureSubscriptionFilter( context: IActionContext, azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, ): Promise { - const startTime = Date.now(); - context.telemetry.properties.subscriptionFilteringAction = 'configure'; - - /** - * Ensure the user is signed in to Azure - */ - - if (!(await azureSubscriptionProvider.isSignedIn())) { - context.telemetry.properties.subscriptionFilteringResult = 'Failed'; - context.telemetry.properties.subscriptionFilteringError = 'NotSignedIn'; - const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; - void vscode.window - .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) - .then(async (input) => { - if (input === signIn) { - await azureSubscriptionProvider.signIn(); - ext.discoveryBranchDataProvider.refresh(); - } - }); - - // return so that the signIn flow can be completed before continuing - return; - } - - const selectedSubscriptionIds = getSelectedSubscriptionIds(); - - // it's an async function so that the wizard when shown can show the 'loading' state - const subscriptionQuickPickItems: () => Promise[]> = async () => { - const subscriptionLoadStartTime = Date.now(); - const allSubscriptions = await azureSubscriptionProvider.getSubscriptions(false); // Get all unfiltered subscriptions - const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); // Apply tenant filtering - const duplicates = getDuplicateSubscriptions(subscriptions); - - // Add telemetry for subscription loading - context.telemetry.measurements.totalSubscriptionsAvailable = subscriptions.length; - context.telemetry.measurements.duplicateSubscriptionsCount = duplicates.length; - context.telemetry.measurements.subscriptionLoadingTimeMs = Date.now() - subscriptionLoadStartTime; - - // Get tenant information for better UX (similar to SelectSubscriptionStep) - const tenantPromise = azureSubscriptionProvider.getTenants().catch(() => undefined); - const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)); - const knownTenants = await Promise.race([tenantPromise, timeoutPromise]); - - // Build tenant display name lookup for better UX - const tenantDisplayNames = new Map(); - if (knownTenants) { - for (const tenant of knownTenants) { - if (tenant.tenantId && tenant.displayName) { - tenantDisplayNames.set(tenant.tenantId, tenant.displayName); - } - } - } - - // Add telemetry for tenant information - context.telemetry.measurements.tenantsWithSubscriptionsCount = tenantDisplayNames.size; - - return subscriptions - .map((subscription) => { - const tenantName = tenantDisplayNames.get(subscription.tenantId); - - // Build description with tenant information - const description = tenantName - ? `${subscription.subscriptionId} (${tenantName})` - : subscription.subscriptionId; - - return >{ - label: duplicates.includes(subscription) - ? subscription.name + ` (${subscription.account?.label})` - : subscription.name, - description, - data: subscription, - group: subscription.account.label, - iconPath: vscode.Uri.joinPath( - ext.context.extensionUri, - 'resources', - 'from_node_modules', - '@microsoft', - 'vscode-azext-azureutils', - 'resources', - 'azureSubscription.svg', - ), - }; - }) - .sort((a, b) => a.label.localeCompare(b.label)); - }; - - const picks = await context.ui.showQuickPick(subscriptionQuickPickItems(), { - canPickMany: true, - placeHolder: l10n.t('Select Subscriptions'), - isPickSelected: (pick) => { - return ( - selectedSubscriptionIds.length === 0 || - selectedSubscriptionIds.includes((pick as IAzureQuickPickItem).data.subscriptionId) - ); - }, - }); - - if (picks) { - // Update the setting with the new selection - const newSelectedIds = picks.map((pick) => `${pick.data.tenantId}/${pick.data.subscriptionId}`); - await setSelectedSubscriptionIds(newSelectedIds); - - // Add telemetry for subscription selection - const totalAvailable = context.telemetry.measurements.totalSubscriptionsAvailable || 0; - context.telemetry.measurements.subscriptionsSelected = picks.length; - context.telemetry.measurements.subscriptionsFiltered = totalAvailable - picks.length; - context.telemetry.properties.allSubscriptionsSelected = (picks.length === totalAvailable).toString(); - context.telemetry.properties.subscriptionFilteringResult = 'Succeeded'; - } else { - context.telemetry.properties.subscriptionFilteringResult = 'Canceled'; - } - - // Add completion timing - context.telemetry.measurements.subscriptionFilteringDurationMs = Date.now() - startTime; + // Import the wizard function to avoid circular dependencies + const { configureAzureSubscriptionFilterWizard } = await import('./configureAzureSubscriptionFilterWizard'); + await configureAzureSubscriptionFilterWizard(context, azureSubscriptionProvider); } diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 034144977..286418454 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -10,7 +10,7 @@ import { QuickPickItemKind, ThemeIcon, Uri, window, type MessageItem, type Quick import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; -import { getDuplicateSubscriptions } from '../subscriptionFiltering'; +import { getDuplicateSubscriptions } from '../subscriptionFiltering/subscriptionFiltering'; import { AzureContextProperties } from './AzureContextProperties'; export class SelectSubscriptionStep extends AzureWizardPromptStep { diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts index 71859108f..3da4c9303 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts @@ -14,7 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; -import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; import { AzureMongoRUSubscriptionItem } from './AzureMongoRUSubscriptionItem'; export class AzureMongoRUServiceRootItem diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts index 6b737c614..d889aea8c 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts @@ -14,7 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; -import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts index 5130a0d7d..1d46573b3 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts @@ -14,7 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; -import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { diff --git a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts index 02c376c92..9f7c03779 100644 --- a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts +++ b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts @@ -19,7 +19,7 @@ import { getSelectedSubscriptionIds, getTenantFilteredSubscriptions, setSelectedSubscriptionIds, -} from '../../api-shared/azure/subscriptionFiltering'; +} from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; export interface ConfigureVmFilterWizardContext extends IActionContext { azureSubscriptionProvider: VSCodeAzureSubscriptionProvider; From e22a25d90885ff84797b6221895685cff6fc451f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 26 Sep 2025 16:02:08 +0200 Subject: [PATCH 51/88] feat: new tenant+subscription filtering --- .../filterProviderContent.ts | 1 + .../credentialsManagement/ExecuteStep.ts | 4 +- .../SelectTenantsStep.ts | 2 +- ...FilteringExecuteStep.ts => ExecuteStep.ts} | 8 +++- .../FilterSubscriptionSubStep.ts | 47 +++++++++---------- .../InitializeFilteringStep.ts | 2 +- ...ts => configureAzureSubscriptionFilter.ts} | 13 ++--- .../azure/subscriptionFiltering/index.ts | 6 +-- ...ing.ts => subscriptionFilteringHelpers.ts} | 16 +------ .../azure/wizard/SelectSubscriptionStep.ts | 2 +- .../AzureMongoRUDiscoveryProvider.ts | 2 +- .../AzureMongoRUServiceRootItem.ts | 2 +- .../AzureDiscoveryProvider.ts | 2 +- .../discovery-tree/AzureServiceRootItem.ts | 2 +- .../discovery-tree/AzureServiceRootItem.ts | 2 +- .../discovery-tree/configureVmFilterWizard.ts | 2 +- 16 files changed, 50 insertions(+), 63 deletions(-) rename src/plugins/api-shared/azure/subscriptionFiltering/{FilteringExecuteStep.ts => ExecuteStep.ts} (96%) rename src/plugins/api-shared/azure/subscriptionFiltering/{configureAzureSubscriptionFilterWizard.ts => configureAzureSubscriptionFilter.ts} (85%) rename src/plugins/api-shared/azure/subscriptionFiltering/{subscriptionFiltering.ts => subscriptionFilteringHelpers.ts} (91%) diff --git a/src/commands/filterProviderContent/filterProviderContent.ts b/src/commands/filterProviderContent/filterProviderContent.ts index 6473edc24..fcef42282 100644 --- a/src/commands/filterProviderContent/filterProviderContent.ts +++ b/src/commands/filterProviderContent/filterProviderContent.ts @@ -33,6 +33,7 @@ export async function filterProviderContent(context: IActionContext, node: TreeE } const providerId = idSections[1]; + context.telemetry.properties.discoveryProviderId = providerId; const provider = DiscoveryService.getProvider(providerId); if (!provider?.configureTreeItemFilter) { diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index 54c9eb8b1..756d88808 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -32,7 +32,9 @@ export class ExecuteStep extends AzureWizardExecuteStep { +export class ExecuteStep extends AzureWizardExecuteStep { public priority: number = 100; public async execute(context: FilteringWizardContext): Promise { diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts index 9c404ace5..3d8d889de 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/FilterSubscriptionSubStep.ts @@ -9,24 +9,31 @@ import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { type FilteringWizardContext } from './FilteringWizardContext'; -import { getDuplicateSubscriptions, getSelectedSubscriptionIds } from './subscriptionFiltering'; +import { getDuplicateSubscriptions, getSelectedSubscriptionIds } from './subscriptionFilteringHelpers'; export class FilterSubscriptionSubStep extends AzureWizardPromptStep { public async prompt(context: FilteringWizardContext): Promise { const selectedSubscriptionIds = getSelectedSubscriptionIds(); const allSubscriptions = context.allSubscriptions || []; - const selectedTenants = context.selectedTenants || []; - - // Filter subscriptions to only show those from selected tenants - const selectedTenantIds = new Set(selectedTenants.map((tenant) => tenant.tenantId)); - const availableSubscriptions = allSubscriptions.filter((subscription) => { - // If no tenants selected (single tenant flow), show all subscriptions - if (selectedTenantIds.size === 0) { - return true; - } - // Otherwise, only show subscriptions from selected tenants - return selectedTenantIds.has(subscription.tenantId); - }); + let availableSubscriptions: AzureSubscription[]; + + // Only filter subscriptions by selected tenants in multi-tenant scenarios + if (context.telemetry.properties.filteringFlow === 'multiTenant') { + const selectedTenants = context.selectedTenants || []; + const selectedTenantIds = new Set(selectedTenants.map((tenant) => tenant.tenantId)); + + availableSubscriptions = allSubscriptions.filter((subscription) => { + // If no tenants selected, show all subscriptions + if (selectedTenantIds.size === 0) { + return true; + } + // Otherwise, only show subscriptions from selected tenants + return selectedTenantIds.has(subscription.tenantId); + }); + } else { + // Single tenant scenario: show all subscriptions without tenant filtering + availableSubscriptions = allSubscriptions; + } // Add telemetry for subscription filtering context.telemetry.measurements.subscriptionsAfterTenantFiltering = availableSubscriptions.length; @@ -93,21 +100,11 @@ export class FilterSubscriptionSubStep extends AzureWizardPromptStep item.data); // Add telemetry for subscription selection - const selectedCount = selectedItems.length; - const unselectedCount = subscriptionItems.length - selectedCount; - - context.telemetry.measurements.subscriptionsSelected = selectedCount; - context.telemetry.measurements.subscriptionsUnselected = unselectedCount; + context.telemetry.measurements.subscriptionsSelected = selectedItems.length; + context.telemetry.measurements.subscriptionsUnselected = subscriptionItems.length - selectedItems.length; // Store the selected subscriptions in context for the execute step context.selectedSubscriptions = selectedSubscriptions; - - // Show warning if nothing selected - if (selectedSubscriptions.length === 0) { - void vscode.window.showWarningMessage( - l10n.t('No subscriptions selected. Service discovery will not show any resources.'), - ); - } } public shouldPrompt(): boolean { diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts index 8a394e57d..0e8ca6436 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts @@ -11,7 +11,7 @@ import { type QuickPickItem } from 'vscode'; import { type FilteringWizardContext } from './FilteringWizardContext'; import { FilterSubscriptionSubStep } from './FilterSubscriptionSubStep'; import { FilterTenantSubStep } from './FilterTenantSubStep'; -import { isTenantFilteredOut } from './subscriptionFiltering'; +import { isTenantFilteredOut } from './subscriptionFilteringHelpers'; /** * Custom error to signal that initialization has completed and wizard should proceed to subwizard diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilterWizard.ts b/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter.ts similarity index 85% rename from src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilterWizard.ts rename to src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter.ts index eee7df285..4dea6736b 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilterWizard.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter.ts @@ -8,19 +8,18 @@ import { AzureWizard, type IActionContext } from '@microsoft/vscode-azext-utils' import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; -import { FilteringExecuteStep } from './FilteringExecuteStep'; +import { ExecuteStep } from './ExecuteStep'; import { type FilteringWizardContext } from './FilteringWizardContext'; import { InitializeFilteringStep } from './InitializeFilteringStep'; /** * Configures the Azure subscription filter using the wizard pattern. */ -export async function configureAzureSubscriptionFilterWizard( +export async function configureAzureSubscriptionFilter( context: IActionContext, azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, ): Promise { - const startTime = Date.now(); - context.telemetry.properties.subscriptionFilteringAction = 'configureWizard'; + context.telemetry.properties.subscriptionFiltering = 'configureAzureSubscriptionFilter'; /** * Ensure the user is signed in to Azure @@ -52,19 +51,17 @@ export async function configureAzureSubscriptionFilterWizard( const wizard = new AzureWizard(wizardContext, { title: l10n.t('Configure Azure Discovery Filters'), promptSteps: [new InitializeFilteringStep()], - executeSteps: [new FilteringExecuteStep()], + executeSteps: [new ExecuteStep()], }); try { await wizard.prompt(); + await wizard.execute(); context.telemetry.properties.subscriptionFilteringResult = 'Succeeded'; } catch (error) { context.telemetry.properties.subscriptionFilteringResult = 'Failed'; context.telemetry.properties.subscriptionFilteringError = error instanceof Error ? error.message : String(error); throw error; - } finally { - // Add completion timing - context.telemetry.measurements.subscriptionFilteringDurationMs = Date.now() - startTime; } } diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/index.ts b/src/plugins/api-shared/azure/subscriptionFiltering/index.ts index cc49a50f9..36664d6eb 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/index.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/index.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export { configureAzureSubscriptionFilterWizard } from './configureAzureSubscriptionFilterWizard'; -export * from './FilteringExecuteStep'; +export { configureAzureSubscriptionFilter } from './configureAzureSubscriptionFilter'; +export * from './ExecuteStep'; export * from './FilteringWizardContext'; export * from './FilterSubscriptionSubStep'; export * from './FilterTenantSubStep'; export * from './InitializeFilteringStep'; -export * from './subscriptionFiltering'; +export * from './subscriptionFilteringHelpers'; diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFiltering.ts b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts similarity index 91% rename from src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFiltering.ts rename to src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts index c60a73dfb..2d1d12c87 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFiltering.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzureSubscription, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; -import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type AzureSubscription } from '@microsoft/vscode-azext-azureauth'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; @@ -181,16 +180,3 @@ export function getDuplicateSubscriptions(subscriptions: AzureSubscription[]): A return subscriptions.filter((s) => names.get(s.name)! > 1); } - -/** - * Configures the Azure subscription filter using the new wizard pattern. - * This is a wrapper function that maintains backward compatibility. - */ -export async function configureAzureSubscriptionFilter( - context: IActionContext, - azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, -): Promise { - // Import the wizard function to avoid circular dependencies - const { configureAzureSubscriptionFilterWizard } = await import('./configureAzureSubscriptionFilterWizard'); - await configureAzureSubscriptionFilterWizard(context, azureSubscriptionProvider); -} diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 286418454..56a33ecbd 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -10,7 +10,7 @@ import { QuickPickItemKind, ThemeIcon, Uri, window, type MessageItem, type Quick import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; -import { getDuplicateSubscriptions } from '../subscriptionFiltering/subscriptionFiltering'; +import { getDuplicateSubscriptions } from '../subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureContextProperties } from './AzureContextProperties'; export class SelectSubscriptionStep extends AzureWizardPromptStep { diff --git a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts index 0623754fc..f785cd7a8 100644 --- a/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-ru/AzureMongoRUDiscoveryProvider.ts @@ -10,7 +10,7 @@ import { ext } from '../../extensionVariables'; import { type DiscoveryProvider } from '../../services/discoveryServices'; import { type TreeElement } from '../../tree/TreeElement'; import { AzureSubscriptionProviderWithFilters } from '../api-shared/azure/AzureSubscriptionProviderWithFilters'; -import { configureAzureSubscriptionFilter } from '../api-shared/azure/subscriptionFiltering'; +import { configureAzureSubscriptionFilter } from '../api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter'; import { AzureContextProperties } from '../api-shared/azure/wizard/AzureContextProperties'; import { SelectSubscriptionStep } from '../api-shared/azure/wizard/SelectSubscriptionStep'; import { AzureMongoRUServiceRootItem } from './discovery-tree/AzureMongoRUServiceRootItem'; diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts index 3da4c9303..e71713761 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts @@ -14,7 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; -import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureMongoRUSubscriptionItem } from './AzureMongoRUSubscriptionItem'; export class AzureMongoRUServiceRootItem diff --git a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts index 27b7c5a21..11b1e312b 100644 --- a/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts +++ b/src/plugins/service-azure-mongo-vcore/AzureDiscoveryProvider.ts @@ -10,7 +10,7 @@ import { ext } from '../../extensionVariables'; import { type DiscoveryProvider } from '../../services/discoveryServices'; import { type TreeElement } from '../../tree/TreeElement'; import { AzureSubscriptionProviderWithFilters } from '../api-shared/azure/AzureSubscriptionProviderWithFilters'; -import { configureAzureSubscriptionFilter } from '../api-shared/azure/subscriptionFiltering'; +import { configureAzureSubscriptionFilter } from '../api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter'; import { AzureContextProperties } from '../api-shared/azure/wizard/AzureContextProperties'; import { SelectSubscriptionStep } from '../api-shared/azure/wizard/SelectSubscriptionStep'; import { AzureServiceRootItem } from './discovery-tree/AzureServiceRootItem'; diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts index d889aea8c..1e9f5f70f 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts @@ -14,7 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; -import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts index 1d46573b3..764a53fc0 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts @@ -14,7 +14,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; -import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; +import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; export class AzureServiceRootItem implements TreeElement, TreeElementWithContextValue, TreeElementWithRetryChildren { diff --git a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts index 9f7c03779..1b1ee2aa4 100644 --- a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts +++ b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts @@ -19,7 +19,7 @@ import { getSelectedSubscriptionIds, getTenantFilteredSubscriptions, setSelectedSubscriptionIds, -} from '../../api-shared/azure/subscriptionFiltering/subscriptionFiltering'; +} from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; export interface ConfigureVmFilterWizardContext extends IActionContext { azureSubscriptionProvider: VSCodeAzureSubscriptionProvider; From 0bdc1188a4185b7f9056d2166f42b898885ce3de Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 26 Sep 2025 17:12:00 +0200 Subject: [PATCH 52/88] feat: sorting tenants --- .../azure/subscriptionFiltering/InitializeFilteringStep.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts index 0e8ca6436..6629d23b0 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts @@ -54,6 +54,13 @@ export class InitializeFilteringStep extends AzureWizardPromptStep { + // Sort by display name if available, otherwise by tenant ID + const aName = a.displayName || a.tenantId || ''; + const bName = b.displayName || b.tenantId || ''; + return aName.localeCompare(bName); + }); + context.telemetry.measurements.tenantLoadTimeMs = Date.now() - tenantLoadStartTime; context.telemetry.measurements.tenantsCount = context.availableTenants.length; From 03ca51b65e95aea258cb15ceff94923a1d354475 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 26 Sep 2025 17:34:42 +0200 Subject: [PATCH 53/88] wip: poc --- .../AccountActionsStep.ts | 70 +++++++++++++++++++ .../SelectAccountStep.ts | 6 +- .../configureAzureCredentials.ts | 3 +- .../azure/credentialsManagement/index.ts | 1 + 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts diff --git a/src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts new file mode 100644 index 000000000..89628a06c --- /dev/null +++ b/src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, GoBackError } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { nonNullValue } from '../../../../utils/nonNull'; +import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; + +interface AccountActionQuickPickItem extends vscode.QuickPickItem { + action?: 'back' | 'signOut'; +} + +export class AccountActionsStep extends AzureWizardPromptStep { + public async prompt(context: CredentialsManagementWizardContext): Promise { + const selectedAccount = nonNullValue( + context.selectedAccount, + 'context.selectedAccount', + 'AccountActionsStep.ts', + ); + + // Create action items for the selected account + const actionItems: AccountActionQuickPickItem[] = [ + { + label: l10n.t('$(arrow-left) Back to account selection'), + detail: l10n.t('Return to the account list'), + iconPath: new vscode.ThemeIcon('arrow-left'), + action: 'back', + }, + { label: '', kind: vscode.QuickPickItemKind.Separator }, + { + label: l10n.t('$(sign-out) Sign out from this account'), + detail: l10n.t('Remove this account from service discovery'), + iconPath: new vscode.ThemeIcon('sign-out'), + action: 'signOut', + }, + ]; + + const selectedAction = await context.ui.showQuickPick(actionItems, { + stepName: 'accountActions', + placeHolder: l10n.t('What would you like to do with {0}?', selectedAccount.label), + suppressPersistence: true, + }); + + // Handle the selected action + if (selectedAction.action === 'back') { + // Clear the selected account to go back to selection + context.selectedAccount = undefined; + context.telemetry.properties.accountAction = 'back'; + + // Use GoBackError to navigate back to the previous step + throw new GoBackError(); + } else if (selectedAction.action === 'signOut') { + // TODO: Implement sign out functionality + context.telemetry.properties.accountAction = 'signOut'; + + // For now, just show a message + void vscode.window.showInformationMessage( + l10n.t('Sign out functionality will be implemented in the next step'), + ); + } + } + + public shouldPrompt(context: CredentialsManagementWizardContext): boolean { + // Only show this step if we have a selected account + return !!context.selectedAccount; + } +} diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index 34977ff2d..c3a1df7f0 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -49,7 +49,7 @@ export class SelectAccountStep extends AzureWizardPromptStep Date: Fri, 26 Sep 2025 18:54:05 +0200 Subject: [PATCH 54/88] chore: removed unused tenant filtering in acccoutn management --- .../manageCredentials.ts | 34 +---- .../AccountActionsStep.ts | 27 ++-- .../CredentialsManagementWizardContext.ts | 4 - .../credentialsManagement/ExecuteStep.ts | 59 +------- .../SelectAccountStep.ts | 24 +++- .../SelectTenantsStep.ts | 136 ------------------ .../configureAzureCredentials.ts | 16 +-- .../azure/credentialsManagement/index.ts | 1 - 8 files changed, 44 insertions(+), 257 deletions(-) delete mode 100644 src/plugins/api-shared/azure/credentialsManagement/SelectTenantsStep.ts diff --git a/src/commands/discoveryService.manageCredentials/manageCredentials.ts b/src/commands/discoveryService.manageCredentials/manageCredentials.ts index 07676b7e2..7de9b86b3 100644 --- a/src/commands/discoveryService.manageCredentials/manageCredentials.ts +++ b/src/commands/discoveryService.manageCredentials/manageCredentials.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import { commands, l10n, window } from 'vscode'; +import { l10n } from 'vscode'; import { Views } from '../../documentdb/Views'; import { ext } from '../../extensionVariables'; import { DiscoveryService } from '../../services/discoveryServices'; @@ -51,38 +51,6 @@ export async function manageCredentials(context: IActionContext, node: TreeEleme // Refresh the discovery branch data provider to show the updated list ext.discoveryBranchDataProvider.refresh(node as TreeElement); - - context.telemetry.properties.result = 'Succeeded'; - - // Only show notification if credentials were actually managed successfully (user didn't cancel) - // TODO: this is not the best way to do this, but this feature has to ship. Refactor to expose results as a result object - if (context.telemetry.properties.credentialsManagementResult === 'Succeeded') { - // Show informational message about potential entry filtering conflicts - // This is only shown when credentials are managed from explicit commands, not from wizards, - // because that's where users interact with settings explicitly and the message makes sense. - // Adding it to every call to the management wizard might put too high mental load on the user. - if (provider?.configureTreeItemFilter) { - const filterAction = l10n.t('Filter Entries Now'); - const cancelAction = l10n.t('Cancel'); - const selectedAction = await window.showInformationMessage( - l10n.t( - 'Credential update completed. If you don\'t see expected entries, use the optional "Filter Entries…" option to adjust your filters.', - ), - filterAction, - cancelAction, - ); - - if (selectedAction === filterAction) { - await commands.executeCommand( - 'vscode-documentdb.command.discoveryView.filterProviderContent', - node, - ); - } - // If selectedAction === cancelAction or undefined (ESC pressed), we do nothing - } else { - void window.showInformationMessage(l10n.t('Credential update completed.')); - } - } } catch (error) { context.telemetry.properties.result = 'Failed'; context.telemetry.properties.errorReason = 'configureCredentialsThrew'; diff --git a/src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts b/src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts index 89628a06c..d78ab4622 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/AccountActionsStep.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizardPromptStep, GoBackError } from '@microsoft/vscode-azext-utils'; +import { AzureWizardPromptStep, GoBackError, UserCancelledError } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { nonNullValue } from '../../../../utils/nonNull'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; interface AccountActionQuickPickItem extends vscode.QuickPickItem { - action?: 'back' | 'signOut'; + action?: 'back' | 'exit'; } export class AccountActionsStep extends AzureWizardPromptStep { @@ -24,23 +24,23 @@ export class AccountActionsStep extends AzureWizardPromptStep; // accountId -> tenants - allTenants?: AzureTenant[]; // All available tenants for the selected account // State tracking shouldRestartWizard?: boolean; diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index 756d88808..3f31443d9 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -12,68 +12,17 @@ import { type CredentialsManagementWizardContext } from './CredentialsManagement export class ExecuteStep extends AzureWizardExecuteStep { public priority: number = 100; + // eslint-disable-next-line @typescript-eslint/require-await public async execute(context: CredentialsManagementWizardContext): Promise { const executeStartTime = Date.now(); const selectedAccount = nonNullValue(context.selectedAccount, 'context.selectedAccount', 'ExecuteStep.ts'); - const selectedTenants = context.selectedTenants || []; - - ext.outputChannel.appendLine( - l10n.t('Saving Azure credentials configuration for account: {0}', selectedAccount.label), - ); - - // Get all available tenants for this account - const allTenantsForAccount = nonNullValue(context.allTenants, 'context.allTenants', 'ExecuteStep.ts'); - const selectedTenantIds = new Set(selectedTenants.map((tenant) => tenant.tenantId || '')); + ext.outputChannel.appendLine(l10n.t('Viewing Azure account information for: {0}', selectedAccount.label)); // Add telemetry for execution - context.telemetry.measurements.tenantFilteringCount = allTenantsForAccount.length; - context.telemetry.measurements.selectedFinalTenantsCount = selectedTenants.length; - context.telemetry.properties.filteringActionType = 'tenantFiltering'; - - // Use the individual add/remove functions to update tenant selections - const { addUnselectedTenant, removeUnselectedTenant } = await import( - '../subscriptionFiltering/subscriptionFilteringHelpers' - ); - - // Process each tenant - add to unselected if not selected, remove from unselected if selected - for (const tenant of allTenantsForAccount) { - const tenantId = tenant.tenantId || ''; - if (selectedTenantIds.has(tenantId)) { - // Tenant is selected, so remove it from unselected list (make it available) - await removeUnselectedTenant(tenantId, selectedAccount.id); - } else { - // Tenant is not selected, so add it to unselected list (filter it out) - await addUnselectedTenant(tenantId, selectedAccount.id); - } - } - - ext.outputChannel.appendLine( - l10n.t( - 'Successfully configured Azure tenant filtering. Selected {0} tenant(s) for account {1}', - selectedTenants.length, - selectedAccount.label, - ), - ); - - if (selectedTenants.length > 0) { - const tenantNames = selectedTenants.map( - (tenant) => tenant.displayName || tenant.tenantId || l10n.t('Unknown tenant'), - ); - ext.outputChannel.appendLine(l10n.t('Selected tenants: {0}', tenantNames.join(', '))); - } else { - ext.outputChannel.appendLine( - l10n.t( - 'No tenants selected. Azure discovery will be filtered to exclude all results for this account.', - ), - ); - } - - // Refresh the discovery tree to apply the new filtering - ext.outputChannel.appendLine(l10n.t('Refreshing Azure discovery tree...')); - ext.discoveryBranchDataProvider.refresh(); + context.telemetry.properties.filteringActionType = 'accountManagement'; - ext.outputChannel.appendLine(l10n.t('Azure credentials configuration completed successfully.')); + ext.outputChannel.appendLine(l10n.t('Azure account management wizard completed.')); // Add completion telemetry context.telemetry.measurements.executionTimeMs = Date.now() - executeStartTime; diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index c3a1df7f0..5c0f5002c 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; +import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; @@ -14,6 +14,7 @@ interface AccountQuickPickItem extends vscode.QuickPickItem { account?: vscode.AuthenticationSessionAccountInformation; isSignInOption?: boolean; isLearnMoreOption?: boolean; + isExitOption?: boolean; } export class SelectAccountStep extends AzureWizardPromptStep { @@ -46,6 +47,12 @@ export class SelectAccountStep extends AzureWizardPromptStep { - public async prompt(context: CredentialsManagementWizardContext): Promise { - const selectedAccount = nonNullValue( - context.selectedAccount, - 'context.selectedAccount', - 'SelectTenantsStep.ts', - ); - - // Create async function to provide better loading UX and debugging experience - const getTenantQuickPickItems = async (): Promise => { - const loadStartTime = Date.now(); - const tenants = await this.getAvailableTenantsForAccount(context); - - // Add telemetry for tenant loading - context.telemetry.measurements.availableTenantsCount = tenants.length; - context.telemetry.measurements.tenantLoadingTimeMs = Date.now() - loadStartTime; - - // Initialize availableTenants map if not exists - if (!context.availableTenants) { - context.availableTenants = new Map(); - } - - // Store tenants for this account - context.availableTenants.set(selectedAccount.id, tenants); - - // Store all tenants for the selected account in context for ExecuteStep - context.allTenants = tenants; - - if (tenants.length === 0) { - void vscode.window.showWarningMessage( - l10n.t( - 'No tenants found for the selected account. Please try signing in again or selecting a different account.', - ), - ); - return []; - } - - // Create quick pick items - const tenantItems: TenantQuickPickItem[] = tenants.map((tenant) => { - const tenantId = tenant.tenantId || ''; - const displayName = tenant.displayName || tenantId; - - return { - label: displayName, - detail: tenantId, - description: tenant.defaultDomain ?? undefined, - iconPath: new vscode.ThemeIcon('organization'), - tenant, - }; - }); - - return tenantItems; - }; - - const selectedItems = await context.ui.showQuickPick(getTenantQuickPickItems(), { - stepName: 'selectTenants', - placeHolder: l10n.t('Select tenants to use'), - canPickMany: true, - matchOnDescription: true, - suppressPersistence: true, - loadingPlaceHolder: l10n.t('Loading Tenants…'), - isPickSelected: (pick) => { - const tenantPick = pick as TenantQuickPickItem; - - // Check if this tenant is currently selected (not filtered out) - if (tenantPick.tenant?.tenantId && selectedAccount?.id) { - return !isTenantFilteredOut(tenantPick.tenant.tenantId, selectedAccount.id); - } - - return false; - }, - }); - - // Extract selected tenants - context.selectedTenants = selectedItems.map((item) => - nonNullValue(item.tenant, 'item.tenant', 'SelectTenantsStep.ts'), - ); - - // Add telemetry for tenant selection - const totalTenants = context.allTenants?.length ?? 0; - context.telemetry.measurements.selectedTenantsCount = selectedItems.length; - context.telemetry.measurements.unselectedTenantsCount = totalTenants - selectedItems.length; - context.telemetry.properties.allTenantsSelected = (selectedItems.length === totalTenants).toString(); - context.telemetry.properties.noTenantsSelected = (selectedItems.length === 0).toString(); - } - - public shouldPrompt(context: CredentialsManagementWizardContext): boolean { - return !!context.selectedAccount && !context.shouldRestartWizard; - } - - private async getAvailableTenantsForAccount(context: CredentialsManagementWizardContext): Promise { - try { - const selectedAccount = nonNullValue( - context.selectedAccount, - 'context.selectedAccount', - 'SelectTenantsStep.ts', - ); - - // Get tenants for the specific account - const tenants = await context.azureSubscriptionProvider.getTenants(selectedAccount); - - return tenants.sort((a, b) => { - // Sort by display name if available, otherwise by tenant ID - const aName = a.displayName || a.tenantId || ''; - const bName = b.displayName || b.tenantId || ''; - return aName.localeCompare(bName); - }); - } catch (error) { - ext.outputChannel.appendLine( - l10n.t( - 'Failed to retrieve tenants for account: {0}', - error instanceof Error ? error.message : String(error), - ), - ); - return []; - } - } -} diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index 9c6e0b806..f546d7512 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -18,10 +18,9 @@ import { AccountActionsStep } from './AccountActionsStep'; import { type CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; import { ExecuteStep } from './ExecuteStep'; import { SelectAccountStep } from './SelectAccountStep'; -import { SelectTenantsStep } from './SelectTenantsStep'; /** - * Internal implementation of Azure credentials configuration. + * Internal implementation of Azure account management. */ async function configureAzureCredentialsInternal( context: IActionContext, @@ -34,21 +33,20 @@ async function configureAzureCredentialsInternal( do { try { - ext.outputChannel.appendLine(l10n.t('Starting Azure credentials configuration wizard')); + ext.outputChannel.appendLine(l10n.t('Starting Azure account management wizard')); // Create wizard context wizardContext = { ...context, selectedAccount: undefined, - selectedTenants: undefined, azureSubscriptionProvider, shouldRestartWizard: false, }; // Create and configure the wizard const wizard = new AzureWizard(wizardContext, { - title: l10n.t('Manage Azure Credentials'), - promptSteps: [new SelectAccountStep(), new AccountActionsStep(), new SelectTenantsStep()], + title: l10n.t('Manage Azure Accounts'), + promptSteps: [new SelectAccountStep(), new AccountActionsStep()], executeSteps: [new ExecuteStep()], }); @@ -66,7 +64,7 @@ async function configureAzureCredentialsInternal( if (error instanceof UserCancelledError) { // User cancelled context.telemetry.properties.credentialsManagementResult = 'Canceled'; - ext.outputChannel.appendLine(l10n.t('Azure credentials configuration was cancelled by user.')); + ext.outputChannel.appendLine(l10n.t('Azure account management was cancelled by user.')); return; } @@ -75,7 +73,7 @@ async function configureAzureCredentialsInternal( context.telemetry.properties.credentialsManagementError = error instanceof Error ? error.name : 'UnknownError'; const errorMessage = error instanceof Error ? error.message : String(error); - ext.outputChannel.appendLine(l10n.t('Azure credentials configuration failed: {0}', errorMessage)); + ext.outputChannel.appendLine(l10n.t('Azure account management failed: {0}', errorMessage)); void vscode.window.showErrorMessage(l10n.t('Failed to configure Azure credentials: {0}', errorMessage)); throw error; } @@ -92,7 +90,7 @@ async function configureAzureCredentialsInternal( * * @param context - The action context * @param azureSubscriptionProvider - The Azure subscription provider with filtering capabilities - * @param node - Optional tree node from which the configuration was initiated + * @param node - Optional tree node from which the account management was initiated */ export async function configureAzureCredentials( context: IActionContext, diff --git a/src/plugins/api-shared/azure/credentialsManagement/index.ts b/src/plugins/api-shared/azure/credentialsManagement/index.ts index 69e039c77..862b190e2 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/index.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/index.ts @@ -8,4 +8,3 @@ export { configureAzureCredentials } from './configureAzureCredentials'; export type { CredentialsManagementWizardContext } from './CredentialsManagementWizardContext'; export { ExecuteStep } from './ExecuteStep'; export { SelectAccountStep } from './SelectAccountStep'; -export { SelectTenantsStep } from './SelectTenantsStep'; From d61c5fd86905bea6b2f5081890375a379e308e26 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 26 Sep 2025 20:07:02 +0200 Subject: [PATCH 55/88] fix: wording --- .../azure/wizard/SelectSubscriptionStep.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 56a33ecbd..76b76da43 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -124,7 +124,7 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { - const configure = l10n.t('Yes, Configure Credentials'); + const configure = l10n.t('Yes, Manage Accounts'); const result = await window.showInformationMessage( l10n.t('No Azure Subscriptions Found'), { modal: true, detail: l10n.t( - 'To connect to Azure resources, you need to configure your Azure credentials and tenant access.\n\n' + - 'Would you like to configure your Azure credentials now?', + 'To connect to Azure resources, you need to sign in to Azure accounts.\n\n' + + 'Would you like to manage your Azure accounts now?', ), }, { title: configure }, @@ -199,11 +199,11 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { await window.showInformationMessage( - l10n.t('Credential Management Completed'), + l10n.t('Account Management Completed'), { modal: true, detail: l10n.t( - 'The credential management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.', + 'The account management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.', ), }, l10n.t('OK'), From efbeb4786deffa54685bdebaae0609f776446057 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 26 Sep 2025 20:19:37 +0200 Subject: [PATCH 56/88] chore: updated labels/titles to use title formatting --- l10n/bundle.l10n.json | 72 +++++++++++-------- .../SelectAccountStep.ts | 4 +- .../configureAzureCredentials.ts | 2 +- .../subscriptionFiltering/ExecuteStep.ts | 8 +-- .../InitializeFilteringStep.ts | 2 +- .../wizard/SelectTenantAndSubscriptionStep.ts | 4 +- .../discovery-wizard/SelectRUClusterStep.ts | 2 +- .../documentdb/DocumentDBResourceItem.ts | 2 +- .../discovery-wizard/SelectClusterStep.ts | 2 +- .../discovery-tree/vm/AzureVMResourceItem.ts | 2 +- 10 files changed, 58 insertions(+), 42 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 8596e7d9e..3139a1d5c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -6,6 +6,7 @@ "\"registerAzureUtilsExtensionVariables\" must be called before using the vscode-azext-azureutils package.": "\"registerAzureUtilsExtensionVariables\" must be called before using the vscode-azext-azureutils package.", "\"registerUIExtensionVariables\" must be called before using the vscode-azureextensionui package.": "\"registerUIExtensionVariables\" must be called before using the vscode-azureextensionui package.", "(recently used)": "(recently used)", + "{0} is currently being used for Azure service discovery": "{0} is currently being used for Azure service discovery", "{countMany} documents have been deleted.": "{countMany} documents have been deleted.", "{countOne} document has been deleted.": "{countOne} document has been deleted.", "{documentCount} documents exported…": "{documentCount} documents exported…", @@ -36,6 +37,7 @@ "A new connection will be added to your Connections View.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the exension settings.": "A new connection will be added to your Connections View.\nDo you want to continue?\n\nNote: You can disable these URL handling confirmations in the exension settings.", "A value is required to proceed.": "A value is required to proceed.", "Account information is incomplete.": "Account information is incomplete.", + "Account Management Completed": "Account Management Completed", "Add new document": "Add new document", "Advanced": "Advanced", "All available providers have been added already.": "All available providers have been added already.", @@ -45,10 +47,12 @@ "An item with id \"{0}\" already exists for workspace \"{1}\".": "An item with id \"{0}\" already exists for workspace \"{1}\".", "API version \"{0}\" for extension id \"{1}\" is no longer supported. Minimum version is \"{2}\".": "API version \"{0}\" for extension id \"{1}\" is no longer supported. Minimum version is \"{2}\".", "API: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\"": "API: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\"", + "Applying Azure discovery filters…": "Applying Azure discovery filters…", "Are you sure?": "Are you sure?", "Attempting to authenticate with \"{cluster}\"…": "Attempting to authenticate with \"{cluster}\"…", "Authenticate to connect with your DocumentDB cluster": "Authenticate to connect with your DocumentDB cluster", - "Authenticate to connect with your MongoDB cluster": "Authenticate to connect with your MongoDB cluster", + "Authenticate to Connect with Your DocumentDB Cluster": "Authenticate to Connect with Your DocumentDB Cluster", + "Authenticate to Connect with Your MongoDB Cluster": "Authenticate to Connect with Your MongoDB Cluster", "Authenticate using a username and password": "Authenticate using a username and password", "Authenticate using Microsoft Entra ID (Azure AD)": "Authenticate using Microsoft Entra ID (Azure AD)", "Authentication configuration is missing for \"{cluster}\".": "Authentication configuration is missing for \"{cluster}\".", @@ -56,13 +60,15 @@ "Authentication data (properties.connectionString) is missing for \"{cluster}\".": "Authentication data (properties.connectionString) is missing for \"{cluster}\".", "Authentication is required to run this action.": "Authentication is required to run this action.", "Authentication is required to use this migration provider.": "Authentication is required to use this migration provider.", + "Azure account management failed: {0}": "Azure account management failed: {0}", + "Azure account management was cancelled by user.": "Azure account management was cancelled by user.", + "Azure account management wizard completed.": "Azure account management wizard completed.", + "Azure accounts used for service discovery:": "Azure accounts used for service discovery:", "Azure Activity": "Azure Activity", "Azure Cosmos DB for MongoDB (RU)": "Azure Cosmos DB for MongoDB (RU)", "Azure Cosmos DB for MongoDB (RU) Emulator": "Azure Cosmos DB for MongoDB (RU) Emulator", "Azure Cosmos DB for MongoDB (vCore)": "Azure Cosmos DB for MongoDB (vCore)", - "Azure credentials configuration completed successfully.": "Azure credentials configuration completed successfully.", - "Azure credentials configuration failed: {0}": "Azure credentials configuration failed: {0}", - "Azure credentials configuration was cancelled by user.": "Azure credentials configuration was cancelled by user.", + "Azure discovery filters applied successfully.": "Azure discovery filters applied successfully.", "Azure Service Discovery": "Azure Service Discovery", "Azure Service Discovery for MongoDB RU": "Azure Service Discovery for MongoDB RU", "Azure sign-in completed successfully": "Azure sign-in completed successfully", @@ -74,6 +80,7 @@ "Azure VM: Connecting to \"{vmName}\" as \"{username}\"…": "Azure VM: Connecting to \"{vmName}\" as \"{username}\"…", "Azure VMs (DocumentDB)": "Azure VMs (DocumentDB)", "Back": "Back", + "Back to account selection": "Back to account selection", "Browse to {mongoExecutableFileName}": "Browse to {mongoExecutableFileName}", "Cancel": "Cancel", "Change page size": "Change page size", @@ -89,6 +96,7 @@ "Click here to retry": "Click here to retry", "Click here to update credentials": "Click here to update credentials", "Click to view resource": "Click to view resource", + "Close the account management wizard": "Close the account management wizard", "Cluster support unknown $(info)": "Cluster support unknown $(info)", "Collection name cannot begin with the system. prefix (Reserved for internal use).": "Collection name cannot begin with the system. prefix (Reserved for internal use).", "Collection name cannot contain .system.": "Collection name cannot contain .system.", @@ -96,8 +104,13 @@ "Collection name cannot contain the null character.": "Collection name cannot contain the null character.", "Collection name is required.": "Collection name is required.", "Collection names should begin with an underscore or a letter character.": "Collection names should begin with an underscore or a letter character.", + "Configure Azure Discovery Filters": "Configure Azure Discovery Filters", "Configure Azure VM Discovery Filters": "Configure Azure VM Discovery Filters", + "Configure Subscription Filter": "Configure Subscription Filter", + "Configure Tenant & Subscription Filters": "Configure Tenant & Subscription Filters", "Configure TLS/SSL Security": "Configure TLS/SSL Security", + "Configuring subscription filtering…": "Configuring subscription filtering…", + "Configuring tenant filtering…": "Configuring tenant filtering…", "Connect to a database": "Connect to a database", "Connected to \"{name}\"": "Connected to \"{name}\"", "Connected to the cluster \"{cluster}\".": "Connected to the cluster \"{cluster}\".", @@ -124,9 +137,6 @@ "Creating resource group \"{0}\" in location \"{1}\"...": "Creating resource group \"{0}\" in location \"{1}\"...", "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...": "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...", "Creating user assigned identity \"{0}\" in location \"{1}\"\"...": "Creating user assigned identity \"{0}\" in location \"{1}\"\"...", - "Credential Management Completed": "Credential Management Completed", - "Credential update completed.": "Credential update completed.", - "Credential update completed. If you don't see expected entries, use the optional \"Filter Entries…\" option to adjust your filters.": "Credential update completed. If you don't see expected entries, use the optional \"Filter Entries…\" option to adjust your filters.", "Credentials updated successfully.": "Credentials updated successfully.", "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}\"", @@ -192,6 +202,8 @@ "Executing the command in shell…": "Executing the command in shell…", "Execution timed out": "Execution timed out", "Execution timed out.": "Execution timed out.", + "Exit": "Exit", + "Exit without making changes": "Exit without making changes", "Expected a file name \"{0}\", but the selected filename is \"{1}\"": "Expected a file name \"{0}\", but the selected filename is \"{1}\"", "Expecting parentheses or quotes at \"{text}\"": "Expecting parentheses or quotes at \"{text}\"", "Export": "Export", @@ -223,14 +235,13 @@ "Failed to process URI: {0}": "Failed to process URI: {0}", "Failed to rename the connection.": "Failed to rename the connection.", "Failed to retrieve Azure accounts: {0}": "Failed to retrieve Azure accounts: {0}", - "Failed to retrieve tenants for account: {0}": "Failed to retrieve tenants for account: {0}", "Failed to save credentials for \"{cluster}\".": "Failed to save credentials for \"{cluster}\".", "Failed to save credentials.": "Failed to save credentials.", "Failed to sign in to Azure: {0}": "Failed to sign in to Azure: {0}", "Failed to store secrets for key {0}:": "Failed to store secrets for key {0}:", "Failed to update the connection.": "Failed to update the connection.", "Failed with code \"{0}\".": "Failed with code \"{0}\".", - "Filter Entries Now": "Filter Entries Now", + "Filter Tenants & Subscriptions (Mock)": "Filter Tenants & Subscriptions (Mock)", "Find Query": "Find Query", "Finished importing": "Finished importing", "Go back.": "Go back.", @@ -256,7 +267,7 @@ "Importing…": "Importing…", "Indexes": "Indexes", "Info from the webview: ": "Info from the webview: ", - "Initializing Credentials Management…": "Initializing Credentials Management…", + "Initializing filtering options…": "Initializing filtering options…", "Inserted {0} document(s). See output for more details.": "Inserted {0} document(s). See output for more details.", "Install Azure Account Extension...": "Install Azure Account Extension...", "Internal error: connectionString must be defined.": "Internal error: connectionString must be defined.", @@ -285,20 +296,21 @@ "Level up": "Level up", "Load More...": "Load More...", "Loading \"{0}\"...": "Loading \"{0}\"...", + "Loading Azure Accounts Used for Service Discovery…": "Loading Azure Accounts Used for Service Discovery…", "Loading cluster details for \"{cluster}\"": "Loading cluster details for \"{cluster}\"", - "Loading clusters…": "Loading clusters…", + "Loading Clusters…": "Loading Clusters…", "Loading Content": "Loading Content", "Loading document {num} of {countUri}": "Loading document {num} of {countUri}", "Loading documents…": "Loading documents…", "Loading migration actions…": "Loading migration actions…", "Loading resources...": "Loading resources...", - "Loading RU clusters…": "Loading RU clusters…", "Loading Subscriptions…": "Loading Subscriptions…", - "Loading Tenants…": "Loading Tenants…", + "Loading Tenant Filter Options…": "Loading Tenant Filter Options…", + "Loading Tenants and Subscription Data…": "Loading Tenants and Subscription Data…", "Loading Virtual Machines…": "Loading Virtual Machines…", "Loading...": "Loading...", "Location": "Location", - "Manage Azure Credentials": "Manage Azure Credentials", + "Manage Azure Accounts": "Manage Azure Accounts", "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.": "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.", "Mongo Shell connected.": "Mongo Shell connected.", "Mongo Shell Error: {error}": "Mongo Shell Error: {error}", @@ -328,8 +340,9 @@ "No scope was provided for the role assignment.": "No scope was provided for the role assignment.", "No session found for id {sessionId}": "No session found for id {sessionId}", "No subscriptions found": "No subscriptions found", - "No tenants found for the selected account. Please try signing in again or selecting a different account.": "No tenants found for the selected account. Please try signing in again or selecting a different account.", - "No tenants selected. Azure discovery will be filtered to exclude all results for this account.": "No tenants selected. Azure discovery will be filtered to exclude all results for this account.", + "No subscriptions found for the selected tenants. Please adjust your tenant selection or check your Azure permissions.": "No subscriptions found for the selected tenants. Please adjust your tenant selection or check your Azure permissions.", + "No tenants found. Please try signing in again or check your Azure permissions.": "No tenants found. Please try signing in again or check your Azure permissions.", + "No tenants selected. Azure discovery will be filtered to exclude all tenant results.": "No tenants selected. Azure discovery will be filtered to exclude all tenant results.", "Not connected to any MongoDB database.": "Not connected to any MongoDB database.", "Note: This confirmation type can be configured in the extension settings.": "Note: This confirmation type can be configured in the extension settings.", "Note: You can disable these URL handling confirmations in the extension settings.": "Note: You can disable these URL handling confirmations in the extension settings.", @@ -358,7 +371,7 @@ "Provider \"{0}\" does not have resource type \"{1}\".": "Provider \"{0}\" does not have resource type \"{1}\".", "Refresh": "Refresh", "Refresh current view": "Refresh current view", - "Refreshing Azure discovery tree...": "Refreshing Azure discovery tree...", + "Refreshing Azure discovery tree…": "Refreshing Azure discovery tree…", "Registering Providers...": "Registering Providers...", "Reload original document from the database": "Reload original document from the database", "Reload Window": "Reload Window", @@ -366,7 +379,8 @@ "Rename Connection": "Rename Connection", "Report an issue": "Report an issue", "Resource group \"{0}\" already exists in subscription \"{1}\".": "Resource group \"{0}\" already exists in subscription \"{1}\".", - "Restarting wizard after account sign-in...": "Restarting wizard after account sign-in...", + "Restarting wizard after account sign-in…": "Restarting wizard after account sign-in…", + "Return to the account list": "Return to the account list", "Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".", "Revisit connection details and try again.": "Revisit connection details and try again.", "Role assignment \"{0}\" created for the {2} resource \"{1}\".": "Role assignment \"{0}\" created for the {2} resource \"{1}\".", @@ -378,7 +392,6 @@ "Save document to the database": "Save document to the database", "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 Azure credentials configuration for account: {0}": "Saving Azure credentials configuration for account: {0}", "Saving credentials for \"{clusterName}\"…": "Saving credentials for \"{clusterName}\"…", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", @@ -386,22 +399,23 @@ "Select a workspace folder": "Select a workspace folder", "Select an authentication method": "Select an authentication method", "Select an authentication method for \"{resourceName}\"": "Select an authentication method for \"{resourceName}\"", - "Select an Azure account to choose which tenants to use": "Select an Azure account to choose which tenants to use", + "Select Cluster (Mock - Single Tenant)": "Select Cluster (Mock - Single Tenant)", "Select Existing": "Select Existing", "Select resource": "Select resource", "Select subscription": "Select subscription", - "Select Subscriptions": "Select Subscriptions", "Select Subscriptions to Display": "Select Subscriptions to Display", + "Select subscriptions to include in service discovery": "Select subscriptions to include in service discovery", "Select Subscriptions...": "Select Subscriptions...", - "Select tenants to use": "Select tenants to use", + "Select tenants to include in subscription discovery": "Select tenants to include in subscription discovery", "Select the error you would like to report": "Select the error you would like to report", "Select the local connection type…": "Select the local connection type…", + "Selected subscriptions: {0}": "Selected subscriptions: {0}", "Selected tenants: {0}": "Selected tenants: {0}", "Service Discovery": "Service Discovery", "Sign In": "Sign In", "Sign in to Azure to continue…": "Sign in to Azure to continue…", "Sign in to Azure...": "Sign in to Azure...", - "Sign in to other Azure accounts or tenants to access more subscriptions": "Sign in to other Azure accounts or tenants to access more subscriptions", + "Sign in to other Azure accounts to access more subscriptions": "Sign in to other Azure accounts to access more subscriptions", "Sign in with a different account…": "Sign in with a different account…", "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.": "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.", "Skip for now": "Skip for now", @@ -409,13 +423,14 @@ "Some items could not be displayed": "Some items could not be displayed", "Specified character lengths should be 1 character or greater.": "Specified character lengths should be 1 character or greater.", "Started executable: \"{command}\". Connecting to host…": "Started executable: \"{command}\". Connecting to host…", - "Starting Azure credentials configuration wizard": "Starting Azure credentials configuration wizard", - "Starting Azure sign-in process...": "Starting Azure sign-in process...", + "Starting Azure account management wizard": "Starting Azure account management wizard", + "Starting Azure sign-in process…": "Starting Azure sign-in process…", "Starting executable: \"{command}\"": "Starting executable: \"{command}\"", "Starts with mongodb:// or mongodb+srv://": "Starts with mongodb:// or mongodb+srv://", "subscription": "subscription", "Subscription ID: {0}": "Subscription ID: {0}", - "Successfully configured Azure tenant filtering. Selected {0} tenant(s) for account {1}": "Successfully configured Azure tenant filtering. Selected {0} tenant(s) for account {1}", + "Successfully configured subscription filtering. Selected {0} subscription(s)": "Successfully configured subscription filtering. Selected {0} subscription(s)", + "Successfully configured tenant filtering. Selected {0} tenant(s)": "Successfully configured tenant filtering. Selected {0} tenant(s)", "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}\".", @@ -430,13 +445,13 @@ "The \"{databaseId}\" database has been deleted.": "The \"{databaseId}\" database has been deleted.", "The \"{name}\" database has been created.": "The \"{name}\" database has been created.", "The \"{newCollectionName}\" collection has been created.": "The \"{newCollectionName}\" collection has been created.", + "The account management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.": "The account management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.", "The collection \"{0}\" already exists in the database \"{1}\".": "The collection \"{0}\" already exists in the database \"{1}\".", "The collection \"{collectionId}\" has been deleted.": "The collection \"{collectionId}\" has been deleted.", "The connection string has been copied to the clipboard": "The connection string has been copied to the clipboard", "The connection string is required.": "The connection string is required.", "The connection will now be opened in the Connections View.": "The connection will now be opened in the Connections View.", "The connection with the name \"{0}\" already exists.": "The connection with the name \"{0}\" already exists.", - "The credential management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.": "The credential management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.", "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.": "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.", "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".": "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".", "The default port: {defaultPort}": "The default port: {defaultPort}", @@ -512,6 +527,7 @@ "Validate": "Validate", "View in Marketplace": "View in Marketplace", "View selected document": "View selected document", + "Viewing Azure account information for: {0}": "Viewing Azure account information for: {0}", "Waiting for Azure sign-in...": "Waiting for Azure sign-in...", "WARNING: Cannot create resource group \"{0}\" because the selected subscription is a concierge subscription. Using resource group \"{1}\" instead.": "WARNING: Cannot create resource group \"{0}\" because the selected subscription is a concierge subscription. Using resource group \"{1}\" instead.", "WARNING: Provider \"{0}\" does not support location \"{1}\". Using \"{2}\" instead.": "WARNING: Provider \"{0}\" does not support location \"{1}\". Using \"{2}\" instead.", @@ -522,8 +538,8 @@ "Would you like to open the Collection View?": "Would you like to open the Collection View?", "Write error: {0}": "Write error: {0}", "Yes": "Yes", - "Yes, Configure Credentials": "Yes, Configure Credentials", "Yes, continue": "Yes, continue", + "Yes, Manage Accounts": "Yes, Manage Accounts", "Yes, open Collection View": "Yes, open Collection View", "Yes, open connection": "Yes, open connection", "Yes, save my credentials": "Yes, save my credentials", diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index 5c0f5002c..b34b4c787 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -78,7 +78,7 @@ export class SelectAccountStep extends AzureWizardPromptStep { try { - ext.outputChannel.appendLine(l10n.t('Starting Azure sign-in process...')); + ext.outputChannel.appendLine(l10n.t('Starting Azure sign-in process…')); const success = await context.azureSubscriptionProvider.signIn(); if (success) { ext.outputChannel.appendLine(l10n.t('Azure sign-in completed successfully')); diff --git a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts index f546d7512..8ad7f4c81 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/configureAzureCredentials.ts @@ -56,7 +56,7 @@ async function configureAzureCredentialsInternal( if (wizardContext.shouldRestartWizard) { context.telemetry.properties.wizardRestarted = 'true'; - ext.outputChannel.appendLine(l10n.t('Restarting wizard after account sign-in...')); + ext.outputChannel.appendLine(l10n.t('Restarting wizard after account sign-in…')); } } catch (error) { context.telemetry.measurements.credentialsManagementDurationMs = Date.now() - startTime; diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts index 33a812483..3794edf1a 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts @@ -19,7 +19,7 @@ export class ExecuteStep extends AzureWizardExecuteStep public async execute(context: FilteringWizardContext): Promise { const executeStartTime = Date.now(); - ext.outputChannel.appendLine(l10n.t('Applying Azure discovery filters...')); + ext.outputChannel.appendLine(l10n.t('Applying Azure discovery filters…')); // Apply tenant filtering if tenants were selected if (context.selectedTenants && context.availableTenants && context.availableTenants.length > 0) { @@ -31,7 +31,7 @@ export class ExecuteStep extends AzureWizardExecuteStep await this.applySubscriptionFiltering(context); } - ext.outputChannel.appendLine(l10n.t('Refreshing Azure discovery tree...')); + ext.outputChannel.appendLine(l10n.t('Refreshing Azure discovery tree…')); ext.discoveryBranchDataProvider.refresh(); ext.outputChannel.appendLine(l10n.t('Azure discovery filters applied successfully.')); @@ -45,7 +45,7 @@ export class ExecuteStep extends AzureWizardExecuteStep const selectedTenants = context.selectedTenants || []; const allTenants = context.availableTenants || []; - ext.outputChannel.appendLine(l10n.t('Configuring tenant filtering...')); + ext.outputChannel.appendLine(l10n.t('Configuring tenant filtering…')); // Get all unique account IDs from subscriptions to apply tenant filtering per account const accountIds = new Set(); @@ -98,7 +98,7 @@ export class ExecuteStep extends AzureWizardExecuteStep private async applySubscriptionFiltering(context: FilteringWizardContext): Promise { const selectedSubscriptions = context.selectedSubscriptions || []; - ext.outputChannel.appendLine(l10n.t('Configuring subscription filtering...')); + ext.outputChannel.appendLine(l10n.t('Configuring subscription filtering…')); // Convert subscriptions to the format expected by setSelectedSubscriptionIds const selectedIds = selectedSubscriptions.map( diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts index 6629d23b0..ef1422222 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts @@ -31,7 +31,7 @@ export class InitializeFilteringStep extends AzureWizardPromptStep { const wizard = new AzureWizard(wizardContext, { promptSteps: [new ChooseAuthMethodStep(), new ProvideUserNameStep(), new ProvidePasswordStep()], - title: l10n.t('Authenticate to connect with your DocumentDB cluster'), + title: l10n.t('Authenticate to Connect with Your DocumentDB Cluster'), showLoadingPrompt: true, }); diff --git a/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts b/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts index d63e31c6a..4ccd5e2ec 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-wizard/SelectClusterStep.ts @@ -62,7 +62,7 @@ export class SelectClusterStep extends AzureWizardPromptStep { const wizard = new AzureWizard(wizardContext, { promptSteps: [new ProvideUserNameStep(), new ProvidePasswordStep()], - title: l10n.t('Authenticate to connect with your MongoDB cluster'), + title: l10n.t('Authenticate to Connect with Your DocumentDB Cluster'), showLoadingPrompt: true, }); From 4fe696d905f5cbeeb17ca5bb2863b0f19f06f205 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 26 Sep 2025 23:49:55 +0200 Subject: [PATCH 57/88] fix: exit wizard when a new account is added --- l10n/bundle.l10n.json | 3 +- .../CredentialsManagementWizardContext.ts | 3 - .../credentialsManagement/ExecuteStep.ts | 2 +- .../SelectAccountStep.ts | 9 +-- .../configureAzureCredentials.ts | 79 +++++++++---------- 5 files changed, 44 insertions(+), 52 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 3139a1d5c..efa4e8d1b 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -52,7 +52,6 @@ "Attempting to authenticate with \"{cluster}\"…": "Attempting to authenticate with \"{cluster}\"…", "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 to Connect with Your MongoDB Cluster": "Authenticate to Connect with Your MongoDB Cluster", "Authenticate using a username and password": "Authenticate using a username and password", "Authenticate using Microsoft Entra ID (Azure AD)": "Authenticate using Microsoft Entra ID (Azure AD)", "Authentication configuration is missing for \"{cluster}\".": "Authentication configuration is missing for \"{cluster}\".", @@ -60,6 +59,7 @@ "Authentication data (properties.connectionString) is missing for \"{cluster}\".": "Authentication data (properties.connectionString) is missing for \"{cluster}\".", "Authentication is required to run this action.": "Authentication is required to run this action.", "Authentication is required to use this migration provider.": "Authentication is required to use this migration provider.", + "Azure account added successfully.": "Azure account added successfully.", "Azure account management failed: {0}": "Azure account management failed: {0}", "Azure account management was cancelled by user.": "Azure account management was cancelled by user.", "Azure account management wizard completed.": "Azure account management wizard completed.", @@ -379,7 +379,6 @@ "Rename Connection": "Rename Connection", "Report an issue": "Report an issue", "Resource group \"{0}\" already exists in subscription \"{1}\".": "Resource group \"{0}\" already exists in subscription \"{1}\".", - "Restarting wizard after account sign-in…": "Restarting wizard after account sign-in…", "Return to the account list": "Return to the account list", "Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".", "Revisit connection details and try again.": "Revisit connection details and try again.", diff --git a/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts index ddf750b20..514ff68ba 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/CredentialsManagementWizardContext.ts @@ -16,7 +16,4 @@ export interface CredentialsManagementWizardContext extends IActionContext { // Available options availableAccounts?: vscode.AuthenticationSessionAccountInformation[]; - - // State tracking - shouldRestartWizard?: boolean; } diff --git a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts index 3f31443d9..b28d1d4d4 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/ExecuteStep.ts @@ -29,6 +29,6 @@ export class ExecuteStep extends AzureWizardExecuteStep Date: Sat, 27 Sep 2025 00:19:45 +0200 Subject: [PATCH 58/88] fix: account step tweak --- .../azure/credentialsManagement/SelectAccountStep.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index 6278b527d..78be00446 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -32,7 +32,6 @@ export class SelectAccountStep extends AzureWizardPromptStep ({ label: account.label, - detail: account.id, iconPath: new vscode.ThemeIcon('account'), account, })); @@ -75,7 +74,7 @@ export class SelectAccountStep extends AzureWizardPromptStep Date: Sat, 27 Sep 2025 00:34:54 +0200 Subject: [PATCH 59/88] fix: empty subscription filters weren't persisted --- .../api-shared/azure/subscriptionFiltering/ExecuteStep.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts index 3794edf1a..c6779959e 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/ExecuteStep.ts @@ -26,10 +26,7 @@ export class ExecuteStep extends AzureWizardExecuteStep await this.applyTenantFiltering(context); } - // Apply subscription filtering if subscriptions were selected - if (context.selectedSubscriptions && context.selectedSubscriptions.length > 0) { - await this.applySubscriptionFiltering(context); - } + await this.applySubscriptionFiltering(context); ext.outputChannel.appendLine(l10n.t('Refreshing Azure discovery tree…')); ext.discoveryBranchDataProvider.refresh(); From 6696e5ba7290455672d962d40b8c8072ffc329f4 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Sat, 27 Sep 2025 00:35:09 +0200 Subject: [PATCH 60/88] l10n --- l10n/bundle.l10n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index efa4e8d1b..b7ca0a677 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -63,7 +63,7 @@ "Azure account management failed: {0}": "Azure account management failed: {0}", "Azure account management was cancelled by user.": "Azure account management was cancelled by user.", "Azure account management wizard completed.": "Azure account management wizard completed.", - "Azure accounts used for service discovery:": "Azure accounts used for service discovery:", + "Azure accounts used for service discovery": "Azure accounts used for service discovery", "Azure Activity": "Azure Activity", "Azure Cosmos DB for MongoDB (RU)": "Azure Cosmos DB for MongoDB (RU)", "Azure Cosmos DB for MongoDB (RU) Emulator": "Azure Cosmos DB for MongoDB (RU) Emulator", From 7f0860853b76dd336eddf549b1bd5b4fa9ac8a65 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Sat, 27 Sep 2025 00:50:43 +0200 Subject: [PATCH 61/88] fix: handle empty subscription filters better --- .../azure/AzureSubscriptionProviderWithFilters.ts | 8 ++++---- .../subscriptionFilteringHelpers.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts index 22a3703a4..f10fb19a2 100644 --- a/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts +++ b/src/plugins/api-shared/azure/AzureSubscriptionProviderWithFilters.ts @@ -23,13 +23,13 @@ export class AzureSubscriptionProviderWithFilters extends VSCodeAzureSubscriptio private getTenantAndSubscriptionFilters(): string[] { // Try the Azure Resource Groups config first const config = vscode.workspace.getConfiguration('azureResourceGroups'); - let fullSubscriptionIds = config.get('selectedSubscriptions', []); + let fullSubscriptionIds = config.get('selectedSubscriptions'); - // If nothing found there, try our fallback storage - if (fullSubscriptionIds.length === 0) { + // If no configuration found (undefined), try our fallback storage + if (fullSubscriptionIds === undefined) { fullSubscriptionIds = ext.context.globalState.get('azure-discovery.selectedSubscriptions', []); } else { - // Sync to our fallback storage if primary storage had data + // Sync to our fallback storage if primary storage had data (even if empty array) void ext.context.globalState.update('azure-discovery.selectedSubscriptions', fullSubscriptionIds); } return fullSubscriptionIds; diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts index 2d1d12c87..42df9bf2c 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts @@ -39,10 +39,10 @@ import { ext } from '../../../../extensionVariables'; export function getSelectedSubscriptionIds(): string[] { // Try the Azure Resource Groups config first (primary storage) const azureResourcesConfig = vscode.workspace.getConfiguration('azureResourceGroups'); - const primarySubscriptionIds = azureResourcesConfig.get('selectedSubscriptions', []); + const primarySubscriptionIds = azureResourcesConfig.get('selectedSubscriptions'); - // If nothing found in primary storage, try our fallback storage - if (primarySubscriptionIds.length === 0) { + // If no configuration found (undefined), try our fallback storage + if (primarySubscriptionIds === undefined) { const fallbackSubscriptionIds = ext.context.globalState.get( 'azure-discovery.selectedSubscriptions', [], @@ -50,8 +50,8 @@ export function getSelectedSubscriptionIds(): string[] { return fallbackSubscriptionIds.map((id) => id.split('/')[1]); } - // Sync to our fallback storage if primary storage had data - // This ensures we maintain a copy if Azure Resources extension is later removed + // Sync from primary storage to fallback storage (even if empty array) + // This ensures we maintain a backup copy in case the Azure Resources extension goes down later void ext.context.globalState.update('azure-discovery.selectedSubscriptions', primarySubscriptionIds); return primarySubscriptionIds.map((id) => id.split('/')[1]); From 10e68dc7df7a485ea87c808b6203dd12d7318e03 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Sat, 27 Sep 2025 01:00:13 +0200 Subject: [PATCH 62/88] chore: refactore / raname files --- .../filterProviderContent.ts | 0 src/documentdb/ClustersExtension.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/commands/{filterProviderContent => discoveryService.filterProviderContent}/filterProviderContent.ts (100%) diff --git a/src/commands/filterProviderContent/filterProviderContent.ts b/src/commands/discoveryService.filterProviderContent/filterProviderContent.ts similarity index 100% rename from src/commands/filterProviderContent/filterProviderContent.ts rename to src/commands/discoveryService.filterProviderContent/filterProviderContent.ts diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index c963827c6..545060e60 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -27,9 +27,9 @@ import { createAzureDatabase } from '../commands/createDatabase/createDatabase'; import { createMongoDocument } from '../commands/createDocument/createDocument'; import { deleteCollection } from '../commands/deleteCollection/deleteCollection'; import { deleteAzureDatabase } from '../commands/deleteDatabase/deleteDatabase'; +import { filterProviderContent } from '../commands/discoveryService.filterProviderContent/filterProviderContent'; import { manageCredentials } from '../commands/discoveryService.manageCredentials/manageCredentials'; import { exportEntireCollection, exportQueryResults } from '../commands/exportDocuments/exportDocuments'; -import { filterProviderContent } from '../commands/filterProviderContent/filterProviderContent'; import { importDocuments } from '../commands/importDocuments/importDocuments'; import { launchShell } from '../commands/launchShell/launchShell'; import { learnMoreAboutServiceProvider } from '../commands/learnMoreAboutServiceProvider/learnMoreAboutServiceProvider'; From 6d88b4ebe47d0edde2efbb2ecad1305a70b0dc9d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Sat, 27 Sep 2025 01:13:40 +0200 Subject: [PATCH 63/88] fix: removed duplicate error messages --- .../api-shared/azure/credentialsManagement/SelectAccountStep.ts | 1 - .../azure/credentialsManagement/configureAzureCredentials.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index 78be00446..b91599ed9 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -142,7 +142,6 @@ export class SelectAccountStep extends AzureWizardPromptStep Date: Sat, 27 Sep 2025 01:15:10 +0200 Subject: [PATCH 64/88] l10n --- l10n/bundle.l10n.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index b7ca0a677..712bccf4c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -215,7 +215,6 @@ "Exporting…": "Exporting…", "Extension dependency with id \"{0}\" must be updated.": "Extension dependency with id \"{0}\" must be updated.", "Failed to access Azure Databases VS Code Extension storage for migration: {error}": "Failed to access Azure Databases VS Code Extension storage for migration: {error}", - "Failed to configure Azure credentials: {0}": "Failed to configure Azure credentials: {0}", "Failed to connect to \"{cluster}\"": "Failed to connect to \"{cluster}\"", "Failed to connect to VM \"{vmName}\"": "Failed to connect to VM \"{vmName}\"", "Failed to create Azure management clients: {0}": "Failed to create Azure management clients: {0}", @@ -237,7 +236,6 @@ "Failed to retrieve Azure accounts: {0}": "Failed to retrieve Azure accounts: {0}", "Failed to save credentials for \"{cluster}\".": "Failed to save credentials for \"{cluster}\".", "Failed to save credentials.": "Failed to save credentials.", - "Failed to sign in to Azure: {0}": "Failed to sign in to Azure: {0}", "Failed to store secrets for key {0}:": "Failed to store secrets for key {0}:", "Failed to update the connection.": "Failed to update the connection.", "Failed with code \"{0}\".": "Failed with code \"{0}\".", From e634d5a8e95a4b165ba678f96bbae978819ec23a Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 29 Sep 2025 10:35:06 +0200 Subject: [PATCH 65/88] chore: removed unused code --- .../wizard/SelectTenantAndSubscriptionStep.ts | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts diff --git a/src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts deleted file mode 100644 index 82e2441eb..000000000 --- a/src/plugins/api-shared/azure/wizard/SelectTenantAndSubscriptionStep.ts +++ /dev/null @@ -1,122 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * REFERENCE IMPLEMENTATION - Mock FilteringInitializeStep - * - * This file demonstrates advanced UX patterns for Azure service discovery: - * - Loading states with loadingPlaceHolder - * - Exception-based flow control for seamless wizard transitions - * - Dynamic subwizard creation based on initialization results - * - Smart routing (single tenant → direct subscription, multiple → tenant selection) - * - * Key UX Features: - * - 5-second initialization with loading animation - * - Automatic progression without user interaction - * - Context-aware subwizard selection - * - Clean exception-driven flow control - * - * This implementation is kept as a reference for future filtering initialization steps. - */ - -import { AzureWizardPromptStep, type IWizardOptions } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import { type QuickPickItem } from 'vscode'; -import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; -import { SelectClusterStep } from '../../../service-azure-mongo-vcore/discovery-wizard/SelectClusterStep'; -import { SelectSubscriptionStep } from './SelectSubscriptionStep'; - -/** - * Custom error to signal that initialization has completed and wizard should proceed to subwizard - */ -class InitializationCompleteError extends Error { - constructor(message: string = 'Initialization completed successfully') { - super(message); - this.name = 'InitializationCompleteError'; - } -} - -/** - * Mock step to demonstrate the FilteringInitializeStep UX pattern with fake delay - */ -export class SelectTenantAndSubscriptionStep extends AzureWizardPromptStep { - public async prompt(context: NewConnectionWizardContext): Promise { - try { - // Use QuickPick with loading state for unified UX demonstration - await context.ui.showQuickPick(this.mockInitializeFilteringData(context), { - placeHolder: l10n.t('Initializing filtering options…'), - loadingPlaceHolder: l10n.t('Loading Tenants and Subscription Data…'), - suppressPersistence: true, - }); - } catch (error) { - // Initialization completed - this is expected behavior - // The exception signals that initialization is done and we should proceed to subwizard - if (error instanceof InitializationCompleteError) { - // Mock: Set fake selected subscription for the rest of the wizard to work - context.properties.selectedSubscription = { - subscriptionId: 'mock-subscription-id', - displayName: 'Mock Subscription', - }; - return; // Proceed to getSubWizard - } - // Re-throw any other errors - throw error; - } - } - - private async mockInitializeFilteringData(context: NewConnectionWizardContext): Promise { - // Mock: Add fake 5-second delay to simulate tenant/subscription loading - await new Promise((resolve) => setTimeout(resolve, 5000)); - - // Mock: Simulate tenant discovery logic - const mockTenantCount = Math.floor(Math.random() * 3) + 1; // 1-3 tenants - - context.telemetry.properties.mockTenantCount = mockTenantCount.toString(); - - if (mockTenantCount === 1) { - // Single tenant: simulate auto-selection and subscription pre-loading - context.telemetry.properties.mockFlow = 'singleTenant'; - // Simulate additional subscription loading delay - await new Promise((resolve) => setTimeout(resolve, 2000)); - } else { - // Multi-tenant: simulate tenant discovery - context.telemetry.properties.mockFlow = 'multiTenant'; - } - - // Throw exception to signal initialization completion and auto-proceed to subwizard - throw new InitializationCompleteError('Tenant and subscription initialization completed'); - } - - // eslint-disable-next-line @typescript-eslint/require-await - public async getSubWizard( - context: NewConnectionWizardContext, - ): Promise> { - const mockTenantCount = parseInt((context.telemetry.properties.mockTenantCount as string) || '1'); - - if (mockTenantCount > 1) { - // Multi-tenant: show both tenant and subscription selection - return { - title: l10n.t('Filter Tenants & Subscriptions (Mock)'), - promptSteps: [ - // Mock tenant selection step (using existing subscription step as placeholder) - new SelectSubscriptionStep(), - new SelectClusterStep(), - ], - executeSteps: [], - }; - } else { - // Single tenant: skip directly to cluster selection - return { - title: l10n.t('Select Cluster (Mock - Single Tenant)'), - promptSteps: [new SelectClusterStep()], - executeSteps: [], - }; - } - } - - public shouldPrompt(): boolean { - return true; - } -} From ffa51526874d46361813845b6d7a3b5f5c435f31 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 29 Sep 2025 11:19:03 +0200 Subject: [PATCH 66/88] feat: provide tenantId when entraid + connection string --- l10n/bundle.l10n.json | 12 +- .../newConnection/PromptConnectionModeStep.ts | 2 + .../newConnection/PromptTenantStep.ts | 208 ++++++++++++++++++ 3 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 src/commands/newConnection/PromptTenantStep.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 712bccf4c..5ea91a91c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -165,6 +165,7 @@ "Don't Ask Again": "Don't Ask Again", "Don't upload": "Don't upload", "Don't warn again": "Don't warn again", + "e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012": "e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012", "e.g., DocumentDB, Environment, Project": "e.g., DocumentDB, Environment, Project", "Edit selected document": "Edit selected document", "Element with id of {rootId} not found.": "Element with id of {rootId} not found.", @@ -180,6 +181,7 @@ "Enter the password for {experience}": "Enter the password for {experience}", "Enter the port number": "Enter the port number", "Enter the port number your DocumentDB uses. The default port: {defaultPort}.": "Enter the port number your DocumentDB uses. The default port: {defaultPort}.", + "Enter the tenant ID (GUID)": "Enter the tenant ID (GUID)", "Enter the username": "Enter the username", "Enter the username for {experience}": "Enter the username for {experience}", "Entra ID for Azure Cosmos DB for MongoDB (vCore)": "Entra ID for Azure Cosmos DB for MongoDB (vCore)", @@ -239,7 +241,6 @@ "Failed to store secrets for key {0}:": "Failed to store secrets for key {0}:", "Failed to update the connection.": "Failed to update the connection.", "Failed with code \"{0}\".": "Failed with code \"{0}\".", - "Filter Tenants & Subscriptions (Mock)": "Filter Tenants & Subscriptions (Mock)", "Find Query": "Find Query", "Finished importing": "Finished importing", "Go back.": "Go back.", @@ -265,7 +266,6 @@ "Importing…": "Importing…", "Indexes": "Indexes", "Info from the webview: ": "Info from the webview: ", - "Initializing filtering options…": "Initializing filtering options…", "Inserted {0} document(s). See output for more details.": "Inserted {0} document(s). See output for more details.", "Install Azure Account Extension...": "Install Azure Account Extension...", "Internal error: connectionString must be defined.": "Internal error: connectionString must be defined.", @@ -305,10 +305,12 @@ "Loading Subscriptions…": "Loading Subscriptions…", "Loading Tenant Filter Options…": "Loading Tenant Filter Options…", "Loading Tenants and Subscription Data…": "Loading Tenants and Subscription Data…", + "Loading Tenants…": "Loading Tenants…", "Loading Virtual Machines…": "Loading Virtual Machines…", "Loading...": "Loading...", "Location": "Location", "Manage Azure Accounts": "Manage Azure Accounts", + "Manually enter a custom tenant ID (GUID)": "Manually enter a custom tenant ID (GUID)", "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.": "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.", "Mongo Shell connected.": "Mongo Shell connected.", "Mongo Shell Error: {error}": "Mongo Shell Error: {error}", @@ -356,6 +358,7 @@ "Please connect to a MongoDB database before running a Scrapbook command.": "Please connect to a MongoDB database before running a Scrapbook command.", "Please edit the connection string.": "Please edit the connection string.", "Please enter a new connection name.": "Please enter a new connection name.", + "Please enter a valid tenant ID in GUID format (e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012)": "Please enter a valid tenant ID in GUID format (e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012)", "Please enter the password for the user \"{username}\"": "Please enter the password for the user \"{username}\"", "Please enter the username": "Please enter the username", "Please enter the word \"{expectedConfirmationWord}\" to confirm the operation.": "Please enter the word \"{expectedConfirmationWord}\" to confirm the operation.", @@ -393,10 +396,10 @@ "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", "Select a location for new resources.": "Select a location for new resources.", + "Select a tenant for Microsoft Entra ID authentication": "Select a tenant for Microsoft Entra ID authentication", "Select a workspace folder": "Select a workspace folder", "Select an authentication method": "Select an authentication method", "Select an authentication method for \"{resourceName}\"": "Select an authentication method for \"{resourceName}\"", - "Select Cluster (Mock - Single Tenant)": "Select Cluster (Mock - Single Tenant)", "Select Existing": "Select Existing", "Select resource": "Select resource", "Select subscription": "Select subscription", @@ -413,6 +416,7 @@ "Sign in to Azure to continue…": "Sign in to Azure to continue…", "Sign in to Azure...": "Sign in to Azure...", "Sign in to other Azure accounts to access more subscriptions": "Sign in to other Azure accounts to access more subscriptions", + "Sign in to other Azure accounts to access more tenants": "Sign in to other Azure accounts to access more tenants", "Sign in with a different account…": "Sign in with a different account…", "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.": "Signing out programmatically is not supported. You must sign out by selecting the account in the Accounts menu and choosing Sign Out.", "Skip for now": "Skip for now", @@ -437,12 +441,14 @@ "Tag can only contain alphanumeric characters, underscores, periods, and hyphens.": "Tag can only contain alphanumeric characters, underscores, periods, and hyphens.", "Tag cannot be empty.": "Tag cannot be empty.", "Tag cannot be longer than 256 characters.": "Tag cannot be longer than 256 characters.", + "Tenant ID cannot be empty": "Tenant ID cannot be empty", "Tenant ID: {0}": "Tenant ID: {0}", "Tenant Name: {0}": "Tenant Name: {0}", "The \"{databaseId}\" database has been deleted.": "The \"{databaseId}\" database has been deleted.", "The \"{name}\" database has been created.": "The \"{name}\" database has been created.", "The \"{newCollectionName}\" collection has been created.": "The \"{newCollectionName}\" collection has been created.", "The account management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.": "The account management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.", + "The account management flow has completed.\n\nPlease try the connection flow again to see your available tenants.": "The account management flow has completed.\n\nPlease try the connection flow again to see your available tenants.", "The collection \"{0}\" already exists in the database \"{1}\".": "The collection \"{0}\" already exists in the database \"{1}\".", "The collection \"{collectionId}\" has been deleted.": "The collection \"{collectionId}\" has been deleted.", "The connection string has been copied to the clipboard": "The connection string has been copied to the clipboard", diff --git a/src/commands/newConnection/PromptConnectionModeStep.ts b/src/commands/newConnection/PromptConnectionModeStep.ts index 0a8a059c9..149d4ebd0 100644 --- a/src/commands/newConnection/PromptConnectionModeStep.ts +++ b/src/commands/newConnection/PromptConnectionModeStep.ts @@ -12,6 +12,7 @@ import { PromptAuthMethodStep } from './PromptAuthMethodStep'; import { PromptConnectionStringStep } from './PromptConnectionStringStep'; import { PromptPasswordStep } from './PromptPasswordStep'; import { PromptServiceDiscoveryStep } from './PromptServiceDiscoveryStep'; +import { PromptTenantStep } from './PromptTenantStep'; import { PromptUsernameStep } from './PromptUsernameStep'; export class PromptConnectionModeStep extends AzureWizardPromptStep { @@ -64,6 +65,7 @@ export class PromptConnectionModeStep extends AzureWizardPromptStep { + public async prompt(context: NewConnectionWizardContext): Promise { + // Create async function to provide better loading UX and debugging experience + const tenantItemsPromise = async (): Promise => { + // Load available tenants from Azure subscription provider + const tenants = await this.getAvailableTenants(context); + context.telemetry.measurements.availableTenantsCount = tenants.length; + + // Create quick pick items + const tenantItems: TenantQuickPickItem[] = [ + { + label: l10n.t('Manually enter a custom tenant ID (GUID)'), + iconPath: new vscode.ThemeIcon('edit'), + isCustomOption: true, + alwaysShow: true, + }, + { + label: l10n.t('Sign in to other Azure accounts to access more tenants'), + iconPath: new vscode.ThemeIcon('key'), + alwaysShow: true, + isSignInOption: true, + }, + { label: '', kind: vscode.QuickPickItemKind.Separator }, + ]; + + // Add available tenants to the list, grouped by account + tenants.forEach((tenant) => { + const item: TenantQuickPickItem & { group?: string } = { + label: tenant.displayName ?? tenant.tenantId ?? '', + detail: tenant.tenantId, + description: tenant.defaultDomain, + group: tenant.account.label, + iconPath: new vscode.ThemeIcon('organization'), + tenant, + }; + tenantItems.push(item); + }); + + return tenantItems; + }; + + const selectedItem = await context.ui.showQuickPick(tenantItemsPromise(), { + stepName: 'selectTenant', + placeHolder: l10n.t('Select a tenant for Microsoft Entra ID authentication'), + suppressPersistence: true, + loadingPlaceHolder: l10n.t('Loading Tenants…'), + enableGrouping: true, + matchOnDescription: true, + }); + + if (selectedItem.isSignInOption) { + // Handle sign in to other Azure accounts + await this.handleSignInToOtherAccounts(context); + await this.showRetryInstructions(); + + // Exit wizard - user needs to restart the connection flow + throw new UserCancelledError('Account management completed'); + } else if (selectedItem.isCustomOption) { + // Show input box for custom tenant ID + const customTenantId = await context.ui.showInputBox({ + prompt: l10n.t('Enter the tenant ID (GUID)'), + placeHolder: l10n.t('e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012'), + validateInput: (input) => this.validateTenantId(input), + }); + + // Normalize tenant ID - add dashes if missing + const normalizedTenantId = this.normalizeTenantId(customTenantId.trim()); + + // Set entraIdAuthConfig with the normalized tenant ID + context.entraIdAuthConfig = { + ...context.entraIdAuthConfig, + tenantId: normalizedTenantId, + }; + } else { + const tenant = nonNullValue(selectedItem.tenant, 'selectedItem.tenant', 'PromptTenantStep.ts'); + + // Set entraIdAuthConfig with the selected tenant ID + context.entraIdAuthConfig = { + ...context.entraIdAuthConfig, + tenantId: tenant.tenantId, + }; + } + + // Add telemetry - track selection method + if (selectedItem.isSignInOption) { + context.telemetry.properties.tenantSelectionMethod = 'signInTriggered'; + } else if (selectedItem.isCustomOption) { + context.telemetry.properties.tenantSelectionMethod = 'custom'; + } else { + context.telemetry.properties.tenantSelectionMethod = 'fromList'; + } + } + + public shouldPrompt(context: NewConnectionWizardContext): boolean { + // Only show this step if Microsoft Entra ID authentication is selected + return context.selectedAuthenticationMethod === AuthMethodId.MicrosoftEntraID; + } + + private async getAvailableTenants(_context: NewConnectionWizardContext): Promise { + try { + // Create a new Azure subscription provider to get tenants + const subscriptionProvider = new VSCodeAzureSubscriptionProvider(); + const tenants = await subscriptionProvider.getTenants(); + + return tenants.sort((a: AzureTenant, b: AzureTenant) => { + // Sort by display name if available, otherwise by tenant ID + const aName = a.displayName || a.tenantId || ''; + const bName = b.displayName || b.tenantId || ''; + return aName.localeCompare(bName); + }); + } catch { + // If we can't load tenants, just return empty array + // User can still use custom tenant ID option + return []; + } + } + + private validateTenantId(input: string): string | undefined { + if (!input || input.trim().length === 0) { + return l10n.t('Tenant ID cannot be empty'); + } + + const trimmedInput = input.trim(); + + // Validation for GUID format - with or without dashes + const guidWithDashesRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const guidWithoutDashesRegex = /^[0-9a-f]{32}$/i; + + if (!guidWithDashesRegex.test(trimmedInput) && !guidWithoutDashesRegex.test(trimmedInput)) { + return l10n.t( + 'Please enter a valid tenant ID in GUID format (e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012)', + ); + } + + return undefined; + } + + private normalizeTenantId(tenantId: string): string { + // If tenant ID already has dashes, return as-is + if (tenantId.includes('-')) { + return tenantId; + } + + // If it's a 32-character hex string without dashes, add them + if (/^[0-9a-f]{32}$/i.test(tenantId)) { + return [ + tenantId.slice(0, 8), + tenantId.slice(8, 12), + tenantId.slice(12, 16), + tenantId.slice(16, 20), + tenantId.slice(20, 32), + ].join('-'); + } + + // Return as-is if it doesn't match expected pattern + return tenantId; + } + + private async handleSignInToOtherAccounts(context: NewConnectionWizardContext): Promise { + // Add telemetry for credential configuration activation + context.telemetry.properties.credentialConfigActivated = 'true'; + context.telemetry.properties.nodeProvided = 'false'; + + // Create a new Azure subscription provider to trigger sign-in + const subscriptionProvider = new VSCodeAzureSubscriptionProvider(); + + // Call the credentials management function directly + const { configureAzureCredentials } = await import('../../plugins/api-shared/azure/credentialsManagement'); + await configureAzureCredentials( + context, + subscriptionProvider as AzureSubscriptionProviderWithFilters, + undefined, + ); + } + + private async showRetryInstructions(): Promise { + await vscode.window.showInformationMessage( + l10n.t('Account Management Completed'), + { + modal: true, + detail: l10n.t( + 'The account management flow has completed.\n\nPlease try the connection flow again to see your available tenants.', + ), + }, + l10n.t('OK'), + ); + } +} From 4b7e76c11171c3040711ec43b36a6e2b64be59f4 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 29 Sep 2025 11:27:40 +0200 Subject: [PATCH 67/88] feat: offer to update tenantId when editing credentials --- l10n/bundle.l10n.json | 2 + src/commands/updateCredentials/ExecuteStep.ts | 15 +- .../updateCredentials/PromptTenantStep.ts | 208 ++++++++++++++++++ .../updateCredentials/updateCredentials.ts | 9 +- 4 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 src/commands/updateCredentials/PromptTenantStep.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 5ea91a91c..64d7e18f5 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -300,6 +300,7 @@ "Loading Content": "Loading Content", "Loading document {num} of {countUri}": "Loading document {num} of {countUri}", "Loading documents…": "Loading documents…", + "Loading Known Tenants…": "Loading Known Tenants…", "Loading migration actions…": "Loading migration actions…", "Loading resources...": "Loading resources...", "Loading Subscriptions…": "Loading Subscriptions…", @@ -449,6 +450,7 @@ "The \"{newCollectionName}\" collection has been created.": "The \"{newCollectionName}\" collection has been created.", "The account management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.": "The account management flow has completed.\n\nPlease try Service Discovery again to see your available subscriptions.", "The account management flow has completed.\n\nPlease try the connection flow again to see your available tenants.": "The account management flow has completed.\n\nPlease try the connection flow again to see your available tenants.", + "The account management flow has completed.\n\nPlease try updating the credentials again to see your available tenants.": "The account management flow has completed.\n\nPlease try updating the credentials again to see your available tenants.", "The collection \"{0}\" already exists in the database \"{1}\".": "The collection \"{0}\" already exists in the database \"{1}\".", "The collection \"{collectionId}\" has been deleted.": "The collection \"{collectionId}\" has been deleted.", "The connection string has been copied to the clipboard": "The connection string has been copied to the clipboard", diff --git a/src/commands/updateCredentials/ExecuteStep.ts b/src/commands/updateCredentials/ExecuteStep.ts index a2d6c504b..1c0581215 100644 --- a/src/commands/updateCredentials/ExecuteStep.ts +++ b/src/commands/updateCredentials/ExecuteStep.ts @@ -49,12 +49,21 @@ export class ExecuteStep extends AzureWizardExecuteStep { + public async prompt(context: UpdateCredentialsWizardContext): Promise { + // Create async function to provide better loading UX and debugging experience + const tenantItemsPromise = async (): Promise => { + // Load available tenants from Azure subscription provider + const tenants = await this.getAvailableTenants(context); + context.telemetry.measurements.availableTenantsCount = tenants.length; + + // Create quick pick items + const tenantItems: TenantQuickPickItem[] = [ + { + label: l10n.t('Manually enter a custom tenant ID (GUID)'), + iconPath: new vscode.ThemeIcon('edit'), + isCustomOption: true, + alwaysShow: true, + }, + { + label: l10n.t('Sign in to other Azure accounts to access more tenants'), + iconPath: new vscode.ThemeIcon('key'), + alwaysShow: true, + isSignInOption: true, + }, + { label: '', kind: vscode.QuickPickItemKind.Separator }, + ]; + + // Add available tenants to the list, grouped by account + tenants.forEach((tenant) => { + const item: TenantQuickPickItem & { group?: string } = { + label: tenant.displayName ?? tenant.tenantId ?? '', + detail: tenant.tenantId, + description: tenant.defaultDomain, + group: tenant.account.label, + iconPath: new vscode.ThemeIcon('organization'), + tenant, + }; + tenantItems.push(item); + }); + + return tenantItems; + }; + + const selectedItem = await context.ui.showQuickPick(tenantItemsPromise(), { + stepName: 'selectTenant', + placeHolder: l10n.t('Select a tenant for Microsoft Entra ID authentication'), + suppressPersistence: true, + loadingPlaceHolder: l10n.t('Loading Tenants…'), + enableGrouping: true, + matchOnDescription: true, + }); + + if (selectedItem.isSignInOption) { + // Handle sign in to other Azure accounts + await this.handleSignInToOtherAccounts(context); + await this.showRetryInstructions(); + + // Exit wizard - user needs to restart the credentials update flow + throw new UserCancelledError('Account management completed'); + } else if (selectedItem.isCustomOption) { + // Show input box for custom tenant ID + const customTenantId = await context.ui.showInputBox({ + prompt: l10n.t('Enter the tenant ID (GUID)'), + placeHolder: l10n.t('e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012'), + validateInput: (input) => this.validateTenantId(input), + }); + + // Normalize tenant ID - add dashes if missing + const normalizedTenantId = this.normalizeTenantId(customTenantId.trim()); + + // Set entraIdAuthConfig with the normalized tenant ID + context.entraIdAuthConfig = { + ...context.entraIdAuthConfig, + tenantId: normalizedTenantId, + }; + } else { + const tenant = nonNullValue(selectedItem.tenant, 'selectedItem.tenant', 'PromptTenantStep.ts'); + + // Set entraIdAuthConfig with the selected tenant ID + context.entraIdAuthConfig = { + ...context.entraIdAuthConfig, + tenantId: tenant.tenantId, + }; + } + + // Add telemetry - track selection method + if (selectedItem.isSignInOption) { + context.telemetry.properties.tenantSelectionMethod = 'signInTriggered'; + } else if (selectedItem.isCustomOption) { + context.telemetry.properties.tenantSelectionMethod = 'custom'; + } else { + context.telemetry.properties.tenantSelectionMethod = 'fromList'; + } + } + + public shouldPrompt(context: UpdateCredentialsWizardContext): boolean { + // Only show this step if Microsoft Entra ID authentication is selected + return context.selectedAuthenticationMethod === AuthMethodId.MicrosoftEntraID; + } + + private async getAvailableTenants(_context: UpdateCredentialsWizardContext): Promise { + try { + // Create a new Azure subscription provider to get tenants + const subscriptionProvider = new VSCodeAzureSubscriptionProvider(); + const tenants = await subscriptionProvider.getTenants(); + + return tenants.sort((a: AzureTenant, b: AzureTenant) => { + // Sort by display name if available, otherwise by tenant ID + const aName = a.displayName || a.tenantId || ''; + const bName = b.displayName || b.tenantId || ''; + return aName.localeCompare(bName); + }); + } catch { + // If we can't load tenants, just return empty array + // User can still use custom tenant ID option + return []; + } + } + + private validateTenantId(input: string): string | undefined { + if (!input || input.trim().length === 0) { + return l10n.t('Tenant ID cannot be empty'); + } + + const trimmedInput = input.trim(); + + // Validation for GUID format - with or without dashes + const guidWithDashesRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const guidWithoutDashesRegex = /^[0-9a-f]{32}$/i; + + if (!guidWithDashesRegex.test(trimmedInput) && !guidWithoutDashesRegex.test(trimmedInput)) { + return l10n.t( + 'Please enter a valid tenant ID in GUID format (e.g., 12345678-1234-1234-1234-123456789012 or 12345678123412341234123456789012)', + ); + } + + return undefined; + } + + private normalizeTenantId(tenantId: string): string { + // If tenant ID already has dashes, return as-is + if (tenantId.includes('-')) { + return tenantId; + } + + // If it's a 32-character hex string without dashes, add them + if (/^[0-9a-f]{32}$/i.test(tenantId)) { + return [ + tenantId.slice(0, 8), + tenantId.slice(8, 12), + tenantId.slice(12, 16), + tenantId.slice(16, 20), + tenantId.slice(20, 32), + ].join('-'); + } + + // Return as-is if it doesn't match expected pattern + return tenantId; + } + + private async handleSignInToOtherAccounts(context: UpdateCredentialsWizardContext): Promise { + // Add telemetry for credential configuration activation + context.telemetry.properties.credentialConfigActivated = 'true'; + context.telemetry.properties.nodeProvided = 'false'; + + // Create a new Azure subscription provider to trigger sign-in + const subscriptionProvider = new VSCodeAzureSubscriptionProvider(); + + // Call the credentials management function directly + const { configureAzureCredentials } = await import('../../plugins/api-shared/azure/credentialsManagement'); + await configureAzureCredentials( + context, + subscriptionProvider as AzureSubscriptionProviderWithFilters, + undefined, + ); + } + + private async showRetryInstructions(): Promise { + await vscode.window.showInformationMessage( + l10n.t('Account Management Completed'), + { + modal: true, + detail: l10n.t( + 'The account management flow has completed.\n\nPlease try updating the credentials again to see your available tenants.', + ), + }, + l10n.t('OK'), + ); + } +} diff --git a/src/commands/updateCredentials/updateCredentials.ts b/src/commands/updateCredentials/updateCredentials.ts index 1fee7770f..42c281515 100644 --- a/src/commands/updateCredentials/updateCredentials.ts +++ b/src/commands/updateCredentials/updateCredentials.ts @@ -17,6 +17,7 @@ import { refreshView } from '../refreshView/refreshView'; import { PromptAuthMethodStep } from '../updateCredentials/PromptAuthMethodStep'; import { ExecuteStep } from './ExecuteStep'; import { PromptPasswordStep } from './PromptPasswordStep'; +import { PromptTenantStep } from './PromptTenantStep'; import { PromptUserNameStep } from './PromptUserNameStep'; import { type UpdateCredentialsWizardContext } from './UpdateCredentialsWizardContext'; @@ -53,6 +54,7 @@ export async function updateCredentials(context: IActionContext, node: DocumentD const wizardContext: UpdateCredentialsWizardContext = { ...context, nativeAuthConfig: connectionCredentials?.secrets.nativeAuthConfig, + entraIdAuthConfig: connectionCredentials?.secrets.entraIdAuthConfig, availableAuthenticationMethods: authMethodsFromString(supportedAuthMethods), selectedAuthenticationMethod: authMethodFromString(connectionCredentials?.properties.selectedAuthMethod), isEmulator: Boolean(node.cluster.emulatorConfiguration?.isEmulator), @@ -61,7 +63,12 @@ export async function updateCredentials(context: IActionContext, node: DocumentD const wizard = new AzureWizard(wizardContext, { title: l10n.t('Update cluster credentials'), - promptSteps: [new PromptAuthMethodStep(), new PromptUserNameStep(), new PromptPasswordStep()], + promptSteps: [ + new PromptAuthMethodStep(), + new PromptTenantStep(), + new PromptUserNameStep(), + new PromptPasswordStep(), + ], executeSteps: [new ExecuteStep()], showLoadingPrompt: true, }); From f55898400c7c4603b45c105b5d315bd4641acccf Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 29 Sep 2025 11:28:50 +0200 Subject: [PATCH 68/88] chore: updated user facing labels --- l10n/bundle.l10n.json | 3 +-- src/commands/newConnection/PromptTenantStep.ts | 2 +- src/commands/updateCredentials/PromptTenantStep.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 64d7e18f5..9b1f55e03 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -300,7 +300,6 @@ "Loading Content": "Loading Content", "Loading document {num} of {countUri}": "Loading document {num} of {countUri}", "Loading documents…": "Loading documents…", - "Loading Known Tenants…": "Loading Known Tenants…", "Loading migration actions…": "Loading migration actions…", "Loading resources...": "Loading resources...", "Loading Subscriptions…": "Loading Subscriptions…", @@ -311,7 +310,7 @@ "Loading...": "Loading...", "Location": "Location", "Manage Azure Accounts": "Manage Azure Accounts", - "Manually enter a custom tenant ID (GUID)": "Manually enter a custom tenant ID (GUID)", + "Manually enter a custom tenant ID": "Manually enter a custom tenant ID", "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.": "Migration of connections from the Azure Databases VS Code Extension to the DocumentDB for VS Code Extension completed: {migratedCount} connections migrated.", "Mongo Shell connected.": "Mongo Shell connected.", "Mongo Shell Error: {error}": "Mongo Shell Error: {error}", diff --git a/src/commands/newConnection/PromptTenantStep.ts b/src/commands/newConnection/PromptTenantStep.ts index c43a2b70c..903538f60 100644 --- a/src/commands/newConnection/PromptTenantStep.ts +++ b/src/commands/newConnection/PromptTenantStep.ts @@ -29,7 +29,7 @@ export class PromptTenantStep extends AzureWizardPromptStep Date: Mon, 29 Sep 2025 12:52:50 +0200 Subject: [PATCH 69/88] feat: improved 'no account / no subscription' flow for azure service discovery --- l10n/bundle.l10n.json | 4 - .../azure/askToConfigureCredentials.ts | 31 +++ .../InitializeFilteringStep.ts | 45 ++++- .../configureAzureSubscriptionFilter.ts | 77 +++++--- .../azure/wizard/SelectSubscriptionStep.ts | 39 +--- .../AzureMongoRUServiceRootItem.ts | 32 ++-- .../discovery-tree/AzureServiceRootItem.ts | 32 ++-- .../discovery-tree/AzureServiceRootItem.ts | 32 ++-- .../VmFilteringWizardContext.ts | 14 ++ .../discovery-tree/VmTagFilterStep.ts | 51 +++++ .../discovery-tree/configureVmFilterWizard.ts | 180 ++---------------- 11 files changed, 246 insertions(+), 291 deletions(-) create mode 100644 src/plugins/api-shared/azure/askToConfigureCredentials.ts create mode 100644 src/plugins/service-azure-vm/discovery-tree/VmFilteringWizardContext.ts create mode 100644 src/plugins/service-azure-vm/discovery-tree/VmTagFilterStep.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 9b1f55e03..95bab5353 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -403,7 +403,6 @@ "Select Existing": "Select Existing", "Select resource": "Select resource", "Select subscription": "Select subscription", - "Select Subscriptions to Display": "Select Subscriptions to Display", "Select subscriptions to include in service discovery": "Select subscriptions to include in service discovery", "Select Subscriptions...": "Select Subscriptions...", "Select tenants to include in subscription discovery": "Select tenants to include in subscription discovery", @@ -412,7 +411,6 @@ "Selected subscriptions: {0}": "Selected subscriptions: {0}", "Selected tenants: {0}": "Selected tenants: {0}", "Service Discovery": "Service Discovery", - "Sign In": "Sign In", "Sign in to Azure to continue…": "Sign in to Azure to continue…", "Sign in to Azure...": "Sign in to Azure...", "Sign in to other Azure accounts to access more subscriptions": "Sign in to other Azure accounts to access more subscriptions", @@ -522,7 +520,6 @@ "Upload": "Upload", "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", - "User is not signed in to Azure.": "User is not signed in to Azure.", "Username and Password": "Username and Password", "Username cannot be empty": "Username cannot be empty", "Username for {resource}": "Username for {resource}", @@ -548,7 +545,6 @@ "Yes, open connection": "Yes, open connection", "Yes, save my credentials": "Yes, save my credentials", "You are not signed in to an Azure account. Please sign in.": "You are not signed in to an Azure account. Please sign in.", - "You are not signed in to Azure. Sign in and retry.": "You are not signed in to Azure. Sign in and retry.", "You are not signed in to the MongoDB Cluster. Please sign in (by expanding the node \"{0}\") and try again.": "You are not signed in to the MongoDB Cluster. Please sign in (by expanding the node \"{0}\") and try again.", "You can connect to a different DocumentDB by:": "You can connect to a different DocumentDB by:", "You clicked a link that wants to open a DocumentDB connection in VS Code.": "You clicked a link that wants to open a DocumentDB connection in VS Code.", diff --git a/src/plugins/api-shared/azure/askToConfigureCredentials.ts b/src/plugins/api-shared/azure/askToConfigureCredentials.ts new file mode 100644 index 000000000..ad23fb4e1 --- /dev/null +++ b/src/plugins/api-shared/azure/askToConfigureCredentials.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as l10n from '@vscode/l10n'; +import { window } from 'vscode'; + +/** + * Shows a modal dialog asking the user if they want to configure/manage their Azure credentials. + * Used when no Azure subscriptions are found or when user is not signed in. + * + * @returns Promise that resolves to 'configure' if user wants to manage accounts, 'cancel' otherwise + */ +export async function askToConfigureCredentials(): Promise<'configure' | 'cancel'> { + const configure = l10n.t('Yes, Manage Accounts'); + + const result = await window.showInformationMessage( + l10n.t('No Azure Subscriptions Found'), + { + modal: true, + detail: l10n.t( + 'To connect to Azure resources, you need to sign in to Azure accounts.\n\n' + + 'Would you like to manage your Azure accounts now?', + ), + }, + { title: configure }, + ); + + return result?.title === configure ? 'configure' : 'cancel'; +} diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts index ef1422222..216c836c8 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts @@ -3,15 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzureTenant } from '@microsoft/vscode-azext-azureauth'; -import { AzureWizardPromptStep, type IWizardOptions } from '@microsoft/vscode-azext-utils'; +import { type AzureTenant, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { + AzureWizardPromptStep, + type IActionContext, + type IWizardOptions, + UserCancelledError, +} from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import * as l10n from '@vscode/l10n'; import { type QuickPickItem } from 'vscode'; +import { askToConfigureCredentials } from '../askToConfigureCredentials'; +import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; import { type FilteringWizardContext } from './FilteringWizardContext'; import { FilterSubscriptionSubStep } from './FilterSubscriptionSubStep'; import { FilterTenantSubStep } from './FilterTenantSubStep'; -import { isTenantFilteredOut } from './subscriptionFilteringHelpers'; +import { getTenantFilteredSubscriptions, isTenantFilteredOut } from './subscriptionFilteringHelpers'; /** * Custom error to signal that initialization has completed and wizard should proceed to subwizard @@ -69,6 +76,20 @@ export class InitializeFilteringStep extends AzureWizardPromptStep { + // Add telemetry for credential configuration activation + context.telemetry.properties.credentialConfigActivated = 'true'; + context.telemetry.properties.nodeProvided = 'false'; + + // Call the credentials management function directly using the subscription provider from context + // The subscription provider in the wizard context is actually AzureSubscriptionProviderWithFilters + const { configureAzureCredentials } = await import('../credentialsManagement'); + await configureAzureCredentials( + context, + subscriptionProvider as AzureSubscriptionProviderWithFilters, + undefined, + ); + } + public shouldPrompt(): boolean { return true; } diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter.ts b/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter.ts index 4dea6736b..f9938ab16 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter.ts @@ -4,54 +4,73 @@ *--------------------------------------------------------------------------------------------*/ import { type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; -import { AzureWizard, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { + AzureWizard, + type AzureWizardExecuteStep, + type AzureWizardPromptStep, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; -import * as vscode from 'vscode'; -import { ext } from '../../../../extensionVariables'; import { ExecuteStep } from './ExecuteStep'; import { type FilteringWizardContext } from './FilteringWizardContext'; import { InitializeFilteringStep } from './InitializeFilteringStep'; +/** + * Options for extending the subscription filtering wizard with additional steps + */ +export interface SubscriptionFilteringOptions { + /** Additional prompt steps to include after the standard filtering steps */ + additionalPromptSteps?: AzureWizardPromptStep[]; + /** Additional execute steps to include after the standard execute steps */ + additionalExecuteSteps?: AzureWizardExecuteStep[]; + /** Function to extend the wizard context with additional properties */ + contextExtender?: (context: FilteringWizardContext) => TContext; + /** Custom title for the wizard */ + title?: string; +} + /** * Configures the Azure subscription filter using the wizard pattern. */ -export async function configureAzureSubscriptionFilter( +export async function configureAzureSubscriptionFilter< + TContext extends FilteringWizardContext = FilteringWizardContext, +>( context: IActionContext, azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, + options?: SubscriptionFilteringOptions, ): Promise { context.telemetry.properties.subscriptionFiltering = 'configureAzureSubscriptionFilter'; - /** - * Ensure the user is signed in to Azure - */ - if (!(await azureSubscriptionProvider.isSignedIn())) { - context.telemetry.properties.subscriptionFilteringResult = 'Failed'; - context.telemetry.properties.subscriptionFilteringError = 'NotSignedIn'; - const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; - void vscode.window - .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) - .then(async (input) => { - if (input === signIn) { - await azureSubscriptionProvider.signIn(); - ext.discoveryBranchDataProvider.refresh(); - } - }); - - // Return so that the signIn flow can be completed before continuing - return; - } - - // Create wizard context - const wizardContext: FilteringWizardContext = { + // Create base wizard context + const baseWizardContext: FilteringWizardContext = { ...context, azureSubscriptionProvider, }; + // Extend context if extender is provided + const wizardContext = options?.contextExtender + ? options.contextExtender(baseWizardContext) + : (baseWizardContext as TContext); + + // Build prompt steps + const promptSteps: AzureWizardPromptStep[] = [ + new InitializeFilteringStep() as AzureWizardPromptStep, + ]; + if (options?.additionalPromptSteps) { + promptSteps.push(...options.additionalPromptSteps); + } + + // Build execute steps + const executeSteps: AzureWizardExecuteStep[] = [new ExecuteStep() as AzureWizardExecuteStep]; + if (options?.additionalExecuteSteps) { + executeSteps.push(...options.additionalExecuteSteps); + } + // Create and run wizard const wizard = new AzureWizard(wizardContext, { - title: l10n.t('Configure Azure Discovery Filters'), - promptSteps: [new InitializeFilteringStep()], - executeSteps: [new ExecuteStep()], + title: options?.title || l10n.t('Configure Azure Discovery Filters'), + promptSteps, + executeSteps, }); try { diff --git a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts index 76b76da43..60f259e00 100644 --- a/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts +++ b/src/plugins/api-shared/azure/wizard/SelectSubscriptionStep.ts @@ -6,9 +6,10 @@ import { VSCodeAzureSubscriptionProvider, type AzureSubscription } from '@microsoft/vscode-azext-azureauth'; import { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; -import { QuickPickItemKind, ThemeIcon, Uri, window, type MessageItem, type QuickPickItem } from 'vscode'; +import { QuickPickItemKind, ThemeIcon, Uri, window, type QuickPickItem } from 'vscode'; import { type NewConnectionWizardContext } from '../../../../commands/newConnection/NewConnectionWizardContext'; import { ext } from '../../../../extensionVariables'; +import { askToConfigureCredentials } from '../askToConfigureCredentials'; import { type AzureSubscriptionProviderWithFilters } from '../AzureSubscriptionProviderWithFilters'; import { getDuplicateSubscriptions } from '../subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureContextProperties } from './AzureContextProperties'; @@ -39,22 +40,6 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { - if (input === signIn) { - void subscriptionProvider.signIn(); - } - }); - - throw new UserCancelledError(l10n.t('User is not signed in to Azure.')); - } - // Store subscriptions outside the async function so we can access them later let subscriptions!: Awaited; @@ -84,7 +69,7 @@ export class SelectSubscriptionStep extends AzureWizardPromptStep { - const configure = l10n.t('Yes, Manage Accounts'); - - const result = await window.showInformationMessage( - l10n.t('No Azure Subscriptions Found'), - { - modal: true, - detail: l10n.t( - 'To connect to Azure resources, you need to sign in to Azure accounts.\n\n' + - 'Would you like to manage your Azure accounts now?', - ), - }, - { title: configure }, - ); - - return result?.title === configure ? 'configure' : 'cancel'; - } - private async configureCredentialsFromWizard( context: NewConnectionWizardContext, subscriptionProvider: VSCodeAzureSubscriptionProvider, diff --git a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts index e71713761..78b0352e6 100644 --- a/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-ru/discovery-tree/AzureMongoRUServiceRootItem.ts @@ -6,7 +6,6 @@ import { type AzureTenant, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; -import { ext } from '../../../extensionVariables'; import { createGenericElementWithContext } from '../../../tree/api/createGenericElementWithContext'; import { type ExtTreeElementBase, type TreeElement } from '../../../tree/TreeElement'; import { @@ -14,6 +13,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; +import { askToConfigureCredentials } from '../../api-shared/azure/askToConfigureCredentials'; import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureMongoRUSubscriptionItem } from './AzureMongoRUSubscriptionItem'; @@ -32,19 +32,17 @@ export class AzureMongoRUServiceRootItem } async getChildren(): Promise { - /** - * This is an important step to ensure that the user is signed in to Azure before listing subscriptions. - */ - if (!(await this.azureSubscriptionProvider.isSignedIn())) { - const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; - void vscode.window - .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) - .then(async (input) => { - if (input === signIn) { - await this.azureSubscriptionProvider.signIn(); - ext.discoveryBranchDataProvider.refresh(); - } - }); + const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); + + if (!subscriptions || subscriptions.length === 0) { + // Show modal dialog for empty state + const configureResult = await askToConfigureCredentials(); + if (configureResult === 'configure') { + // Note to future maintainers: 'void' is important here so that the return below returns the error node. + // Otherwise, the /retry node might be duplicated as we're inside of tree node with a loading state (the node items are being swapped etc.) + void vscode.commands.executeCommand('vscode-documentdb.command.discoveryView.manageCredentials', this); + } return [ createGenericElementWithContext({ @@ -58,12 +56,6 @@ export class AzureMongoRUServiceRootItem ]; } - const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); - const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); - if (!subscriptions || subscriptions.length === 0) { - return []; - } - // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however // this lead to incorrect responses from getSubscriptions. We didn't investigate diff --git a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts index 1e9f5f70f..366d1e33d 100644 --- a/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-mongo-vcore/discovery-tree/AzureServiceRootItem.ts @@ -6,7 +6,6 @@ import { type AzureTenant, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; -import { ext } from '../../../extensionVariables'; import { createGenericElementWithContext } from '../../../tree/api/createGenericElementWithContext'; import { type ExtTreeElementBase, type TreeElement } from '../../../tree/TreeElement'; import { @@ -14,6 +13,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; +import { askToConfigureCredentials } from '../../api-shared/azure/askToConfigureCredentials'; import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; @@ -30,19 +30,17 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext } async getChildren(): Promise { - /** - * This is an important step to ensure that the user is signed in to Azure before listing subscriptions. - */ - if (!(await this.azureSubscriptionProvider.isSignedIn())) { - const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; - void vscode.window - .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) - .then(async (input) => { - if (input === signIn) { - await this.azureSubscriptionProvider.signIn(); - ext.discoveryBranchDataProvider.refresh(); - } - }); + const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); + + if (!subscriptions || subscriptions.length === 0) { + // Show modal dialog for empty state + const configureResult = await askToConfigureCredentials(); + if (configureResult === 'configure') { + // Note to future maintainers: 'void' is important here so that the return below returns the error node. + // Otherwise, the /retry node might be duplicated as we're inside of tree node with a loading state (the node items are being swapped etc.) + void vscode.commands.executeCommand('vscode-documentdb.command.discoveryView.manageCredentials', this); + } return [ createGenericElementWithContext({ @@ -56,12 +54,6 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext ]; } - const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); - const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); - if (!subscriptions || subscriptions.length === 0) { - return []; - } - // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however // this lead to incorrect responses from getSubscriptions. We didn't investigate diff --git a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts index 764a53fc0..c70f7398d 100644 --- a/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts +++ b/src/plugins/service-azure-vm/discovery-tree/AzureServiceRootItem.ts @@ -6,7 +6,6 @@ import { type AzureTenant, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; -import { ext } from '../../../extensionVariables'; import { createGenericElementWithContext } from '../../../tree/api/createGenericElementWithContext'; import { type ExtTreeElementBase, type TreeElement } from '../../../tree/TreeElement'; import { @@ -14,6 +13,7 @@ import { type TreeElementWithContextValue, } from '../../../tree/TreeElementWithContextValue'; import { type TreeElementWithRetryChildren } from '../../../tree/TreeElementWithRetryChildren'; +import { askToConfigureCredentials } from '../../api-shared/azure/askToConfigureCredentials'; import { getTenantFilteredSubscriptions } from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; import { AzureSubscriptionItem } from './AzureSubscriptionItem'; @@ -30,19 +30,17 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext } async getChildren(): Promise { - /** - * This is an important step to ensure that the user is signed in to Azure before listing subscriptions. - */ - if (!(await this.azureSubscriptionProvider.isSignedIn())) { - const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; - void vscode.window - .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) - .then(async (input) => { - if (input === signIn) { - await this.azureSubscriptionProvider.signIn(); - ext.discoveryBranchDataProvider.refresh(); - } - }); + const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); + const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); + + if (!subscriptions || subscriptions.length === 0) { + // Show modal dialog for empty state + const configureResult = await askToConfigureCredentials(); + if (configureResult === 'configure') { + // Note to future maintainers: 'void' is important here so that the return below returns the error node. + // Otherwise, the /retry node might be duplicated as we're inside of tree node with a loading state (the node items are being swapped etc.) + void vscode.commands.executeCommand('vscode-documentdb.command.discoveryView.manageCredentials', this); + } return [ createGenericElementWithContext({ @@ -56,12 +54,6 @@ export class AzureServiceRootItem implements TreeElement, TreeElementWithContext ]; } - const allSubscriptions = await this.azureSubscriptionProvider.getSubscriptions(true); - const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); - if (!subscriptions || subscriptions.length === 0) { - return []; - } - // This information is extracted to improve the UX, that's why there are fallbacks to 'undefined' // Note to future maintainers: we used to run getSubscriptions and getTenants "in parallel", however // this lead to incorrect responses from getSubscriptions. We didn't investigate diff --git a/src/plugins/service-azure-vm/discovery-tree/VmFilteringWizardContext.ts b/src/plugins/service-azure-vm/discovery-tree/VmFilteringWizardContext.ts new file mode 100644 index 000000000..096a2a8e3 --- /dev/null +++ b/src/plugins/service-azure-vm/discovery-tree/VmFilteringWizardContext.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type FilteringWizardContext } from '../../api-shared/azure/subscriptionFiltering/FilteringWizardContext'; + +/** + * Extended wizard context for VM-specific filtering that includes tag filtering + */ +export interface VmFilteringWizardContext extends FilteringWizardContext { + /** The Azure VM tag to filter by */ + vmTag?: string; +} diff --git a/src/plugins/service-azure-vm/discovery-tree/VmTagFilterStep.ts b/src/plugins/service-azure-vm/discovery-tree/VmTagFilterStep.ts new file mode 100644 index 000000000..fb143104a --- /dev/null +++ b/src/plugins/service-azure-vm/discovery-tree/VmTagFilterStep.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import { ext } from '../../../extensionVariables'; +import { type VmFilteringWizardContext } from './VmFilteringWizardContext'; + +/** + * Wizard step for configuring Azure VM tag filtering + */ +export class VmTagFilterStep extends AzureWizardPromptStep { + public async prompt(context: VmFilteringWizardContext): Promise { + const defaultTag = ext.context.globalState.get('azure-vm-discovery.tag', 'DocumentDB'); + + const result = await context.ui.showInputBox({ + prompt: l10n.t('Enter the Azure VM tag to filter by'), + value: defaultTag, + placeHolder: l10n.t('e.g., DocumentDB, Environment, Project'), + validateInput: (value: string) => { + if (!value) { + return l10n.t('Tag cannot be empty.'); + } + if (!/^[a-zA-Z0-9_.-]+$/.test(value)) { + return l10n.t('Tag can only contain alphanumeric characters, underscores, periods, and hyphens.'); + } + if (value.length > 256) { + return l10n.t('Tag cannot be longer than 256 characters.'); + } + return undefined; + }, + }); + + if (result !== undefined) { + // Input box returns undefined if cancelled + await ext.context.globalState.update('azure-vm-discovery.tag', result); + context.vmTag = result; + context.telemetry.properties.tagConfigured = 'true'; + context.telemetry.properties.tagValue = result; + } else { + context.telemetry.properties.tagConfigured = 'cancelled'; + // Do not change existing tag if cancelled + } + } + + public shouldPrompt(_context: VmFilteringWizardContext): boolean { + return true; // Always show this step + } +} diff --git a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts index 1b1ee2aa4..834f39f7b 100644 --- a/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts +++ b/src/plugins/service-azure-vm/discovery-tree/configureVmFilterWizard.ts @@ -3,171 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzureSubscription, type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; -import { - AzureWizard, - AzureWizardPromptStep, - UserCancelledError, - type IActionContext, - type IAzureQuickPickItem, -} from '@microsoft/vscode-azext-utils'; +import { type VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { type IActionContext } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; -import * as vscode from 'vscode'; -import { ext } from '../../../extensionVariables'; import { - getDuplicateSubscriptions, - getSelectedSubscriptionIds, - getTenantFilteredSubscriptions, - setSelectedSubscriptionIds, -} from '../../api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers'; - -export interface ConfigureVmFilterWizardContext extends IActionContext { - azureSubscriptionProvider: VSCodeAzureSubscriptionProvider; -} - -class SubscriptionFilterStep extends AzureWizardPromptStep { - public async prompt(context: ConfigureVmFilterWizardContext): Promise { - const azureSubscriptionProvider = context.azureSubscriptionProvider; - - if (!(await azureSubscriptionProvider.isSignedIn())) { - const signIn: vscode.MessageItem = { title: l10n.t('Sign In') }; - void vscode.window - .showInformationMessage(l10n.t('You are not signed in to Azure. Sign in and retry.'), signIn) - .then(async (input) => { - if (input === signIn) { - await azureSubscriptionProvider.signIn(); - ext.discoveryBranchDataProvider.refresh(); - } - }); - - throw new UserCancelledError(l10n.t('User is not signed in to Azure.')); - } - - const selectedSubscriptionIds = getSelectedSubscriptionIds(); - - const subscriptionQuickPickItemsProvider: () => Promise< - IAzureQuickPickItem[] - > = async () => { - const allSubscriptions = await azureSubscriptionProvider.getSubscriptions(false); // Get all unfiltered subscriptions - const subscriptions = getTenantFilteredSubscriptions(allSubscriptions); // Apply tenant filtering - const duplicates = getDuplicateSubscriptions(subscriptions); - - return subscriptions - .map( - (subscription) => - >{ - label: duplicates.includes(subscription) - ? subscription.name + ` (${subscription.account?.label})` - : subscription.name, - description: subscription.subscriptionId, - data: subscription, - group: subscription.account.label, - iconPath: vscode.Uri.joinPath( - ext.context.extensionUri, - 'resources', - 'from_node_modules', - '@microsoft', - 'vscode-azext-azureutils', - 'resources', - 'azureSubscription.svg', - ), - }, - ) - .sort((a, b) => a.label.localeCompare(b.label)); - }; - - const picks = await context.ui.showQuickPick(subscriptionQuickPickItemsProvider(), { - canPickMany: true, - placeHolder: l10n.t('Select Subscriptions to Display'), - isPickSelected: (pick) => { - return ( - selectedSubscriptionIds.length === 0 || - selectedSubscriptionIds.includes( - (pick as IAzureQuickPickItem).data.subscriptionId, - ) - ); - }, - suppressPersistence: true, // Recommended for multi-step wizards - }); - - if (picks !== undefined) { - // User made a choice (could be an empty array if they deselected all) - const newSelectedIds = picks.map((pick) => `${pick.data.tenantId}/${pick.data.subscriptionId}`); - await setSelectedSubscriptionIds(newSelectedIds); - context.telemetry.properties.subscriptionsConfigured = 'true'; - context.telemetry.properties.subscriptionCount = String(newSelectedIds.length); - } else { - // User cancelled the quick pick (e.g., pressed Esc) - context.telemetry.properties.subscriptionsConfigured = 'cancelled'; - // Do not change existing selection if cancelled - } - } - - public shouldPrompt(_context: ConfigureVmFilterWizardContext): boolean { - return true; // Always show this step - } -} - -class TagFilterStep extends AzureWizardPromptStep { - public async prompt(context: ConfigureVmFilterWizardContext): Promise { - const defaultTag = ext.context.globalState.get('azure-vm-discovery.tag', 'DocumentDB'); - - const result = await context.ui.showInputBox({ - prompt: l10n.t('Enter the Azure VM tag to filter by'), - value: defaultTag, - placeHolder: l10n.t('e.g., DocumentDB, Environment, Project'), - validateInput: (value: string) => { - if (!value) { - return l10n.t('Tag cannot be empty.'); - } - if (!/^[a-zA-Z0-9_.-]+$/.test(value)) { - return l10n.t('Tag can only contain alphanumeric characters, underscores, periods, and hyphens.'); - } - if (value.length > 256) { - return l10n.t('Tag cannot be longer than 256 characters.'); - } - return undefined; - }, - }); - - if (result !== undefined) { - // Input box returns undefined if cancelled - await ext.context.globalState.update('azure-vm-discovery.tag', result); - context.telemetry.properties.tagConfigured = 'true'; - context.telemetry.properties.tagValue = result; - } else { - context.telemetry.properties.tagConfigured = 'cancelled'; - // Do not change existing tag if cancelled - } - } - - public shouldPrompt(_context: ConfigureVmFilterWizardContext): boolean { - return true; // Always show this step - } -} - + configureAzureSubscriptionFilter, + type SubscriptionFilteringOptions, +} from '../../api-shared/azure/subscriptionFiltering/configureAzureSubscriptionFilter'; +import { type VmFilteringWizardContext } from './VmFilteringWizardContext'; +import { VmTagFilterStep } from './VmTagFilterStep'; + +/** + * Configures the Azure VM discovery filters, including both subscription/tenant filtering and VM-specific tag filtering + */ export async function configureVmFilter( baseContext: IActionContext, azureSubscriptionProvider: VSCodeAzureSubscriptionProvider, ): Promise { - const wizardContext: ConfigureVmFilterWizardContext = { - ...baseContext, - azureSubscriptionProvider: azureSubscriptionProvider, - telemetry: { - // Ensure telemetry object and its properties are initialized - properties: { ...(baseContext.telemetry?.properties || {}) }, - measurements: { ...(baseContext.telemetry?.measurements || {}) }, - suppressIfSuccessful: baseContext.telemetry?.suppressIfSuccessful || false, - suppressAll: baseContext.telemetry?.suppressAll || false, - }, - }; - - const wizard = new AzureWizard(wizardContext, { + const options: SubscriptionFilteringOptions = { title: l10n.t('Configure Azure VM Discovery Filters'), - promptSteps: [new SubscriptionFilterStep(), new TagFilterStep()], - executeSteps: [], // Configuration happens in prompt steps, no separate execution steps - }); + additionalPromptSteps: [new VmTagFilterStep()], + contextExtender: (context) => + ({ + ...context, + // Initialize VM-specific properties + vmTag: undefined, + }) as VmFilteringWizardContext, + }; - await wizard.prompt(); - // Data is saved by the prompt steps themselves. + await configureAzureSubscriptionFilter(baseContext, azureSubscriptionProvider, options); } From 518678d8a62e052d2d3cb951cad0cf475c9cdf4d Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 29 Sep 2025 13:39:00 +0200 Subject: [PATCH 70/88] Update src/documentdb/CredentialCache.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/documentdb/CredentialCache.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/documentdb/CredentialCache.ts b/src/documentdb/CredentialCache.ts index 1d5199b0c..c6aa97ef9 100644 --- a/src/documentdb/CredentialCache.ts +++ b/src/documentdb/CredentialCache.ts @@ -203,10 +203,10 @@ export class CredentialCache { // Convert central auth configs to local cache format let cacheEntraIdConfig: EntraIdAuthConfig | undefined; if (secrets.entraIdAuthConfig) { - cacheEntraIdConfig = { - tenantId: secrets.entraIdAuthConfig.tenantId, // Preserve optional nature for backward compatibility - }; + // Preserve all optional fields for backward compatibility + cacheEntraIdConfig = { ...secrets.entraIdAuthConfig }; } + // Use structured configurations const username = secrets.nativeAuthConfig?.connectionUser ?? ''; From abe873c248f8ac500f86051d997eba86deb4e188 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 29 Sep 2025 13:47:53 +0200 Subject: [PATCH 71/88] prettier-fix --- src/documentdb/CredentialCache.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/documentdb/CredentialCache.ts b/src/documentdb/CredentialCache.ts index c6aa97ef9..6d9abb587 100644 --- a/src/documentdb/CredentialCache.ts +++ b/src/documentdb/CredentialCache.ts @@ -206,7 +206,6 @@ export class CredentialCache { // Preserve all optional fields for backward compatibility cacheEntraIdConfig = { ...secrets.entraIdAuthConfig }; } - // Use structured configurations const username = secrets.nativeAuthConfig?.connectionUser ?? ''; From 69330617a4a1e36614f774eef982edbd96c00f36 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 1 Oct 2025 15:49:10 +0200 Subject: [PATCH 72/88] wip: first draft of the release pipeline --- .azure-pipelines/release.yml | 306 +++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 .azure-pipelines/release.yml diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml new file mode 100644 index 000000000..68735d1e1 --- /dev/null +++ b/.azure-pipelines/release.yml @@ -0,0 +1,306 @@ +parameters: + # The intended extension version to publish. + # This is used to verify the version in package.json matches the version to publish to avoid accidental publishing. + - name: publishVersion + displayName: "Publish Version" + type: string + + # Customize the environment to associate the deployment with. + # Useful to control which group of people should be required to approve the deployment. + # Deprecated on OneBranch pipelines, use `ob_release_environment` variable and ApprovalService instead. + #- name: environmentName + # type: string + # default: AzCodeDeploy + + # When true, skips the deployment job which actually publishes the extension + - name: dryRun + displayName: "Dry Run without publishing" + type: boolean + default: true + + - name: "debug" + displayName: "Enable debug output" + type: boolean + default: false + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + pipelines: + - pipeline: build # Alias for your build pipeline source + project: "CosmosDB" + source: '\VSCode Extensions\vscode-documentdb Build VSIX' # name of the pipeline that produces the artifacts + +variables: + system.debug: ${{ parameters.debug }} + # Required by MicroBuild template + TeamName: "Desktop Tools" + WindowsContainerImage: "onebranch.azurecr.io/windows/ltsc2022/vse2022:latest" # Docker image which is used to build the project https://aka.ms/obpipelines/containers + +extends: + template: v2/OneBranch.Official.CrossPlat.yml@templates + + parameters: + # remove for release pipeline? + cloudvault: # https://aka.ms/obpipelines/cloudvault + enabled: false + globalSdl: # https://aka.ms/obpipelines/sdl + asyncSdl: + enabled: false + tsa: + enabled: false # onebranch publish all sdl results to TSA. If TSA is disabled all SDL tools will forced into'break' build mode. + #configFile: '$(Build.SourcesDirectory)/.azure-pipelines/compliance/tsaoptions.json' + credscan: + suppressionsFile: $(Build.SourcesDirectory)/.azure-pipelines/compliance/CredScanSuppressions.json + policheck: + break: true # always break the build on policheck issues. You can disable it by setting to 'false' + suppression: + suppressionFile: $(Build.SourcesDirectory)/.config/guardian/.gdnsuppress + codeql: + excludePathPatterns: "**/.vscode-test, dist" # Exclude .vscode-test and dist directories from CodeQL alerting + compiled: + ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: + enabled: true + ${{ else }}: + enabled: false + tsaEnabled: false # See 'Codeql.TSAEnabled' in the Addition Options section below + componentgovernance: + ignoreDirectories: $(Build.SourcesDirectory)/.vscode-test + featureFlags: + linuxEsrpSigning: true + WindowsHostVersion: + Version: 2022 + # end of remove for release pipeline + + release: + category: NonAzure # NonAzure category is used to indicate that this is not an Azure service + + stages: + ## Uncomment this stage to validate the service connection and retrieve the user ID of the Azure DevOps Service Connection user. + ## NOTE: this has to be a separate stage with pool type 'windows' to ensure that the Azure CLI task can run successfully, + ## which is not supported on 'release' pool type. + ## See https://aka.ms/VSM-MS-Publisher-Automate for more details. + #- stage: ValidateServiceConnection + # displayName: Validate Service Connection + # jobs: + # - job: ValidateServiceConnection + # displayName: "\U00002713 Validate Service Connection" + # pool: + # type: windows + # variables: + # ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts + # steps: + # # Get the user ID of the Azure DevOps Service Connection user to use for publishing + # - task: AzureCLI@2 + # displayName: 'Get AzDO User ID' + # inputs: + # azureSubscription: 'CosmosDB VSCode Publishing' + # scriptType: pscore + # scriptLocation: inlineScript + # inlineScript: | + # az rest -u https://app.vssps.visualstudio.com/_apis/profile/profiles/me --resource 499b84ac-1321-427f-aa17-267ca6975798 + ## END of ValidateServiceConnection stage + + - stage: Release + displayName: Release extension + variables: + - name: ob_release_environment + #value: Test # should be Test, PPE or Production + value: Production # should be Test, PPE or Production + jobs: + - job: ReleaseValidation + displayName: "\U00002713 Validate Artifacts" + templateContext: + inputs: + - input: pipelineArtifact + pipeline: build + targetPath: $(System.DefaultWorkingDirectory) + artifactName: drop_BuildStage_Main + pool: + type: release + variables: + ob_outputDirectory: "$(Build.ArtifactStagingDirectory)" # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts + steps: + # Modify the build number to include repo name, extension version, and if dry run is true + - task: PowerShell@2 + displayName: "\U0001F449 Prepend version from package.json to build number" + env: + dryRun: ${{ parameters.dryRun }} + inputs: + targetType: "inline" + script: | + # Get the version from package.json + $packageJsonPath = "$(System.DefaultWorkingDirectory)/package.json" + if (-not (Test-Path $packageJsonPath)) { + Write-Error "[Error] package.json not found at $packageJsonPath" + exit 1 + } + + $packageJson = Get-Content $packageJsonPath | ConvertFrom-Json + $npmVersionString = $packageJson.version + if (-not $npmVersionString) { + Write-Error "[Error] Version not found in package.json" + exit 1 + } + + $isDryRun = "$env:dryRun" + $currentBuildNumber = "$(Build.BuildId)" + + $repoName = "$(Build.Repository.Name)" + $repoNameParts = $repoName -split '/' + $repoNameWithoutOwner = $repoNameParts[-1] + + $dryRunSuffix = "" + if ($isDryRun -eq 'True') { + Write-Output "Dry run was set to True. Adding 'dry' to the build number." + $dryRunSuffix = "-dry" + } + + $newBuildNumber = "$repoNameWithoutOwner-$npmVersionString$dryRunSuffix-$currentBuildNumber" + Write-Output "Setting build number to: $newBuildNumber" + Write-Output "##vso[build.updatebuildnumber]$newBuildNumber" + + # For safety, verify the version in package.json matches the version to publish entered by the releaser + # If they don't match, this step fails + - task: PowerShell@2 + displayName: "\U0001F449 Verify publish version" + env: + publishVersion: ${{ parameters.publishVersion }} + inputs: + targetType: "inline" + script: | + # Get the version from package.json + $packageJsonPath = "$(System.DefaultWorkingDirectory)/package.json" + if (-not (Test-Path $packageJsonPath)) { + Write-Error "[Error] package.json not found at $packageJsonPath" + exit 1 + } + + $packageJson = Get-Content $packageJsonPath | ConvertFrom-Json + $npmVersionString = $packageJson.version + $publishVersion = "$env:publishVersion" + + Write-Output "Package.json version: $npmVersionString" + Write-Output "Requested publish version: $publishVersion" + + # Validate both versions are semantic versions + $semverPattern = '^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$' + if ($npmVersionString -notmatch $semverPattern) { + Write-Error "[Error] Version in package.json ($npmVersionString) is not a valid semantic version" + exit 1 + } + if ($publishVersion -notmatch $semverPattern) { + Write-Error "[Error] Publish version ($publishVersion) is not a valid semantic version" + exit 1 + } + + if ($npmVersionString -eq $publishVersion) { + Write-Output "[Success] Publish version matches package.json version. Proceeding with release." + } else { + Write-Error "[Error] Publish version '$publishVersion' doesn't match version found in package.json '$npmVersionString'. Cancelling release." + exit 1 + } + + # Find the vsix to release and set the vsix file name variable + # Fails with an error if more than one .vsix file is found, or if no .vsix file is found + - task: PowerShell@2 + displayName: "\U0001F449 Find and Set .vsix File Variable" + name: setVsixFileNameStep + inputs: + targetType: "inline" + script: | + # Get all .vsix files in the current directory + Write-Output "Searching for .vsix files in: $(System.DefaultWorkingDirectory)" + Write-Output "Directory contents:" + Get-ChildItem -Path $(System.DefaultWorkingDirectory) -File | Where-Object { $_.Extension -in @('.vsix', '.json', '.p7s', '.manifest') } | Select-Object Name, Length, LastWriteTime | Format-Table + + $vsixFiles = Get-ChildItem -Path $(System.DefaultWorkingDirectory) -Filter *.vsix -File + + # Check if more than one .vsix file is found + if ($vsixFiles.Count -gt 1) { + Write-Error "[Error] More than one .vsix file found: $($vsixFiles.Name -join ', ')" + exit 1 + } elseif ($vsixFiles.Count -eq 0) { + Write-Error "[Error] No .vsix files found in $(System.DefaultWorkingDirectory)" + exit 1 + } else { + # Set the pipeline variable + $vsixFileName = $vsixFiles.Name + $vsixFileSize = [math]::Round($vsixFiles.Length / 1MB, 2) + Write-Output "##vso[task.setvariable variable=vsixFileName;isOutput=true]$vsixFileName" + Write-Output "[Success] Found .vsix file: $vsixFileName (${vsixFileSize} MB)" + } + + - task: PowerShell@2 + displayName: "\U0001F449 Verify Publishing Files" + inputs: + targetType: "inline" + script: | + $vsixFileName = "$(setVsixFileNameStep.vsixFileName)" + if (-not $vsixFileName) { + Write-Error "[Error] vsixFileName variable not defined." + exit 1 + } + + $vsixPath = "$(System.DefaultWorkingDirectory)/$vsixFileName" + $manifestPath = "$(System.DefaultWorkingDirectory)/extension.manifest" + $signaturePath = "$(System.DefaultWorkingDirectory)/extension.signature.p7s" + + Write-Output "Validating required files for publishing:" + + if (Test-Path -Path $vsixPath) { + $vsixSize = [math]::Round((Get-Item $vsixPath).Length / 1MB, 2) + Write-Output "✓ VSIX file found: $vsixFileName (${vsixSize} MB)" + } else { + Write-Error "[Error] The specified VSIX file does not exist: $vsixPath" + exit 1 + } + + if (Test-Path -Path $manifestPath) { + Write-Output "✓ Manifest file found: extension.manifest" + } else { + Write-Warning "[Warning] Manifest file not found: $manifestPath" + } + + if (Test-Path -Path $signaturePath) { + Write-Output "✓ Signature file found: extension.signature.p7s" + } else { + Write-Warning "[Warning] Signature file not found: $signaturePath" + } + + Write-Output "[Success] $vsixFileName is ready for publishing." + + - job: PublishExtension + displayName: "\U00002713 Publish Extension" + condition: and(succeeded(), ${{ eq(parameters.dryRun, false) }}) + dependsOn: ReleaseValidation + pool: + type: release + variables: + vsixFileName: $[ dependencies.ReleaseValidation.outputs['setVsixFileNameStep.vsixFileName'] ] + templateContext: + inputs: + - input: pipelineArtifact + pipeline: build + targetPath: $(System.DefaultWorkingDirectory) + artifactName: drop_BuildStage_Main + workflow: vsce + vsce: + serviceConnection: "CosmosDB VSCode Publishing" # azureRM service connection for the managed identity used to publish the extension. Only this publishing auth method is supported. + vsixPath: "$(vsixFileName)" # Path to VSIX file in artifact + #preRelease: true # default false. Whether the extension is a pre-release. + signaturePath: $(System.DefaultWorkingDirectory)/extension.signature.p7s # optional + manifestPath: $(System.DefaultWorkingDirectory)/extension.manifest # optional + useCustomVSCE: true # for the time being, you must supply a feed in your project with @vscode/vsce@3.3.2 + feed: + organization: msdata + project: CosmosDB + feedName: vscode-documentdb + steps: + # we need a noop step otherwise the vsce template won't run + - pwsh: Write-Output "Done" + condition: ${{ eq(parameters.dryRun, true) }} # noop this condition is always false + displayName: "\U0001F449 Post-Publishing" From f616c6f8ca0158d89200b318b55fdd8d32dfd58b Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 10 Oct 2025 10:16:11 +0000 Subject: [PATCH 73/88] feat: added documentation for azure discovery account management --- docs/learn-more/index.md | 2 + docs/learn-more/managing-azure-discovery.md | 155 ++++++++++++++++++ ...discovery-azure-cosmosdb-for-mongodb-ru.md | 32 +++- ...covery-azure-cosmosdb-for-mongodb-vcore.md | 33 ++-- .../learn-more/service-discovery-azure-vms.md | 61 +++++-- 5 files changed, 249 insertions(+), 34 deletions(-) create mode 100644 docs/learn-more/managing-azure-discovery.md diff --git a/docs/learn-more/index.md b/docs/learn-more/index.md index 18fe4bcd4..a99aa726d 100644 --- a/docs/learn-more/index.md +++ b/docs/learn-more/index.md @@ -11,7 +11,9 @@ This section contains additional documentation for features and concepts in Docu ## Available Topics - [Service Discovery](./service-discovery.md) + - [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery.md) - [Service Discovery: Azure CosmosDB for MongoDB (vCore)](./service-discovery-azure-cosmosdb-for-mongodb-vcore.md) + - [Service Discovery: Azure CosmosDB for MongoDB (RU)](./service-discovery-azure-cosmosdb-for-mongodb-ru.md) - [Service Discovery: Azure VMs (DocumentDB)](./service-discovery-azure-vms.md) - [Local Connection](./local-connection.md) - [Local Connection: Azure CosmosDB for MongoDB (RU) Emulator](./local-connection-mongodb-ru.md) diff --git a/docs/learn-more/managing-azure-discovery.md b/docs/learn-more/managing-azure-discovery.md new file mode 100644 index 000000000..8d3494ef1 --- /dev/null +++ b/docs/learn-more/managing-azure-discovery.md @@ -0,0 +1,155 @@ + + +> **Learn More** — [Back to Learn More Index](./index) + +--- + +# Managing Azure Discovery (Accounts, Tenants, and Subscriptions) + +When using Azure-based service discovery providers in DocumentDB for VS Code, you have access to shared features for managing your Azure credentials and filtering which resources are displayed. These features are consistent across all Azure service discovery providers: + +- [Azure CosmosDB for MongoDB (RU)](./service-discovery-azure-cosmosdb-for-mongodb-ru) +- [Azure CosmosDB for MongoDB (vCore)](./service-discovery-azure-cosmosdb-for-mongodb-vcore) +- [Azure VMs (DocumentDB)](./service-discovery-azure-vms) + +--- + +## Managing Azure Accounts + +The **Manage Credentials** feature allows you to view and manage which Azure accounts are being used for service discovery within the extension. + +### How to Access + +You can access the credential management feature in two ways: + +1. **From the context menu**: Right-click on an Azure service discovery provider and select `Manage Credentials...` +2. **From the Service Discovery panel**: Click the `key icon` next to the service discovery provider name + +### Available Actions + +When you open the credential management wizard, you can: + +1. **View signed-in accounts**: See all Azure accounts currently authenticated in VS Code and available for service discovery +2. **Sign in with a different account**: Add additional Azure accounts for accessing more resources +3. **View active account details**: See which account is currently being used for a specific service discovery provider +4. **Exit without changes**: Close the wizard without making modifications + +### Account Selection + +``` +┌────────────────────────────────────────────┐ +│ Azure accounts used for service discovery │ +├────────────────────────────────────────────┤ +│ 👤 user1@contoso.com │ +│ 👤 user2@fabrikam.com │ +├────────────────────────────────────────────┤ +│ 🔐 Sign in with a different account… │ +│ ✖️ Exit without making changes │ +└────────────────────────────────────────────┘ +``` + +### Signing Out from an Azure Account + +The credential management wizard does **not** provide a sign-out option. If you need to sign out from an Azure account: + +1. Click on the **"Accounts"** icon in the VS Code Activity Bar (bottom left corner) +2. Select the account you want to sign out from +3. Choose **"Sign Out"** + +> **⚠️ Important**: Signing out from an Azure account in VS Code will sign you out globally across VS Code, not just from the DocumentDB for VS Code extension. This may affect other extensions that use the same Azure account. + +--- + +## Filtering Azure Resources + +The **Filter** feature allows you to control which Azure resources are displayed in the Service Discovery panel by selecting specific tenants and subscriptions. + +### How to Access + +You can access the filtering feature by clicking the **funnel icon** next to the service discovery provider name in the Service Discovery panel. + +### Filtering Flow + +The filtering wizard guides you through selecting which Azure resources to display: + +#### Single-Tenant Scenario + +If you have access to only one Azure tenant, the wizard will skip tenant selection and proceed directly to subscription filtering: + +``` +┌────────────────────────────────────────────┐ +│ Select subscriptions to include in │ +│ service discovery │ +├────────────────────────────────────────────┤ +│ ☑️ Production Subscription │ +│ (sub-id-123) (Contoso) │ +│ ☑️ Development Subscription │ +│ (sub-id-456) (Contoso) │ +│ ☐ Test Subscription │ +│ (sub-id-789) (Contoso) │ +└────────────────────────────────────────────┘ +``` + +#### Multi-Tenant Scenario + +If you have access to multiple Azure tenants, the wizard will first ask you to select tenants, then filter subscriptions based on your tenant selection: + +``` +Step 1: Select Tenants +┌────────────────────────────────────────────┐ +│ Select tenants to include in subscription │ +│ discovery │ +├────────────────────────────────────────────┤ +│ ☑️ Contoso │ +│ (tenant-id-123) contoso.onmicrosoft.com │ +│ ☑️ Fabrikam │ +│ (tenant-id-456) fabrikam.onmicrosoft.com │ +│ ☐ Adventure Works │ +│ (tenant-id-789) adventureworks.com │ +└────────────────────────────────────────────┘ + +Step 2: Select Subscriptions (filtered by selected tenants) +┌────────────────────────────────────────────┐ +│ Select subscriptions to include in │ +│ service discovery │ +├────────────────────────────────────────────┤ +│ ☑️ Contoso Production │ +│ (sub-id-123) (Contoso) │ +│ ☑️ Contoso Development │ +│ (sub-id-456) (Contoso) │ +│ ☑️ Fabrikam Production │ +│ (sub-id-789) (Fabrikam) │ +└────────────────────────────────────────────┘ +``` + +### Filter Persistence + +Your filtering selections are **automatically saved and persisted** across VS Code sessions. When you reopen the filtering wizard, your previous selections will be pre-selected, making it easy to adjust your filters incrementally. + +### How Filtering Works in Different Contexts + +The filtering behavior differs depending on how you access service discovery: + +#### From the Service Discovery Panel + +When working within the **Service Discovery** panel in the sidebar: + +- Your filter selections (tenants and subscriptions) are **applied automatically** +- Only resources from selected tenants and subscriptions are displayed +- The filter persists until you change it + +#### From the "Add New Connection" Wizard + +When adding a new connection via the **"Add New Connection"** wizard: + +- **No filtering is applied** by default +- You will see **all subscriptions from all tenants** you have access to +- You must select one subscription to continue, but the full list is available +- This ensures you can always access any resource when explicitly adding a connection + +## Related Documentation + +- [Service Discovery Overview](./service-discovery) +- [Azure CosmosDB for MongoDB (RU) Service Discovery](./service-discovery-azure-cosmosdb-for-mongodb-ru) +- [Azure CosmosDB for MongoDB (vCore) Service Discovery](./service-discovery-azure-cosmosdb-for-mongodb-vcore) +- [Azure VMs (DocumentDB) Service Discovery](./service-discovery-azure-vms) diff --git a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-ru.md b/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-ru.md index 3df3b4ff5..762a06fd9 100644 --- a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-ru.md +++ b/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-ru.md @@ -9,6 +9,12 @@ The **Azure CosmosDB for MongoDB (RU)** plugin is available as part of the [Service Discovery](./service-discovery) feature in DocumentDB for VS Code. This plugin helps you find and connect to Azure Cosmos DB accounts provisioned with Request Units (RU) for the MongoDB API by handling authentication, resource discovery, and connection creation from inside the extension. +> **📘 Managing Azure Resources**: This provider shares common Azure management features with other Azure-based providers. See [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery) for detailed information about: +> +> - Managing Azure accounts and credentials +> - Filtering by tenants and subscriptions +> - Troubleshooting common issues + ## How to Access You can access this plugin in two ways: @@ -23,24 +29,32 @@ You can access this plugin in two ways: When you use the Azure CosmosDB for MongoDB (RU) plugin, the extension performs the following steps: 1. **Authentication:** - The plugin uses your Azure credentials available in VS Code. If needed, it will prompt you to sign in via the standard Azure sign-in flows. + The plugin uses your Azure credentials available in VS Code. If needed, it will prompt you to sign in via the standard Azure sign-in flows. See [Managing Azure Accounts](./managing-azure-discovery#managing-azure-accounts) for details on managing your credentials. + +2. **Resource Filtering (Service Discovery Panel):** + When accessing this plugin from the Service Discovery panel, you can control which resources are displayed by filtering tenants and subscriptions. Click the funnel icon next to the provider name to configure filters. See [Filtering Azure Resources](./managing-azure-discovery#filtering-azure-resources) for more information. -2. **Subscription Discovery:** - The plugin lists subscriptions available to your account so you can pick where to search for resources. +3. **Subscription and Account Discovery:** + - **From Service Discovery Panel**: The plugin lists subscriptions based on your configured filters, allowing you to browse RU-based Cosmos DB accounts within selected subscriptions. + - **From Add New Connection Wizard**: All subscriptions from all tenants are shown without pre-filtering. You select one subscription to view its resources. -3. **Account Discovery:** +4. **Account Discovery:** The provider queries Azure using the CosmosDB Management Client and filters results by the MongoDB "kind" for RU-based accounts. This ensures the list contains accounts that support the MongoDB API under RU provisioning. -4. **Connection Options:** +5. **Connection Options:** - Expand an account entry to view databases and connection options. - Save an account to your `DocumentDB Connections` list using the context menu or the save icon next to its name. - When connecting or saving, the extension will extract credentials or connection details from Azure where available. If multiple authentication methods are supported, you will be prompted to choose one. -## Additional Notes +## Managing Credentials and Filters + +This provider supports the following management features: + +- **Manage Credentials**: View and manage Azure accounts used for service discovery. Right-click the provider or click the gear icon. +- **Filter Resources**: Control which tenants and subscriptions are displayed. Click the funnel icon next to the provider name. +- **Refresh**: Reload the resource list after making changes. Click the refresh icon. -- You can filter subscriptions in the Service Discovery panel to limit the scope of discovery if you have access to many subscriptions. -- The provider reuses shared authentication and subscription selection flows used across other Service Discovery plugins. -- If you save a discovered account, the saved connection will appear in your Connections view for later use. +For detailed instructions on these features, see [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery). ## Feedback and Contributions diff --git a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md b/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md index 6d9d91aaf..2cf5f5ef6 100644 --- a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md +++ b/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md @@ -8,6 +8,12 @@ The **Azure CosmosDB for MongoDB (vCore)** plugin is available as part of the [Service Discovery](./service-discovery) feature in DocumentDB for VS Code. This plugin helps you connect to your Azure CosmosDB for MongoDB (vCore) clusters by handling authentication, resource discovery, and connection management within the extension. +> **📘 Managing Azure Resources**: This provider shares common Azure management features with other Azure-based providers. See [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery) for detailed information about: +> +> - Managing Azure accounts and credentials +> - Filtering by tenants and subscriptions +> - Troubleshooting common issues + ## How to Access You can access this plugin in two ways: @@ -22,27 +28,32 @@ You can access this plugin in two ways: When you use the Azure CosmosDB for MongoDB (vCore) plugin, the following steps are performed: 1. **Authentication:** - The plugin authenticates you with Azure using your credentials. + The plugin authenticates you with Azure using your credentials. See [Managing Azure Accounts](./managing-azure-discovery#managing-azure-accounts) for details on managing your Azure accounts. -2. **Subscription Discovery:** - All available Azure subscriptions are listed. +2. **Resource Filtering (Service Discovery Panel):** + When accessing this plugin from the Service Discovery panel, you can control which resources are displayed by filtering tenants and subscriptions. Click the funnel icon next to the provider name to configure filters. See [Filtering Azure Resources](./managing-azure-discovery#filtering-azure-resources) for more information. - > **Tip:** You can `filter` which subscriptions are shown in the `Service Discovery` panel. Click the funnel icon next to the service discovery provider name, wait for the list to populate, and select the subscriptions you want to include. - > - > ![Service Discovery Filter Feature Location](./images/service-discovery-filter-azure-vcore.png) +3. **Subscription and Cluster Discovery:** + - **From Service Discovery Panel**: The plugin lists subscriptions based on your configured filters, allowing you to browse vCore clusters within selected subscriptions. + - **From Add New Connection Wizard**: All subscriptions from all tenants are shown without pre-filtering. You select one subscription to view its resources. -3. **Cluster Discovery:** +4. **Cluster Discovery:** The plugin enumerates all Azure CosmosDB for MongoDB (vCore) clusters available in your selected subscriptions. -4. **Connection Options:** +5. **Connection Options:** - You can connect to a cluster by expanding its entry in the tree view. - You can save a cluster to your `DocumentDB Connections` list using the context menu or by clicking the save icon next to its name. - When connecting or saving, the extension detects the authentication methods supported by the cluster (e.g., **Username/Password** or **Entra ID**). If multiple are available, you will be prompted to choose your preferred method. -## Additional Notes +## Managing Credentials and Filters + +This provider supports the following management features: + +- **Manage Credentials**: View and manage Azure accounts used for service discovery. Right-click the provider or click the gear icon. +- **Filter Resources**: Control which tenants and subscriptions are displayed. Click the funnel icon next to the provider name. +- **Refresh**: Reload the resource list after making changes. Click the refresh icon. -- Subscription filtering helps you focus on relevant resources, especially if you have access to many Azure subscriptions. -- All authentication and discovery steps are handled within the extension, so you do not need to manually gather connection strings or resource details. +For detailed instructions on these features, see [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery). ## Feedback and Contributions diff --git a/docs/learn-more/service-discovery-azure-vms.md b/docs/learn-more/service-discovery-azure-vms.md index 2cf9f55f8..f65d622c6 100644 --- a/docs/learn-more/service-discovery-azure-vms.md +++ b/docs/learn-more/service-discovery-azure-vms.md @@ -8,6 +8,12 @@ The **Azure VMs (DocumentDB)** plugin is available as part of the [Service Discovery](./service-discovery) feature in DocumentDB for VS Code. This plugin helps you locate and connect to your virtual machines hosted in Azure that are running self-hosted DocumentDB or MongoDB instances. +> **📘 Managing Azure Resources**: This provider shares common Azure management features with other Azure-based providers. See [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery) for detailed information about: +> +> - Managing Azure accounts and credentials +> - Filtering by tenants and subscriptions +> - Troubleshooting common issues + ## How to Access You can access this plugin in two ways: @@ -22,29 +28,56 @@ You can access this plugin in two ways: When you use the Azure VMs (DocumentDB) plugin, the following steps are performed: 1. **Authentication:** - The plugin authenticates you with Azure using your credentials. + The plugin authenticates you with Azure using your credentials. See [Managing Azure Accounts](./managing-azure-discovery#managing-azure-accounts) for details on managing your Azure accounts. -2. **Subscription Discovery:** - All available Azure subscriptions are listed. +2. **Resource Filtering (Service Discovery Panel):** + When accessing this plugin from the Service Discovery panel, you can control which resources are displayed by filtering tenants and subscriptions. Click the funnel icon next to the provider name to configure filters. See [Filtering Azure Resources](./managing-azure-discovery#filtering-azure-resources) for more information. - > **Tip:** You can `filter` which subscriptions are shown in the `Service Discovery` panel. Click the funnel icon next to the service discovery provider name, wait for the list to populate, and select the subscriptions you want to include. - > - > ![Service Discovery Filter Feature Location](./images/service-discovery-filter-vm.png) +3. **Subscription Discovery:** + - **From Service Discovery Panel**: The plugin lists subscriptions based on your configured filters, allowing you to browse VMs within selected subscriptions. + - **From Add New Connection Wizard**: All subscriptions from all tenants are shown without pre-filtering. You select one subscription to view its resources. -3. **VM Filtering by Tag:** - The plugin searches for virtual machines within your selected subscriptions that have a specific tag assigned. By default, the tag is set to `DocumentDB`, but you can change this in the filter function as needed. +4. **VM Filtering by Tag:** + The plugin searches for virtual machines within your selected subscriptions that have a specific tag assigned. + - **Default Tag**: By default, the tag is set to `DocumentDB` + - **Custom Tags**: When using Service Discovery from within the `DocumentDB Connections` area (via "Add New Connection"), you'll be prompted to confirm or change the tag used for filtering + - **Service Discovery Panel**: The Service Discovery panel works with the default `DocumentDB` tag, but you can change this using the filter feature - > **Tip:** When using Service Discovery from within the `DocumentDB Connections` area, you'll always be asked to confirm the `tag` used. The `Service Discovery` area works with the default `DocumentDB` tag. Changing it is possible using the `filter` feature. + > **💡 Tip**: To use this plugin effectively, ensure your Azure VMs running DocumentDB or MongoDB instances are tagged appropriately. You can add or modify tags in the Azure Portal under the VM's "Tags" section. -4. **Connection Options:** +5. **Connection Options:** - You can connect to a VM by expanding its entry in the tree view. - You can save a VM to your `DocumentDB Connections` list using the context menu or by clicking the save icon next to its name. + - When connecting, you'll be prompted to provide connection details for the DocumentDB/MongoDB instance running on the VM. + +## Managing Credentials and Filters + +This provider supports the following management features: + +- **Manage Credentials**: View and manage Azure accounts used for service discovery. Right-click the provider or click the gear icon. +- **Filter Resources**: Control which tenants and subscriptions are displayed, and customize the VM tag filter. Click the funnel icon next to the provider name. +- **Refresh**: Reload the resource list after making changes. Click the refresh icon. + +For detailed instructions on account and subscription management, see [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery). + +### VM-Specific Filtering + +In addition to the standard tenant and subscription filtering, the Azure VMs provider includes tag-based filtering: + +``` +Filter Flow for Azure VMs: -## Additional Notes +Step 1: Select Tenants (if multi-tenant) +Step 2: Select Subscriptions +Step 3: Configure VM Tag Filter +┌────────────────────────────────────────────┐ +│ Enter the tag name to filter VMs │ +├────────────────────────────────────────────┤ +│ DocumentDB │ ← Default value +└────────────────────────────────────────────┘ +``` -- Tag-based filtering helps you focus on relevant virtual machines, especially if you have many resources in your Azure environment. -- All authentication and discovery steps are handled within the extension, so you do not need to manually gather connection strings or VM details. -- You can change the tag used for filtering in the filter function if your environment uses a different tagging convention. +The tag filter is also persisted and will be pre-filled with your last selection when you reopen the filter wizard. ## Feedback and Contributions From b53a6736d1ba9102108d076e9a25f7b17e659b31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:09:59 +0000 Subject: [PATCH 74/88] chore(deps-dev): bump tar-fs from 2.1.3 to 2.1.4 Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.3 to 2.1.4. - [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.3...v2.1.4) --- updated-dependencies: - dependency-name: tar-fs dependency-version: 2.1.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4363c39d..5d33696d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18258,9 +18258,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", "optional": true, From b08ce9b209bfc8205c45273f315ab3d93eb79831 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 10 Oct 2025 16:00:57 +0200 Subject: [PATCH 75/88] feat: content for the help and feedback view --- package.json | 2 +- src/documentdb/ClustersExtension.ts | 12 ++ src/documentdb/Views.ts | 1 + src/extensionVariables.ts | 3 + .../HelpAndFeedbackBranchDataProvider.ts | 143 ++++++++++++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts diff --git a/package.json b/package.json index eab1f8ea0..d9e6fc046 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "icon": "$(plug)" }, { - "id": "documentDBHelp", + "id": "helpAndFeedbackView", "name": "Help and Feedback", "visibility": "collapsed", "icon": "$(question)" diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 545060e60..611cc2935 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -58,6 +58,7 @@ import { ClustersWorkspaceBranchDataProvider } from '../tree/azure-workspace-vie import { DocumentDbWorkspaceResourceProvider } from '../tree/azure-workspace-view/DocumentDbWorkspaceResourceProvider'; import { ConnectionsBranchDataProvider } from '../tree/connections-view/ConnectionsBranchDataProvider'; import { DiscoveryBranchDataProvider } from '../tree/discovery-view/DiscoveryBranchDataProvider'; +import { HelpAndFeedbackBranchDataProvider } from '../tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider'; import { registerCommandWithModalErrors, registerCommandWithTreeNodeUnwrappingAndModalErrors, @@ -101,6 +102,16 @@ export class ClustersExtension implements vscode.Disposable { ext.context.subscriptions.push(treeView); } + registerHelpAndFeedbackTree(_activateContext: IActionContext): void { + ext.helpAndFeedbackBranchDataProvider = new HelpAndFeedbackBranchDataProvider(); + + const treeView = vscode.window.createTreeView(Views.HelpAndFeedbackView, { + treeDataProvider: ext.helpAndFeedbackBranchDataProvider, + }); + + ext.context.subscriptions.push(treeView); + } + async registerAzureResourcesIntegration(activateContext: IActionContext): Promise { // Dynamic registration so this file compiles when the enum members aren't present // This is how we detect whether the update to Azure Resources has been deployed @@ -151,6 +162,7 @@ export class ClustersExtension implements vscode.Disposable { this.registerDiscoveryServices(activateContext); this.registerConnectionsTree(activateContext); this.registerDiscoveryTree(activateContext); + this.registerHelpAndFeedbackTree(activateContext); //// General Commands: diff --git a/src/documentdb/Views.ts b/src/documentdb/Views.ts index 7e95735ec..d5f331908 100644 --- a/src/documentdb/Views.ts +++ b/src/documentdb/Views.ts @@ -8,6 +8,7 @@ export enum Views { DiscoveryView = 'discoveryView', // do not change this value AzureResourcesView = 'azureResourcesView', AzureWorkspaceView = 'azureWorkspaceView', + HelpAndFeedbackView = 'helpAndFeedbackView', // do not change this value /** * Note to future maintainers: do not modify these string constants. diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 67ce815a7..623808eb2 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -14,6 +14,7 @@ import { type ClustersWorkspaceBranchDataProvider } from './tree/azure-workspace import { type DocumentDbWorkspaceResourceProvider } from './tree/azure-workspace-view/DocumentDbWorkspaceResourceProvider'; import { type ConnectionsBranchDataProvider } from './tree/connections-view/ConnectionsBranchDataProvider'; import { type DiscoveryBranchDataProvider } from './tree/discovery-view/DiscoveryBranchDataProvider'; +import { type HelpAndFeedbackBranchDataProvider } from './tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider'; import { type TreeElement } from './tree/TreeElement'; /** @@ -54,6 +55,8 @@ export namespace ext { export let discoveryBranchDataProvider: DiscoveryBranchDataProvider; + export let helpAndFeedbackBranchDataProvider: HelpAndFeedbackBranchDataProvider; + export namespace settingsKeys { export const shellPath = 'documentDB.mongoShell.path'; export const shellArgs = 'documentDB.mongoShell.args'; diff --git a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts new file mode 100644 index 000000000..418eea906 --- /dev/null +++ b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + callWithTelemetryAndErrorHandling, + createGenericElement, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { Views } from '../../documentdb/Views'; +import { BaseExtendedTreeDataProvider } from '../BaseExtendedTreeDataProvider'; +import { type TreeElement } from '../TreeElement'; +import { isTreeElementWithContextValue } from '../TreeElementWithContextValue'; + +/** + * Tree data provider for the Help and Feedback view. + * + * This provider displays a static list of helpful links including: + * - What's New (changelog) + * - Extension Documentation + * - DocumentDB Documentation + * - Suggest a Feature (HATs survey) + * - Report a Bug + * - Create Free Azure DocumentDB Cluster + * + * All items are leaf nodes (no children) that open external links when clicked. + */ +export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvider { + constructor() { + super(); + } + + async getChildren(element?: TreeElement): Promise { + return callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.view = Views.HelpAndFeedbackView; + + if (!element) { + context.telemetry.properties.parentNodeContext = 'root'; + + // Clear cache for root-level items + this.clearParentCache(); + + const rootItems = this.getRootItems(); + + // Process root items + if (rootItems) { + for (const item of rootItems) { + if (isTreeElementWithContextValue(item)) { + this.appendContextValues(item, Views.HelpAndFeedbackView); + } + + // Register root items in cache + this.registerNodeInCache(item); + } + } + + return rootItems; + } + + // No children for leaf nodes + context.telemetry.properties.parentNodeContext = (await element.getTreeItem()).contextValue; + return undefined; + }); + } + + /** + * Helper function to get the root items of the help and feedback tree. + * These are static link items with no children. + */ + private getRootItems(): TreeElement[] | null | undefined { + const parentId = Views.HelpAndFeedbackView; + + const rootItems: TreeElement[] = [ + createGenericElement({ + contextValue: 'helpItem', + id: `${parentId}/whats-new`, + label: vscode.l10n.t("What's New"), + iconPath: new vscode.ThemeIcon('megaphone'), + commandId: 'vscode.open', + commandArgs: [ + vscode.Uri.parse('https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md'), + ], + }) as TreeElement, + + createGenericElement({ + contextValue: 'helpItem', + id: `${parentId}/extension-docs`, + label: vscode.l10n.t('Extension Documentation'), + iconPath: new vscode.ThemeIcon('book'), + commandId: 'vscode.open', + commandArgs: [vscode.Uri.parse('https://github.com/microsoft/vscode-documentdb#readme')], + }) as TreeElement, + + createGenericElement({ + contextValue: 'helpItem', + id: `${parentId}/documentdb-docs`, + label: vscode.l10n.t('DocumentDB Documentation'), + iconPath: new vscode.ThemeIcon('library'), + commandId: 'vscode.open', + commandArgs: [vscode.Uri.parse('https://github.com/microsoft/documentdb')], + }) as TreeElement, + + createGenericElement({ + contextValue: 'feedbackItem', + id: `${parentId}/suggest-feature`, + label: vscode.l10n.t('Suggest a Feature'), + iconPath: new vscode.ThemeIcon('lightbulb'), + commandId: 'vscode.open', + commandArgs: [ + vscode.Uri.parse( + 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=feature-request&template=feature_request.md', + ), + ], + }) as TreeElement, + + createGenericElement({ + contextValue: 'feedbackItem', + id: `${parentId}/report-bug`, + label: vscode.l10n.t('Report a Bug'), + iconPath: new vscode.ThemeIcon('bug'), + commandId: 'vscode.open', + commandArgs: [ + vscode.Uri.parse( + 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=bug&template=bug_report.md', + ), + ], + }) as TreeElement, + + createGenericElement({ + contextValue: 'actionItem', + id: `${parentId}/create-free-cluster`, + label: vscode.l10n.t('Create Free Azure DocumentDB Cluster'), + iconPath: new vscode.ThemeIcon('add'), + commandId: 'vscode.open', + commandArgs: [vscode.Uri.parse('https://aka.ms/tryvcore')], + }) as TreeElement, + ]; + + return rootItems; + } +} From 51dec118acb7da7ec9592e48a65b45d536d940cc Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 10 Oct 2025 16:07:26 +0200 Subject: [PATCH 76/88] updated the open url command --- .../helpAndFeedback.openUrl/openUrl.ts | 22 ++++++++++++ src/documentdb/ClustersExtension.ts | 3 ++ .../HelpAndFeedbackBranchDataProvider.ts | 36 +++++++++---------- 3 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 src/commands/helpAndFeedback.openUrl/openUrl.ts diff --git a/src/commands/helpAndFeedback.openUrl/openUrl.ts b/src/commands/helpAndFeedback.openUrl/openUrl.ts new file mode 100644 index 000000000..0d7f734bd --- /dev/null +++ b/src/commands/helpAndFeedback.openUrl/openUrl.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { openUrl } from '../../utils/openUrl'; + +/** + * Opens a URL from the Help and Feedback view with telemetry tracking. + * + * @param context - Action context for telemetry + * @param url - The URL to open + */ +export async function openHelpAndFeedbackUrl(context: IActionContext, url: string): Promise { + // Log the URL to telemetry + context.telemetry.properties.url = url; + context.telemetry.properties.source = 'helpAndFeedbackView'; + + // Open the URL + await openUrl(url); +} diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 611cc2935..bfa06a545 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -30,6 +30,7 @@ import { deleteAzureDatabase } from '../commands/deleteDatabase/deleteDatabase'; import { filterProviderContent } from '../commands/discoveryService.filterProviderContent/filterProviderContent'; import { manageCredentials } from '../commands/discoveryService.manageCredentials/manageCredentials'; import { exportEntireCollection, exportQueryResults } from '../commands/exportDocuments/exportDocuments'; +import { openHelpAndFeedbackUrl } from '../commands/helpAndFeedback.openUrl/openUrl'; import { importDocuments } from '../commands/importDocuments/importDocuments'; import { launchShell } from '../commands/launchShell/launchShell'; import { learnMoreAboutServiceProvider } from '../commands/learnMoreAboutServiceProvider/learnMoreAboutServiceProvider'; @@ -273,6 +274,8 @@ export class ClustersExtension implements vscode.Disposable { registerCommand('vscode-documentdb.command.internal.documentView.open', openDocumentView); + registerCommand('vscode-documentdb.command.internal.helpAndFeedback.openUrl', openHelpAndFeedbackUrl); + registerCommandWithTreeNodeUnwrapping('vscode-documentdb.command.internal.retry', retryAuthentication); registerCommandWithTreeNodeUnwrapping('vscode-documentdb.command.internal.revealView', revealView); diff --git a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts index 418eea906..f1e1e1ba3 100644 --- a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts +++ b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts @@ -21,11 +21,13 @@ import { isTreeElementWithContextValue } from '../TreeElementWithContextValue'; * - What's New (changelog) * - Extension Documentation * - DocumentDB Documentation - * - Suggest a Feature (HATs survey) - * - Report a Bug + * - Suggest a Feature (GitHub issue template) + * - Report a Bug (GitHub issue template) * - Create Free Azure DocumentDB Cluster * * All items are leaf nodes (no children) that open external links when clicked. + * The links are opened using the 'vscode-documentdb.command.internal.helpAndFeedback.openUrl' command, + * which provides telemetry tracking for each URL opened. */ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvider { constructor() { @@ -78,10 +80,8 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi id: `${parentId}/whats-new`, label: vscode.l10n.t("What's New"), iconPath: new vscode.ThemeIcon('megaphone'), - commandId: 'vscode.open', - commandArgs: [ - vscode.Uri.parse('https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md'), - ], + commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', + commandArgs: ['https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md'], }) as TreeElement, createGenericElement({ @@ -89,8 +89,8 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi id: `${parentId}/extension-docs`, label: vscode.l10n.t('Extension Documentation'), iconPath: new vscode.ThemeIcon('book'), - commandId: 'vscode.open', - commandArgs: [vscode.Uri.parse('https://github.com/microsoft/vscode-documentdb#readme')], + commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', + commandArgs: ['https://github.com/microsoft/vscode-documentdb#readme'], }) as TreeElement, createGenericElement({ @@ -98,8 +98,8 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi id: `${parentId}/documentdb-docs`, label: vscode.l10n.t('DocumentDB Documentation'), iconPath: new vscode.ThemeIcon('library'), - commandId: 'vscode.open', - commandArgs: [vscode.Uri.parse('https://github.com/microsoft/documentdb')], + commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', + commandArgs: ['https://github.com/microsoft/documentdb'], }) as TreeElement, createGenericElement({ @@ -107,11 +107,9 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi id: `${parentId}/suggest-feature`, label: vscode.l10n.t('Suggest a Feature'), iconPath: new vscode.ThemeIcon('lightbulb'), - commandId: 'vscode.open', + commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', commandArgs: [ - vscode.Uri.parse( - 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=feature-request&template=feature_request.md', - ), + 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=feature-request&template=feature_request.md', ], }) as TreeElement, @@ -120,11 +118,9 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi id: `${parentId}/report-bug`, label: vscode.l10n.t('Report a Bug'), iconPath: new vscode.ThemeIcon('bug'), - commandId: 'vscode.open', + commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', commandArgs: [ - vscode.Uri.parse( - 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=bug&template=bug_report.md', - ), + 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=bug&template=bug_report.md', ], }) as TreeElement, @@ -133,8 +129,8 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi id: `${parentId}/create-free-cluster`, label: vscode.l10n.t('Create Free Azure DocumentDB Cluster'), iconPath: new vscode.ThemeIcon('add'), - commandId: 'vscode.open', - commandArgs: [vscode.Uri.parse('https://aka.ms/tryvcore')], + commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', + commandArgs: ['https://aka.ms/tryvcore'], }) as TreeElement, ]; From 9ba3810483da3b2dfb91db01a20193a511e34fdb Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 10 Oct 2025 16:23:11 +0200 Subject: [PATCH 77/88] Updated target URLs --- l10n/bundle.l10n.json | 7 +++++++ .../HelpAndFeedbackBranchDataProvider.ts | 17 +++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 95bab5353..f0272f0a1 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -104,6 +104,7 @@ "Collection name cannot contain the null character.": "Collection name cannot contain the null character.", "Collection name is required.": "Collection name is required.", "Collection names should begin with an underscore or a letter character.": "Collection names should begin with an underscore or a letter character.", + "Comprehensive Changelog": "Comprehensive Changelog", "Configure Azure Discovery Filters": "Configure Azure Discovery Filters", "Configure Azure VM Discovery Filters": "Configure Azure VM Discovery Filters", "Configure Subscription Filter": "Configure Subscription Filter", @@ -130,6 +131,7 @@ "Create Collection…": "Create Collection…", "Create database": "Create database", "Create Database…": "Create Database…", + "Create Free Azure DocumentDB Cluster": "Create Free Azure DocumentDB Cluster", "Create new {0}...": "Create new {0}...", "Creating \"{nodeName}\"…": "Creating \"{nodeName}\"…", "Creating {0}...": "Creating {0}...", @@ -158,6 +160,7 @@ "Document must be an object.": "Document must be an object.", "Document must be an object. Skipping…": "Document must be an object. Skipping…", "DocumentDB and MongoDB Accounts": "DocumentDB and MongoDB Accounts", + "DocumentDB Documentation": "DocumentDB Documentation", "DocumentDB for VS Code is not signed in to Azure": "DocumentDB for VS Code is not signed in to Azure", "DocumentDB Local": "DocumentDB Local", "Documents": "Documents", @@ -216,6 +219,7 @@ "Exporting documents": "Exporting documents", "Exporting…": "Exporting…", "Extension dependency with id \"{0}\" must be updated.": "Extension dependency with id \"{0}\" must be updated.", + "Extension Documentation": "Extension Documentation", "Failed to access Azure Databases VS Code Extension storage for migration: {error}": "Failed to access Azure Databases VS Code Extension storage for migration: {error}", "Failed to connect to \"{cluster}\"": "Failed to connect to \"{cluster}\"", "Failed to connect to VM \"{vmName}\"": "Failed to connect to VM \"{vmName}\"", @@ -378,6 +382,7 @@ "Reload Window": "Reload Window", "Remind Me Later": "Remind Me Later", "Rename Connection": "Rename Connection", + "Report a Bug": "Report a Bug", "Report an issue": "Report an issue", "Resource group \"{0}\" already exists in subscription \"{1}\".": "Resource group \"{0}\" already exists in subscription \"{1}\".", "Return to the account list": "Return to the account list", @@ -433,6 +438,7 @@ "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}\".", + "Suggest a Feature": "Suggest a Feature", "Sure!": "Sure!", "Switch to the new \"Connections View\"…": "Switch to the new \"Connections View\"…", "Table View": "Table View", @@ -533,6 +539,7 @@ "WARNING: Cannot create resource group \"{0}\" because the selected subscription is a concierge subscription. Using resource group \"{1}\" instead.": "WARNING: Cannot create resource group \"{0}\" because the selected subscription is a concierge subscription. Using resource group \"{1}\" instead.", "WARNING: Provider \"{0}\" does not support location \"{1}\". Using \"{2}\" instead.": "WARNING: Provider \"{0}\" does not support location \"{1}\". Using \"{2}\" instead.", "WARNING: Resource does not support extended location \"{0}\". Using \"{1}\" instead.": "WARNING: Resource does not support extended location \"{0}\". Using \"{1}\" instead.", + "What's New": "What's New", "Where to save the exported documents?": "Where to save the exported documents?", "with Popover": "with Popover", "Working…": "Working…", diff --git a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts index f1e1e1ba3..4b505251d 100644 --- a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts +++ b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts @@ -81,6 +81,15 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi label: vscode.l10n.t("What's New"), iconPath: new vscode.ThemeIcon('megaphone'), commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', + commandArgs: ['https://github.com/microsoft/vscode-documentdb/discussions/categories/announcements'], + }) as TreeElement, + + createGenericElement({ + contextValue: 'helpItem', + id: `${parentId}/changelog`, + label: vscode.l10n.t('Comprehensive Changelog'), + iconPath: new vscode.ThemeIcon('history'), + commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', commandArgs: ['https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md'], }) as TreeElement, @@ -90,7 +99,7 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi label: vscode.l10n.t('Extension Documentation'), iconPath: new vscode.ThemeIcon('book'), commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', - commandArgs: ['https://github.com/microsoft/vscode-documentdb#readme'], + commandArgs: ['https://microsoft.github.io/vscode-documentdb/'], }) as TreeElement, createGenericElement({ @@ -99,7 +108,7 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi label: vscode.l10n.t('DocumentDB Documentation'), iconPath: new vscode.ThemeIcon('library'), commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', - commandArgs: ['https://github.com/microsoft/documentdb'], + commandArgs: ['https://documentdb.io'], }) as TreeElement, createGenericElement({ @@ -109,7 +118,7 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi iconPath: new vscode.ThemeIcon('lightbulb'), commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', commandArgs: [ - 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=feature-request&template=feature_request.md', + 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=user%20feedback', ], }) as TreeElement, @@ -120,7 +129,7 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi iconPath: new vscode.ThemeIcon('bug'), commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', commandArgs: [ - 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=bug&template=bug_report.md', + 'https://github.com/microsoft/vscode-documentdb/issues/new?assignees=&labels=user%20feedback,bug', ], }) as TreeElement, From c116f65ff622c7dde40346bc8c78aba1bc66b9a2 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 10 Oct 2025 16:34:33 +0200 Subject: [PATCH 78/88] Updated labels --- l10n/bundle.l10n.json | 2 +- .../help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index f0272f0a1..9c0cf9f31 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -84,6 +84,7 @@ "Browse to {mongoExecutableFileName}": "Browse to {mongoExecutableFileName}", "Cancel": "Cancel", "Change page size": "Change page size", + "Changelog": "Changelog", "Check document syntax": "Check document syntax", "Choose a cluster…": "Choose a cluster…", "Choose a RU cluster…": "Choose a RU cluster…", @@ -104,7 +105,6 @@ "Collection name cannot contain the null character.": "Collection name cannot contain the null character.", "Collection name is required.": "Collection name is required.", "Collection names should begin with an underscore or a letter character.": "Collection names should begin with an underscore or a letter character.", - "Comprehensive Changelog": "Comprehensive Changelog", "Configure Azure Discovery Filters": "Configure Azure Discovery Filters", "Configure Azure VM Discovery Filters": "Configure Azure VM Discovery Filters", "Configure Subscription Filter": "Configure Subscription Filter", diff --git a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts index 4b505251d..636a863a4 100644 --- a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts +++ b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts @@ -87,7 +87,7 @@ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvi createGenericElement({ contextValue: 'helpItem', id: `${parentId}/changelog`, - label: vscode.l10n.t('Comprehensive Changelog'), + label: vscode.l10n.t('Changelog'), iconPath: new vscode.ThemeIcon('history'), commandId: 'vscode-documentdb.command.internal.helpAndFeedback.openUrl', commandArgs: ['https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md'], From 00b439a3515616b53eba5be5c6438366a308c435 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 15:38:20 +0200 Subject: [PATCH 79/88] Version bump to `0.5.0` --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d33696d4..07b58bce8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb", - "version": "0.4.2-alpha", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb", - "version": "0.4.2-alpha", + "version": "0.5.0", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@azure/arm-compute": "^22.4.0", diff --git a/package.json b/package.json index d9e6fc046..812a698b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb", - "version": "0.4.2-alpha", + "version": "0.5.0", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "publisher": "ms-azuretools", "displayName": "DocumentDB for VS Code", From 33b8cc4df12e2087a71343b1127dea0c162258ca Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 16:15:40 +0200 Subject: [PATCH 80/88] chore: updated docs folder structure --- docs/index.md | 36 +++++++++--------- docs/learn-more/index.md | 21 ---------- docs/manual/index.md | 11 ------ docs/release-notes/0.2.1.md | 4 +- docs/release-notes/0.2.2.md | 4 +- docs/release-notes/0.2.3.md | 2 +- docs/release-notes/0.2.4.md | 2 +- docs/release-notes/0.3.md | 2 +- docs/release-notes/0.4.md | 4 +- .../data-migrations.md | 6 +-- .../how-to-construct-url.md | 5 +-- .../images/local-connection.png | Bin .../service-discovery-activation-vm.png | Bin .../images/service-discovery-activation.png | Bin .../service-discovery-filter-azure-vcore.png | Bin .../images/service-discovery-filter-vm.png | Bin .../images/service-discovery-filter.png | Bin .../images/service-discovery-introduction.png | Bin .../local-connection-documentdb-local.md | 4 +- .../local-connection-mongodb-ru.md | 4 +- .../local-connection.md | 5 +-- .../managing-azure-discovery.md | 10 ++--- ...discovery-azure-cosmosdb-for-mongodb-ru.md | 7 ++-- ...covery-azure-cosmosdb-for-mongodb-vcore.md | 6 +-- .../service-discovery-azure-vms.md | 6 +-- .../service-discovery.md | 4 +- 26 files changed, 49 insertions(+), 94 deletions(-) delete mode 100644 docs/learn-more/index.md delete mode 100644 docs/manual/index.md rename docs/{learn-more => user-manual}/data-migrations.md (96%) rename docs/{manual => user-manual}/how-to-construct-url.md (98%) rename docs/{learn-more => user-manual}/images/local-connection.png (100%) rename docs/{learn-more => user-manual}/images/service-discovery-activation-vm.png (100%) rename docs/{learn-more => user-manual}/images/service-discovery-activation.png (100%) rename docs/{learn-more => user-manual}/images/service-discovery-filter-azure-vcore.png (100%) rename docs/{learn-more => user-manual}/images/service-discovery-filter-vm.png (100%) rename docs/{learn-more => user-manual}/images/service-discovery-filter.png (100%) rename docs/{learn-more => user-manual}/images/service-discovery-introduction.png (100%) rename docs/{learn-more => user-manual}/local-connection-documentdb-local.md (84%) rename docs/{learn-more => user-manual}/local-connection-mongodb-ru.md (91%) rename docs/{learn-more => user-manual}/local-connection.md (95%) rename docs/{learn-more => user-manual}/managing-azure-discovery.md (95%) rename docs/{learn-more => user-manual}/service-discovery-azure-cosmosdb-for-mongodb-ru.md (92%) rename docs/{learn-more => user-manual}/service-discovery-azure-cosmosdb-for-mongodb-vcore.md (91%) rename docs/{learn-more => user-manual}/service-discovery-azure-vms.md (93%) rename docs/{learn-more => user-manual}/service-discovery.md (95%) diff --git a/docs/index.md b/docs/index.md index c3183d3b4..8deaa9f25 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,29 +42,31 @@ Your feedback, contributions, and ideas shape the future of the extension. ## User Manual -The User Manual provides guidance on using DocumentDB for VS Code: +The User Manual provides guidance on using DocumentDB for VS Code. It contains detailed documentation for specific features and concepts. These documents provide additional context and examples for features you encounter while using the extension: -- [How to Construct a URL That Opens a Connection in the Extension](./manual/how-to-construct-url.md) +### Connecting to Databases -## Learn More +- [Connecting with a URL](./user-manual/how-to-construct-url) +- [Service Discovery](./user-manual/service-discovery) + - [Azure Cosmos DB for MongoDB (vCore)](./user-manual/service-discovery-azure-cosmosdb-for-mongodb-vcore) + - [Azure Cosmos DB for MongoDB (RU)](./user-manual/service-discovery-azure-cosmosdb-for-mongodb-ru) + - [Azure VMs (DocumentDB)](./user-manual/service-discovery-azure-vms) + - [Managing Azure Subscriptions](./user-manual/managing-azure-discovery) +- [Connecting to Local Instances](./user-manual/local-connection) + - [Azure Cosmos DB for MongoDB (RU) Emulator](./user-manual/local-connection-mongodb-ru) + - [DocumentDB Local](./user-manual/local-connection-documentdb-local) -This section contains detailed documentation for specific features and concepts that are directly accessible from within the DocumentDB for VS Code extension. These documents provide additional context and examples for features you encounter while using the extension: +### Data Management -- [Service Discovery](./learn-more/service-discovery.md) - - [Service Discovery: Azure CosmosDB for MongoDB (vCore)](./learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md) - - [Service Discovery: Azure VMs (DocumentDB)](./learn-more/service-discovery-azure-vms.md) -- [Local Connection](./learn-more/local-connection.md) - - [Local Connection: Azure CosmosDB for MongoDB (RU) Emulator](./learn-more/local-connection-mongodb-ru.md) - - [Local Connection: DocumentDB Local](./learn-more/local-connection-documentdb-local.md) -- [Data Migrations](./learn-more/data-migrations.md) ⚠️ _Experimental_ +- [Data Migrations (Experimental)](./user-manual/data-migrations) ## Release Notes Explore the history of updates and improvements to the DocumentDB for VS Code extension. Each release brings new features, enhancements, and fixes to improve your experience. -- [0.4](./release-notes/0.4.md) -- [0.3, 0.3.1](./release-notes/0.3.md) -- [0.2.4](./release-notes/0.2.4.md) -- [0.2.3](./release-notes/0.2.3.md) -- [0.2.2](./release-notes/0.2.2.md) -- [0.2.1](./release-notes/0.2.1.md) +- [0.4](./release-notes/0.4) +- [0.3, 0.3.1](./release-notes/0.3) +- [0.2.4](./release-notes/0.2.4) +- [0.2.3](./release-notes/0.2.3) +- [0.2.2](./release-notes/0.2.2) +- [0.2.1](./release-notes/0.2.1) diff --git a/docs/learn-more/index.md b/docs/learn-more/index.md deleted file mode 100644 index a99aa726d..000000000 --- a/docs/learn-more/index.md +++ /dev/null @@ -1,21 +0,0 @@ - - -> **Manual** — [Back to Home](../index.md) - ---- - -# Learn More - -This section contains additional documentation for features and concepts in DocumentDB for VS Code. These documents are linked from within the DocumentDB for VS Code Extension, but we encourage you to explore this documentation directly for more details and context. - -## Available Topics - -- [Service Discovery](./service-discovery.md) - - [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](./managing-azure-discovery.md) - - [Service Discovery: Azure CosmosDB for MongoDB (vCore)](./service-discovery-azure-cosmosdb-for-mongodb-vcore.md) - - [Service Discovery: Azure CosmosDB for MongoDB (RU)](./service-discovery-azure-cosmosdb-for-mongodb-ru.md) - - [Service Discovery: Azure VMs (DocumentDB)](./service-discovery-azure-vms.md) -- [Local Connection](./local-connection.md) - - [Local Connection: Azure CosmosDB for MongoDB (RU) Emulator](./local-connection-mongodb-ru.md) - - [Local Connection: DocumentDB Local](./local-connection-documentdb-local.md) -- [Data Migrations](./data-migrations.md) ⚠️ _Experimental_ diff --git a/docs/manual/index.md b/docs/manual/index.md deleted file mode 100644 index 8013d6a80..000000000 --- a/docs/manual/index.md +++ /dev/null @@ -1,11 +0,0 @@ - - -> **Manual** — [Back to Home](../index.md) - ---- - -# Manual - -## How to Construct a URL That Opens a Connection in the Extension - -[Learn how to construct a URL that opens a connection in the extension](./how-to-construct-url.md) diff --git a/docs/release-notes/0.2.1.md b/docs/release-notes/0.2.1.md index 949a4c94a..34ccba08c 100644 --- a/docs/release-notes/0.2.1.md +++ b/docs/release-notes/0.2.1.md @@ -1,6 +1,6 @@ -> **Release Notes** — [Back to Home](../index.md) +> **Release Notes** — [Back to Release Notes](../index#release-notes) --- @@ -31,7 +31,7 @@ This release introduces two new **Service Discovery Providers**, making it easie These providers help reduce manual configuration so you can focus more on your data and applications. -[Learn more about Service Discovery Providers in DocumentDB for VS Code →](https://microsoft.github.io/vscode-documentdb/learn-more/service-discovery.html) +[Learn more about Service Discovery Providers in DocumentDB for VS Code →](../user-manual/service-discovery) ### 3️⃣ **Developer Productivity Features** diff --git a/docs/release-notes/0.2.2.md b/docs/release-notes/0.2.2.md index 8905c45f8..ec24e5f76 100644 --- a/docs/release-notes/0.2.2.md +++ b/docs/release-notes/0.2.2.md @@ -1,6 +1,6 @@ -> **Release Notes** — [Back to Home](../index.md) +> **Release Notes** — [Back to Release Notes](../index#release-notes) --- @@ -20,7 +20,7 @@ We're introducing an **experimental data migration framework** that enables thir - Rich context-aware workflows that pass database and collection info directly to the provider. - Custom UI integration, authentication handling, and progress tracking. -This is an opt-in preview aimed at extension authors and early adopters. [Learn how to participate in the preview →](https://microsoft.github.io/vscode-documentdb/data-migrations) +This is an opt-in preview aimed at extension authors and early adopters. ### 2️⃣ **URL Handler for Direct Database Navigation** diff --git a/docs/release-notes/0.2.3.md b/docs/release-notes/0.2.3.md index 25e50ef50..841883c57 100644 --- a/docs/release-notes/0.2.3.md +++ b/docs/release-notes/0.2.3.md @@ -1,6 +1,6 @@ -> **Release Notes** — [Back to Home](../index.md) +> **Release Notes** — [Back to Release Notes](../index#release-notes) --- diff --git a/docs/release-notes/0.2.4.md b/docs/release-notes/0.2.4.md index 1a1d39000..ab8ece60d 100644 --- a/docs/release-notes/0.2.4.md +++ b/docs/release-notes/0.2.4.md @@ -1,6 +1,6 @@ -> **Release Notes** — [Back to Home](../index.md) +> **Release Notes** — [Back to Release Notes](../index#release-notes) --- diff --git a/docs/release-notes/0.3.md b/docs/release-notes/0.3.md index bde2492a1..1f65a2a19 100644 --- a/docs/release-notes/0.3.md +++ b/docs/release-notes/0.3.md @@ -1,6 +1,6 @@ -> **Release Notes** — [Back to Home](../index.md) +> **Release Notes** — [Back to Release Notes](../index#release-notes) --- diff --git a/docs/release-notes/0.4.md b/docs/release-notes/0.4.md index be07f3239..a9820ef86 100644 --- a/docs/release-notes/0.4.md +++ b/docs/release-notes/0.4.md @@ -1,6 +1,6 @@ -> **Release Notes** — [Back to Home](../index.md) +> **Release Notes** — [Back to Release Notes](../index#release-notes) --- @@ -40,7 +40,7 @@ We've expanded our service discovery capabilities by adding a dedicated provider - **Consistent User Experience**: The new provider uses the same authentication and wizard-based workflow (select subscription → select cluster → connect) that users are already familiar with. - **Optimized for RU**: The provider uses RU-specific Azure APIs to ensure accurate and reliable discovery. -[Learn more about Service Discovery →](../learn-more/service-discovery.md) +[Learn more about Service Discovery →](../user-manual/service-discovery) ### 3️⃣ **Official DocumentDB Logo and Branding** ([#246](https://github.com/microsoft/vscode-documentdb/pull/246)) diff --git a/docs/learn-more/data-migrations.md b/docs/user-manual/data-migrations.md similarity index 96% rename from docs/learn-more/data-migrations.md rename to docs/user-manual/data-migrations.md index 96453fc49..509a506ef 100644 --- a/docs/learn-more/data-migrations.md +++ b/docs/user-manual/data-migrations.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Learn More Index](./index.md) +> **User Manual** — [Back to User Manual](../index#user-manual) --- @@ -81,7 +79,7 @@ This context allows providers to offer intelligent, targeted migration options b For detailed API documentation, plugin development information, and technical specifications, please refer to: -**[Migration API Documentation](https://github.com/microsoft/vscode-documentdb/tree/main/api/README.md)** +**[Migration API Documentation](https://github.com/microsoft/vscode-documentdb/tree/main/api/README)** The API documentation includes: diff --git a/docs/manual/how-to-construct-url.md b/docs/user-manual/how-to-construct-url.md similarity index 98% rename from docs/manual/how-to-construct-url.md rename to docs/user-manual/how-to-construct-url.md index 3bdf61efa..a6892d801 100644 --- a/docs/manual/how-to-construct-url.md +++ b/docs/user-manual/how-to-construct-url.md @@ -1,6 +1,4 @@ - - -> **Manual** — [Back to Documentation Index](./index.md) +> **User Manual** — [Back to User Manual](../index#user-manual) --- @@ -115,7 +113,6 @@ When you click a DocumentDB for VS Code URL, the following process occurs: 1. **Activation**: The `vscode://` prefix tells the operating system to activate VS Code. The `ms-azuretools.vscode-documentdb` segment activates the **DocumentDB for VS Code** extension. 2. **Connection Handling**: - - The extension parses the `connectionString` parameter and creates a new connection in the Connections View. - If a connection with the same host and username already exists, the existing connection will be selected instead of creating a duplicate. diff --git a/docs/learn-more/images/local-connection.png b/docs/user-manual/images/local-connection.png similarity index 100% rename from docs/learn-more/images/local-connection.png rename to docs/user-manual/images/local-connection.png diff --git a/docs/learn-more/images/service-discovery-activation-vm.png b/docs/user-manual/images/service-discovery-activation-vm.png similarity index 100% rename from docs/learn-more/images/service-discovery-activation-vm.png rename to docs/user-manual/images/service-discovery-activation-vm.png diff --git a/docs/learn-more/images/service-discovery-activation.png b/docs/user-manual/images/service-discovery-activation.png similarity index 100% rename from docs/learn-more/images/service-discovery-activation.png rename to docs/user-manual/images/service-discovery-activation.png diff --git a/docs/learn-more/images/service-discovery-filter-azure-vcore.png b/docs/user-manual/images/service-discovery-filter-azure-vcore.png similarity index 100% rename from docs/learn-more/images/service-discovery-filter-azure-vcore.png rename to docs/user-manual/images/service-discovery-filter-azure-vcore.png diff --git a/docs/learn-more/images/service-discovery-filter-vm.png b/docs/user-manual/images/service-discovery-filter-vm.png similarity index 100% rename from docs/learn-more/images/service-discovery-filter-vm.png rename to docs/user-manual/images/service-discovery-filter-vm.png diff --git a/docs/learn-more/images/service-discovery-filter.png b/docs/user-manual/images/service-discovery-filter.png similarity index 100% rename from docs/learn-more/images/service-discovery-filter.png rename to docs/user-manual/images/service-discovery-filter.png diff --git a/docs/learn-more/images/service-discovery-introduction.png b/docs/user-manual/images/service-discovery-introduction.png similarity index 100% rename from docs/learn-more/images/service-discovery-introduction.png rename to docs/user-manual/images/service-discovery-introduction.png diff --git a/docs/learn-more/local-connection-documentdb-local.md b/docs/user-manual/local-connection-documentdb-local.md similarity index 84% rename from docs/learn-more/local-connection-documentdb-local.md rename to docs/user-manual/local-connection-documentdb-local.md index 1798e66c2..15bc234cb 100644 --- a/docs/learn-more/local-connection-documentdb-local.md +++ b/docs/user-manual/local-connection-documentdb-local.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Local Connection](./local-connection) +> **User Manual** — [Back to User Manual](../index#user-manual) --- diff --git a/docs/learn-more/local-connection-mongodb-ru.md b/docs/user-manual/local-connection-mongodb-ru.md similarity index 91% rename from docs/learn-more/local-connection-mongodb-ru.md rename to docs/user-manual/local-connection-mongodb-ru.md index 75a2cb429..92497bd35 100644 --- a/docs/learn-more/local-connection-mongodb-ru.md +++ b/docs/user-manual/local-connection-mongodb-ru.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Local Connection](./local-connection) +> **User Manual** — [Back to User Manual](../index#user-manual) --- diff --git a/docs/learn-more/local-connection.md b/docs/user-manual/local-connection.md similarity index 95% rename from docs/learn-more/local-connection.md rename to docs/user-manual/local-connection.md index 52a114200..0ca85f317 100644 --- a/docs/learn-more/local-connection.md +++ b/docs/user-manual/local-connection.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Learn More Index](./index) +> **User Manual** — [Back to User Manual](../index#user-manual) --- @@ -16,7 +14,6 @@ You have two main options for connecting to a local instance: - **Use Preconfigured Options:** The extension provides ready-to-use configurations for popular local setups: - - **[Azure CosmosDB for MongoDB (RU) Emulator](./local-connection-mongodb-ru)** - **[DocumentDB Local](./local-connection-documentdb-local)** diff --git a/docs/learn-more/managing-azure-discovery.md b/docs/user-manual/managing-azure-discovery.md similarity index 95% rename from docs/learn-more/managing-azure-discovery.md rename to docs/user-manual/managing-azure-discovery.md index 8d3494ef1..06a2155c4 100644 --- a/docs/learn-more/managing-azure-discovery.md +++ b/docs/user-manual/managing-azure-discovery.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Learn More Index](./index) +> **User Manual** — [Back to User Manual](../index#user-manual) --- @@ -8,10 +6,12 @@ When using Azure-based service discovery providers in DocumentDB for VS Code, you have access to shared features for managing your Azure credentials and filtering which resources are displayed. These features are consistent across all Azure service discovery providers: -- [Azure CosmosDB for MongoDB (RU)](./service-discovery-azure-cosmosdb-for-mongodb-ru) -- [Azure CosmosDB for MongoDB (vCore)](./service-discovery-azure-cosmosdb-for-mongodb-vcore) +- [Azure Cosmos DB for MongoDB (RU)](./service-discovery-azure-cosmosdb-for-mongodb-ru) +- [Azure Cosmos DB for MongoDB (vCore)](./service-discovery-azure-cosmosdb-for-mongodb-vcore) - [Azure VMs (DocumentDB)](./service-discovery-azure-vms) +For a general overview of service discovery, see the [Service Discovery](./service-discovery) documentation. + --- ## Managing Azure Accounts diff --git a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-ru.md b/docs/user-manual/service-discovery-azure-cosmosdb-for-mongodb-ru.md similarity index 92% rename from docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-ru.md rename to docs/user-manual/service-discovery-azure-cosmosdb-for-mongodb-ru.md index 762a06fd9..4f5b7b660 100644 --- a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-ru.md +++ b/docs/user-manual/service-discovery-azure-cosmosdb-for-mongodb-ru.md @@ -1,7 +1,4 @@ - - - -> **Learn More** — [Back to Learn More Index](./index) +> **User Manual** — [Back to User Manual](../index#user-manual) --- @@ -46,6 +43,8 @@ When you use the Azure CosmosDB for MongoDB (RU) plugin, the extension performs - Save an account to your `DocumentDB Connections` list using the context menu or the save icon next to its name. - When connecting or saving, the extension will extract credentials or connection details from Azure where available. If multiple authentication methods are supported, you will be prompted to choose one. +For an overview of how service discovery works, see the [Service Discovery](./service-discovery) documentation. For details on managing your Azure accounts and subscriptions, refer to the [Managing Azure Subscriptions](./managing-azure-discovery) guide. + ## Managing Credentials and Filters This provider supports the following management features: diff --git a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md b/docs/user-manual/service-discovery-azure-cosmosdb-for-mongodb-vcore.md similarity index 91% rename from docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md rename to docs/user-manual/service-discovery-azure-cosmosdb-for-mongodb-vcore.md index 2cf5f5ef6..8211faaad 100644 --- a/docs/learn-more/service-discovery-azure-cosmosdb-for-mongodb-vcore.md +++ b/docs/user-manual/service-discovery-azure-cosmosdb-for-mongodb-vcore.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Learn More Index](./index) +> **User Manual** — [Back to User Manual](../index#user-manual) --- @@ -45,6 +43,8 @@ When you use the Azure CosmosDB for MongoDB (vCore) plugin, the following steps - You can save a cluster to your `DocumentDB Connections` list using the context menu or by clicking the save icon next to its name. - When connecting or saving, the extension detects the authentication methods supported by the cluster (e.g., **Username/Password** or **Entra ID**). If multiple are available, you will be prompted to choose your preferred method. +For an overview of how service discovery works, see the [Service Discovery](./service-discovery) documentation. For details on managing your Azure accounts and subscriptions, refer to the [Managing Azure Subscriptions](./managing-azure-discovery) guide. + ## Managing Credentials and Filters This provider supports the following management features: diff --git a/docs/learn-more/service-discovery-azure-vms.md b/docs/user-manual/service-discovery-azure-vms.md similarity index 93% rename from docs/learn-more/service-discovery-azure-vms.md rename to docs/user-manual/service-discovery-azure-vms.md index f65d622c6..585765522 100644 --- a/docs/learn-more/service-discovery-azure-vms.md +++ b/docs/user-manual/service-discovery-azure-vms.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Learn More Index](./index) +> **User Manual** — [Back to User Manual](../index#user-manual) --- @@ -50,6 +48,8 @@ When you use the Azure VMs (DocumentDB) plugin, the following steps are performe - You can save a VM to your `DocumentDB Connections` list using the context menu or by clicking the save icon next to its name. - When connecting, you'll be prompted to provide connection details for the DocumentDB/MongoDB instance running on the VM. +For an overview of how service discovery works, see the [Service Discovery](./service-discovery) documentation. For details on managing your Azure accounts and subscriptions, refer to the [Managing Azure Subscriptions](./managing-azure-discovery) guide. + ## Managing Credentials and Filters This provider supports the following management features: diff --git a/docs/learn-more/service-discovery.md b/docs/user-manual/service-discovery.md similarity index 95% rename from docs/learn-more/service-discovery.md rename to docs/user-manual/service-discovery.md index 07b621ffb..45fef396f 100644 --- a/docs/learn-more/service-discovery.md +++ b/docs/user-manual/service-discovery.md @@ -1,6 +1,4 @@ - - -> **Learn More** — [Back to Learn More Index](./index) +> **User Manual** — [Back to User Manual](../index#user-manual) --- From 375e935548d7a5fecff8c63432af01e92a210722 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 16:43:39 +0200 Subject: [PATCH 81/88] Release notes --- docs/release-notes/0.5.md | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/release-notes/0.5.md diff --git a/docs/release-notes/0.5.md b/docs/release-notes/0.5.md new file mode 100644 index 000000000..eee880605 --- /dev/null +++ b/docs/release-notes/0.5.md @@ -0,0 +1,48 @@ + + +> **Release Notes** — [Back to Release Notes](../index.md#release-notes) + +--- + +# DocumentDB for VS Code Extension v0.5 + +We are excited to announce the release of **DocumentDB for VS Code Extension v0.5.0**. This update significantly enhances security and multi-tenant workflows with improved Microsoft Entra ID support, introduces a new "Help and Feedback" view, and delivers several key bug fixes to improve stability and user experience. + +## What's New in v0.5 + +### ⭐ Enhanced Microsoft Entra ID Support for Multi-Account and Multi-Tenant Scenarios + +Continuing our focus on enterprise-grade security from release 0.3.0, we have overhauled our Microsoft Entra ID integration to fully support multi-account and multi-tenant environments. This enables uninterrupted workflows for developers working across different organizations and directories. + +- **Multi-Account Management**: You can now sign in with multiple Azure accounts and easily switch between them without leaving VS Code. A new **Manage Credentials** feature allows you to view all authenticated accounts and add new ones on the fly. +- **Multi-Tenant Filtering**: For users with access to multiple Azure tenants, a new filtering wizard lets you select exactly which tenants and subscriptions should be visible in the Service Discovery panel. Your selections are saved and persisted across sessions, ensuring a clean and relevant view of your resources. +- **Consistent Context**: The extension now correctly respects the tenant context associated with each resource, resolving previous inconsistencies and ensuring a reliable experience when interacting with clusters across different tenants. + +This work was completed as part of PR [#277](https://github.com/microsoft/vscode-documentdb/pull/277) and addresses the following issues: [#285](https://github.com/microsoft/vscode-documentdb/issues/285), [#284](https://github.com/microsoft/vscode-documentdb/issues/284), [#265](https://github.com/microsoft/vscode-documentdb/issues/265), [#243](https://github.com/microsoft/vscode-documentdb/issues/243) + +For more details, please see our new documentation on [Managing Azure Discovery (Accounts, Tenants, and Subscriptions)](../user-manual/managing-azure-discovery.md). + +### ⭐ New "Help and Feedback" View + +We've added a new **Help and Feedback** view to the extension sidebar, providing a central place to access important resources. This view makes it easier than ever to get help, provide feedback, and stay up-to-date with the latest changes. + +The new view includes quick links to: + +- **Documentation**: Access the full user manual. +- **Changelog**: See what's new in the latest release. +- **Report an Issue**: Quickly file a bug report on GitHub. +- **Request a Feature**: Share your ideas for new features. +- **Join the Discussion**: Connect with the community on our GitHub discussion board. + +This feature was introduced in PR [#289](https://github.com/microsoft/vscode-documentdb/pull/289). + +## Key Fixes and Improvements + +- **Users are asked to re-enter password when launching the shell ([#285](https://github.com/microsoft/vscode-documentdb/issues/285))** + - Fixed a regression where users with saved credentials were still prompted for a password when launching the shell. The original functionality has been restored. + +- **Service Discovery + Entra ID implementation does not use tenant information ([#276](https://github.com/microsoft/vscode-documentdb/issues/276))** + - Resolved an issue where the extension would fail to respect the tenant context when interacting with Azure resources from a non-default tenant. The extension now correctly handles tenant information, preventing inconsistent states. + +- **Updating connection authentication from EntraID to UserName/Password fails ([#284](https://github.com/microsoft/vscode-documentdb/issues/284))** + - Corrected a failure that occurred when updating a connection's authentication method from Entra ID to a username/password. The connection now updates and connects successfully. From 66a6a53d7e3048115bf905f954c2bc8e9140d0a2 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 16:49:31 +0200 Subject: [PATCH 82/88] changelog --- CHANGELOG.md | 13 +++++++++++++ docs/release-notes/0.5.md | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 073a2a527..cee1622fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 0.5.0 + +### New Features & Improvements + +- **Enhanced Microsoft Entra ID Support**: Overhauled Microsoft Entra ID integration to fully support multi-account and multi-tenant environments, enabling uninterrupted workflows for developers working across different organizations. This includes multi-account management and multi-tenant filtering. [#277](https://github.com/microsoft/vscode-documentdb/pull/277), [#285](https://github.com/microsoft/vscode-documentdb/issues/285), [#284](https://github.com/microsoft/vscode-documentdb/issues/284), [#265](https://github.com/microsoft/vscode-documentdb/issues/265), [#243](https://github.com/microsoft/vscode-documentdb/issues/243) +- **New "Help and Feedback" View**: Added a new view to the extension sidebar, providing a central place to access documentation, see the changelog, report issues, and request features. [#289](https://github.com/microsoft/vscode-documentdb/pull/289) + +### Fixes + +- **Password Re-entry on Shell Launch**: Fixed a regression where users with saved credentials were still prompted for a password when launching the shell. [#285](https://github.com/microsoft/vscode-documentdb/issues/285) +- **Tenant Information in Service Discovery**: Resolved an issue where the extension would fail to respect the tenant context when interacting with Azure resources from a non-default tenant. [#276](https://github.com/microsoft/vscode-documentdb/issues/276) +- **Connection Authentication Update**: Corrected a failure that occurred when updating a connection's authentication method from Entra ID to a username/password. [#284](https://github.com/microsoft/vscode-documentdb/issues/284) + ## 0.4.1 ### Improvement diff --git a/docs/release-notes/0.5.md b/docs/release-notes/0.5.md index eee880605..8b364b611 100644 --- a/docs/release-notes/0.5.md +++ b/docs/release-notes/0.5.md @@ -46,3 +46,8 @@ This feature was introduced in PR [#289](https://github.com/microsoft/vscode-doc - **Updating connection authentication from EntraID to UserName/Password fails ([#284](https://github.com/microsoft/vscode-documentdb/issues/284))** - Corrected a failure that occurred when updating a connection's authentication method from Entra ID to a username/password. The connection now updates and connects successfully. + +## Changelog + +See the full changelog entry for this release: +➡️ [CHANGELOG.md#050](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#050) From 6ef00856caa6c29aece71fa6893f7ff4048de638 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 16:51:01 +0200 Subject: [PATCH 83/88] release notes + changelog vcore clarification --- CHANGELOG.md | 2 +- docs/release-notes/0.5.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cee1622fd..f7a2d8213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### New Features & Improvements -- **Enhanced Microsoft Entra ID Support**: Overhauled Microsoft Entra ID integration to fully support multi-account and multi-tenant environments, enabling uninterrupted workflows for developers working across different organizations. This includes multi-account management and multi-tenant filtering. [#277](https://github.com/microsoft/vscode-documentdb/pull/277), [#285](https://github.com/microsoft/vscode-documentdb/issues/285), [#284](https://github.com/microsoft/vscode-documentdb/issues/284), [#265](https://github.com/microsoft/vscode-documentdb/issues/265), [#243](https://github.com/microsoft/vscode-documentdb/issues/243) +- **Enhanced Microsoft Entra ID Support**: Overhauled Microsoft Entra ID integration for Azure Cosmos DB for MongoDB (vCore) to fully support multi-account and multi-tenant environments, enabling uninterrupted workflows for developers working across different organizations. This includes multi-account management and multi-tenant filtering. [#277](https://github.com/microsoft/vscode-documentdb/pull/277), [#285](https://github.com/microsoft/vscode-documentdb/issues/285), [#284](https://github.com/microsoft/vscode-documentdb/issues/284), [#265](https://github.com/microsoft/vscode-documentdb/issues/265), [#243](https://github.com/microsoft/vscode-documentdb/issues/243) - **New "Help and Feedback" View**: Added a new view to the extension sidebar, providing a central place to access documentation, see the changelog, report issues, and request features. [#289](https://github.com/microsoft/vscode-documentdb/pull/289) ### Fixes diff --git a/docs/release-notes/0.5.md b/docs/release-notes/0.5.md index 8b364b611..487b41ada 100644 --- a/docs/release-notes/0.5.md +++ b/docs/release-notes/0.5.md @@ -12,7 +12,7 @@ We are excited to announce the release of **DocumentDB for VS Code Extension v0. ### ⭐ Enhanced Microsoft Entra ID Support for Multi-Account and Multi-Tenant Scenarios -Continuing our focus on enterprise-grade security from release 0.3.0, we have overhauled our Microsoft Entra ID integration to fully support multi-account and multi-tenant environments. This enables uninterrupted workflows for developers working across different organizations and directories. +Continuing our focus on enterprise-grade security from release 0.3.0, we have overhauled our Microsoft Entra ID integration for Azure Cosmos DB for MongoDB (vCore) to fully support multi-account and multi-tenant environments. This enables uninterrupted workflows for developers working across different organizations and directories. - **Multi-Account Management**: You can now sign in with multiple Azure accounts and easily switch between them without leaving VS Code. A new **Manage Credentials** feature allows you to view all authenticated accounts and add new ones on the fly. - **Multi-Tenant Filtering**: For users with access to multiple Azure tenants, a new filtering wizard lets you select exactly which tenants and subscriptions should be visible in the Service Discovery panel. Your selections are saved and persisted across sessions, ensuring a clean and relevant view of your resources. From 9cf892d41973b3e4da6946c544cebe115f6bd965 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 17:11:33 +0200 Subject: [PATCH 84/88] feat: telemetry update --- .../api-shared/azure/credentialsManagement/SelectAccountStep.ts | 2 +- .../azure/subscriptionFiltering/InitializeFilteringStep.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts index b91599ed9..8f7134a0a 100644 --- a/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts +++ b/src/plugins/api-shared/azure/credentialsManagement/SelectAccountStep.ts @@ -27,7 +27,7 @@ export class SelectAccountStep extends AzureWizardPromptStep ({ diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts index 216c836c8..8edc606f6 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts @@ -93,6 +93,7 @@ export class InitializeFilteringStep extends AzureWizardPromptStep Date: Mon, 13 Oct 2025 17:12:40 +0200 Subject: [PATCH 85/88] chore: minor refactor Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../HelpAndFeedbackBranchDataProvider.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts index 636a863a4..1d1dbfe34 100644 --- a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts +++ b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts @@ -30,9 +30,6 @@ import { isTreeElementWithContextValue } from '../TreeElementWithContextValue'; * which provides telemetry tracking for each URL opened. */ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvider { - constructor() { - super(); - } async getChildren(element?: TreeElement): Promise { return callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { From 8f9c560e1aa8c6f5b24894c816e3c97e877cbfc2 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 17:12:51 +0200 Subject: [PATCH 86/88] chore: typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../azure/subscriptionFiltering/InitializeFilteringStep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts index 8edc606f6..f52330545 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/InitializeFilteringStep.ts @@ -47,7 +47,7 @@ export class InitializeFilteringStep extends AzureWizardPromptStep Date: Mon, 13 Oct 2025 17:13:05 +0200 Subject: [PATCH 87/88] chore: typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../azure/subscriptionFiltering/subscriptionFilteringHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts index 42df9bf2c..05470d8b2 100644 --- a/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts +++ b/src/plugins/api-shared/azure/subscriptionFiltering/subscriptionFilteringHelpers.ts @@ -28,7 +28,7 @@ import { ext } from '../../../../extensionVariables'; /** * Returns the currently selected subscription IDs from the shared configuration. - * The ID of the tenant is being excluced from the ID. + * The ID of the tenant is being excluded from the ID. * The IDs are stored in the format 'tenantId/subscriptionId'. * For example: 'tenantId/subscriptionId'. * The function returns an array of subscription IDs without the tenant ID. From dacf9ab6914793d77792267ca04c44e86adb1611 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Oct 2025 17:18:58 +0200 Subject: [PATCH 88/88] prettier-fix --- .../help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts index 1d1dbfe34..081e4f634 100644 --- a/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts +++ b/src/tree/help-and-feedback-view/HelpAndFeedbackBranchDataProvider.ts @@ -30,7 +30,6 @@ import { isTreeElementWithContextValue } from '../TreeElementWithContextValue'; * which provides telemetry tracking for each URL opened. */ export class HelpAndFeedbackBranchDataProvider extends BaseExtendedTreeDataProvider { - async getChildren(element?: TreeElement): Promise { return callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.view = Views.HelpAndFeedbackView;