From b7e982e78ac6c70649215bebe5988cc76d0243d3 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Thu, 25 Mar 2021 16:41:08 -0700 Subject: [PATCH] Add prompt to install required extensions for resource deployment (#14870) --- .../src/localizedConstants.ts | 4 ++ .../src/services/uriHandlerService.ts | 53 ++++++++++++++++++- .../browser/extensions.contribution.ts | 28 +++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts index d6257f9c04..6954ec8c35 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -40,6 +40,10 @@ export const optionsTypeRadioOrDropdown = localize('optionsTypeRadioOrDropdown', export const azdataEulaNotAccepted = localize('azdataEulaNotAccepted', "Deployment cannot continue. Azure Data CLI license terms have not yet been accepted. Please accept the EULA to enable the features that requires Azure Data CLI."); export const azdataEulaDeclined = localize('azdataEulaDeclined', "Deployment cannot continue. Azure Data CLI license terms were declined.You can either Accept EULA to continue or Cancel this operation"); export const acceptEulaAndSelect = localize('deploymentDialog.RecheckEulaButton', "Accept EULA & Select"); +export const extensionRequiredPrompt = (extensionName: string) => localize('resourceDeployment.extensionRequiredPrompt', "The '{0}' extension is required to deploy this resource, do you want to install it now?", extensionName); +export const install = localize('resourceDeployment.install', "Install"); +export const installingExtension = (extensionName: string) => localize('resourceDeployment.installingExtension', "Installing extension '{0}'...", extensionName); +export const unknownExtension = (extensionId: string) => localize('resourceDeployment.unknownExtension', "Unknown extension '{0}'", extensionId); export const resourceTypePickerDialogTitle = localize('resourceTypePickerDialog.title', "Select the deployment options"); export const resourceTypeSearchBoxDescription = localize('resourceTypePickerDialog.resourceSearchPlaceholder', "Filter resources..."); diff --git a/extensions/resource-deployment/src/services/uriHandlerService.ts b/extensions/resource-deployment/src/services/uriHandlerService.ts index 07b10db8ff..8ff752b2f5 100644 --- a/extensions/resource-deployment/src/services/uriHandlerService.ts +++ b/extensions/resource-deployment/src/services/uriHandlerService.ts @@ -5,23 +5,74 @@ import * as vscode from 'vscode'; import { ResourceTypeService } from './resourceTypeService'; +import * as loc from '../localizedConstants'; + +interface IGalleryExtension { + name: string; + version: string; + date: string; + displayName: string; + publisherId: string; + publisher: string; + publisherDisplayName: string; + description: string; + preview: boolean; +} export class UriHandlerService implements vscode.UriHandler { constructor(private _resourceTypeService: ResourceTypeService) { } - handleUri(uri: vscode.Uri): vscode.ProviderResult { + async handleUri(uri: vscode.Uri): Promise { // Path to start a deployment // Supported URI parameters : // - type (optional) : The resource type to start the deployment for + // - extension (optional) : The ID of the extension that is required to start the deployment // - params (optional) : A JSON blob of variable names/values to pass as initial values to the wizard. Note // that the JSON blob must be URI-encoded in order to be properly handled // Example URIs : // azuredatastudio://Microsoft.resource-deployment/deploy // azuredatastudio://Microsoft.resource-deployment/deploy?type=arc-controller + // azuredatastudio://Microsoft.resource-deployment/deploy?type=arc-controller&extension=Microsoft.arc // azuredatastudio://Microsoft.resource-deployment/deploy?type=arc-controller¶ms=%7B%22AZDATA_NB_VAR_ARC_SUBSCRIPTION%22%3A%22abdcef12-3456-7890-abcd-ef1234567890%22%2C%22AZDATA_NB_VAR_ARC_RESOURCE_GROUP%22%3A%22my-rg%22%2C%22AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION%22%3A%22westus%22%2C%22AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME%22%3A%22arc-dc%22%7D if (uri.path === '/deploy') { const params = uri.query.split('&').map(kv => kv.split('=')); const paramType = params.find(param => param[0] === 'type')?.[1]; + const extensionId = params.find(param => param[0] === 'extension')?.[1]; + if (extensionId) { + const installedExtension = vscode.extensions.getExtension(extensionId); + if (!installedExtension) { + // The required extension isn't installed, prompt user to install it + const extensionGalleryInfo = await vscode.commands.executeCommand('workbench.extensions.getExtensionFromGallery', extensionId); + if (extensionGalleryInfo) { + const response = await vscode.window.showInformationMessage( + loc.extensionRequiredPrompt(extensionGalleryInfo.displayName), + loc.install); + if (response === loc.install) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.installingExtension(extensionGalleryInfo.displayName), + cancellable: false + }, async (_progress, _token) => { + await vscode.commands.executeCommand('workbench.extensions.installExtension', extensionId); + } + ); + } else { + // If user didn't install extension we wouldn't expect the deployment to work so just return + console.log(`User cancelled out of prompt to install required extension '${extensionId}' for Resource Deployment URI`); + return; + } + } else { + // If we can't find the extension in the gallery then we won't be able to install it - so just inform the user + // that the ID is invalid and return since we wouldn't expect the deployment to work without the extension + vscode.window.showErrorMessage(loc.unknownExtension(extensionId)); + return; + } + } else { + // Extension is already installed, ensure that it's activated before continuing on + await installedExtension.activate(); + } + } const wizardParams = JSON.parse(params.find(param => param[0] === 'params')?.[1] ?? '{}'); const resourceType = this._resourceTypeService.getResourceTypes().find(type => type.name === paramType); diff --git a/src/sql/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/sql/workbench/contrib/extensions/browser/extensions.contribution.ts index 286866787e..1a6542d4a7 100644 --- a/src/sql/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/sql/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -7,8 +7,10 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsLabel, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { OpenExtensionAuthoringDocsAction } from 'sql/workbench/contrib/extensions/browser/extensionsActions'; +import { localize } from 'vs/nls'; +import { deepClone } from 'vs/base/common/objects'; // Global Actions const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); @@ -23,3 +25,27 @@ CommandsRegistry.registerCommand('azdata.extension.open', (accessor: ServicesAcc throw new Error('Extension id is not provided'); } }); + +CommandsRegistry.registerCommand({ + id: 'workbench.extensions.getExtensionFromGallery', + description: { + description: localize('workbench.extensions.getExtensionFromGallery.description', "Gets extension information from the gallery"), + args: [ + { + name: localize('workbench.extensions.getExtensionFromGallery.arg.name', "Extension id"), + schema: { + 'type': ['string'] + } + } + ] + }, + handler: async (accessor, arg: string) => { + const extensionGalleryService = accessor.get(IExtensionGalleryService); + const extension = await extensionGalleryService.getCompatibleExtension({ id: arg }); + if (extension) { + return deepClone(extension); + } else { + throw new Error(localize('notFound', "Extension '{0}' not found.", arg)); + } + } +});