From 5d07a3272e1a78426f1620b7c4df255acbf32753 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Fri, 26 Feb 2021 15:50:55 -0800 Subject: [PATCH] Add URI handler for resource deployment (#14470) * Add URL handler for resource deployment * Add tests --- extensions/resource-deployment/src/main.ts | 3 + .../src/services/uriHandlerService.ts | 40 ++++++++++ .../src/test/services/apiService.test.ts | 2 +- .../test/services/resourceTypeService.test.ts | 4 +- .../test/services/uriHandlerService.test.ts | 78 +++++++++++++++++++ .../src/ui/modelViewUtils.ts | 8 +- 6 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 extensions/resource-deployment/src/services/uriHandlerService.ts create mode 100644 extensions/resource-deployment/src/test/services/uriHandlerService.test.ts diff --git a/extensions/resource-deployment/src/main.ts b/extensions/resource-deployment/src/main.ts index e5de7574be..fcb323be28 100644 --- a/extensions/resource-deployment/src/main.ts +++ b/extensions/resource-deployment/src/main.ts @@ -14,6 +14,7 @@ import { DeploymentInputDialog } from './ui/deploymentInputDialog'; import { ResourceTypePickerDialog } from './ui/resourceTypePickerDialog'; import * as rd from 'resource-deployment'; import { getExtensionApi } from './api'; +import { UriHandlerService } from './services/uriHandlerService'; const localize = nls.loadMessageBundle(); @@ -31,6 +32,8 @@ export async function activate(context: vscode.ExtensionContext): Promise console.error(message)); return undefined; } + const uriHandlerService = new UriHandlerService(resourceTypeService); + vscode.window.registerUriHandler(uriHandlerService); /** * Opens a new ResourceTypePickerDialog * @param defaultResourceTypeName The resource type name to have selected by default diff --git a/extensions/resource-deployment/src/services/uriHandlerService.ts b/extensions/resource-deployment/src/services/uriHandlerService.ts new file mode 100644 index 0000000000..07b10db8ff --- /dev/null +++ b/extensions/resource-deployment/src/services/uriHandlerService.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ResourceTypeService } from './resourceTypeService'; + +export class UriHandlerService implements vscode.UriHandler { + constructor(private _resourceTypeService: ResourceTypeService) { } + + handleUri(uri: vscode.Uri): vscode.ProviderResult { + // Path to start a deployment + // Supported URI parameters : + // - type (optional) : The resource type to start the deployment for + // - 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¶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 wizardParams = JSON.parse(params.find(param => param[0] === 'params')?.[1] ?? '{}'); + + const resourceType = this._resourceTypeService.getResourceTypes().find(type => type.name === paramType); + if (paramType && !resourceType) { + console.warn(`Unknown resource type ${paramType}`); + } + + if (resourceType) { + this._resourceTypeService.startDeployment(resourceType, undefined, wizardParams); + } else { + return vscode.commands.executeCommand('azdata.resource.deploy'); + } + + } + } +} diff --git a/extensions/resource-deployment/src/test/services/apiService.test.ts b/extensions/resource-deployment/src/test/services/apiService.test.ts index f6b82a629c..3435256191 100644 --- a/extensions/resource-deployment/src/test/services/apiService.test.ts +++ b/extensions/resource-deployment/src/test/services/apiService.test.ts @@ -5,7 +5,7 @@ import 'mocha'; import { apiService } from '../../services/apiService'; -import assert = require('assert'); +import * as assert from 'assert'; describe('API Service Tests', function (): void { it('get azurecoreApi returns azure api', () => { diff --git a/extensions/resource-deployment/src/test/services/resourceTypeService.test.ts b/extensions/resource-deployment/src/test/services/resourceTypeService.test.ts index 37a04ef4f2..4c1f8fbdd1 100644 --- a/extensions/resource-deployment/src/test/services/resourceTypeService.test.ts +++ b/extensions/resource-deployment/src/test/services/resourceTypeService.test.ts @@ -5,8 +5,8 @@ import 'mocha'; import * as TypeMoq from 'typemoq'; -import assert = require('assert'); -import should = require('should'); +import * as assert from 'assert'; +import * as should from 'should'; import { EOL } from 'os'; import { ResourceTypeService, processWhenClause } from '../../services/resourceTypeService'; import { IPlatformService } from '../../services/platformService'; diff --git a/extensions/resource-deployment/src/test/services/uriHandlerService.test.ts b/extensions/resource-deployment/src/test/services/uriHandlerService.test.ts new file mode 100644 index 0000000000..f3408b9036 --- /dev/null +++ b/extensions/resource-deployment/src/test/services/uriHandlerService.test.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { ResourceTypeService } from '../../services/resourceTypeService'; +import { UriHandlerService } from '../../services/uriHandlerService'; +import { ResourceType } from '../../interfaces'; + +const resourceType1Name = 'resource-type-1'; + +const mockResourceTypes: ResourceType[] = [ + { + name: resourceType1Name, + displayName: 'Resource Type 1', + description: '', + platforms: '*', + icon: '', + options: [], + providers: [], + helpTexts: [], + getOkButtonText: (selectedOptions: { option: string, value: string }[]) => undefined, + getProvider: (selectedOptions: { option: string, value: string }[]) => undefined, + getAgreementInfo: (selectedOptions: { option: string, value: string }[]) => undefined, + getHelpText: (selectedOption: { option: string, value: string }[]) => undefined + } +]; + +describe('uriHandlerService Tests', function (): void { + + afterEach(function (): void { + sinon.restore(); + }); + + const resourceTypeServiceMock = TypeMoq.Mock.ofType(); + resourceTypeServiceMock.setup(x => x.getResourceTypes()).returns(() => { + return mockResourceTypes; + }); + const uriHandlerService = new UriHandlerService(resourceTypeServiceMock.object); + + it('unknown path is ignored', async function (): Promise { + const uri = vscode.Uri.parse('azuredatastudio://Microsoft.resource-deployment/badPath'); + await uriHandlerService.handleUri(uri); + }); + + describe('deploy path', function (): void { + it('no parameters', async function (): Promise { + const executeCommandStub = sinon.stub(vscode.commands, 'executeCommand'); + const uri = vscode.Uri.parse('azuredatastudio://Microsoft.resource-deployment/deploy'); + await uriHandlerService.handleUri(uri); + sinon.assert.calledOnce(executeCommandStub); + }); + + it('unknown type', async function (): Promise { + const executeCommandStub = sinon.stub(vscode.commands, 'executeCommand'); + const uri = vscode.Uri.parse('azuredatastudio://Microsoft.resource-deployment/deploy?type=unknown-type'); + await uriHandlerService.handleUri(uri); + sinon.assert.calledOnce(executeCommandStub); + }); + + it('with type only', async function (): Promise { + const uri = vscode.Uri.parse(`azuredatastudio://Microsoft.resource-deployment/deploy?type=${resourceType1Name}`); + await uriHandlerService.handleUri(uri); + resourceTypeServiceMock.verify(x => x.startDeployment(TypeMoq.It.isObjectWith(mockResourceTypes[0]), undefined, TypeMoq.It.isObjectWith({})), TypeMoq.Times.once()); + }); + + it('with parameters', async function (): Promise { + const params = { 'param1': 'value1', 'param2': 'value2' }; + const uri = vscode.Uri.parse(`azuredatastudio://Microsoft.resource-deployment/deploy?type=${resourceType1Name}¶ms=${encodeURIComponent(JSON.stringify(params))}`); + await uriHandlerService.handleUri(uri); + resourceTypeServiceMock.verify(x => x.startDeployment(TypeMoq.It.isObjectWith(mockResourceTypes[0]), undefined, TypeMoq.It.isObjectWith(params)), TypeMoq.Times.once()); + }); + }); +}); diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index bf7196073b..ff93d68ed1 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -1062,7 +1062,7 @@ async function processAzureAccountField(context: AzureAccountFieldContext): Prom // Check if we have an initial subscription value - if we do then the user isn't going to be allowed to change any of the // Azure values so we can skip adding the account picker - const hasInitialSubscriptionValue = !!context.initialVariableValues?.[context.fieldInfo.subscriptionVariableName || ''].toString(); + const hasInitialSubscriptionValue = !!context.initialVariableValues?.[context.fieldInfo.subscriptionVariableName || '']?.toString(); if (!hasInitialSubscriptionValue) { accountComponents = createAzureAccountDropdown(context); accountDropdown = accountComponents.accountDropdown; @@ -1215,7 +1215,7 @@ function createAzureSubscriptionComponent( cssStyles: context.fieldInfo.labelCSSStyles }); - const defaultValue = context.initialVariableValues?.[context.fieldInfo.subscriptionVariableName || ''].toString() ?? (context.fieldInfo.required ? undefined : ''); + const defaultValue = context.initialVariableValues?.[context.fieldInfo.subscriptionVariableName || '']?.toString() ?? (context.fieldInfo.required ? undefined : ''); let subscriptionInputInfo: InputComponentInfo; let setValueFunc: (value: InputValueType) => void; // If we have an default value then we don't allow users to modify it - so use a disabled text input box instead @@ -1373,7 +1373,7 @@ function createAzureResourceGroupsComponent( width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); - const defaultValue = context.initialVariableValues?.[context.fieldInfo.resourceGroupVariableName || ''].toString() ?? (context.fieldInfo.required ? undefined : ''); + const defaultValue = context.initialVariableValues?.[context.fieldInfo.resourceGroupVariableName || '']?.toString() ?? (context.fieldInfo.required ? undefined : ''); let resourceGroupInputInfo: InputComponentInfo; // If we have an default value then we don't allow users to modify it - so use a disabled text input box instead if (defaultValue) { @@ -1478,7 +1478,7 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext): width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); - const defaultValue = context.initialVariableValues?.[context.fieldInfo.locationVariableName || ''].toString() ?? (context.fieldInfo.required ? undefined : ''); + const defaultValue = context.initialVariableValues?.[context.fieldInfo.locationVariableName || '']?.toString() ?? (context.fieldInfo.required ? undefined : ''); let locationInputInfo: InputComponentInfo; // If we have an default value then we don't allow users to modify it - so use a disabled text input box instead if (defaultValue) {