From 4d78aefe573a737a35b6685ecb3ab3b9d491c73f Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Fri, 26 Mar 2021 10:32:28 -0700 Subject: [PATCH] Fixing bugs for migration extension private preview 1. (#14872) * Fixing Database backup page target layout * Filtering out Azure sql db issues from assessment results Correcting the database count for issued databases in sku rec page. * Adding copy migration details button to migration status * Adding start migration button to toolbar * Fixing a syntax error in package.json * Adding rg and location to target selection page Filtering storage account by target location. * Fixing dashboard title to azure sql migration * Not making assessment targets selected by default. * Adding tooltip for database and instance table items. * Fixing duplicate task widget * Some fixes mentioned in the PR Localizing button text renaming a var changing null to undefined. * Adding enum for Migration target types * Fixing a critical multi db migration bug because of unhandled race condition * Adding Azure location api to azure core * Adding source database info in status --- .../src/azureResource/azure-resource.d.ts | 19 +++ .../azurecore/src/azureResource/utils.ts | 36 ++++- extensions/azurecore/src/azurecore.d.ts | 2 + extensions/azurecore/src/extension.ts | 5 + extensions/machine-learning/src/test/stubs.ts | 3 + extensions/sql-migration/package.json | 2 +- extensions/sql-migration/src/api/azure.ts | 18 ++- .../sql-migration/src/constants/strings.ts | 9 +- .../assessmentResults/sqlDatabasesTree.ts | 20 +-- .../migrationCutoverDialog.ts | 37 ++++- .../sql-migration/src/models/stateMachine.ts | 139 ++++++++++++++---- .../src/wizard/databaseBackupPage.ts | 67 ++++----- .../src/wizard/skuRecommendationPage.ts | 64 +++++++- .../sql-migration/src/wizard/summaryPage.ts | 2 +- .../src/wizard/tempTargetSelectionPage.ts | 111 -------------- 15 files changed, 336 insertions(+), 198 deletions(-) delete mode 100644 extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index 46a080a9fa..bc8080891c 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -71,6 +71,25 @@ declare module 'azureResource' { export interface AzureResourceResourceGroup extends AzureResource { } + export interface AzureLocation { + id: string, + name: string, + displayName: string, + regionalDisplayName: string, + metadata: { + regionType: string, + regionCategory: string, + geographyGroup: string, + longitude: number, + latitude: number, + physicalLocation: string, + pairedRegion: { + name: string, + id: string, + }[], + }, + } + export interface AzureResourceDatabase extends AzureSqlResource { serverName: string; serverFullName: string; diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts index 248c59525e..56e0c2a235 100644 --- a/extensions/azurecore/src/azureResource/utils.ts +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -7,7 +7,7 @@ import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { TokenCredentials } from '@azure/ms-rest-js'; import axios, { AxiosRequestConfig } from 'axios'; import * as azdata from 'azdata'; -import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod } from 'azurecore'; +import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult } from 'azurecore'; import { azureResource } from 'azureResource'; import { EOL } from 'os'; import * as nls from 'vscode-nls'; @@ -142,6 +142,40 @@ export async function getResourceGroups(appContext: AppContext, account?: azdata return result; } +export async function getLocations(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise { + const result: GetLocationsResult = { locations: [], errors: [] }; + if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) { + const error = new Error(invalidAzureAccount); + if (!ignoreErrors) { + throw error; + } + result.errors.push(error); + return result; + } + await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => { + try { + const path = `/subscriptions/${subscription.id}/locations?api-version=2020-01-01`; + const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors); + result.locations.push(...response.response.data.value); + result.errors.push(...response.errors); + } catch (err) { + const error = new Error(localize('azure.accounts.getLocations.queryError', "Error fetching locations for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}", + account.displayInfo.displayName, + account.displayInfo.userId, + subscription.id, + subscription.name, + tenant.id, + err instanceof Error ? err.message : err)); + console.warn(error); + if (!ignoreErrors) { + throw error; + } + result.errors.push(error); + } + })); + return result; +} + export async function runResourceQuery( account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index d1ece2b923..1a5d5de7d7 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -245,6 +245,7 @@ declare module 'azurecore' { export interface IExtension { getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise; getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise; + getLocations(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise; getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; @@ -275,6 +276,7 @@ declare module 'azurecore' { export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] }; export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] }; + export type GetLocationsResult = {locations: azureResource.AzureLocation[], errors: Error[] }; export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; export type GetSqlServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; export type GetSqlVMServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index 2c86cb2112..d5c1e69024 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -147,6 +147,11 @@ export async function activate(context: vscode.ExtensionContext): Promise { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); }, + getLocations(account?: azdata.Account, + subscription?: azureResource.AzureResourceSubscription, + ignoreErrors?: boolean): Promise { + return azureResourceUtils.getLocations(appContext, account, subscription, ignoreErrors); + }, provideResources(): azureResource.IAzureResourceProvider[] { const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures'); const providers: azureResource.IAzureResourceProvider[] = [ diff --git a/extensions/machine-learning/src/test/stubs.ts b/extensions/machine-learning/src/test/stubs.ts index 97b4eeb92a..7eca3bbebb 100644 --- a/extensions/machine-learning/src/test/stubs.ts +++ b/extensions/machine-learning/src/test/stubs.ts @@ -8,6 +8,9 @@ import * as azurecore from 'azurecore'; import { azureResource } from 'azureResource'; export class AzurecoreApiStub implements azurecore.IExtension { + getLocations(_account?: azdata.Account, _subscription?: azureResource.AzureResourceSubscription, _ignoreErrors?: boolean): Promise { + throw new Error('Method not implemented.'); + } makeAzureRestRequest(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _serviceUrl: string, _requestType: azurecore.HttpRequestMethod, _requestBody?: any, _ignoreErrors?: boolean): Promise { throw new Error('Method not implemented.'); } diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 05ccf7440a..745ad26f7b 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.4", + "version": "0.0.5", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index fcaa9cdf3c..e6a4973c95 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -26,6 +26,20 @@ export async function getSubscriptions(account: azdata.Account): Promise { + const api = await getAzureCoreAPI(); + const response = await api.getLocations(account, subscription, true); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + sortResourceArrayByName(response.locations); + const supportedLocations = ['eastus2', 'eastus2euap']; + const filteredLocations = response.locations.filter(loc => { + return supportedLocations.includes(loc.name); + }); + return filteredLocations; +} + export type AzureProduct = azureResource.AzureGraphResource; export async function getResourceGroups(account: azdata.Account, subscription: Subscription): Promise { @@ -65,9 +79,9 @@ export type SqlVMServer = { tenantId: string, subscriptionId: string }; -export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise { +export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { const api = await getAzureCoreAPI(); - const path = `/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`; + const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroup.name}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 7f8b3a4a21..12e92d528b 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -87,10 +87,12 @@ export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migrat export const DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL = localize('sql.migration.database.backup.email.notification.label', "Email notifications"); export const DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL = localize('sql.migration.database.backup.email.notification.checkbox.label', "Notify me when migration is complete"); export const NO_SUBSCRIPTIONS_FOUND = localize('sql.migration.no.subscription.found', "No subscription found"); +export const NO_LOCATION_FOUND = localize('sql.migration.no.location.found', "No location found"); export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage account found"); export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found"); export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found"); export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "Please select a valid subscription to proceed."); +export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "Please select a valid location to proceed."); export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "Please select a valid storage account to proceed."); export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare.error', "Please select a valid file share to proceed."); export const INVALID_BLOBCONTAINER_ERROR = localize('sql.migration.invalid.blobContainer.error', "Please select a valid blob container to proceed."); @@ -108,7 +110,7 @@ export function TARGET_FILE_SHARE(dbName: string): string { export function TARGET_BLOB_CONTAINER(dbName: string): string { return localize('sql.migration.blob.container', "Select the container that contains the backup files for ‘{0}’", dbName); } -export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter network share path information for selected databases"); +export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter target names for selected databases"); export const ENTER_BLOB_CONTAINER_INFORMATION = localize('sql.migration.blob.container.information', "Enter the target name and select the blob container location for selected databases"); export const ENTER_FILE_SHARE_INFORMATION = localize('sql.migration.enter.file.share.information', "Enter the target name and select the file share location of selected databases"); @@ -152,7 +154,7 @@ export function SERVICE_NOT_READY(serviceName: string): string { export function SERVICE_READY(serviceName: string, host: string): string { return localize('sql.migration.service.ready', "Azure Data Migration Service '{0}' is connected to self-hosted Integration Runtime running on the node - {1}", serviceName, host); } -export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource Groups found"); +export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource groups found"); export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "Please select a valid resource group to proceed."); export const INVALID_REGION_ERROR = localize('sql.migration.invalid.region.error', "Please select a valid region to proceed."); export const INVALID_SERVICE_NAME_ERROR = localize('sql.migration.invalid.service.name.error', "Please enter a valid name for the Migration Service."); @@ -230,6 +232,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 SOURCE_DATABASE = localize('sql.migration.source.database', "Source database"); export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server"); export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version"); export const TARGET_SERVER = localize('sql.migration.target.server', "Target server"); @@ -255,7 +258,7 @@ export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) { return localize('sql.migration.active.backup.files.multiple.items', "Active Backup files ({0} items)", fileCount); } } - +export const COPY_MIGRATION_DETAILS = localize('sql.migration.copy.migration.details', "Copy Migration Details"); //Migration status dialog export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations"); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index 9ce8100f57..933f553fad 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -166,14 +166,14 @@ export class SqlDatabaseTree { { displayName: constants.INSTANCE, valueType: azdata.DeclarativeDataType.component, - width: 150, + width: 130, isReadOnly: true, headerCssStyles: headerLeft }, { displayName: constants.WARNINGS, valueType: azdata.DeclarativeDataType.string, - width: 50, + width: 30, isReadOnly: true, headerCssStyles: headerRight } @@ -595,9 +595,9 @@ export class SqlDatabaseTree { public selectedDbs(): string[] { let result: string[] = []; - this._databaseTable.dataValues?.forEach((arr) => { + this._databaseTable.dataValues?.forEach((arr, index) => { if (arr[0].value === true) { - result.push(arr[1].value.toString()); + result.push(this._dbNames[index]); } }); return result; @@ -615,8 +615,6 @@ export class SqlDatabaseTree { ); }); this._assessmentResultsTable.dataValues = assessmentResults; - this._selectedIssue = this._activeIssues[0]; - this.refreshAssessmentDetails(); } public refreshAssessmentDetails(): void { @@ -734,14 +732,8 @@ export class SqlDatabaseTree { ); }); } - this._dbName.value = this._serverName; this._instanceTable.dataValues = instanceTableValues; this._databaseTable.dataValues = databaseTableValues; - if (this._targetType === MigrationTargetType.SQLMI) { - this._activeIssues = this._model._assessmentResults.issues; - this._selectedIssue = this._model._assessmentResults?.issues[0]; - this.refreshResults(); - } } private createIconTextCell(icon: IconPath, text: string): azdata.FlexContainer { @@ -755,8 +747,10 @@ export class SqlDatabaseTree { }).component(); const textComponent = this._view.modelBuilder.text().withProps({ value: text, + title: text, CSSStyles: { - 'margin': '0px' + 'margin': '0px', + 'width': '110px' } }).component(); diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index ce1a4b6269..802f0aaecc 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -10,6 +10,8 @@ import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel'; import * as loc from '../../constants/strings'; import { getSqlServerName } from '../../api/utils'; import { EOL } from 'os'; +import * as vscode from 'vscode'; + export class MigrationCutoverDialog { private _dialogObject!: azdata.window.Dialog; private _view!: azdata.ModelView; @@ -20,9 +22,11 @@ export class MigrationCutoverDialog { private _refreshButton!: azdata.ButtonComponent; private _cancelButton!: azdata.ButtonComponent; private _refreshLoader!: azdata.LoadingComponent; + private _copyDatabaseMigrationDetails!: azdata.ButtonComponent; private _serverName!: azdata.TextComponent; private _serverVersion!: azdata.TextComponent; + private _sourceDatabase!: azdata.TextComponent; private _targetServer!: azdata.TextComponent; private _targetVersion!: azdata.TextComponent; private _migrationStatus!: azdata.TextComponent; @@ -46,9 +50,11 @@ export class MigrationCutoverDialog { let tab = azdata.window.createTab(''); tab.registerContent(async (view: azdata.ModelView) => { this._view = view; + const sourceDatabase = this.createInfoField(loc.SOURCE_DATABASE, ''); const sourceDetails = this.createInfoField(loc.SOURCE_SERVER, ''); const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, ''); + this._sourceDatabase = sourceDatabase.text; this._serverName = sourceDetails.text; this._serverVersion = sourceVersion.text; @@ -56,6 +62,11 @@ export class MigrationCutoverDialog { flexFlow: 'column' }).component(); + flexServer.addItem(sourceDatabase.flexContainer, { + CSSStyles: { + 'width': '150px' + } + }); flexServer.addItem(sourceDetails.flexContainer, { CSSStyles: { 'width': '150px' @@ -336,6 +347,27 @@ export class MigrationCutoverDialog { } }); + this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.copy, + iconHeight: '16px', + iconWidth: '16px', + label: loc.COPY_MIGRATION_DETAILS, + height: '55px', + width: '100px' + }).component(); + + this._copyDatabaseMigrationDetails.onDidClick(async (e) => { + await this.refreshStatus(); + vscode.env.clipboard.writeText(JSON.stringify(this._model.migrationStatus, undefined, 2)); + }); + + header.addItem(this._copyDatabaseMigrationDetails, { + flex: '0', + CSSStyles: { + 'width': '100px' + } + }); + this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ loading: false, height: '55px' @@ -344,6 +376,7 @@ export class MigrationCutoverDialog { header.addItem(this._refreshLoader, { flex: '0' }); + return header; } @@ -363,6 +396,7 @@ export class MigrationCutoverDialog { }; const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId); const sqlServerName = this._model._migration.sourceConnectionProfile.serverName; + const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName; const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!); const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion; const targetServerName = this._model._migration.targetManagedInstance.name; @@ -402,6 +436,7 @@ export class MigrationCutoverDialog { } }); + this._sourceDatabase.value = sourceDatabaseName; this._serverName.value = sqlServerName; this._serverVersion.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`; @@ -414,7 +449,7 @@ export class MigrationCutoverDialog { this._lastAppliedLSN.value = lastAppliedSSN!; this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename; - this._lastAppliedBackupTakenOn.value = new Date(lastAppliedBackupFileTakenOn!).toLocaleString(); + this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? new Date(lastAppliedBackupFileTakenOn).toLocaleString() : ''; this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length); diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 3a6c56708f..aaad81063c 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -8,7 +8,7 @@ import { azureResource } from 'azureResource'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; -import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer } from '../api/azure'; +import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups } from '../api/azure'; import { SKURecommendations } from './externalContract'; import * as constants from '../constants/strings'; import { MigrationLocalStorage } from './migrationLocalStorage'; @@ -34,8 +34,9 @@ export enum State { } export enum MigrationTargetType { - SQLVM = 'sqlvm', - SQLMI = 'sqlmi' + SQLVM = 'AzureSqlVirtualMachine', + SQLMI = 'AzureSqlManagedInstance', + SQLDB = 'AzureSqlDatabase' } export enum MigrationSourceAuthenticationType { @@ -63,7 +64,7 @@ export interface NetworkShare { export interface DatabaseBackupModel { migrationCutover: MigrationCutover; networkContainerType: NetworkContainerType; - networkShareLocations: string[]; + networkShareLocation: string; windowsUser: string; password: string; subscription: azureResource.AzureResourceSubscription; @@ -100,6 +101,10 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _subscriptions!: azureResource.AzureResourceSubscription[]; public _targetSubscription!: azureResource.AzureResourceSubscription; + public _locations!: azureResource.AzureLocation[]; + public _location!: azureResource.AzureLocation; + public _resourceGroups!: azureResource.AzureResourceResourceGroup[]; + public _resourceGroup!: azureResource.AzureResourceResourceGroup; public _targetManagedInstances!: SqlManagedInstance[]; public _targetSqlVirtualMachines!: SqlVMServer[]; public _targetServerInstance!: SqlManagedInstance; @@ -173,11 +178,13 @@ export class MigrationStateModel implements Model, vscode.Disposable { }); assessmentResults?.items.forEach((item) => { - const dbIndex = serverDatabases.indexOf(item.databaseName); - if (dbIndex === -1) { - serverLevelAssessments.push(item); - } else { - databaseLevelAssessments[dbIndex].issues.push(item); + if (item.appliesToMigrationTargetPlatform === MigrationTargetType.SQLMI) { + const dbIndex = serverDatabases.indexOf(item.databaseName); + if (dbIndex === -1) { + serverLevelAssessments.push(item); + } else { + databaseLevelAssessments[dbIndex].issues.push(item); + } } }); @@ -313,10 +320,88 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._subscriptions[index]; } - public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription): Promise { + public async getAzureLocationDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise { + let locationValues: azdata.CategoryValue[] = []; + try { + this._locations = await getLocations(this._azureAccount, subscription); + this._locations.forEach((loc) => { + locationValues.push({ + name: loc.name, + displayName: loc.displayName + }); + }); + + if (locationValues.length === 0) { + locationValues = [ + { + displayName: constants.INVALID_LOCATION_ERROR, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + locationValues = [ + { + displayName: constants.INVALID_LOCATION_ERROR, + name: '' + } + ]; + } + + return locationValues; + } + + public getLocation(index: number): azureResource.AzureLocation { + return this._locations[index]; + } + + public async getAzureResourceGroupDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise { + let resourceGroupValues: azdata.CategoryValue[] = []; + try { + this._resourceGroups = await getResourceGroups(this._azureAccount, subscription); + this._resourceGroups.forEach((rg) => { + resourceGroupValues.push({ + name: rg.id, + displayName: rg.name + }); + }); + + if (resourceGroupValues.length === 0) { + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + + return resourceGroupValues; + } + + public getAzureResourceGroup(index: number): azureResource.AzureResourceResourceGroup { + return this._resourceGroups[index]; + } + + + public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { let managedInstanceValues: azdata.CategoryValue[] = []; try { - this._targetManagedInstances = await getAvailableManagedInstanceProducts(this._azureAccount, subscription); + this._targetManagedInstances = (await getAvailableManagedInstanceProducts(this._azureAccount, subscription)).filter((mi) => { + if (mi.location === location.name && mi.resourceGroup?.toLocaleLowerCase() === resourceGroup.name.toLowerCase()) { + return true; + } + return false; + }); this._targetManagedInstances.forEach((managedInstance) => { managedInstanceValues.push({ name: managedInstance.id, @@ -348,13 +433,19 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._targetManagedInstances[index]; } - public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription): Promise { + public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { let virtualMachineValues: azdata.CategoryValue[] = []; try { - this._targetSqlVirtualMachines = await getAvailableSqlVMs(this._azureAccount, subscription); - virtualMachineValues = this._targetSqlVirtualMachines.filter((virtualMachine) => { - return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms. - }).map((virtualMachine) => { + this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription, resourceGroup)).filter((virtualMachine) => { + if (virtualMachine.location === location.name) { + if (virtualMachine.properties.sqlImageOffer) { + return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms. + } + return true; // Returning all VMs that don't have this property as we don't want to accidentally skip valid vms. + } + return false; + }); + virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => { return { name: virtualMachine.id, displayName: `${virtualMachine.name}` @@ -388,7 +479,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise { let storageAccountValues: azdata.CategoryValue[] = []; try { - this._storageAccounts = await getAvailableStorageAccounts(this._azureAccount, subscription); + this._storageAccounts = (await getAvailableStorageAccounts(this._azureAccount, subscription)).filter(sa => sa.location === this._targetServerInstance.location); this._storageAccounts.forEach((storageAccount) => { storageAccountValues.push({ name: storageAccount.id, @@ -564,17 +655,16 @@ export class MigrationStateModel implements Model, vscode.Disposable { } }; - this._migrationDbs.forEach(async (db, index) => { - - requestBody.properties.sourceDatabaseName = db; + for (let i = 0; i < this._migrationDbs.length; i++) { + requestBody.properties.sourceDatabaseName = this._migrationDbs[i]; try { - requestBody.properties.backupConfiguration.sourceLocation.fileShare!.path = this._databaseBackup.networkShareLocations[index]; + requestBody.properties.backupConfiguration.sourceLocation.fileShare!.path = this._databaseBackup.networkShareLocation; const response = await startDatabaseMigration( this._azureAccount, this._targetSubscription, this._sqlMigrationService?.properties.location!, this._targetServerInstance, - this._targetDatabaseNames[index], + this._targetDatabaseNames[i], requestBody ); if (response.status === 201) { @@ -586,14 +676,13 @@ export class MigrationStateModel implements Model, vscode.Disposable { this._targetSubscription, this._sqlMigrationService ); - vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', db, this._targetServerInstance.name, this._targetDatabaseNames[index])); + vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._migrationDbs[i], this._targetServerInstance.name, this._targetDatabaseNames[i])); } } catch (e) { console.log(e); vscode.window.showInformationMessage(e); } - - }); + } } } diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 190a448bcb..7058bc1d5e 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -332,12 +332,32 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); } }); - + const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({ + value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, + requiredIndicator: true + }).component(); + const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({ + placeHolder: '\\\\Servername.domainname.com\\Backupfolder', + required: true, + validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION + }).withValidation((component) => { + if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { + if (component.value) { + if (!/(?<=\\\\)[^\\]*/.test(component.value)) { + return false; + } + } + } + return true; + }).component(); + networkLocationInputBox.onTextChanged((value) => { + this.validateFields(); + this.migrationStateModel._databaseBackup.networkShareLocation = value; + }); const networkShareDatabaseConfigHeader = view.modelBuilder.text().withProps({ value: constants.ENTER_NETWORK_SHARE_INFORMATION }).component(); - this._networkShareDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); @@ -347,14 +367,16 @@ export class DatabaseBackupPage extends MigrationWizardPage { [ azureAccountHelpText, networkShareHelpText, - subscriptionLabel, - this._networkShareContainerSubscriptionDropdown, - storageAccountLabel, - this._networkShareContainerStorageAccountDropdown, + networkLocationInputBoxLabel, + networkLocationInputBox, windowsUserAccountLabel, this._windowsUserAccountText, passwordLabel, this._passwordText, + subscriptionLabel, + this._networkShareContainerSubscriptionDropdown, + storageAccountLabel, + this._networkShareContainerStorageAccountDropdown, networkShareDatabaseConfigHeader, this._networkShareDatabaseConfigContainer ] @@ -374,7 +396,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._fileShareDropdowns = []; this._blobContainerDropdowns = []; this.migrationStateModel._targetDatabaseNames = []; - this.migrationStateModel._databaseBackup.networkShareLocations = []; this.migrationStateModel._databaseBackup.fileShares = []; this.migrationStateModel._databaseBackup.blobContainers = []; this._networkShareDatabaseConfigContainer.clearItems(); @@ -389,42 +410,16 @@ export class DatabaseBackupPage extends MigrationWizardPage { requiredIndicator: true }).component(); const targetNameNetworkInputBox = this._view.modelBuilder.inputBox().withProps({ - required: true + required: true, + value: db }).component(); targetNameNetworkInputBox.onTextChanged((value) => { this.migrationStateModel._targetDatabaseNames[index] = value; }); - - const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({ - value: constants.TARGET_NETWORK_SHARE_LOCATION(db), - requiredIndicator: true - }).component(); - const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({ - placeHolder: '\\\\Servername.domainname.com\\Backupfolder', - required: true, - validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION - }).withValidation((component) => { - if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { - if (component.value) { - if (!/(?<=\\\\)[^\\]*/.test(component.value)) { - return false; - } - } - } - return true; - }).component(); - networkLocationInputBox.onTextChanged((value) => { - this.validateFields(); - this.migrationStateModel._databaseBackup.networkShareLocations[index] = value; - }); - this.migrationStateModel._databaseBackup.networkShareLocations.push(undefined!); - this._networkShareLocations.push(networkLocationInputBox); this._networkShareDatabaseConfigContainer.addItems( [ targetNameNetworkInputBoxLabel, - targetNameNetworkInputBox, - networkLocationInputBoxLabel, - networkLocationInputBox + targetNameNetworkInputBox ] ); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 81dca3f961..b302f04ab3 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -26,6 +26,8 @@ export class SKURecommendationPage extends MigrationWizardPage { private _chooseTargetComponent!: azdata.DivContainer; private _azureSubscriptionText!: azdata.TextComponent; private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent; + private _azureLocationDropdown!: azdata.DropDownComponent; + private _azureResourceGroupDropdown!: azdata.DropDownComponent; private _resourceDropdownLabel!: azdata.TextComponent; private _resourceDropdown!: azdata.DropDownComponent; private _rbg!: azdata.RadioCardGroupComponent; @@ -66,6 +68,35 @@ export class SKURecommendationPage extends MigrationWizardPage { this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index); this.migrationStateModel._targetServerInstance = undefined!; this.migrationStateModel._sqlMigrationService = undefined!; + this.populateLocationAndResourceGroupDropdown(); + } + }); + this._resourceDropdownLabel = view.modelBuilder.text().withProps({ + value: constants.MANAGED_INSTANCE + }).component(); + + const azureLocationLabel = view.modelBuilder.text().withProps({ + value: constants.LOCATION + }).component(); + this._azureLocationDropdown = view.modelBuilder.dropDown().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 + }).component(); + + + const azureResourceGroupLabel = view.modelBuilder.text().withProps({ + value: constants.RESOURCE_GROUP + }).component(); + this._azureResourceGroupDropdown = view.modelBuilder.dropDown().component(); + this._azureResourceGroupDropdown.onValueChanged((e) => { + if (e.selected) { + this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); this.populateResourceInstanceDropdown(); } }); @@ -91,6 +122,10 @@ export class SKURecommendationPage extends MigrationWizardPage { [ managedInstanceSubscriptionDropdownLabel, this._managedInstanceSubscriptionDropdown, + azureLocationLabel, + this._azureLocationDropdown, + azureResourceGroupLabel, + this._azureResourceGroupDropdown, this._resourceDropdownLabel, this._resourceDropdown ] @@ -284,16 +319,30 @@ export class SKURecommendationPage extends MigrationWizardPage { } } + public async populateLocationAndResourceGroupDropdown(): Promise { + this._azureResourceGroupDropdown.loading = true; + this._azureLocationDropdown.loading = true; + try { + this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription); + this._azureLocationDropdown.values = await this.migrationStateModel.getAzureLocationDropdownValues(this.migrationStateModel._targetSubscription); + } catch (e) { + console.log(e); + } finally { + this._azureResourceGroupDropdown.loading = false; + this._azureLocationDropdown.loading = false; + } + } + private async populateResourceInstanceDropdown(): Promise { this._resourceDropdown.loading = true; try { if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) { this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE; - this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription); + this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); } else { this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE; - this._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription); + this._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); } } catch (e) { console.log(e); @@ -322,6 +371,13 @@ export class SKURecommendationPage extends MigrationWizardPage { if ((this._managedInstanceSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { errors.push(constants.INVALID_SUBSCRIPTION_ERROR); } + if ((this._azureLocationDropdown.value).displayName === constants.NO_LOCATION_FOUND) { + errors.push(constants.INVALID_LOCATION_ERROR); + } + + if ((this._managedInstanceSubscriptionDropdown.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) { + errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); + } const resourceDropdownValue = (this._resourceDropdown.value).displayName; if (resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) { errors.push(constants.NO_MANAGED_INSTANCE_FOUND); @@ -377,8 +433,8 @@ export class SKURecommendationPage extends MigrationWizardPage { if (this.migrationStateModel._assessmentResults) { const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments.length; - const dbWithIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length > 0).length; - const miCardText = `${dbWithIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`; + 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)`; this._rbg.cards[0].descriptions[1].textValue = miCardText; const vmCardText = `${dbCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._vmDbs.length} selected)`; diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 00458364fe..07072800fc 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -72,6 +72,7 @@ export class SummaryPage extends MigrationWizardPage { flexContainer.addItems( [ createInformationRow(this._view, constants.TYPE, constants.NETWORK_SHARE), + createInformationRow(this._view, constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, this.migrationStateModel._databaseBackup.networkShareLocation), createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.windowsUser), createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name), createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.storageAccount.name), @@ -80,7 +81,6 @@ export class SummaryPage extends MigrationWizardPage { ); this.migrationStateModel._migrationDbs.forEach((db, index) => { flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NAME_FOR_DATABASE(db), this.migrationStateModel._targetDatabaseNames[index])); - flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NETWORK_SHARE_LOCATION(db), this.migrationStateModel._databaseBackup.networkShareLocations[index])); }); break; case NetworkContainerType.FILE_SHARE: diff --git a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts deleted file mode 100644 index e9f3fa8f6d..0000000000 --- a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts +++ /dev/null @@ -1,111 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; -import * as constants from '../constants/strings'; -import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; - -export class TempTargetSelectionPage extends MigrationWizardPage { - - private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent; - private _managedInstanceDropdown!: azdata.DropDownComponent; - - constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { - super(wizard, azdata.window.createWizardPage(constants.TARGET_SELECTION_PAGE_TITLE), migrationStateModel); - } - - protected async registerContent(view: azdata.ModelView): Promise { - - const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text() - .withProps({ - value: constants.SUBSCRIPTION - }).component(); - - this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown() - .withProps({ - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._managedInstanceSubscriptionDropdown.onValueChanged((e) => { - if (e.selected) { - this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index); - this.migrationStateModel._targetServerInstance = undefined!; - this.migrationStateModel._sqlMigrationService = undefined!; - this.populateManagedInstanceDropdown(); - } - }); - - const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.MANAGED_INSTANCE - }).component(); - - this._managedInstanceDropdown = view.modelBuilder.dropDown() - .withProps({ - width: WIZARD_INPUT_COMPONENT_WIDTH - }).component(); - this._managedInstanceDropdown.onValueChanged((e) => { - if (e.selected) { - this.migrationStateModel._sqlMigrationServices = undefined!; - this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index); - } - }); - - const targetContainer = view.modelBuilder.flexContainer().withItems( - [ - managedInstanceSubscriptionDropdownLabel, - this._managedInstanceSubscriptionDropdown, - managedInstanceDropdownLabel, - this._managedInstanceDropdown - ] - ).withLayout({ - flexFlow: 'column' - }).component(); - - const form = view.modelBuilder.formContainer() - .withFormItems( - [ - { - component: targetContainer - } - ] - ); - await view.initializeModel(form.component()); - } - public async onPageEnter(): Promise { - this.populateSubscriptionDropdown(); - } - public async onPageLeave(): Promise { - } - protected async handleStateChange(e: StateChangeEvent): Promise { - } - - private async populateSubscriptionDropdown(): Promise { - if (!this.migrationStateModel._targetSubscription) { - this._managedInstanceSubscriptionDropdown.loading = true; - this._managedInstanceDropdown.loading = true; - try { - this._managedInstanceSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues(); - } catch (e) { - console.log(e); - } finally { - this._managedInstanceSubscriptionDropdown.loading = false; - } - } - } - - private async populateManagedInstanceDropdown(): Promise { - if (!this.migrationStateModel._targetServerInstance) { - this._managedInstanceDropdown.loading = true; - try { - this._managedInstanceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription); - } catch (e) { - console.log(e); - } finally { - this._managedInstanceDropdown.loading = false; - } - } - } -}