diff --git a/api/package-lock.json b/api/package-lock.json index b6e21413..a1866d67 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@microsoft/api-extractor": "^7.38.0", diff --git a/api/package.json b/api/package.json index 64e5757d..3404c598 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.0", "description": "Extension API for VS Code DocumentDB extension (preview)", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/api/src/utils/getApi.ts b/api/src/utils/getApi.ts index 4c4af96f..22ff7bec 100644 --- a/api/src/utils/getApi.ts +++ b/api/src/utils/getApi.ts @@ -14,7 +14,7 @@ const DOCUMENTDB_EXTENSION_ID = 'ms-azuretools.vscode-documentdb'; */ interface DocumentDBApiConfig { 'x-documentdbApi'?: { - registeredClients?: string[]; + verifiedClients?: string[]; }; } @@ -44,11 +44,11 @@ function isValidPackageJson(packageJson: unknown): packageJson is DocumentDBApiC * ``` */ export async function getDocumentDBExtensionApi( - _context: vscode.ExtensionContext, + context: vscode.ExtensionContext, apiVersionRange: string, ): Promise { // Get the calling extension's ID from the context - const callingExtensionId = _context.extension.id; + const callingExtensionId = context.extension.id; // Get the DocumentDB extension to access its package.json configuration const extension = vscode.extensions.getExtension(DOCUMENTDB_EXTENSION_ID); @@ -58,15 +58,15 @@ export async function getDocumentDBExtensionApi( // Check if the calling extension is whitelisted const packageJson = extension.packageJSON as unknown; - const registeredClients = isValidPackageJson(packageJson) - ? packageJson['x-documentdbApi']?.registeredClients + const verifiedClients = isValidPackageJson(packageJson) + ? packageJson['x-documentdbApi']?.verifiedClients : undefined; - if (!registeredClients || !Array.isArray(registeredClients)) { - throw new Error(`DocumentDB for VS Code API configuration is invalid. No registered clients found.`); + if (!verifiedClients || !Array.isArray(verifiedClients)) { + throw new Error(`DocumentDB for VS Code API configuration is invalid. No verified client list found.`); } - if (!registeredClients.includes(callingExtensionId)) { + if (!verifiedClients.includes(callingExtensionId)) { throw new Error( `Extension '${callingExtensionId}' is not authorized to use the DocumentDB for VS Code API. ` + `This is an experimental API with whitelisted access. ` + @@ -89,5 +89,20 @@ export async function getDocumentDBExtensionApi( console.warn(`API version mismatch. Expected ${apiVersionRange}, got ${api.apiVersion}`); } + try { + // going via an "internal" command here to avoid making the registration function public + const success = await vscode.commands.executeCommand( + 'vscode-documentdb.command.internal.api.registerClientExtension', + context.extension.id, + ); + + if (success !== true) { + console.warn(`Client registration may have failed for "${callingExtensionId}"`); + } + } catch (error) { + // Log error but don't fail API retrieval + console.warn(`Failed to register client "${callingExtensionId}": ${error}`); + } + return api; } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 09713b7e..1828031d 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -43,6 +43,7 @@ "An error has occurred. Check output window for more details.": "An error has occurred. Check output window for more details.", "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 client extension: \"{clientExtensionId}\"": "API: Registered new client extension: \"{clientExtensionId}\"", "API: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\"": "API: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\"", "Are you sure?": "Are you sure?", "Attempting to authenticate with \"{cluster}\"…": "Attempting to authenticate with \"{cluster}\"…", diff --git a/package.json b/package.json index a5a8007b..342d43db 100644 --- a/package.json +++ b/package.json @@ -417,7 +417,7 @@ { "//": "Data Migration", "category": "DocumentDB", - "command": "vscode-documentdb.command.chooseDataMigrationExtension", + "command": "vscode-documentdb.command.accessDataMigrationServices", "title": "Data Migration…" }, { @@ -636,8 +636,8 @@ }, { "//": "[Collection] Data Migration", - "command": "vscode-documentdb.command.chooseDataMigrationExtension", - "when": "view =~ /connectionsView|discoveryView/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i && migrationProvidersAvailable", + "command": "vscode-documentdb.command.accessDataMigrationServices", + "when": "view =~ /connectionsView|discoveryView/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "1@2" }, { @@ -748,7 +748,7 @@ "when": "never" }, { - "command": "vscode-documentdb.command.chooseDataMigrationExtension", + "command": "vscode-documentdb.command.accessDataMigrationServices", "when": "never" }, { @@ -912,9 +912,16 @@ ] }, "x-documentdbApi": { - "registeredClients": [ - "vscode-cosmosdb", - "vscode-mongo-migration" + "verifiedClients": [ + "ms-azurecosmosdbtools.vscode-mongo-migration" ] - } + }, + "x-announcedMigrationProviders": [ + { + "id": "ms-azurecosmosdbtools.vscode-mongo-migration", + "name": "Azure Cosmos DB Migration", + "description": "Assess and migrate your databases to Azure Cosmos DB.", + "url": "https://marketplace.visualstudio.com/items?itemName=ms-azurecosmosdbtools.vscode-mongo-migration" + } + ] } diff --git a/src/commands/accessDataMigrationServices/accessDataMigrationServices.ts b/src/commands/accessDataMigrationServices/accessDataMigrationServices.ts new file mode 100644 index 00000000..daff8c36 --- /dev/null +++ b/src/commands/accessDataMigrationServices/accessDataMigrationServices.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { nonNullValue, type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import { commands, QuickPickItemKind, type QuickPickItem } from 'vscode'; +import { CredentialCache } from '../../documentdb/CredentialCache'; +import { MigrationService } from '../../services/migrationServices'; +import { type ClusterItemBase } from '../../tree/documentdb/ClusterItemBase'; +import { openUrl } from '../../utils/openUrl'; + +const ANNOUNCED_PROVIDER_PREFIX = 'announced-provider'; + +export async function accessDataMigrationServices(context: IActionContext, node: ClusterItemBase) { + const installedProviders: (QuickPickItem & { id: string })[] = MigrationService.listProviders() + // Map to QuickPickItem format + .map((provider) => ({ + id: provider.id, + label: provider.label, + detail: provider.description, + iconPath: provider.iconPath, + + group: 'Installed Providers', + alwaysShow: true, + })) + // Sort alphabetically + .sort((a, b) => a.label.localeCompare(b.label)); + + const announcedProviders: (QuickPickItem & { id: string })[] = MigrationService.listAnnouncedProviders(true) + // Map to QuickPickItem format + .map((provider) => ({ + id: `${ANNOUNCED_PROVIDER_PREFIX}-${provider.id}`, // please note, the prefix is a magic string here, and needed to correctly support vs code marketplace integration + label: `$(extensions) ${provider.name}`, + detail: `Open the VS Code Marketplace to learn more about "${provider.name}"`, + url: provider.url, + + marketplaceId: provider.id, + group: 'Visit Marketplace', + alwaysShow: true, + })) + // Sort alphabetically + .sort((a, b) => a.label.localeCompare(b.label)); + + const commonItems = [ + // { + // id: 'addMigrationProvider', + // label: l10n.t('Add New Migration Provider…'), + // detail: l10n.t('Explore more data migration providers.'), + // iconPath: new ThemeIcon('plus'), + + // group: 'Migration Providers', + // alwaysShow: true, + // }, + { label: '', kind: QuickPickItemKind.Separator }, + { + id: 'learnMore', + label: l10n.t('Learn more…'), + detail: l10n.t('Learn more about DocumentDB and MongoDB migrations.'), + + url: 'https://aka.ms/vscode-documentdb-migration-support', + + group: 'Learn More', + alwaysShow: true, + }, + ]; + + const selectedItem = await context.ui.showQuickPick( + [...installedProviders, ...announcedProviders, ...commonItems], + { + enableGrouping: true, + placeHolder: l10n.t('Choose the data migration provider…'), + stepName: 'selectMigrationProvider', + suppressPersistence: true, + }, + ); + + context.telemetry.properties.connectionMode = selectedItem.id; + + if (selectedItem.id === 'learnMore') { + context.telemetry.properties.migrationLearnMore = 'true'; + if ('url' in selectedItem && selectedItem.url) { + await openUrl(selectedItem.url); + } + } + + if (selectedItem.id?.startsWith(ANNOUNCED_PROVIDER_PREFIX)) { + context.telemetry.properties.migrationAddProvider = 'true'; + if ('marketplaceId' in selectedItem && selectedItem.marketplaceId) { + commands.executeCommand('extension.open', selectedItem.marketplaceId); + } + } + + // if (selectedItem.id === 'addMigrationProvider') { + // context.telemetry.properties.addMigrationProvider = 'true'; + // commands.executeCommand('workbench.extensions.search', '"DocumentDB Migration Plugin"'); + // return; + // } + + if (installedProviders.some((provider) => provider.id === selectedItem.id)) { + const selectedProvider = MigrationService.getProvider(nonNullValue(selectedItem.id, 'selectedItem.id')); + + if (!selectedProvider) { + return; + } + + context.telemetry.properties.migrationProvider = selectedProvider.id; + + // Check if the selected provider requires authentication for the default action + if (selectedProvider.requiresAuthentication) { + const authenticated = await ensureAuthentication(context, node); + if (!authenticated) { + void context.ui.showWarningMessage( + l10n.t('Authentication is required to use this migration provider.'), + { + modal: true, + detail: l10n.t('Please authenticate first by expanding the tree item of the selected cluster.'), + }, + ); + return; + } + } + + try { + // Construct the options object with available context + const options = { + connectionString: await node.getConnectionString(), + extendedProperties: { + clusterId: node.cluster.id, + }, + }; + + // Get available actions from the provider + const availableActions = await selectedProvider.getAvailableActions(options); + + if (availableActions.length === 0) { + // No actions available, execute default action + return selectedProvider.executeAction(options); + } + + // Extend actions with Learn More option if provider has a learn more URL + const extendedActions: (QuickPickItem & { + id: string; + url?: string; + requiresAuthentication?: boolean; + })[] = [...availableActions]; + + const url = selectedProvider.getLearnMoreUrl?.(); + + if (url) { + extendedActions.push( + { id: 'separator', label: '', kind: QuickPickItemKind.Separator }, + { + id: 'learnMore', + label: l10n.t('Learn more…'), + detail: l10n.t('Learn more about {0}.', selectedProvider.label), + url, + alwaysShow: true, + }, + ); + } + + // Show action picker to user + const selectedAction = await context.ui.showQuickPick(extendedActions, { + placeHolder: l10n.t('Choose the migration action…'), + stepName: 'selectMigrationAction', + suppressPersistence: true, + }); + + if (selectedAction.id === 'learnMore') { + context.telemetry.properties.migrationLearnMore = 'true'; + if (selectedAction.url) { + await openUrl(selectedAction.url); + } + return; + } + + // Check if selected action requires authentication + if (selectedAction.requiresAuthentication) { + const authenticated = await ensureAuthentication(context, node); + if (!authenticated) { + void context.ui.showWarningMessage(l10n.t('Authentication is required to run this action.'), { + modal: true, + detail: l10n.t('Please authenticate first by expanding the tree item of the selected cluster.'), + }); + return; + } + } + + context.telemetry.properties.migrationAction = selectedAction.id; + + // Execute the selected action + await selectedProvider.executeAction(options, selectedAction.id); + } catch (error) { + // Log the error and re-throw to be handled by the caller + console.error('Error during migration provider execution:', error); + throw error; + } + } +} + +/** + * Ensures the user is authenticated for migration operations. + * This function should be implemented to handle the specific authentication flow + * required by the host extension. + * + * @param context - The action context for UI operations and telemetry + * @returns Promise - true if authentication succeeded, false otherwise + */ +async function ensureAuthentication(_context: IActionContext, _node: ClusterItemBase): Promise { + if (CredentialCache.hasCredentials(_node.cluster.id)) { + return Promise.resolve(true); // Credentials already exist, no need to authenticate again + } + + return Promise.resolve(false); // Return false until implementation is complete +} diff --git a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts deleted file mode 100644 index 946623a8..00000000 --- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts +++ /dev/null @@ -1,194 +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 { nonNullValue, type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as l10n from '@vscode/l10n'; -import { QuickPickItemKind, type QuickPickItem } from 'vscode'; -import { CredentialCache } from '../../documentdb/CredentialCache'; -import { MigrationService } from '../../services/migrationServices'; -import { type ClusterItemBase } from '../../tree/documentdb/ClusterItemBase'; -import { openUrl } from '../../utils/openUrl'; - -export async function chooseDataMigrationExtension(context: IActionContext, node: ClusterItemBase) { - const migrationProviders: (QuickPickItem & { id: string })[] = MigrationService.listProviders() - // Map to QuickPickItem format - .map((provider) => ({ - id: provider.id, - label: provider.label, - detail: provider.description, - iconPath: provider.iconPath, - - group: 'Migration Providers', - alwaysShow: true, - })) - // Sort alphabetically - .sort((a, b) => a.label.localeCompare(b.label)); - - const commonItems = [ - // { - // id: 'addMigrationProvider', - // label: l10n.t('Add New Migration Provider…'), - // detail: l10n.t('Explore more data migration providers.'), - // iconPath: new ThemeIcon('plus'), - - // group: 'Migration Providers', - // alwaysShow: true, - // }, - { label: '', kind: QuickPickItemKind.Separator }, - { - id: 'learnMore', - label: l10n.t('Learn more…'), - detail: l10n.t('Learn more about DocumentDB and MongoDB migrations.'), - - learnMoreUrl: 'https://aka.ms/vscode-documentdb-migration-support', - alwaysShow: true, - group: 'Learn More', - }, - ]; - - const selectedItem = await context.ui.showQuickPick([...migrationProviders, ...commonItems], { - enableGrouping: true, - placeHolder: l10n.t('Choose the data migration provider…'), - stepName: 'selectMigrationProvider', - suppressPersistence: true, - }); - - context.telemetry.properties.connectionMode = selectedItem.id; - - if (selectedItem.id === 'learnMore') { - context.telemetry.properties.migrationLearnMore = 'true'; - if ('learnMoreUrl' in selectedItem && selectedItem.learnMoreUrl) { - await openUrl(selectedItem.learnMoreUrl); - } - } - - // if (selectedItem.id === 'addMigrationProvider') { - // context.telemetry.properties.addMigrationProvider = 'true'; - // commands.executeCommand('workbench.extensions.search', '"DocumentDB Migration Plugin"'); - // return; - // } - - if (migrationProviders.some((provider) => provider.id === selectedItem.id)) { - const selectedProvider = MigrationService.getProvider(nonNullValue(selectedItem.id, 'selectedItem.id')); - - if (selectedProvider) { - context.telemetry.properties.migrationProvider = selectedProvider.id; - - // Check if the selected provider requires authentication for the default action - if (selectedProvider.requiresAuthentication) { - const authenticated = await ensureAuthentication(context, node); - if (!authenticated) { - void context.ui.showWarningMessage( - l10n.t('Authentication is required to use this migration provider.'), - { - modal: true, - detail: l10n.t( - 'Please authenticate first by expanding the tree item of the selected cluster.', - ), - }, - ); - return; - } - } - - try { - // Construct the options object with available context - const options = { - connectionString: await node.getConnectionString(), - extendedProperties: { - clusterId: node.cluster.id, - }, - }; - - // Get available actions from the provider - const availableActions = await selectedProvider.getAvailableActions(options); - - if (availableActions.length === 0) { - // 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, - }, - ); - } - - // Show action picker to user - const selectedAction = await context.ui.showQuickPick(extendedActions, { - placeHolder: l10n.t('Choose the migration action…'), - stepName: 'selectMigrationAction', - suppressPersistence: true, - }); - - if (selectedAction.id === 'learnMore') { - context.telemetry.properties.migrationLearnMore = 'true'; - if (selectedAction.learnMoreUrl) { - await openUrl(selectedAction.learnMoreUrl); - } - return; - } - - // Check if selected action requires authentication - if (selectedAction.requiresAuthentication) { - const authenticated = await ensureAuthentication(context, node); - if (!authenticated) { - void context.ui.showWarningMessage( - l10n.t('Authentication is required to run this action.'), - { - modal: true, - detail: l10n.t( - 'Please authenticate first by expanding the tree item of the selected cluster.', - ), - }, - ); - return; - } - } - - context.telemetry.properties.migrationAction = selectedAction.id; - - // Execute the selected action - await selectedProvider.executeAction(options, selectedAction.id); - } - } catch (error) { - // Log the error and re-throw to be handled by the caller - console.error('Error during migration provider execution:', error); - throw error; - } - } - } -} - -/** - * Ensures the user is authenticated for migration operations. - * This function should be implemented to handle the specific authentication flow - * required by the host extension. - * - * @param context - The action context for UI operations and telemetry - * @returns Promise - true if authentication succeeded, false otherwise - */ -async function ensureAuthentication(_context: IActionContext, _node: ClusterItemBase): Promise { - if (CredentialCache.hasCredentials(_node.cluster.id)) { - return Promise.resolve(true); // Credentials already exist, no need to authenticate again - } - - return Promise.resolve(false); // Return false until implementation is complete -} diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 167e5602..ff5b0d51 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -17,9 +17,9 @@ import { } from '@microsoft/vscode-azext-utils'; import { AzExtResourceType } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; +import { accessDataMigrationServices } from '../commands/accessDataMigrationServices/accessDataMigrationServices'; import { addConnectionFromRegistry } from '../commands/addConnectionFromRegistry/addConnectionFromRegistry'; import { addDiscoveryRegistry } from '../commands/addDiscoveryRegistry/addDiscoveryRegistry'; -import { chooseDataMigrationExtension } from '../commands/chooseDataMigrationExtension/chooseDataMigrationExtension'; import { copyAzureConnectionString } from '../commands/copyConnectionString/copyConnectionString'; import { createCollection } from '../commands/createCollection/createCollection'; import { createAzureDatabase } from '../commands/createDatabase/createDatabase'; @@ -168,8 +168,8 @@ export class ClustersExtension implements vscode.Disposable { }); registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.chooseDataMigrationExtension', - chooseDataMigrationExtension, + 'vscode-documentdb.command.accessDataMigrationServices', + accessDataMigrationServices, ); //// Registry Commands: diff --git a/src/extension.ts b/src/extension.ts index aea1dc8d..fb1c7091 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,6 +10,7 @@ import { callWithTelemetryAndErrorHandling, createApiProvider, createAzExtLogOutputChannel, + registerCommand, registerErrorHandler, registerUIExtensionVariables, TreeElementStateManager, @@ -73,7 +74,7 @@ export async function activateInternal( // Create the DocumentDB Extension API const documentDBApi: DocumentDBExtensionApi = { - apiVersion: '0.2.0', + apiVersion: '0.3.0', migration: { registerProvider: (provider) => { MigrationService.registerProvider(provider); @@ -88,6 +89,24 @@ export async function activateInternal( }, }; + registerCommand( + 'vscode-documentdb.command.internal.api.registerClientExtension', + (_context: IActionContext, clientExtensionId: string) => { + try { + MigrationService.registerClientExtension(clientExtensionId); + ext.outputChannel.appendLine( + vscode.l10n.t('API: Registered new client extension: "{clientExtensionId}"', { + clientExtensionId, + }), + ); + return true; + } catch (error) { + console.error('Failed to register client:', error); + return false; + } + }, + ); + // Return both the DocumentDB API and Azure Extension API return { ...documentDBApi, diff --git a/src/services/migrationServices.ts b/src/services/migrationServices.ts index b437b073..fe4adf09 100644 --- a/src/services/migrationServices.ts +++ b/src/services/migrationServices.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ext } from '../extensionVariables'; /** * Represents basic information about a migration provider. @@ -56,6 +57,16 @@ export interface ActionsOptions { extendedProperties?: { [key: string]: string | undefined }; } +/** + * Interface for announced provider configuration + */ +export interface AnnouncedMigrationProvider { + id: string; + name: string; + description: string; + url: string; +} + /** * Private implementation of MigrationService that manages migration providers * for migration-related functionality. @@ -67,6 +78,19 @@ export interface ActionsOptions { */ class MigrationServiceImpl { private migrationProviders: Map = new Map(); + private registeredClientExtensions: Set = new Set(); + + /** + * Registers the calling extension as a client of the DocumentDB API. + * This is used to correctly handle announced providers and avoiding "double-announcements". + * @param clientContext The context of the calling extension. + */ + public registerClientExtension(clientExtensionId: string): void { + this.registeredClientExtensions.add(clientExtensionId); + + // Note: we don't support "unregistering" clients, this would require + // detecting when the extension is deactivated and removing it from the set... + } public registerProvider(provider: MigrationProvider): void { this.migrationProviders.set(provider.id, provider); @@ -94,6 +118,35 @@ class MigrationServiceImpl { return providers; } + public listAnnouncedProviders(hideInstalled: boolean = true): AnnouncedMigrationProvider[] { + const packageJson = ext.context.extension.packageJSON as unknown; + if (!packageJson || !packageJson['x-announcedMigrationProviders']) { + return []; + } + + const announcedProviders = packageJson['x-announcedMigrationProviders'] as AnnouncedMigrationProvider[]; + + if (hideInstalled) { + // Filter out providers that are already registered + const filteredList = announcedProviders.filter( + (provider) => !this.registeredClientExtensions.has(provider.id), + ); + + // Hardcoded fix for an older version of the migration extension that used a generic provider ID. + // If the old provider is detected, we hide the announcement to avoid duplicates. + const oldMigrationProvider = this.migrationProviders.get('one-action-provider'); + if (oldMigrationProvider?.label === 'Pre-Migration Assessment for Azure Cosmos DB') { + return filteredList.filter( + (provider) => provider.id !== 'ms-azurecosmosdbtools.vscode-mongo-migration', + ); + } + + return filteredList; + } + + return announcedProviders; + } + /** * Updates the VS Code context to reflect the current state of migration providers. * Sets 'migrationProvidersAvailable' to true when providers are registered.