From 636dd53c645e1a9b6d78a91e7f1856fc0b750d15 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Tue, 8 Oct 2024 12:40:45 +0530 Subject: [PATCH 01/12] phase 2 provisioning --- .../sql-migration/src/constants/strings.ts | 13 +++--- .../GenerateProvisioningScriptDialog.ts | 30 +++++++++++-- .../SelectStorageAccountDialog.ts | 43 ++++++++++++++----- .../assessmentDetailsHeader.ts | 2 +- .../assessmentSummaryCard.ts | 2 +- 5 files changed, 67 insertions(+), 23 deletions(-) diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 15fe28a3972f..e4b988b54b5e 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -260,11 +260,12 @@ export const IMPORT_PERFORMANCE_DATA = localize('sql.migration.sku.import.perfor export const IMPORT_PERFORMANCE_DATA_DIALOG_DESCRIPTION = localize('sql.migration.sku.import.performance.data.dialog.description', "Import this data file from an existing folder, if you have already collected it using Data Migration Assistant."); export const IMPORT_PERFORMANCE_DATA_DIALOG_HELPER_MESSAGE = localize('sql.migration.sku.import.performance.data.dialog.helper.message', "Select a folder on your local drive"); export const IMPORT_PERFORMANCE_DATA_DIALOG_OPEN_FOLDER = localize('sql.migration.sku.import.performance.data.dialog.open.folder', "Select a folder"); -export const UPLOAD_TEMPLATE_TO_AZURE = localize('sql.migration.target.upload.to.azure', "Save to Azure blob container"); -export const TARGET_PROVISIONING_TITLE = localize('sql.migration.target.provisioning.title', "Save Template"); -export const GENERATE_ARM_TEMPLATE = localize('sql.migration.target.provisioning.generate.template', "Generate Template"); +export const UPLOAD_TEMPLATE_TO_AZURE = localize('sql.migration.target.provisioning.upload.to.azure', "Deploy to Azure"); +export const SAVE_TO_DEVICE = localize('sql.migration.target.provisioning.generate.template', "Save to device"); +export const COPY_TO_CLIPBOARD = localize('sql.migration.target.provisioning.copy.to.clipboard', "Copy to clipboard"); + export const CLOSE_DIALOG = localize('sql.migration.target.provisioning.close', "Close"); -export const TARGET_PROVISIONING_DESCRIPTION = localize('sql.migration.target.provisioning.description', "Below is the ARM script for the recommended target SKU. You can save the script as template."); +export const TARGET_PROVISIONING_DESCRIPTION = localize('sql.migration.target.provisioning.description', "Below is the ARM script for the recommended target SKU. You can use the following two methods to deploy target SKU to Azure.\n 1.Click on the \"Deploy to Azure\" command to deploy the target resource. This option requires an Azure blob container account.\n 2.Click on \"Save to device\" to save the ARM script and then manually deploy the target resource."); export const DISPLAY_ARM_TEMPLATE_LIMIT = localize('sql.migration.target.provisioning.template.display.limit', "A single ARM template has a deployment limitation of a maximum 50 Azure SQL Databases. The template for the first 50 databases is displayed below. To view all templates, save the template JSON file in a local storage or Azure Blob storage.") // allow-any-unicode-next-line @@ -1730,10 +1731,10 @@ export const MIGRATION_SERVICE_SELECT_SERVICE_LABEL = localize('sql.migration.se export const MIGRATION_SERVICE_SELECT_SERVICE_PROMPT = localize('sql.migration.select.service.prompt', 'Select a Database Migration Service'); // Upload Arm Template Dialog -export const SELECT_STORAGE_ACCOUNT_TITLE = localize('sql.migration.select.storage.account.title', "Select Azure Storage Account"); -export const STORAGE_ACCOUNT_SELECT_HEADING = localize('sql.migration.select.storage.account.heading', "Enter the details below to select the Azure Storage account and save the script as template"); +export const STORAGE_ACCOUNT_SELECT_HEADING = localize('sql.migration.select.storage.account.heading', "You need to provide an Azure blob container account to deploy the target SKU. Select the account details below:"); export const STORAGE_ACCOUNT_SELECT_LABEL = localize('sql.migration.select.storage.account.label', "Storage Account"); export const SAVE_LABEL = localize('sql.migration.target.provisioning.save', "Save"); +export const DEPLOY_LABEL = localize('sql.migration.target.provisioning.save', "Deploy"); export const TARGET_STORAGE_ACCOUNT_INFO = localize('sql.migration.storage.account', "Your Storage Account name"); export const TARGET_BLOB_CONTAINER_INFO = localize('sql.migration.storage.account.blob.container', "Your Blob Container name"); diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts index b86d12364d71..138b7d2852fd 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts @@ -86,7 +86,7 @@ export class GenerateProvisioningScriptDialog { const saveTemplateButton = _view.modelBuilder.button() .withProps({ buttonType: azdata.ButtonType.Normal, - label: constants.TARGET_PROVISIONING_TITLE, + label: constants.SAVE_TO_DEVICE, width: 117, height: 36, iconHeight: 16, @@ -120,7 +120,7 @@ export class GenerateProvisioningScriptDialog { .withProps({ buttonType: azdata.ButtonType.Normal, label: constants.UPLOAD_TEMPLATE_TO_AZURE, - width: 180, + width: 125, height: 36, iconHeight: 16, iconWidth: 16, @@ -135,6 +135,26 @@ export class GenerateProvisioningScriptDialog { await selectAzureAccountDialog.initialize(); }); + const copyToClipboardButton = _view.modelBuilder.button() + .withProps({ + buttonType: azdata.ButtonType.Normal, + label: constants.COPY_TO_CLIPBOARD, + width: 130, + height: 36, + iconHeight: 16, + iconWidth: 16, + iconPath: IconPathHelper.copy, + CSSStyles: { + ...styles.TOOLBAR_CSS + } + }).component(); + + copyToClipboardButton.onDidClick(async () => { + if (this.model._armTemplateResult.templates) { + void vscode.env.clipboard.writeText(this.model._armTemplateResult.templates[0]); + } + }); + const buttonsContainer = _view.modelBuilder.flexContainer().withProps({ CSSStyles: { 'display': 'flex', @@ -145,8 +165,10 @@ export class GenerateProvisioningScriptDialog { } }).component(); - buttonsContainer.addItem(saveTemplateButton); buttonsContainer.addItem(uploadTemlateToAzureButton); + buttonsContainer.addItem(saveTemplateButton); + buttonsContainer.addItem(copyToClipboardButton) + const container = _view.modelBuilder.flexContainer(). withProps({ @@ -178,7 +200,7 @@ export class GenerateProvisioningScriptDialog { if (!this._isOpen) { this._isOpen = true; - this.dialog = azdata.window.createModelViewDialog(constants.TARGET_PROVISIONING_TITLE, 'ViewArmTemplateDialog', 'medium'); + this.dialog = azdata.window.createModelViewDialog(constants.UPLOAD_TEMPLATE_TO_AZURE, 'ViewArmTemplateDialog', 'medium'); this.dialog.okButton.label = constants.CLOSE_DIALOG; this.dialog.okButton.position = 'left'; diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts index 748d98e07714..e443e69e340d 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts @@ -12,7 +12,7 @@ import * as utils from '../../api/utils'; import { StorageAccount } from '../../api/azure'; import { logError, TelemetryViews } from '../../telemetry'; import { MigrationStateModel } from '../../models/stateMachine'; -import { StorageSharedKeyCredential, BlockBlobClient, BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob'; +import { generateAccountSASQueryParameters, StorageSharedKeyCredential, BlockBlobClient, AccountSASServices, AccountSASResourceTypes, AccountSASPermissions, SASProtocol } from '@azure/storage-blob'; import { getStorageAccountAccessKeys } from '../../api/azure'; import { MigrationTargetType } from '../../api/utils'; @@ -70,7 +70,7 @@ export class SelectStorageAccountDialog { protected readonly migrationStateModel: MigrationStateModel, public _targetType: MigrationTargetType ) { this._dialog = azdata.window.createModelViewDialog( - constants.SELECT_STORAGE_ACCOUNT_TITLE, + constants.UPLOAD_TEMPLATE_TO_AZURE, 'SelectStorageAccountDialog', 460, 'normal' @@ -89,7 +89,7 @@ export class SelectStorageAccountDialog { }); - this._dialog.okButton.label = constants.SAVE_LABEL; + this._dialog.okButton.label = constants.DEPLOY_LABEL; this._disposables.push( this._dialog.okButton.onClick(async (value) => { await this.uploadTemplate(); @@ -120,7 +120,7 @@ export class SelectStorageAccountDialog { return this._view.modelBuilder.text() .withProps({ value: constants.STORAGE_ACCOUNT_SELECT_HEADING, - CSSStyles: { ...styles.PAGE_TITLE_CSS } + CSSStyles: { ...styles.BODY_CSS } }).component(); } @@ -589,23 +589,32 @@ export class SelectStorageAccountDialog { const containerName = this._blobContainer.name; const templates = this.migrationStateModel._armTemplateResult.templates!; const sharedKeyCredential = new StorageSharedKeyCredential(this._storageAccount.name, storageKeys.keyName1); - - const sasToken = generateBlobSASQueryParameters({ - containerName, - permissions: BlobSASPermissions.parse("racwd"), - expiresOn: new Date(new Date().valueOf() + 86400), - }, + var deployToAzureUrl: string = ""; + + const sasOptions = { + services: AccountSASServices.parse("b").toString(), // blobs + resourceTypes: AccountSASResourceTypes.parse("sco").toString(), // service, container, object + permissions: AccountSASPermissions.parse("rwdlacupi"), // permissions + protocol: SASProtocol.Https, + startsOn: new Date(), + expiresOn: new Date(new Date().valueOf() + (30 * 60 * 1000)), // 30 minutes + }; + + const sasToken = generateAccountSASQueryParameters( + sasOptions, sharedKeyCredential ).toString(); try { for (let i = 0; i < templates.length; i++) { const blobName = utils.generateTemplatePath(this.migrationStateModel, this._targetType, i + 1); - const sasUrl = `https://${accountName}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`; + var sasUrl = `https://${accountName}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`; + deployToAzureUrl = sasUrl; const blockBlobClient = new BlockBlobClient(sasUrl); await blockBlobClient.upload(templates[i], templates[i].length); } void vscode.window.showInformationMessage(constants.UPLOAD_TEMPLATE_SUCCESS); + this.DeployToAzure(deployToAzureUrl); } catch (e) { logError(TelemetryViews.UploadArmTemplateDialog, 'ArmTemplateUploadError', e); @@ -613,4 +622,16 @@ export class SelectStorageAccountDialog { } } + + private DeployToAzure(deployToAzureUrl: string) { + deployToAzureUrl = encodeURIComponent(deployToAzureUrl); + let scheme = 'https'; + let authority = 'portal.azure.com'; + let path = '/'; + let fragment = 'create/Microsoft.Template/uri/' + deployToAzureUrl; + let query = ''; + let uri = vscode.Uri.from({ scheme, authority, path, query, fragment }); + void vscode.env.openExternal(uri); + } + } diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsHeader.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsHeader.ts index abf483cb6e6b..51ea8cc7ff62 100644 --- a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsHeader.ts +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsHeader.ts @@ -196,7 +196,7 @@ export class AssessmentDetailsHeader { }); this._generateTemplateLink = this._view.modelBuilder.hyperlink().withProps({ - label: constants.GENERATE_ARM_TEMPLATE, + label: constants.UPLOAD_TEMPLATE_TO_AZURE, url: '', height: 18, CSSStyles: styles.VIEW_DETAILS_GENERATE_TEMPLATE_LINK diff --git a/extensions/sql-migration/src/wizard/skuRecommendation/assessmentSummaryCard.ts b/extensions/sql-migration/src/wizard/skuRecommendation/assessmentSummaryCard.ts index c940153781e5..5d87e5c64285 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendation/assessmentSummaryCard.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendation/assessmentSummaryCard.ts @@ -408,7 +408,7 @@ export class AssessmentSummaryCard implements vscode.Disposable { }); this._generateTemplateLink = view.modelBuilder.hyperlink().withProps({ - label: constants.GENERATE_ARM_TEMPLATE, + label: constants.UPLOAD_TEMPLATE_TO_AZURE, url: '', height: 18, CSSStyles: styles.VIEW_DETAILS_GENERATE_TEMPLATE_LINK From 335f4e23718b0d5f51be45126c38c78549064458 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Wed, 9 Oct 2024 01:14:08 +0530 Subject: [PATCH 02/12] change url open method --- .../sql-migration/src/constants/strings.ts | 2 +- .../SelectStorageAccountDialog.ts | 44 +++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index e4b988b54b5e..eb87cec5776c 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -1744,7 +1744,7 @@ export const SELECT_A_STORAGE_ACCOUNT = localize('sql.migration.select.storage.s export const STORAGE_ACCOUNT_SUBSCRIPTION_INFO = localize('sql.migration.storage.account.subscription', "Subscription name for your Storage Account"); export const SAVE_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.save.template.success', "Template saved successfully"); export const SAVE_TEMPLATE_FAIL = localize('sql.migration.target.provisioning.save.template.fail', "Failed to save ARM Template"); -export const UPLOAD_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.upload.template.success', "Template uploaded successfully"); +export const UPLOAD_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.upload.template.success', "Azure Portal Custom Deployment page with parameters pre-filled with the default values from the template has been opened in browser. There can be multiple windows if you are provisioning more than 50 Azure SQL DBs."); export const UPLOAD_TEMPLATE_FAIL = localize('sql.migration.target.provisioning.upload.template.fail', "Failed to upload ARM Template"); diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts index e443e69e340d..789c40fa6f67 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts @@ -20,6 +20,7 @@ const INPUT_COMPONENT_WIDTH = '100%'; const STYLE_HIDE = { 'display': 'none' }; const STYLE_ShOW = { 'display': 'inline' }; const CONTROL_MARGIN = '20px'; +const exec = require('child_process').exec; export const BODY_CSS = { 'font-size': '13px', 'line-height': '18px', @@ -589,7 +590,6 @@ export class SelectStorageAccountDialog { const containerName = this._blobContainer.name; const templates = this.migrationStateModel._armTemplateResult.templates!; const sharedKeyCredential = new StorageSharedKeyCredential(this._storageAccount.name, storageKeys.keyName1); - var deployToAzureUrl: string = ""; const sasOptions = { services: AccountSASServices.parse("b").toString(), // blobs @@ -605,16 +605,18 @@ export class SelectStorageAccountDialog { sharedKeyCredential ).toString(); + let sasUrls: string[] = []; + try { for (let i = 0; i < templates.length; i++) { const blobName = utils.generateTemplatePath(this.migrationStateModel, this._targetType, i + 1); var sasUrl = `https://${accountName}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`; - deployToAzureUrl = sasUrl; + sasUrls.push(sasUrl); const blockBlobClient = new BlockBlobClient(sasUrl); await blockBlobClient.upload(templates[i], templates[i].length); } - void vscode.window.showInformationMessage(constants.UPLOAD_TEMPLATE_SUCCESS); - this.DeployToAzure(deployToAzureUrl); + + this.DeployToAzure(sasUrls); } catch (e) { logError(TelemetryViews.UploadArmTemplateDialog, 'ArmTemplateUploadError', e); @@ -623,15 +625,31 @@ export class SelectStorageAccountDialog { } - private DeployToAzure(deployToAzureUrl: string) { - deployToAzureUrl = encodeURIComponent(deployToAzureUrl); - let scheme = 'https'; - let authority = 'portal.azure.com'; - let path = '/'; - let fragment = 'create/Microsoft.Template/uri/' + deployToAzureUrl; - let query = ''; - let uri = vscode.Uri.from({ scheme, authority, path, query, fragment }); - void vscode.env.openExternal(uri); + private DeployToAzure(sasUrls: string[]) { + let opener; + switch (process.platform) { + case 'darwin': + opener = 'open'; + break; + case 'win32': + opener = 'start'; + break; + default: + opener = 'xdg-open'; + break; + } + + for (let i = 0; i < sasUrls.length; i++) { + // generate custom deployment URL for each ARM template. + // In case of SQL DB we can have more than 1 ARM template file since a single file has a limit of 50 DBs. + let deployToAzureUrl = 'https://portal.azure.com/#create/Microsoft.Template/uri/' + encodeURIComponent(sasUrls[i]); + + // open the custom deployment URL in browser. + exec(`${opener} ${deployToAzureUrl}`); + } + + void vscode.window.showInformationMessage(constants.UPLOAD_TEMPLATE_SUCCESS); + } } From 9f00e08c86377c73a7fec4f8c7ce52e7f1e5e7f3 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Wed, 9 Oct 2024 16:18:54 +0530 Subject: [PATCH 03/12] changed icon --- extensions/sql-migration/images/Azure.svg | 23 +++++++++++++++++++ .../src/constants/iconPathHelper.ts | 5 ++++ .../sql-migration/src/constants/strings.ts | 2 +- .../GenerateProvisioningScriptDialog.ts | 2 +- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 extensions/sql-migration/images/Azure.svg diff --git a/extensions/sql-migration/images/Azure.svg b/extensions/sql-migration/images/Azure.svg new file mode 100644 index 000000000000..27cf129308e6 --- /dev/null +++ b/extensions/sql-migration/images/Azure.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index a1fe274e0e22..07e598397fb3 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -61,6 +61,7 @@ export class IconPathHelper { public static emptyState: IconPath; public static save: IconPath; public static runScript: IconPath; + public static Azure: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.copy = { @@ -263,5 +264,9 @@ export class IconPathHelper { light: context.asAbsolutePath('images/runScript.svg'), dark: context.asAbsolutePath('images/runScript.svg') }; + IconPathHelper.Azure = { + light: context.asAbsolutePath('images/Azure.svg'), + dark: context.asAbsolutePath('images/Azure.svg') + }; } } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index eb87cec5776c..141afaae36c1 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -1744,7 +1744,7 @@ export const SELECT_A_STORAGE_ACCOUNT = localize('sql.migration.select.storage.s export const STORAGE_ACCOUNT_SUBSCRIPTION_INFO = localize('sql.migration.storage.account.subscription', "Subscription name for your Storage Account"); export const SAVE_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.save.template.success', "Template saved successfully"); export const SAVE_TEMPLATE_FAIL = localize('sql.migration.target.provisioning.save.template.fail', "Failed to save ARM Template"); -export const UPLOAD_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.upload.template.success', "Azure Portal Custom Deployment page with parameters pre-filled with the default values from the template has been opened in browser. There can be multiple windows if you are provisioning more than 50 Azure SQL DBs."); +export const UPLOAD_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.upload.template.success', "Azure Portal Custom Deployment page with parameters pre-filled with the default values from the template has been opened in browser. \n \"Note:There can be multiple windows if you are provisioning more than 50 Azure SQL DBs.\""); export const UPLOAD_TEMPLATE_FAIL = localize('sql.migration.target.provisioning.upload.template.fail', "Failed to upload ARM Template"); diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts index 138b7d2852fd..1218fee57c85 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts @@ -124,7 +124,7 @@ export class GenerateProvisioningScriptDialog { height: 36, iconHeight: 16, iconWidth: 16, - iconPath: IconPathHelper.import, + iconPath: IconPathHelper.Azure, CSSStyles: { ...styles.TOOLBAR_CSS } From 9f2d734b7326496f02f3957b5940de5dad90cb95 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Fri, 11 Oct 2024 15:15:12 +0530 Subject: [PATCH 04/12] change sastoken method --- .../SelectStorageAccountDialog.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts index 789c40fa6f67..3e21f53c7ba3 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts @@ -12,7 +12,7 @@ import * as utils from '../../api/utils'; import { StorageAccount } from '../../api/azure'; import { logError, TelemetryViews } from '../../telemetry'; import { MigrationStateModel } from '../../models/stateMachine'; -import { generateAccountSASQueryParameters, StorageSharedKeyCredential, BlockBlobClient, AccountSASServices, AccountSASResourceTypes, AccountSASPermissions, SASProtocol } from '@azure/storage-blob'; +import { StorageSharedKeyCredential, BlockBlobClient, BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob'; import { getStorageAccountAccessKeys } from '../../api/azure'; import { MigrationTargetType } from '../../api/utils'; @@ -591,17 +591,12 @@ export class SelectStorageAccountDialog { const templates = this.migrationStateModel._armTemplateResult.templates!; const sharedKeyCredential = new StorageSharedKeyCredential(this._storageAccount.name, storageKeys.keyName1); - const sasOptions = { - services: AccountSASServices.parse("b").toString(), // blobs - resourceTypes: AccountSASResourceTypes.parse("sco").toString(), // service, container, object - permissions: AccountSASPermissions.parse("rwdlacupi"), // permissions - protocol: SASProtocol.Https, - startsOn: new Date(), - expiresOn: new Date(new Date().valueOf() + (30 * 60 * 1000)), // 30 minutes - }; - - const sasToken = generateAccountSASQueryParameters( - sasOptions, + const sasToken = generateBlobSASQueryParameters( + { + containerName, + permissions: BlobSASPermissions.parse("racwd"), + expiresOn: new Date(new Date().valueOf() + 86400), + }, sharedKeyCredential ).toString(); From 81fb0f4ac4f30f3904964de45a396878d33e402c Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Mon, 14 Oct 2024 18:37:05 +0530 Subject: [PATCH 05/12] update STS version --- extensions/sql-migration/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/sql-migration/config.json b/extensions/sql-migration/config.json index 9a387c2e25cd..3462d7c9ec79 100644 --- a/extensions/sql-migration/config.json +++ b/extensions/sql-migration/config.json @@ -1,7 +1,7 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.migration-{#fileName#}", "useDefaultLinuxRuntime": true, - "version": "5.0.20241003.1", + "version": "5.0.20241014.2", "downloadFileNames": { "Windows_86": "win-x86-net8.0.zip", "Windows": "win-x64-net8.0.zip", From da82743eac08d7a902fa053909b0797af51d0ed2 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Tue, 15 Oct 2024 14:46:04 +0530 Subject: [PATCH 06/12] telemetry changes --- .../sql-migration/src/constants/strings.ts | 2 ++ .../GenerateProvisioningScriptDialog.ts | 30 +++++++++++++++---- .../SelectStorageAccountDialog.ts | 12 +++++++- extensions/sql-migration/src/telemetry.ts | 7 ++++- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 141afaae36c1..d0acecfa1869 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -263,6 +263,7 @@ export const IMPORT_PERFORMANCE_DATA_DIALOG_OPEN_FOLDER = localize('sql.migratio export const UPLOAD_TEMPLATE_TO_AZURE = localize('sql.migration.target.provisioning.upload.to.azure', "Deploy to Azure"); export const SAVE_TO_DEVICE = localize('sql.migration.target.provisioning.generate.template', "Save to device"); export const COPY_TO_CLIPBOARD = localize('sql.migration.target.provisioning.copy.to.clipboard', "Copy to clipboard"); +export const ARM_TEMPLATE_GENERATE_FAILED = localize('sql.migration.target.provisioning.copy.to.clipboard', "Failed to generate ARM template"); export const CLOSE_DIALOG = localize('sql.migration.target.provisioning.close', "Close"); export const TARGET_PROVISIONING_DESCRIPTION = localize('sql.migration.target.provisioning.description', "Below is the ARM script for the recommended target SKU. You can use the following two methods to deploy target SKU to Azure.\n 1.Click on the \"Deploy to Azure\" command to deploy the target resource. This option requires an Azure blob container account.\n 2.Click on \"Save to device\" to save the ARM script and then manually deploy the target resource."); @@ -1743,6 +1744,7 @@ export const STORAGE_ACCOUNT_RESOURCE_GROUP_INFO = localize('sql.migration.stora export const SELECT_A_STORAGE_ACCOUNT = localize('sql.migration.select.storage.select.a.storage.account', "Select a Storage Account"); export const STORAGE_ACCOUNT_SUBSCRIPTION_INFO = localize('sql.migration.storage.account.subscription', "Subscription name for your Storage Account"); export const SAVE_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.save.template.success', "Template saved successfully"); +export const COPY_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.copy.template.success', "Template copied successfully"); export const SAVE_TEMPLATE_FAIL = localize('sql.migration.target.provisioning.save.template.fail', "Failed to save ARM Template"); export const UPLOAD_TEMPLATE_SUCCESS = localize('sql.migration.target.provisioning.upload.template.success', "Azure Portal Custom Deployment page with parameters pre-filled with the default values from the template has been opened in browser. \n \"Note:There can be multiple windows if you are provisioning more than 50 Azure SQL DBs.\""); export const UPLOAD_TEMPLATE_FAIL = localize('sql.migration.target.provisioning.upload.template.fail', "Failed to upload ARM Template"); diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts index 1218fee57c85..ad3554697edf 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts @@ -11,7 +11,7 @@ import { MigrationStateModel } from '../../models/stateMachine'; import * as constants from '../../constants/strings'; import * as styles from '../../constants/styles'; import * as utils from '../../api/utils'; -import { logError, TelemetryViews } from '../../telemetry'; +import { logError, TelemetryViews, sendButtonClickEvent, TelemetryAction, sendSqlMigrationActionEvent } from '../../telemetry'; import { IconPathHelper } from '../../constants/iconPathHelper'; import { SelectStorageAccountDialog } from './SelectStorageAccountDialog'; @@ -108,6 +108,12 @@ export class GenerateProvisioningScriptDialog { fs.writeFileSync(destinationFilePath!, this._armTemplateText!); } void vscode.window.showInformationMessage(constants.SAVE_TEMPLATE_SUCCESS); + // emit Telemetry for the success. + sendSqlMigrationActionEvent( + TelemetryViews.ProvisioningScriptWizard, + TelemetryAction.SaveArmTemplateSuccess, + {}, {} + ); } catch (e) { logError(TelemetryViews.ProvisioningScriptWizard, 'ArmTemplateSavetoLocalError', e); @@ -150,8 +156,15 @@ export class GenerateProvisioningScriptDialog { }).component(); copyToClipboardButton.onDidClick(async () => { - if (this.model._armTemplateResult.templates) { + if (this.model._armTemplateResult.templates?.[0]) { void vscode.env.clipboard.writeText(this.model._armTemplateResult.templates[0]); + void vscode.window.showInformationMessage(constants.COPY_TEMPLATE_SUCCESS); + // emit Telemetry for the success. + sendSqlMigrationActionEvent( + TelemetryViews.ProvisioningScriptWizard, + TelemetryAction.CopyArmTemplateSuccess, + {}, {} + ); } }); @@ -187,9 +200,13 @@ export class GenerateProvisioningScriptDialog { } private async displayArmTemplate(): Promise { - this._armTemplateTextBox.value = this.model._armTemplateResult.templates ? - this.model._armTemplateResult.templates[0] : - this.model._armTemplateResult.generateTemplateError?.message; + if (this.model._armTemplateResult.templates?.[0]) { + this._armTemplateTextBox.value = this.model._armTemplateResult.templates[0]; + } + else { + this._armTemplateTextBox.value = constants.ARM_TEMPLATE_GENERATE_FAILED; + await vscode.window.showErrorMessage(constants.ARM_TEMPLATE_GENERATE_FAILED); + } if (this.model._armTemplateResult.templates?.length! > 1 && this._targetType === utils.MigrationTargetType.SQLDB) { await vscode.window.showInformationMessage(constants.DISPLAY_ARM_TEMPLATE_LIMIT); @@ -213,6 +230,9 @@ export class GenerateProvisioningScriptDialog { azdata.window.openDialog(this.dialog); await Promise.all(dialogSetupPromises); + // emit Telemetry for opening of Wizard. + sendButtonClickEvent(this.model, TelemetryViews.ProvisioningScriptWizard, TelemetryAction.OpenTargetProvisioningWizard, "", constants.UPLOAD_TEMPLATE_TO_AZURE); + const skuRecommendationReportFilePath = this.getSkuRecommendationReportFilePath(this._targetType); await this.model.getArmTemplate(skuRecommendationReportFilePath); const error = this.model._armTemplateResult.generateTemplateError; diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts index 3e21f53c7ba3..72f2835b05e2 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts @@ -10,7 +10,7 @@ import * as styles from '../../constants/styles'; import * as constants from '../../constants/strings'; import * as utils from '../../api/utils'; import { StorageAccount } from '../../api/azure'; -import { logError, TelemetryViews } from '../../telemetry'; +import { logError, TelemetryViews, sendButtonClickEvent, TelemetryAction, sendSqlMigrationActionEvent } from '../../telemetry'; import { MigrationStateModel } from '../../models/stateMachine'; import { StorageSharedKeyCredential, BlockBlobClient, BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob'; import { getStorageAccountAccessKeys } from '../../api/azure'; @@ -96,6 +96,9 @@ export class SelectStorageAccountDialog { await this.uploadTemplate(); })); azdata.window.openDialog(this._dialog); + + // emit Telemetry for opening of Dialog. + sendButtonClickEvent(this.migrationStateModel, TelemetryViews.ProvisioningScriptWizard, TelemetryAction.OpenDeployArmTemplateDialog, "", constants.UPLOAD_TEMPLATE_TO_AZURE); } protected async registerContent(view: azdata.ModelView): Promise { @@ -645,6 +648,13 @@ export class SelectStorageAccountDialog { void vscode.window.showInformationMessage(constants.UPLOAD_TEMPLATE_SUCCESS); + // emit Telemetry for the success. + sendSqlMigrationActionEvent( + TelemetryViews.UploadArmTemplateDialog, + TelemetryAction.OpenCustomDeploymentPortalSuccess, + {}, {} + ); + } } diff --git a/extensions/sql-migration/src/telemetry.ts b/extensions/sql-migration/src/telemetry.ts index 85a7ef1fdc64..736a1c890d2c 100644 --- a/extensions/sql-migration/src/telemetry.ts +++ b/extensions/sql-migration/src/telemetry.ts @@ -89,7 +89,12 @@ export enum TelemetryAction { TdeConfigurationAlreadyMigrated = 'TdeConfigurationAlreadyMigrated', TdeConfigurationCancelled = 'TdeConfigurationCancelled', ImportAssessmentSuccess = 'ImportAssessmentSuccess', - ImportAssessmentFailed = 'ImportAssessmentFailed' + ImportAssessmentFailed = 'ImportAssessmentFailed', + SaveArmTemplateSuccess = 'SaveArmTemplateSuccess', + CopyArmTemplateSuccess = 'CopyArmTemplateSuccess', + OpenCustomDeploymentPortalSuccess = 'OpenCustomDeploymentPortalSuccess', + OpenTargetProvisioningWizard = 'OpenTargetProvisioningWizard', + OpenDeployArmTemplateDialog = 'OpenDeployArmTemplateDialog' } export function logError(telemetryView: TelemetryViews, err: string, error: any): void { From 71b023a522f3405ce4f70f6a4cc0b112e6d178d3 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Wed, 16 Oct 2024 01:45:27 +0530 Subject: [PATCH 07/12] upload template fix --- .../SelectStorageAccountDialog.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts index 72f2835b05e2..170b6d76c96f 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/SelectStorageAccountDialog.ts @@ -12,7 +12,7 @@ import * as utils from '../../api/utils'; import { StorageAccount } from '../../api/azure'; import { logError, TelemetryViews, sendButtonClickEvent, TelemetryAction, sendSqlMigrationActionEvent } from '../../telemetry'; import { MigrationStateModel } from '../../models/stateMachine'; -import { StorageSharedKeyCredential, BlockBlobClient, BlobSASPermissions, generateBlobSASQueryParameters } from '@azure/storage-blob'; +import { StorageSharedKeyCredential, BlockBlobClient, AccountSASServices, AccountSASResourceTypes, AccountSASPermissions, generateAccountSASQueryParameters } from '@azure/storage-blob'; import { getStorageAccountAccessKeys } from '../../api/azure'; import { MigrationTargetType } from '../../api/utils'; @@ -594,12 +594,15 @@ export class SelectStorageAccountDialog { const templates = this.migrationStateModel._armTemplateResult.templates!; const sharedKeyCredential = new StorageSharedKeyCredential(this._storageAccount.name, storageKeys.keyName1); - const sasToken = generateBlobSASQueryParameters( - { - containerName, - permissions: BlobSASPermissions.parse("racwd"), - expiresOn: new Date(new Date().valueOf() + 86400), - }, + const sasOptions = { + services: AccountSASServices.parse("b").toString(), // blobs + resourceTypes: AccountSASResourceTypes.parse("sco").toString(), // service, container, object + permissions: AccountSASPermissions.parse("rwdlacu"), // permissions + expiresOn: new Date(new Date().valueOf() + (1440 * 60 * 1000)), // 24 hrs + }; + + const sasToken = generateAccountSASQueryParameters( + sasOptions, sharedKeyCredential ).toString(); From a69e68b117728b34136d311ff5aa851de5595ca1 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Wed, 23 Oct 2024 02:03:36 +0530 Subject: [PATCH 08/12] paasing targettype instead of filepath --- .../GenerateProvisioningScriptDialog.ts | 14 +------------- .../sql-migration/src/models/stateMachine.ts | 4 ++-- extensions/sql-migration/src/service/features.ts | 4 ++-- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts index ad3554697edf..9755c4fbf051 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts @@ -233,8 +233,7 @@ export class GenerateProvisioningScriptDialog { // emit Telemetry for opening of Wizard. sendButtonClickEvent(this.model, TelemetryViews.ProvisioningScriptWizard, TelemetryAction.OpenTargetProvisioningWizard, "", constants.UPLOAD_TEMPLATE_TO_AZURE); - const skuRecommendationReportFilePath = this.getSkuRecommendationReportFilePath(this._targetType); - await this.model.getArmTemplate(skuRecommendationReportFilePath); + await this.model.getArmTemplate(this._targetType); const error = this.model._armTemplateResult.generateTemplateError; if (error) { @@ -248,17 +247,6 @@ export class GenerateProvisioningScriptDialog { } } - private getSkuRecommendationReportFilePath(targetType: string): string { - let fileName; - this.model._skuRecommendationReportFilePaths.forEach(function (filePath) { - if (filePath.includes(targetType)) { - fileName = filePath.substring(0, filePath.lastIndexOf(".")) + ".json"; - } - }); - - return fileName!; - } - protected async execute() { this._isOpen = false; } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index c7756266d5bf..6c5915783cbe 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -495,9 +495,9 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._assessmentResults; } - public async getArmTemplate(skuRecommendationReportFilePath: string): Promise { + public async getArmTemplate(targetType: string): Promise { try { - const response = (await this.migrationService.getArmTemplate(skuRecommendationReportFilePath))! + const response = (await this.migrationService.getArmTemplate(targetType))! if (response) { this._armTemplateResult = { templates: response diff --git a/extensions/sql-migration/src/service/features.ts b/extensions/sql-migration/src/service/features.ts index ab2d50e4ea6d..b74bd8e092f4 100644 --- a/extensions/sql-migration/src/service/features.ts +++ b/extensions/sql-migration/src/service/features.ts @@ -88,9 +88,9 @@ export class SqlMigrationService extends MigrationExtensionService implements co return undefined; } - async getArmTemplate(skuRecommendationReportFilePath: string): Promise { + async getArmTemplate(targetType: string): Promise { try { - const response = this._client.sendRequest(contracts.GetSqlMigrationGenerateArmTemplateRequest.type, skuRecommendationReportFilePath); + const response = this._client.sendRequest(contracts.GetSqlMigrationGenerateArmTemplateRequest.type, targetType); return response; } catch (e) { From ac374dc697038858515cf871c0b2208b1f469d9d Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Thu, 24 Oct 2024 17:06:22 +0530 Subject: [PATCH 09/12] update sts and linux file --- extensions/sql-migration/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/sql-migration/config.json b/extensions/sql-migration/config.json index 3462d7c9ec79..f2b24cac2a2e 100644 --- a/extensions/sql-migration/config.json +++ b/extensions/sql-migration/config.json @@ -1,12 +1,12 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.migration-{#fileName#}", "useDefaultLinuxRuntime": true, - "version": "5.0.20241014.2", + "version": "5.0.20241024.1", "downloadFileNames": { "Windows_86": "win-x86-net8.0.zip", "Windows": "win-x64-net8.0.zip", "OSX": "osx-x64-net8.0.tar.gz", - "Linux": "rhel-x64-net8.0.tar.gz" + "Linux": "linux-x64-net8.0.tar.gz" }, "installDirectory": "./migrationService/{#platform#}/{#version#}", "executableFiles": ["MicrosoftSqlToolsMigration", "MicrosoftSqlToolsMigration.exe"], From dbc3ed3623d02e82c13bf765e89d0158e924ae42 Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Thu, 24 Oct 2024 17:13:15 +0530 Subject: [PATCH 10/12] string change --- extensions/sql-migration/src/constants/strings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 09784712203c..8b397d3252d2 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -263,7 +263,7 @@ export const IMPORT_PERFORMANCE_DATA_DIALOG_OPEN_FOLDER = localize('sql.migratio export const UPLOAD_TEMPLATE_TO_AZURE = localize('sql.migration.target.provisioning.upload.to.azure', "Deploy to Azure"); export const SAVE_TO_DEVICE = localize('sql.migration.target.provisioning.generate.template', "Save to device"); export const COPY_TO_CLIPBOARD = localize('sql.migration.target.provisioning.copy.to.clipboard', "Copy to clipboard"); -export const ARM_TEMPLATE_GENERATE_FAILED = localize('sql.migration.target.provisioning.copy.to.clipboard', "Failed to generate ARM template"); +export const ARM_TEMPLATE_GENERATE_FAILED = localize('sql.migration.target.provisioning.arm.template.generation.failed', "Failed to generate ARM template"); export const CLOSE_DIALOG = localize('sql.migration.target.provisioning.close', "Close"); export const TARGET_PROVISIONING_DESCRIPTION = localize('sql.migration.target.provisioning.description', "Below is the ARM script for the recommended target SKU. You can use the following two methods to deploy target SKU to Azure.\n 1.Click on the \"Deploy to Azure\" command to deploy the target resource. This option requires an Azure blob container account.\n 2.Click on \"Save to device\" to save the ARM script and then manually deploy the target resource."); From e1c5f2ca9e53c4ce85663a835a0731c1b523b1fb Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Thu, 24 Oct 2024 22:19:41 +0530 Subject: [PATCH 11/12] check if armtemplateresult is undefined --- .../GenerateProvisioningScriptDialog.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts index 9755c4fbf051..c31875592f06 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts @@ -200,7 +200,7 @@ export class GenerateProvisioningScriptDialog { } private async displayArmTemplate(): Promise { - if (this.model._armTemplateResult.templates?.[0]) { + if (this.model._armTemplateResult?.templates?.[0]) { this._armTemplateTextBox.value = this.model._armTemplateResult.templates[0]; } else { @@ -208,7 +208,7 @@ export class GenerateProvisioningScriptDialog { await vscode.window.showErrorMessage(constants.ARM_TEMPLATE_GENERATE_FAILED); } - if (this.model._armTemplateResult.templates?.length! > 1 && this._targetType === utils.MigrationTargetType.SQLDB) { + if (this.model._armTemplateResult?.templates?.length! > 1 && this._targetType === utils.MigrationTargetType.SQLDB) { await vscode.window.showInformationMessage(constants.DISPLAY_ARM_TEMPLATE_LIMIT); } } From 30ff0b7c24f3b0224bddca264fc3163edfacb4ae Mon Sep 17 00:00:00 2001 From: ROSHAN SAHU Date: Thu, 24 Oct 2024 22:53:57 +0530 Subject: [PATCH 12/12] check arm template undefined --- .../GenerateProvisioningScriptDialog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts index c31875592f06..a1720286d9a7 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/GenerateProvisioningScriptDialog.ts @@ -156,7 +156,7 @@ export class GenerateProvisioningScriptDialog { }).component(); copyToClipboardButton.onDidClick(async () => { - if (this.model._armTemplateResult.templates?.[0]) { + if (this.model._armTemplateResult?.templates?.[0]) { void vscode.env.clipboard.writeText(this.model._armTemplateResult.templates[0]); void vscode.window.showInformationMessage(constants.COPY_TEMPLATE_SUCCESS); // emit Telemetry for the success.