diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index 5eca8ce26a..a7f89d76c4 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -91,6 +91,32 @@ declare module 'azureResource' { } export interface AzureSqlManagedInstance extends AzureGraphResource { + sku: { + capacity: number; + family: string; + name: string; + tier: 'GeneralPurpose' | 'BusinessCritical'; + }, + properties: { + provisioningState: string, + storageAccountType: string, + maintenanceConfigurationId: string, + state: string, + licenseType: string, + zoneRedundant: false, + fullyQualifiedDomainName: string, + collation: string, + administratorLogin: string, + minimalTlsVersion: string, + subnetId: string, + publicDataEndpointEnabled: boolean, + storageSizeInGB: number, + timezoneId: string, + proxyOverride: string, + vCores: number, + dnsZone: string, + } + } export interface ManagedDatabase { diff --git a/extensions/sql-migration/README.md b/extensions/sql-migration/README.md new file mode 100644 index 0000000000..950f4161d6 --- /dev/null +++ b/extensions/sql-migration/README.md @@ -0,0 +1,30 @@ +# Azure SQL Migration +Azure SQL Migration extension can be used to determine readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance or SQL Server on Azure Virtual Machine. + +## Installation +From Azure Data Studio extension gallery, install the latest version of “Azure SQL Migration” extension and launch the wizard as shown below. + +![migration-animation](https://github.com/microsoft/azuredatastudio/blob/main/extensions/sql-migration/images/ADSMigration.gif) + + +## Things you need before starting Azure SQL migration +- Azure account details +- Azure SQL Managed Instance or SQL Server on Azure Virtual Machine +- Backup location details + +## Getting started +Refer to getting started document (https://aka.ms/ads-sql-migration) for detailed documentation on capabilities and current limitations. + +## Need assistance or have questions/feedback +Please reach out to DMSFeedback@microsoft.com + + +## Code of Conduct +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Privacy Statement +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. + +## License +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt). diff --git a/extensions/sql-migration/images/ADSMigration.gif b/extensions/sql-migration/images/ADSMigration.gif new file mode 100644 index 0000000000..e93b992f4b Binary files /dev/null and b/extensions/sql-migration/images/ADSMigration.gif differ diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 89cd7c5ee5..479ed91f3b 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -2,7 +2,7 @@ "name": "sql-migration", "displayName": "%displayName%", "description": "%description%", - "version": "0.0.11", + "version": "0.0.12", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", @@ -10,7 +10,7 @@ "aiKey": "06ba2446-fa56-40aa-853a-26b73255b723", "engines": { "vscode": "*", - "azdata": ">=1.27.0" + "azdata": ">=1.29.0" }, "activationEvents": [ "onDashboardOpen", diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index ba9d7966a0..12cf0ae9f0 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -291,7 +291,7 @@ function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void }); } -function getResourceGroupFromId(id: string): string { +export function getResourceGroupFromId(id: string): string { return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase(); } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 3e0a8de6bc..a277176e53 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -45,6 +45,12 @@ export const SUBSCRIPTION_SELECTION_PAGE_TITLE = localize('sql.migration.wizard. export const SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE = localize('sql.migration.wizard.subscription.azure.account.title', "Azure Account"); export const SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE = localize('sql.migration.wizard.subscription.azure.subscription.title', "Azure Subscription"); export const SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE = localize('sql.migration.wizard.subscription.azure.product.title', "Azure Product"); +export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI = localize('sql.migration.sku.recommendation.view.assessment.mi', "View assessment results and select one or more database(s) to migrate to Azure SQL Managed Instance (PaaS)"); +export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM = localize('sql.migration.sku.recommendation.view.assessment.vm', "View assessment results and select one or more database(s) to migrate to SQL Server on Azure Virtual Machine (IaaS)"); +export const VIEW_SELECT_BUTTON_LABEL = localize('sql.migration.view.select.button.label', "View/Select"); +export function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string { + return localize('total.databases.selected', "{0} of {1} Database(s) selected.", selectedDbCount, totalDbCount); +} export const ASSESSMENT_COMPLETED = (serverName: string): string => { return localize('sql.migration.generic.congratulations', "We have completed the assessment of your SQL Server Instance '{0}'.", serverName); @@ -52,6 +58,9 @@ export const ASSESSMENT_COMPLETED = (serverName: string): string => { export function ASSESSMENT_TILE(serverName: string): string { return localize('sql.migration.assessment', "Assessment Dialog for '{0}'", serverName); } +export function CAN_BE_MIGRATED(eligibleDbs: number, totalDbs: number): string { + return localize('sql.migration.can.be.migrated', "{0} out of {1} databases can be migrated", eligibleDbs, totalDbs); +} // Accounts page export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Azure Account"); @@ -270,6 +279,7 @@ export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP //Migration cutover dialog export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover"); +export const COMPLETE_CUTOVER = localize('sql.migration.complete.cutover', "Complete cutover"); export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database name"); export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server"); export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version"); @@ -299,8 +309,23 @@ export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) { } export const COPY_MIGRATION_DETAILS = localize('sql.migration.copy.migration.details', "Copy Migration Details"); export const DETAILS_COPIED = localize('sql.migration.details.copied', "Details copied"); +export const CANCEL_MIGRATION_CONFIRMATION = localize('sql.cancel.migration.confirmation', "Are you sure you want to cancel this migration?"); +export const YES = localize('sql.migration.yes', "Yes"); +export const NO = localize('sql.migration.no', "No"); - +//Migration confirm cutover dialog +export const BUSINESS_CRITICAL_INFO = localize('sql.migration.bc.info', "Managed Instance migration cutover for Business Critical service tier can take significantly longer than General Purpose as three secondary replicas have to be seeded for Always On High Availability group. This operation duration depends on the size of data. Seeding speed in 90% of cases is 220 GB/hour or higher."); +export const CUTOVER_HELP_MAIN = localize('sql.migration.cutover.help.main', "When you are ready to do the migration cutover, perform the following steps to complete the database migration. Please note that the database is ready for cutover only after a full backup has been restored on the target Azure SQL Database Managed Instance."); +export const CUTOVER_HELP_STEP1 = localize('sql.migration.cutover.step.1', "1. Stop all the incoming transactions coming to the source database."); +export const CUTOVER_HELP_STEP2 = localize('sql.migration.cutover.step.2', "2. Take the final transaction log backup and provide backup file in the SMB network share."); +export const CUTOVER_HELP_STEP3 = localize('sql.migration.cutover.step.3', "3. Make sure all the pending log backups are restored on the target. At that point, “Pending log backups” counter shows zero and then perform the cutover. Performing cutover operation without applying all the transaction log backup files may result in loss of data."); +export function PENDING_BACKUPS(count: number): string { + return localize('sql.migartion.cutover.pending.backup', "Pending log backups: {0}", count); +} +export const CONFIRM_CUTOVER_CHECKBOX = localize('sql.migration.confirm.checkbox.message', "Confirm all pending log backups are restored"); +export function CUTOVER_IN_PROGRESS(dbName: string): string { + return localize('sql.migration.cutover.in.progress', "Cutover in progress for database '{0}'", dbName); +} //Migration status dialog export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations"); export const ONLINE = localize('sql.migration.online', "Online"); diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts new file mode 100644 index 0000000000..879ffa8acc --- /dev/null +++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel'; +import * as constants from '../../constants/strings'; +import * as vscode from 'vscode'; +import { SqlManagedInstance } from '../../api/azure'; + +export class ConfirmCutoverDialog { + private _dialogObject!: azdata.window.Dialog; + private _view!: azdata.ModelView; + + constructor(private migrationCutoverModel: MigrationCutoverDialogModel) { + this._dialogObject = azdata.window.createModelViewDialog('', 'ConfirmCutoverDialog', 500); + } + + async initialize(): Promise { + let tab = azdata.window.createTab(''); + tab.registerContent(async (view: azdata.ModelView) => { + this._view = view; + + const completeCutoverText = view.modelBuilder.text().withProps({ + value: constants.COMPLETE_CUTOVER, + CSSStyles: { + 'font-size': '20px', + 'font-weight': 'bold', + 'margin-bottom': '0px' + } + }).component(); + + const sourceDatabaseText = view.modelBuilder.text().withProps({ + value: this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName, + CSSStyles: { + 'font-size': '10px', + 'margin': '5px 0px 10px 0px' + } + }).component(); + + const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component(); + + let infoDisplay = 'none'; + if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances') + && (this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') { + infoDisplay = 'inline'; + } + + const businessCriticalinfoBox = this._view.modelBuilder.infoBox().withProps({ + text: constants.BUSINESS_CRITICAL_INFO, + style: 'information', + CSSStyles: { + 'font-size': '13px', + 'display': infoDisplay + } + }).component(); + + const helpMainText = this._view.modelBuilder.text().withProps({ + value: constants.CUTOVER_HELP_MAIN, + CSSStyles: { + 'font-size': '13px', + } + }).component(); + + const helpStepsText = this._view.modelBuilder.text().withProps({ + value: `${constants.CUTOVER_HELP_STEP1} + ${constants.CUTOVER_HELP_STEP2} + ${constants.CUTOVER_HELP_STEP3}`, + CSSStyles: { + 'font-size': '13px', + } + }).component(); + + const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length; + const pendingText = this._view.modelBuilder.text().withProps({ + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + }, + value: constants.PENDING_BACKUPS(pendingBackupCount!) + }).component(); + + const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({ + CSSStyles: { + 'font-size': '13px', + }, + label: constants.CONFIRM_CUTOVER_CHECKBOX, + }).component(); + + confirmCheckbox.onChanged(e => { + this._dialogObject.okButton.enabled = e; + }); + + + const container = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).withItems([ + completeCutoverText, + sourceDatabaseText, + separator, + businessCriticalinfoBox, + helpMainText, + helpStepsText, + pendingText, + confirmCheckbox + ]).component(); + + + this._dialogObject.okButton.enabled = false; + this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER; + this._dialogObject.okButton.onClick((e) => { + this.migrationCutoverModel.startCutover(); + vscode.window.showInformationMessage(constants.CUTOVER_IN_PROGRESS(this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName)); + }); + + const formBuilder = view.modelBuilder.formContainer().withFormItems( + [ + { + component: container + } + ], + { + horizontal: false + } + ); + const form = formBuilder.withLayout({ width: '100%' }).component(); + return view.initializeModel(form); + }); + this._dialogObject.content = [tab]; + azdata.window.openDialog(this._dialogObject); + } +} diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index f27fb9ee4b..b7a953f81b 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -11,6 +11,7 @@ import * as loc from '../../constants/strings'; import { getSqlServerName } from '../../api/utils'; import { EOL } from 'os'; import * as vscode from 'vscode'; +import { ConfirmCutoverDialog } from './confirmCutoverDialog'; export class MigrationCutoverDialog { private _dialogObject!: azdata.window.Dialog; @@ -41,8 +42,6 @@ export class MigrationCutoverDialog { private fileTable!: azdata.TableComponent; - private _startCutover!: boolean; - constructor(migration: MigrationContext) { this._model = new MigrationCutoverDialogModel(migration); this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 1000); @@ -333,22 +332,17 @@ export class MigrationCutoverDialog { iconPath: IconPathHelper.cutover, iconHeight: '14px', iconWidth: '12px', - label: 'Start Cutover', + label: loc.COMPLETE_CUTOVER, height: '20px', - width: '100px', + width: '130px', enabled: false }).component(); this._cutoverButton.onDidClick(async (e) => { - if (this._startCutover) { - await this._model.startCutover(); - this.refreshStatus(); - } else { - this._dialogObject.message = { - text: loc.CANNOT_START_CUTOVER_ERROR, - level: azdata.window.MessageLevel.Error - }; - } + await this.refreshStatus(); + const dialog = new ConfirmCutoverDialog(this._model); + await dialog.initialize(); + await this.refreshStatus(); }); headerActions.addItem(this._cutoverButton, { @@ -365,7 +359,12 @@ export class MigrationCutoverDialog { }).component(); this._cancelButton.onDidClick((e) => { - this.cancelMigration(); + vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => { + if (v === loc.YES) { + await this.cancelMigration(); + await this.refreshStatus(); + } + }); }); headerActions.addItem(this._cancelButton, { @@ -537,14 +536,9 @@ export class MigrationCutoverDialog { row.lastLSN ]; }); - if (this._model.migrationStatus.properties.migrationStatusDetails?.isFullBackupRestored) { - this._startCutover = true; - } if (migrationStatusTextValue === MigrationStatus.InProgress) { - const fileNotRestored = await tableData.some(file => file.status !== 'Restored' && file.status !== 'Ignored'); - this._cutoverButton.enabled = !fileNotRestored; - this._cancelButton.enabled = true; + this._cutoverButton.enabled = tableData.length > 0; } else { this._cutoverButton.enabled = false; this._cancelButton.enabled = false; diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index 447e2f5989..85f69baccf 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { azureResource } from 'azureResource'; -import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails } from '../api/azure'; +import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails, SqlVMServer } from '../api/azure'; import * as azdata from 'azdata'; @@ -62,7 +62,7 @@ export class MigrationLocalStorage { public static saveMigration( connectionProfile: azdata.connection.ConnectionProfile, migrationContext: DatabaseMigration, - targetMI: SqlManagedInstance, + targetMI: SqlManagedInstance | SqlVMServer, azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription, controller: SqlMigrationService, @@ -93,7 +93,7 @@ export class MigrationLocalStorage { export interface MigrationContext { sourceConnectionProfile: azdata.connection.ConnectionProfile, migrationContext: DatabaseMigration, - targetManagedInstance: SqlManagedInstance, + targetManagedInstance: SqlManagedInstance | SqlVMServer, azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription, controller: SqlMigrationService, diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 86bb7d5a64..2165896c04 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -108,7 +108,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _resourceGroup!: azureResource.AzureResourceResourceGroup; public _targetManagedInstances!: SqlManagedInstance[]; public _targetSqlVirtualMachines!: SqlVMServer[]; - public _targetServerInstance!: SqlManagedInstance; + public _targetServerInstance!: SqlManagedInstance | SqlVMServer; public _databaseBackup!: DatabaseBackupModel; public _migrationDbs: string[] = []; public _storageAccounts!: StorageAccount[]; @@ -444,7 +444,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getManagedDatabases(): Promise { return (await getSqlManagedInstanceDatabases(this._azureAccount, this._targetSubscription, - this._targetServerInstance)).map(t => t.name); + this._targetServerInstance)).map(t => t.name); } public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index 447ca946fe..f05cd423f4 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -37,9 +37,9 @@ export class AccountsSelectionPage extends MigrationWizardPage { const azureAccountLabel = view.modelBuilder.text().withProps({ value: constants.ACCOUNTS_SELECTION_PAGE_TITLE, - requiredIndicator: true, CSSStyles: { - 'margin': '0px' + 'font-size': '13px', + 'font-weight': 'bold', } }).component(); @@ -96,7 +96,10 @@ export class AccountsSelectionPage extends MigrationWizardPage { const linkAccountButton = view.modelBuilder.hyperlink() .withProps({ label: constants.ACCOUNT_LINK_BUTTON_LABEL, - url: '' + url: '', + CSSStyles: { + 'font-size': '13px', + } }) .component(); @@ -130,9 +133,9 @@ export class AccountsSelectionPage extends MigrationWizardPage { const azureTenantDropdownLabel = view.modelBuilder.text().withProps({ value: constants.AZURE_TENANT, - requiredIndicator: true, CSSStyles: { - 'margin': '0px' + 'font-size': '13px', + 'font-weight': 'bold' } }).component(); @@ -185,7 +188,7 @@ export class AccountsSelectionPage extends MigrationWizardPage { public async onPageEnter(): Promise { this.wizard.registerNavigationValidator(pageChangeInfo => { - if (this.migrationStateModel._azureAccount.isStale === true) { + if (this.migrationStateModel._azureAccount?.isStale === true) { this.wizard.message = { text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount) }; diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 3c44f08c45..96c0670e5e 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -10,7 +10,7 @@ import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog'; import * as constants from '../constants/strings'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; -import { getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../api/azure'; +import { getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlManagedInstance, SqlMigrationService } from '../api/azure'; import { IconPathHelper } from '../constants/iconPathHelper'; export class IntergrationRuntimePage extends MigrationWizardPage { @@ -400,7 +400,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { } this._dmsDropdown.loading = true; try { - this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetServerInstance, resourceGroupName); + this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetServerInstance, resourceGroupName); let index = -1; if (this.migrationStateModel._sqlMigrationService) { index = (this._dmsDropdown.values).findIndex(v => v.displayName.toLowerCase() === this.migrationStateModel._sqlMigrationService.name.toLowerCase()); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index d558b464a0..9dc5b38f74 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -20,7 +20,6 @@ export interface Product { } export class SKURecommendationPage extends MigrationWizardPage { - private _view!: azdata.ModelView; private _igComponent!: azdata.TextComponent; private _assessmentStatusIcon!: azdata.ImageComponent; @@ -42,12 +41,17 @@ export class SKURecommendationPage extends MigrationWizardPage { private _formContainer!: azdata.ComponentBuilder; private _assessmentLoader!: azdata.LoadingComponent; private _rootContainer!: azdata.FlexContainer; + private _viewAssessmentsHelperText!: azdata.TextComponent; + private _databaseSelectedHelperText!: azdata.TextComponent; + private assessmentGroupContainer!: azdata.FlexContainer; + private _targetContainer!: azdata.FlexContainer; private _supportedProducts: Product[] = [ { type: MigrationTargetType.SQLMI, name: constants.SKU_RECOMMENDATION_MI_CARD_TEXT, - icon: IconPathHelper.sqlMiLogo + icon: IconPathHelper.sqlMiLogo, + }, { type: MigrationTargetType.SQLVM, @@ -81,112 +85,27 @@ export class SKURecommendationPage extends MigrationWizardPage { this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved + const chooseYourTargetText = this._view.modelBuilder.text().withProps({ + value: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + 'margin-top': '16px' + } + }).component(); + const statusContainer = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).withItems( [ igContainer, - this._detailsComponent + this._detailsComponent, + chooseYourTargetText ] ).component(); this._chooseTargetComponent = await this.createChooseTargetComponent(view); - this._azureSubscriptionText = this.createAzureSubscriptionText(view); - - - const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.SUBSCRIPTION, - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().withProps({ - width: WIZARD_INPUT_COMPONENT_WIDTH, - editable: true - }).component(); - this._managedInstanceSubscriptionDropdown.onValueChanged((e) => { - if (e) { - const selectedIndex = (this._managedInstanceSubscriptionDropdown.values)?.findIndex(v => v.displayName === e); - this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(selectedIndex); - this.migrationStateModel._targetServerInstance = undefined!; - this.migrationStateModel._sqlMigrationService = undefined!; - this.populateLocationAndResourceGroupDropdown(); - } - }); - this._resourceDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.MANAGED_INSTANCE, - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - - const azureLocationLabel = view.modelBuilder.text().withProps({ - value: constants.LOCATION, - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._azureLocationDropdown = view.modelBuilder.dropDown().withProps({ - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._azureLocationDropdown.onValueChanged((e) => { - if (e.selected) { - this.migrationStateModel._location = this.migrationStateModel.getLocation(e.index); - this.populateResourceInstanceDropdown(); - } - }); - this._resourceDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.MANAGED_INSTANCE, - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - - - const azureResourceGroupLabel = view.modelBuilder.text().withProps({ - value: constants.RESOURCE_GROUP, - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._azureResourceGroupDropdown = view.modelBuilder.dropDown().withProps({ - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._azureResourceGroupDropdown.onValueChanged((e) => { - if (e.selected) { - this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); - this.populateResourceInstanceDropdown(); - } - }); - this._resourceDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.MANAGED_INSTANCE, - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - - this._resourceDropdown = view.modelBuilder.dropDown().withProps({ - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._resourceDropdown.onValueChanged((e) => { - if (e.selected && - e.selected !== constants.NO_MANAGED_INSTANCE_FOUND && - e.selected !== constants.NO_VIRTUAL_MACHINE_FOUND) { - this.migrationStateModel._sqlMigrationServices = undefined!; - if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) { - this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(e.index); - } else { - this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index); - } - } - }); - - const targetContainer = view.modelBuilder.flexContainer().withItems( - [ - managedInstanceSubscriptionDropdownLabel, - this._managedInstanceSubscriptionDropdown, - azureLocationLabel, - this._azureLocationDropdown, - azureResourceGroupLabel, - this._azureResourceGroupDropdown, - this._resourceDropdownLabel, - this._resourceDropdown - ] - ).withLayout({ - flexFlow: 'column' - }).component(); - - - - - + this.assessmentGroupContainer = await this.createViewAssessmentsContainer(); + this._targetContainer = this.createTargetDropdownContainer(); this._formContainer = view.modelBuilder.formContainer().withFormItems( [ { @@ -194,14 +113,13 @@ export class SKURecommendationPage extends MigrationWizardPage { component: statusContainer }, { - title: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET, component: this._chooseTargetComponent }, { - component: this._azureSubscriptionText + component: this.assessmentGroupContainer }, { - component: targetContainer + component: this._targetContainer } ] ).withProps({ @@ -237,7 +155,8 @@ export class SKURecommendationPage extends MigrationWizardPage { CSSStyles: { 'font-size': '14px', 'margin': '0 0 0 8px', - 'line-height': '20px' + 'line-height': '20px', + 'font-weight': 'bold' } }).component(); return component; @@ -256,87 +175,46 @@ export class SKURecommendationPage extends MigrationWizardPage { this._rbg = this._view!.modelBuilder.radioCardGroup().withProps({ cards: [], - cardWidth: '600px', - cardHeight: '40px', - orientation: azdata.Orientation.Vertical, - iconHeight: '30px', - iconWidth: '30px' + iconHeight: '35px', + iconWidth: '35px', + cardWidth: '250px', + cardHeight: '130px', + iconPosition: 'left', + CSSStyles: { + 'margin-top': '0px' + } }).component(); this._supportedProducts.forEach((product) => { - const descriptions: azdata.RadioCardDescription[] = [ - { - textValue: product.name, - textStyles: { - 'font-size': '14px', - 'font-weight': 'bold', - 'line-height': '20px' - }, - linkDisplayValue: 'Learn more', - linkStyles: { - 'font-size': '14px', - 'line-height': '20px' - }, - displayLinkCodicon: true, - linkCodiconStyles: { - 'font-size': '14px', - 'line-height': '20px' - }, - }, - { - textValue: '0 selected', - textStyles: { - 'font-size': '13px', - 'line-height': '18px' - }, - linkStyles: { - 'font-size': '14px', - 'line-height': '20px' - }, - linkDisplayValue: 'View/Change', - displayLinkCodicon: true, - linkCodiconStyles: { - 'font-size': '13px', - 'line-height': '18px' - } - } - ]; this._rbg.cards.push({ id: product.type, icon: product.icon, - descriptions + descriptions: [ + { + textValue: product.name, + textStyles: { + 'font-size': '14px', + 'font-weight': 'bold' + } + }, + { + textValue: '', + textStyles: { + 'font-size': '13px', + } + } + ] }); }); - const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName; - let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI); - let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM); - - this._rbg.onLinkClick(async (value) => { - if (value.cardId === MigrationTargetType.SQLVM) { - this._rbg.selectedCardId = MigrationTargetType.SQLVM; - if (value.description.linkDisplayValue === 'View/Change') { - await vmDialog.openDialog(); - } else if (value.description.linkDisplayValue === 'Learn more') { - vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/azure/azure-sql/virtual-machines/windows/sql-server-on-azure-vm-iaas-what-is-overview')); - } - } else if (value.cardId === MigrationTargetType.SQLMI) { - this._rbg.selectedCardId = MigrationTargetType.SQLMI; - if (value.description.linkDisplayValue === 'View/Change') { - await miDialog.openDialog(); - } else if (value.description.linkDisplayValue === 'Learn more') { - vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/azure/azure-sql/managed-instance/sql-managed-instance-paas-overview ')); - } + this._rbg.onSelectionChanged((value) => { + if (value) { + this.assessmentGroupContainer.display = 'inline'; + this.changeTargetType(value.cardId); } }); - this._rbg.onSelectionChanged((value) => { - this.changeTargetType(value.cardId); - }); - - this._rbg.selectedCardId = MigrationTargetType.SQLMI; - this._rbgLoader = this._view.modelBuilder.loadingComponent().withItem( this._rbg ).component(); @@ -349,17 +227,180 @@ export class SKURecommendationPage extends MigrationWizardPage { return component; } + private async createViewAssessmentsContainer(): Promise { + this._viewAssessmentsHelperText = this._view.modelBuilder.text().withProps({ + value: constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + }, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + + const button = this._view.modelBuilder.button().withProps({ + label: constants.VIEW_SELECT_BUTTON_LABEL, + width: 100 + }).component(); + + const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName; + let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI); + let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM); + + button.onDidClick(async (e) => { + if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) { + this._rbg.selectedCardId = MigrationTargetType.SQLVM; + await vmDialog.openDialog(); + } else if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) { + this._rbg.selectedCardId = MigrationTargetType.SQLMI; + await miDialog.openDialog(); + } + }); + + this._databaseSelectedHelperText = this._view.modelBuilder.text().withProps({ + CSSStyles: { + 'font-size': '13px', + } + }).component(); + + const container = this._view.modelBuilder.flexContainer().withItems([ + this._viewAssessmentsHelperText, + button, + this._databaseSelectedHelperText + ]).withProps({ + 'display': 'none' + }).component(); + return container; + } + + private createTargetDropdownContainer(): azdata.FlexContainer { + this._azureSubscriptionText = this._view.modelBuilder.text().withProps({ + CSSStyles: { + 'font-size': '13px', + 'line-height': '18px' + } + }).component(); + + + const managedInstanceSubscriptionDropdownLabel = this._view.modelBuilder.text().withProps({ + value: constants.SUBSCRIPTION, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } + }).component(); + this._managedInstanceSubscriptionDropdown = this._view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true + }).component(); + this._managedInstanceSubscriptionDropdown.onValueChanged((e) => { + if (e) { + const selectedIndex = (this._managedInstanceSubscriptionDropdown.values)?.findIndex(v => v.displayName === e); + this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(selectedIndex); + this.migrationStateModel._targetServerInstance = undefined!; + this.migrationStateModel._sqlMigrationService = undefined!; + this.populateLocationAndResourceGroupDropdown(); + } + }); + + const azureLocationLabel = this._view.modelBuilder.text().withProps({ + value: constants.LOCATION, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } + }).component(); + this._azureLocationDropdown = this._view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._azureLocationDropdown.onValueChanged((e) => { + if (e.selected) { + this.migrationStateModel._location = this.migrationStateModel.getLocation(e.index); + this.populateResourceInstanceDropdown(); + } + }); + + const azureResourceGroupLabel = this._view.modelBuilder.text().withProps({ + value: constants.RESOURCE_GROUP, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } + }).component(); + this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._azureResourceGroupDropdown.onValueChanged((e) => { + if (e.selected) { + this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); + this.populateResourceInstanceDropdown(); + } + }); + this._resourceDropdownLabel = this._view.modelBuilder.text().withProps({ + value: constants.MANAGED_INSTANCE, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } + }).component(); + + this._resourceDropdown = this._view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._resourceDropdown.onValueChanged((e) => { + if (e?.selected && + e.selected !== constants.NO_MANAGED_INSTANCE_FOUND && + e.selected !== constants.NO_VIRTUAL_MACHINE_FOUND) { + this.migrationStateModel._sqlMigrationServices = undefined!; + if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) { + this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(e.index); + } else { + this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index); + } + } + }); + + return this._view.modelBuilder.flexContainer().withItems( + [ + this._azureSubscriptionText, + managedInstanceSubscriptionDropdownLabel, + this._managedInstanceSubscriptionDropdown, + azureLocationLabel, + this._azureLocationDropdown, + azureResourceGroupLabel, + this._azureResourceGroupDropdown, + this._resourceDropdownLabel, + this._resourceDropdown + ] + ).withLayout({ + flexFlow: 'column', + }).withProps({ + CSSStyles: { + 'display': 'none' + } + }).component(); + + } + private changeTargetType(newTargetType: string) { if (newTargetType === MigrationTargetType.SQLMI) { + this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI; + this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._miDbs.length, this.migrationStateModel._serverDatabases.length); this.migrationStateModel._targetType = MigrationTargetType.SQLMI; this._azureSubscriptionText.value = constants.SELECT_AZURE_MI; this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs; } else { + this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM; + this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._vmDbs.length, this.migrationStateModel._serverDatabases.length); this.migrationStateModel._targetType = MigrationTargetType.SQLVM; this._azureSubscriptionText.value = constants.SELECT_AZURE_VM; this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs; } this.migrationStateModel.refreshDatabaseBackupPage = true; + this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline'; this.populateResourceInstanceDropdown(); } @@ -378,17 +419,6 @@ export class SKURecommendationPage extends MigrationWizardPage { this._assessmentLoader.loading = false; } - private createAzureSubscriptionText(view: azdata.ModelView): azdata.TextComponent { - const component = view.modelBuilder.text().withProps({ - CSSStyles: { - 'font-size': '13px', - 'line-height': '18px' - } - }).component(); - - return component; - } - private async populateSubscriptionDropdown(): Promise { if (!this.migrationStateModel._targetSubscription) { this._managedInstanceSubscriptionDropdown.loading = true; @@ -518,34 +548,32 @@ export class SKURecommendationPage extends MigrationWizardPage { text: '', level: azdata.window.MessageLevel.Error }; - if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) { this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs; } else { this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs; } + this._azureResourceGroupDropdown.display = (!this._rbg.selectedCardId) ? 'none' : 'inline'; + this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline'; + if (this.migrationStateModel._assessmentResults) { const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments.length; const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length === 0).length; - const miCardText = `${dbWithoutIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`; + const miCardText = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); this._rbg.cards[0].descriptions[1].textValue = miCardText; - const vmCardText = `${dbCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._vmDbs.length} selected)`; + const vmCardText = constants.CAN_BE_MIGRATED(dbCount, dbCount); this._rbg.cards[1].descriptions[1].textValue = vmCardText; this._rbg.updateProperties({ cards: this._rbg.cards }); } else { - - const miCardText = `${this.migrationStateModel._miDbs.length} selected`; - this._rbg.cards[0].descriptions[1].textValue = miCardText; - - const vmCardText = `${this.migrationStateModel._vmDbs.length} selected`; - this._rbg.cards[1].descriptions[1].textValue = vmCardText; + this._rbg.cards[0].descriptions[1].textValue = ''; + this._rbg.cards[1].descriptions[1].textValue = ''; this._rbg.updateProperties({ cards: this._rbg.cards diff --git a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts index 620db32c33..74d8524678 100644 --- a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts +++ b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts @@ -58,14 +58,18 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { this._view, constants.ENTER_YOUR_SQL_CREDS, { - 'width': '600px' + 'width': '600px', + 'font-size': '13px', } ); const serverLabel = this._view.modelBuilder.text().withProps({ value: constants.SERVER, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } }).component(); const server = this._view.modelBuilder.inputBox().withProps({ @@ -76,8 +80,11 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { const authenticationTypeLable = this._view.modelBuilder.text().withProps({ value: constants.AUTHENTICATION_TYPE, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } }).component(); const authenticationTypeInput = this._view.modelBuilder.inputBox().withProps({ @@ -88,8 +95,11 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { const usernameLable = this._view.modelBuilder.text().withProps({ value: constants.USERNAME, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } }).component(); this._usernameInput = this._view.modelBuilder.inputBox().withProps({ value: username, @@ -103,8 +113,11 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { const passwordLabel = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } }).component(); this._password = this._view.modelBuilder.inputBox().withProps({ value: (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password, diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 408d61a882..8282520de7 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -5,9 +5,10 @@ import * as azdata from 'azdata'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationMode, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; +import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { createHeadingTextComponent, createInformationRow } from './wizardController'; +import { getResourceGroupFromId } from '../api/azure'; export class SummaryPage extends MigrationWizardPage { private _view!: azdata.ModelView; @@ -43,11 +44,11 @@ export class SummaryPage extends MigrationWizardPage { createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()), createHeadingTextComponent(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE), - createInformationRow(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE), + createInformationRow(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE), createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)), - createInformationRow(this._view, constants.RESOURCE_GROUP, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.resourceGroup!)), - createInformationRow(this._view, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)), + createInformationRow(this._view, constants.RESOURCE_GROUP, getResourceGroupFromId(this.migrationStateModel._targetServerInstance.id)), + createInformationRow(this._view, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)), createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL), createInformationRow(this._view, constants.MODE, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE ? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL : constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL),