diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index d22330dbf3..6b95349c40 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": "1.0.0", + "version": "1.0.1", "publisher": "Microsoft", "preview": false, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index ece16679a2..cca1166e4b 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -3,13 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, CategoryValue, DropDownComponent, IconPath } from 'azdata'; +import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata'; import { IconPathHelper } from '../constants/iconPathHelper'; -import { DAYS, HRS, MINUTE, SEC } from '../constants/strings'; import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage'; import * as crypto from 'crypto'; -import { DatabaseMigration } from './azure'; +import * as azure from './azure'; +import { azureResource, Tenant } from 'azurecore'; +import * as constants from '../constants/strings'; +import { logError, TelemetryViews } from '../telemtery'; export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -77,21 +79,21 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date): let hours = (time / (1000 * 60 * 60)).toFixed(1); let days = (time / (1000 * 60 * 60 * 24)).toFixed(1); if (time / 1000 < 60) { - return SEC(parseFloat(seconds)); + return constants.SEC(parseFloat(seconds)); } else if (time / (1000 * 60) < 60) { - return MINUTE(parseFloat(minutes)); + return constants.MINUTE(parseFloat(minutes)); } else if (time / (1000 * 60 * 60) < 24) { - return HRS(parseFloat(hours)); + return constants.HRS(parseFloat(hours)); } else { - return DAYS(parseFloat(days)); + return constants.DAYS(parseFloat(days)); } } -export function filterMigrations(databaseMigrations: DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): DatabaseMigration[] { - let filteredMigration: DatabaseMigration[] = []; +export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): azure.DatabaseMigration[] { + let filteredMigration: azure.DatabaseMigration[] = []; if (statusFilter === AdsMigrationStatus.ALL) { filteredMigration = databaseMigrations; } else if (statusFilter === AdsMigrationStatus.ONGOING) { @@ -141,12 +143,17 @@ export function convertIsoTimeToLocalTime(isoTime: string): Date { export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?: string, useDisplayName: boolean = true): void { if (dropDown.values && dropDown.values.length > 0) { - const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1; - if (selectedIndex > -1) { - selectDropDownIndex(dropDown, selectedIndex); + let selectedIndex; + if (value) { + if (useDisplayName) { + selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.displayName?.toLowerCase() === value.toLowerCase()); + } else { + selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.name?.toLowerCase() === value.toLowerCase()); + } } else { - selectDropDownIndex(dropDown, 0); + selectedIndex = -1; } + selectDropDownIndex(dropDown, selectedIndex > -1 ? selectedIndex : 0); } } @@ -160,18 +167,6 @@ export function selectDropDownIndex(dropDown: DropDownComponent, index: number): dropDown.value = undefined; } -export function findDropDownItemIndex(dropDown: DropDownComponent, value: string, useDisplayName: boolean = true): number { - if (value && dropDown.values && dropDown.values.length > 0) { - const searachValue = value?.toLowerCase(); - return useDisplayName - ? dropDown.values.findIndex((v: any) => - (v as CategoryValue)?.displayName?.toLowerCase() === searachValue) - : dropDown.values.findIndex((v: any) => - (v as CategoryValue)?.name?.toLowerCase() === searachValue); - } - return -1; -} - export function hashString(value: string): string { if (value?.length > 0) { return crypto.createHash('sha512').update(value).digest('hex'); @@ -261,3 +256,532 @@ export function clearDialogMessage(dialog: window.Dialog): void { export function getUserHome(): string | undefined { return process.env.HOME || process.env.USERPROFILE; } + +export async function getAzureAccounts(): Promise { + let azureAccounts: Account[] = []; + try { + azureAccounts = await accounts.getAllAccounts(); + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getAzureAccounts', e); + } + return azureAccounts; +} + +export async function getAzureAccountsDropdownValues(accounts: Account[]): Promise { + let accountsValues: CategoryValue[] = []; + accounts.forEach((account) => { + accountsValues.push({ + name: account.displayInfo.userId, + displayName: account.isStale + ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName) + : account.displayInfo.displayName + }); + }); + if (accountsValues.length === 0) { + accountsValues = [ + { + displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, + name: '' + } + ]; + } + return accountsValues; +} + +export function getAzureTenants(account?: Account): Tenant[] { + let tenants: Tenant[] = []; + try { + if (account) { + tenants = account.properties.tenants; + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getAzureTenants', e); + } + return tenants; +} + +export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise { + let tenantsValues: CategoryValue[] = []; + tenants.forEach((tenant) => { + tenantsValues.push({ + name: tenant.id, + displayName: tenant.displayName + }); + }); + if (tenantsValues.length === 0) { + tenantsValues = [ + { + displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, + name: '' + } + ]; + } + return tenantsValues; +} + +export async function getAzureSubscriptions(account?: Account): Promise { + let subscriptions: azureResource.AzureResourceSubscription[] = []; + try { + if (account) { + subscriptions = !account.isStale ? await azure.getSubscriptions(account) : []; + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getAzureSubscriptions', e); + } + subscriptions.sort((a, b) => a.name.localeCompare(b.name)); + return subscriptions; +} + +export async function getAzureSubscriptionsDropdownValues(subscriptions: azureResource.AzureResourceSubscription[]): Promise { + let subscriptionsValues: CategoryValue[] = []; + subscriptions.forEach((subscription) => { + subscriptionsValues.push({ + name: subscription.id, + displayName: `${subscription.name} - ${subscription.id}` + }); + }); + if (subscriptionsValues.length === 0) { + subscriptionsValues = [ + { + displayName: constants.NO_SUBSCRIPTIONS_FOUND, + name: '' + } + ]; + } + return subscriptionsValues; +} + +export async function getSqlManagedInstanceLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, managedInstances?: azureResource.AzureSqlManagedInstance[]): Promise { + let locations: azureResource.AzureLocation[] = []; + try { + if (account && subscription && managedInstances) { + locations = await azure.getLocations(account, subscription); + locations = locations.filter((loc, i) => managedInstances.some(mi => mi.location.toLowerCase() === loc.name.toLowerCase())); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceLocations', e); + } + locations.sort((a, b) => a.displayName.localeCompare(b.displayName)); + return locations; +} + +export async function getSqlVirtualMachineLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, virtualMachines?: azure.SqlVMServer[]): Promise { + let locations: azureResource.AzureLocation[] = []; + try { + if (account && subscription && virtualMachines) { + locations = await azure.getLocations(account, subscription); + locations = locations.filter((loc, i) => virtualMachines.some(vm => vm.location.toLowerCase() === loc.name.toLowerCase())); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getSqlVirtualMachineLocations', e); + } + locations.sort((a, b) => a.displayName.localeCompare(b.displayName)); + return locations; +} + +export async function getSqlMigrationServiceLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, migrationServices?: azure.SqlMigrationService[]): Promise { + let locations: azureResource.AzureLocation[] = []; + try { + if (account && subscription && migrationServices) { + locations = await azure.getLocations(account, subscription); + locations = locations.filter((loc, i) => migrationServices.some(dms => dms.location.toLowerCase() === loc.name.toLowerCase())); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getSqlMigrationServiceLocations', e); + } + locations.sort((a, b) => a.displayName.localeCompare(b.displayName)); + return locations; +} + +export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise { + let locationValues: CategoryValue[] = []; + locations.forEach((loc) => { + locationValues.push({ + name: loc.name, + displayName: loc.displayName + }); + }); + if (locationValues.length === 0) { + locationValues = [ + { + displayName: constants.NO_LOCATION_FOUND, + name: '' + } + ]; + } + return locationValues; +} + +export async function getSqlManagedInstanceResourceGroups(managedInstances?: azureResource.AzureSqlManagedInstance[], location?: azureResource.AzureLocation): Promise { + let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; + try { + if (managedInstances && location) { + resourceGroups = managedInstances + .filter((mi) => mi.location.toLowerCase() === location.name.toLowerCase()) + .map((mi) => { + return { + id: azure.getFullResourceGroupFromId(mi.id), + name: azure.getResourceGroupFromId(mi.id), + subscription: { + id: mi.subscriptionId + }, + tenant: mi.tenantId + }; + }); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceResourceGroups', e); + } + + // remove duplicates + resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); + return resourceGroups; +} + +export async function getSqlVirtualMachineResourceGroups(virtualMachines?: azure.SqlVMServer[], location?: azureResource.AzureLocation): Promise { + let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; + try { + if (virtualMachines && location) { + resourceGroups = virtualMachines + .filter((vm) => vm.location.toLowerCase() === location.name.toLowerCase()) + .map((vm) => { + return { + id: azure.getFullResourceGroupFromId(vm.id), + name: azure.getResourceGroupFromId(vm.id), + subscription: { + id: vm.subscriptionId + }, + tenant: vm.tenantId + }; + }); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getSqlVirtualMachineResourceGroups', e); + } + + // remove duplicates + resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); + return resourceGroups; +} + +export async function getStorageAccountResourceGroups(storageAccounts?: azure.StorageAccount[], location?: azureResource.AzureLocation): Promise { + let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; + try { + if (storageAccounts && location) { + resourceGroups = storageAccounts + .filter((sa) => sa.location.toLowerCase() === location.name.toLowerCase()) + .map((sa) => { + return { + id: azure.getFullResourceGroupFromId(sa.id), + name: azure.getResourceGroupFromId(sa.id), + subscription: { + id: sa.subscriptionId + }, + tenant: sa.tenantId + }; + }); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getStorageAccountResourceGroups', e); + } + + // remove duplicates + resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); + return resourceGroups; +} + +export async function getSqlMigrationServiceResourceGroups(migrationServices?: azure.SqlMigrationService[], location?: azureResource.AzureLocation): Promise { + let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; + try { + if (migrationServices && location) { + resourceGroups = migrationServices + .filter((dms) => dms.properties.provisioningState === ProvisioningState.Succeeded && dms.location.toLowerCase() === location.name.toLowerCase()) + .map((dms) => { + return { + id: azure.getFullResourceGroupFromId(dms.id), + name: azure.getResourceGroupFromId(dms.id), + subscription: { + id: dms.properties.subscriptionId + }, + }; + }); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getSqlMigrationServiceResourceGroups', e); + } + + // remove duplicates + resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); + return resourceGroups; +} + +export async function getAllResourceGroups(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise { + let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; + try { + if (account && subscription) { + resourceGroups = await azure.getResourceGroups(account, subscription); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getAllResourceGroups', e); + } + resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); + return resourceGroups; +} + +export async function getAzureResourceGroupsDropdownValues(resourceGroups: azureResource.AzureResourceResourceGroup[]): Promise { + let resourceGroupValues: CategoryValue[] = []; + resourceGroups.forEach((rg) => { + resourceGroupValues.push({ + name: rg.id, + displayName: rg.name + }); + }); + if (resourceGroupValues.length === 0) { + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + return resourceGroupValues; +} + +export async function getManagedInstances(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise { + let managedInstances: azureResource.AzureSqlManagedInstance[] = []; + try { + if (account && subscription) { + managedInstances = await azure.getAvailableManagedInstanceProducts(account, subscription); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getManagedInstances', e); + } + managedInstances.sort((a, b) => a.name.localeCompare(b.name)); + return managedInstances; +} + +export async function getManagedInstancesDropdownValues(managedInstances: azureResource.AzureSqlManagedInstance[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { + let managedInstancesValues: CategoryValue[] = []; + if (location && resourceGroup) { + managedInstances.forEach((managedInstance) => { + if (managedInstance.location.toLowerCase() === location.name.toLowerCase() && managedInstance.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase()) { + let managedInstanceValue: CategoryValue; + if (managedInstance.properties.state === 'Ready') { + managedInstanceValue = { + name: managedInstance.id, + displayName: managedInstance.name + }; + } else { + managedInstanceValue = { + name: managedInstance.id, + displayName: constants.UNAVAILABLE_TARGET_PREFIX(managedInstance.name) + }; + } + managedInstancesValues.push(managedInstanceValue); + } + }); + } + + if (managedInstancesValues.length === 0) { + managedInstancesValues = [ + { + displayName: constants.NO_MANAGED_INSTANCE_FOUND, + name: '' + } + ]; + } + return managedInstancesValues; +} + +export async function getVirtualMachines(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise { + let virtualMachines: azure.SqlVMServer[] = []; + try { + if (account && subscription) { + virtualMachines = (await azure.getAvailableSqlVMs(account, subscription)).filter((virtualMachine) => { + 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. + }); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getVirtualMachines', e); + } + virtualMachines.sort((a, b) => a.name.localeCompare(b.name)); + return virtualMachines; +} + +export async function getVirtualMachinesDropdownValues(virtualMachines: azure.SqlVMServer[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { + let virtualMachineValues: CategoryValue[] = []; + if (location && resourceGroup) { + virtualMachines.forEach((virtualMachine) => { + if (virtualMachine.location.toLowerCase() === location.name.toLowerCase() && azure.getResourceGroupFromId(virtualMachine.id).toLowerCase() === resourceGroup.name.toLowerCase()) { + let virtualMachineValue: CategoryValue; + if (virtualMachine.properties.provisioningState === ProvisioningState.Succeeded) { + virtualMachineValue = { + name: virtualMachine.id, + displayName: virtualMachine.name + }; + } else { + virtualMachineValue = { + name: virtualMachine.id, + displayName: constants.UNAVAILABLE_TARGET_PREFIX(virtualMachine.name) + }; + } + virtualMachineValues.push(virtualMachineValue); + } + }); + } + + if (virtualMachineValues.length === 0) { + virtualMachineValues = [ + { + displayName: constants.NO_VIRTUAL_MACHINE_FOUND, + name: '' + } + ]; + } + return virtualMachineValues; +} + +export async function getStorageAccounts(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise { + let storageAccounts: azure.StorageAccount[] = []; + try { + if (account && subscription) { + storageAccounts = await azure.getAvailableStorageAccounts(account, subscription); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getStorageAccounts', e); + } + storageAccounts.sort((a, b) => a.name.localeCompare(b.name)); + return storageAccounts; +} + +export async function getStorageAccountsDropdownValues(storageAccounts: azure.StorageAccount[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { + let storageAccountValues: CategoryValue[] = []; + storageAccounts.forEach((storageAccount) => { + if (storageAccount.location.toLowerCase() === location.name.toLowerCase() && storageAccount.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase()) { + storageAccountValues.push({ + name: storageAccount.id, + displayName: storageAccount.name + }); + } + }); + + if (storageAccountValues.length === 0) { + storageAccountValues = [ + { + displayName: constants.NO_STORAGE_ACCOUNT_FOUND, + name: '' + } + ]; + } + return storageAccountValues; +} + +export async function getAzureSqlMigrationServices(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise { + let sqlMigrationServices: azure.SqlMigrationService[] = []; + try { + if (account && subscription) { + sqlMigrationServices = (await azure.getSqlMigrationServices(account, subscription)).filter(dms => { + return dms.properties.provisioningState === ProvisioningState.Succeeded; + }); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getAzureSqlMigrationServices', e); + } + sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name)); + return sqlMigrationServices; +} + +export async function getAzureSqlMigrationServicesDropdownValues(sqlMigrationServices: azure.SqlMigrationService[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { + let SqlMigrationServicesValues: CategoryValue[] = []; + if (location && resourceGroup) { + sqlMigrationServices.forEach((sqlMigrationService) => { + if (sqlMigrationService.location.toLowerCase() === location.name.toLowerCase() && sqlMigrationService.properties.resourceGroup.toLowerCase() === resourceGroup.name.toLowerCase()) { + SqlMigrationServicesValues.push({ + name: sqlMigrationService.id, + displayName: sqlMigrationService.name + }); + } + }); + } + + if (SqlMigrationServicesValues.length === 0) { + SqlMigrationServicesValues = [ + { + displayName: constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR, + name: '' + } + ]; + } + return SqlMigrationServicesValues; +} + +export async function getBlobContainer(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount): Promise { + let blobContainers: azureResource.BlobContainer[] = []; + try { + if (account && subscription && storageAccount) { + blobContainers = await azure.getBlobContainers(account, subscription, storageAccount); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getBlobContainer', e); + } + blobContainers.sort((a, b) => a.name.localeCompare(b.name)); + return blobContainers; +} + +export async function getBlobContainersValues(blobContainers: azureResource.BlobContainer[]): Promise { + let blobContainersValues: CategoryValue[] = []; + blobContainers.forEach((blobContainer) => { + blobContainersValues.push({ + name: blobContainer.id, + displayName: blobContainer.name + }); + }); + if (blobContainersValues.length === 0) { + blobContainersValues = [ + { + displayName: constants.NO_BLOBCONTAINERS_FOUND, + name: '' + } + ]; + } + return blobContainersValues; +} + +export async function getBlobLastBackupFileNames(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount, blobContainer?: azureResource.BlobContainer): Promise { + let lastFileNames: azureResource.Blob[] = []; + try { + if (account && subscription && storageAccount && blobContainer) { + lastFileNames = await azure.getBlobs(account, subscription, storageAccount, blobContainer.name); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getBlobLastBackupFileNames', e); + } + lastFileNames.sort((a, b) => a.name.localeCompare(b.name)); + return lastFileNames; +} + +export async function getBlobLastBackupFileNamesValues(lastFileNames: azureResource.Blob[]): Promise { + let lastFileNamesValues: CategoryValue[] = []; + lastFileNames.forEach((lastFileName) => { + lastFileNamesValues.push({ + name: lastFileName.name, + displayName: lastFileName.name + }); + }); + if (lastFileNamesValues.length === 0) { + lastFileNamesValues = [ + { + displayName: constants.NO_BLOBFILES_FOUND, + name: '' + } + ]; + } + return lastFileNamesValues; +} diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 3ae9d46d99..5acdd4acb5 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -246,17 +246,17 @@ export function AZURE_SQL_TARGET_PAGE_DESCRIPTION(targetInstance: string = 'inst // Managed Instance export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Managed Instance"); -export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instance found."); +export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instances found."); export const INVALID_MANAGED_INSTANCE_ERROR = localize('sql.migration.invalid.managedInstance.error', "To continue, select a valid managed instance."); -export function UNAVAILABLE_MANAGED_INSTANCE_PREFIX(miName: string): string { - return localize('sql.migration.unavailable.managedInstance', "(Unavailable) {0}", miName); +export function UNAVAILABLE_TARGET_PREFIX(targetName: string): string { + return localize('sql.migration.unavailable.target', "(Unavailable) {0}", targetName); } // Virtual Machine export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "SQL Server on Azure Virtual Machines"); export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE_SHORT = localize('sql.migration.azure.sql.database.virtual.machine.short', "SQL Server on Azure VM"); -export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachine.found', "No virtual machine found."); +export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachine.found', "No virtual machines found."); export const INVALID_VIRTUAL_MACHINE_ERROR = localize('sql.migration.invalid.virtualMachine.error', "To continue, select a valid virtual machine."); // Azure SQL Database @@ -264,10 +264,16 @@ export const AZURE_SQL_DATABASE = localize('sql.migration.azure.sql.database', " // Target info tooltip export const TARGET_SUBSCRIPTION_INFO = localize('sql.migration.sku.subscription', "Subscription name for your Azure SQL target"); -export const TARGET_LOCATION_INFO = localize('sql.migration.sku.location', "Azure region for your Azure SQL target"); -export const TARGET_RESOURCE_GROUP_INFO = localize('sql.migration.sku.resource_group', "Resource group for your Azure SQL target"); +export const TARGET_LOCATION_INFO = localize('sql.migration.sku.location', "Azure region for your Azure SQL target. Only regions that contain a target eligible for migration will be shown."); +export const TARGET_RESOURCE_GROUP_INFO = localize('sql.migration.sku.resource_group', "Resource group for your Azure SQL target. Only resource groups that contain a target eligible for migration will be shown."); export const TARGET_RESOURCE_INFO = localize('sql.migration.sku.resource', "Your Azure SQL target resource name"); +// DMS tooltip +export const DMS_SUBSCRIPTION_INFO = localize('sql.migration.dms.subscription', "Subscription name for your Azure Database Migration Service"); +export const DMS_LOCATION_INFO = localize('sql.migration.dms.location', "Azure region for your Azure Database Migration Service. Only regions that contain a service will be shown."); +export const DMS_RESOURCE_GROUP_INFO = localize('sql.migration.dms.resource_group', "Resource group for your Azure SQL target. Only resource groups that contain a service will be shown."); +export const DMS_RESOURCE_INFO = localize('sql.migration.dms.resource', "Your Azure Database Migration Service resource name"); + // Accounts page export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Azure account"); export const ACCOUNTS_SELECTION_PAGE_DESCRIPTION = localize('sql.migration.wizard.account.description', "Select an Azure account linked to Azure Data Studio, or link one now."); @@ -297,6 +303,9 @@ export function ACCOUNT_ACCESS_ERROR(account: AzureAccount, error: Error) { export function MI_NOT_READY_ERROR(miName: string, state: string): string { return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state); } +export function VM_NOT_READY_ERROR(miName: string, state: string): string { + return localize('sql.migration.vm.not.ready', "The virtual machine '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available virtual machine.", miName, state); +} export const SELECT_AN_ACCOUNT = localize('sql.migration.select.service.select.a.', "Sign into Azure and select an account"); export const SELECT_A_TENANT = localize('sql.migration.select.service.select.a.tenant', "Select a tenant"); @@ -352,10 +361,10 @@ export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migrat export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION = localize('sql.migration.database.migration.mode.offline.description', "Application downtime will start when the migration starts."); export const NETWORK_SHARE_PATH_FORMAT = localize('sql.migration.network.share.path.format', "\\\\Servername.domainname.com\\Backupfolder"); export const WINDOWS_USER_ACCOUNT = localize('sql.migration.windows.user.account', "Domain\\username"); -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_SUBSCRIPTIONS_FOUND = localize('sql.migration.no.subscription.found', "No subscriptions found."); +export const NO_LOCATION_FOUND = localize('sql.migration.no.location.found', "No locations found."); export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource groups found."); -export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage account found."); +export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage accounts 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 NO_BLOBFILES_FOUND = localize('sql.migration.no.blobFiles.found', "No blob files found."); diff --git a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts index b9b235504e..e4cea41df8 100644 --- a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts @@ -15,7 +15,7 @@ import { IconPathHelper } from '../../constants/iconPathHelper'; import { CreateResourceGroupDialog } from '../createResourceGroup/createResourceGroupDialog'; import { createAuthenticationKeyTable } from '../../wizard/integrationRuntimePage'; import * as EventEmitter from 'events'; -import { clearDialogMessage } from '../../api/utils'; +import * as utils from '../../api/utils'; import * as styles from '../../constants/styles'; export class CreateSqlMigrationServiceDialog { @@ -44,7 +44,8 @@ export class CreateSqlMigrationServiceDialog { private _view!: azdata.ModelView; private _createdMigrationService!: SqlMigrationService; - private _selectedResourceGroup!: string; + private _resourceGroups!: azureResource.AzureResourceResourceGroup[]; + private _selectedResourceGroup!: azureResource.AzureResourceResourceGroup; private _testConnectionButton!: azdata.window.Button; private _doneButtonEvent: EventEmitter = new EventEmitter(); @@ -79,11 +80,11 @@ export class CreateSqlMigrationServiceDialog { const subscription = this._model._targetSubscription; - const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue)?.name; + const resourceGroup = this._selectedResourceGroup; const location = this._model._targetServerInstance.location; const serviceName = this.migrationServiceNameText.value; - const formValidationErrors = this.validateCreateServiceForm(subscription, resourceGroup, location, serviceName); + const formValidationErrors = this.validateCreateServiceForm(subscription, resourceGroup.name, location, serviceName); if (formValidationErrors.length > 0) { this.setDialogMessage(formValidationErrors); @@ -93,12 +94,12 @@ export class CreateSqlMigrationServiceDialog { } try { - clearDialogMessage(this._dialogObject); + utils.clearDialogMessage(this._dialogObject); this._selectedResourceGroup = resourceGroup; this._createdMigrationService = await createSqlMigrationService( this._model._azureAccount, subscription, - resourceGroup, + resourceGroup.name, location, serviceName!, this._model._sessionId); @@ -203,7 +204,7 @@ export class CreateSqlMigrationServiceDialog { this._isBlobContainerUsed = this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER; return new Promise((resolve) => { - this._doneButtonEvent.once('done', (createdDms: SqlMigrationService, selectedResourceGroup: string) => { + this._doneButtonEvent.once('done', (createdDms: SqlMigrationService, selectedResourceGroup: azureResource.AzureResourceResourceGroup) => { azdata.window.closeDialog(this._dialogObject); resolve( { @@ -256,6 +257,16 @@ export class CreateSqlMigrationServiceDialog { } }).component(); + this._disposables.push( + this.migrationServiceResourceGroupDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined') { + const selectedResourceGroup = this._resourceGroups.find(rg => rg.name === value || constants.NEW_RESOURCE_GROUP(rg.name) === value); + this._selectedResourceGroup = (selectedResourceGroup) + ? selectedResourceGroup + : undefined!; + } + })); + const migrationServiceNameLabel = this._view.modelBuilder.text().withProps({ value: constants.NAME, description: constants.MIGRATION_SERVICE_NAME_INFO, @@ -277,6 +288,8 @@ export class CreateSqlMigrationServiceDialog { const createResourceGroupDialog = new CreateResourceGroupDialog(this._model._azureAccount, this._model._targetSubscription, this._model._targetServerInstance.location); const createdResourceGroup = await createResourceGroupDialog.initialize(); if (createdResourceGroup) { + this._resourceGroups.push(createdResourceGroup); + this._selectedResourceGroup = createdResourceGroup; this.migrationServiceResourceGroupDropdown.loading = true; (this.migrationServiceResourceGroupDropdown.values).unshift({ displayName: constants.NEW_RESOURCE_GROUP(createdResourceGroup.name), @@ -376,12 +389,9 @@ export class CreateSqlMigrationServiceDialog { private async populateResourceGroups(): Promise { this.migrationServiceResourceGroupDropdown.loading = true; try { - this.migrationServiceResourceGroupDropdown.values = (await this._model.getAzureResourceGroupDropdownValues(this._model._targetSubscription)).map(v => { - return { - name: v.displayName, - displayName: v.displayName - }; - }); + this._resourceGroups = await utils.getAllResourceGroups(this._model._azureAccount, this._model._targetSubscription); + this.migrationServiceResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups); + const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.name.toLowerCase() === this._resourceGroupPreset.toLowerCase()); this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0]; } finally { @@ -496,7 +506,7 @@ export class CreateSqlMigrationServiceDialog { let migrationServiceStatus!: SqlMigrationService; for (let i = 0; i < maxRetries; i++) { try { - clearDialogMessage(this._dialogObject); + utils.clearDialogMessage(this._dialogObject); migrationServiceStatus = await getSqlMigrationService( this._model._azureAccount, subscription, @@ -649,5 +659,5 @@ export class CreateSqlMigrationServiceDialog { export interface CreateSqlMigrationServiceDialogResult { service: SqlMigrationService, - resourceGroup: string + resourceGroup: azureResource.AzureResourceResourceGroup } diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index 5f0cbaae06..a9d684488c 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -355,6 +355,11 @@ export class MigrationStatusDialog { this._searchBox.value!); this._filteredMigrations.sort((m1, m2) => { + if (!m1.properties?.startedOn) { + return 1; + } else if (!m2.properties?.startedOn) { + return -1; + } return new Date(m1.properties?.startedOn) > new Date(m2.properties?.startedOn) ? -1 : 1; }); diff --git a/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts index 955d6e6532..c9e842d297 100644 --- a/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts @@ -9,8 +9,8 @@ import * as azurecore from 'azurecore'; import { MigrationLocalStorage, MigrationServiceContext } from '../../models/migrationLocalStorage'; import * as styles from '../../constants/styles'; import * as constants from '../../constants/strings'; -import { findDropDownItemIndex, selectDefaultDropdownValue, deepClone } from '../../api/utils'; -import { getFullResourceGroupFromId, getLocations, getSqlMigrationServices, getSubscriptions, SqlMigrationService } from '../../api/azure'; +import * as utils from '../../api/utils'; +import { SqlMigrationService } from '../../api/azure'; import { logError, TelemetryViews } from '../../telemtery'; const CONTROL_MARGIN = '20px'; @@ -77,12 +77,12 @@ export class SelectMigrationServiceDialog { }); this._dialog.okButton.label = constants.MIGRATION_SERVICE_SELECT_APPLY_LABEL; - this._dialog.okButton.position = 'left'; + this._dialog.okButton.position = 'right'; this._dialog.cancelButton.position = 'right'; this._deleteButton = azdata.window.createButton( constants.MIGRATION_SERVICE_CLEAR, - 'right'); + 'left'); this._disposables.push( this._deleteButton.onClick(async (value) => { await MigrationLocalStorage.saveMigrationServiceContext({}); @@ -140,11 +140,13 @@ export class SelectMigrationServiceDialog { }).component(); this._disposables.push( this._azureAccountsDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value); - this._serviceContext.azureAccount = (selectedIndex > -1) - ? deepClone(this._azureAccounts[selectedIndex]) - : undefined!; - await this._populateTentantsDropdown(); + if (value && value !== 'undefined') { + const selectedAccount = this._azureAccounts.find(account => account.displayInfo.displayName === value); + this._serviceContext.azureAccount = (selectedAccount) + ? utils.deepClone(selectedAccount) + : undefined!; + await this._populateTentantsDropdown(); + } })); const linkAccountButton = this._view.modelBuilder.hyperlink() @@ -185,11 +187,14 @@ export class SelectMigrationServiceDialog { }).component(); this._disposables.push( this._accountTenantDropdown.onValueChanged(async value => { - const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value); - this._serviceContext.tenant = (selectedIndex > -1) - ? deepClone(this._accountTenants[selectedIndex]) - : undefined!; - await this._populateSubscriptionDropdown(); + if (value && value !== 'undefined') { + const selectedTenant = this._accountTenants.find(tenant => tenant.displayName === value); + if (selectedTenant) { + this._serviceContext.tenant = utils.deepClone(selectedTenant); + this._serviceContext.azureAccount!.properties.tenants = [selectedTenant]; + } + await this._populateSubscriptionDropdown(); + } })); this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer() @@ -207,7 +212,7 @@ export class SelectMigrationServiceDialog { const subscriptionDropdownLabel = this._view.modelBuilder.text() .withProps({ value: constants.SUBSCRIPTION, - description: constants.TARGET_SUBSCRIPTION_INFO, + description: constants.DMS_SUBSCRIPTION_INFO, requiredIndicator: true, CSSStyles: { ...LABEL_CSS } }).component(); @@ -223,17 +228,19 @@ export class SelectMigrationServiceDialog { }).component(); this._disposables.push( this._azureSubscriptionDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureSubscriptionDropdown, value); - this._serviceContext.subscription = (selectedIndex > -1) - ? deepClone(this._subscriptions[selectedIndex]) - : undefined!; - await this._populateLocationDropdown(); + if (value && value !== 'undefined') { + const selectedSubscription = this._subscriptions.find(subscription => `${subscription.name} - ${subscription.id}` === value); + this._serviceContext.subscription = (selectedSubscription) + ? utils.deepClone(selectedSubscription) + : undefined!; + await this._populateLocationDropdown(); + } })); const azureLocationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, - description: constants.TARGET_LOCATION_INFO, + description: constants.DMS_LOCATION_INFO, requiredIndicator: true, CSSStyles: { ...LABEL_CSS } }).component(); @@ -249,17 +256,20 @@ export class SelectMigrationServiceDialog { }).component(); this._disposables.push( this._azureLocationDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureLocationDropdown, value); - this._serviceContext.location = (selectedIndex > -1) - ? deepClone(this._locations[selectedIndex]) - : undefined!; - await this._populateResourceGroupDropdown(); + if (value && value !== 'undefined') { + const selectedLocation = this._locations.find(location => location.displayName === value); + this._serviceContext.location = (selectedLocation) + ? utils.deepClone(selectedLocation) + : undefined!; + await this._populateResourceGroupDropdown(); + await this._populateMigrationServiceDropdown(); + } })); const azureResourceGroupLabel = this._view.modelBuilder.text() .withProps({ value: constants.RESOURCE_GROUP, - description: constants.TARGET_RESOURCE_GROUP_INFO, + description: constants.DMS_RESOURCE_GROUP_INFO, requiredIndicator: true, CSSStyles: { ...LABEL_CSS } }).component(); @@ -275,11 +285,13 @@ export class SelectMigrationServiceDialog { }).component(); this._disposables.push( this._azureResourceGroupDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureResourceGroupDropdown, value); - this._serviceContext.resourceGroup = (selectedIndex > -1) - ? deepClone(this._resourceGroups[selectedIndex]) - : undefined!; - await this._populateMigrationServiceDropdown(); + if (value && value !== 'undefined') { + const selectedResourceGroup = this._resourceGroups.find(rg => rg.name === value); + this._serviceContext.resourceGroup = (selectedResourceGroup) + ? utils.deepClone(selectedResourceGroup) + : undefined!; + await this._populateMigrationServiceDropdown(); + } })); this._azureServiceDropdownLabel = this._view.modelBuilder.text() @@ -301,11 +313,13 @@ export class SelectMigrationServiceDialog { }).component(); this._disposables.push( this._azureServiceDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureServiceDropdown, value, true); - this._serviceContext.migrationService = (selectedIndex > -1) - ? deepClone(this._sqlMigrationServices.find(service => service.name === value)) - : undefined!; - await this._updateButtonState(); + if (value && value !== 'undefined') { + const selectedDms = this._sqlMigrationServices.find(dms => dms.name === value); + this._serviceContext.migrationService = (selectedDms) + ? utils.deepClone(selectedDms) + : undefined!; + await this._updateButtonState(); + } })); this._disposables.push( @@ -335,13 +349,13 @@ export class SelectMigrationServiceDialog { private async _populateAzureAccountsDropdown(): Promise { try { this._azureAccountsDropdown.loading = true; - this._azureAccountsDropdown.values = await this._getAccountDropdownValues(); + this._azureAccounts = await utils.getAzureAccounts(); + this._azureAccountsDropdown.values = await utils.getAzureAccountsDropdownValues(this._azureAccounts); if (this._azureAccountsDropdown.values.length > 0) { - selectDefaultDropdownValue( + utils.selectDefaultDropdownValue( this._azureAccountsDropdown, this._serviceContext.azureAccount?.displayInfo?.userId, false); - this._azureAccountsDropdown.loading = false; } } catch (error) { logError(TelemetryViews.SelectMigrationServiceDialog, '_populateAzureAccountsDropdown', error); @@ -356,18 +370,17 @@ export class SelectMigrationServiceDialog { private async _populateTentantsDropdown(): Promise { try { this._accountTenantDropdown.loading = true; - this._accountTenantDropdown.values = this._getTenantDropdownValues( - this._serviceContext.azureAccount); + this._accountTenants = utils.getAzureTenants(this._serviceContext.azureAccount); + this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this._accountTenants); await this._accountTenantFlexContainer.updateCssStyles( this._accountTenants.length > 1 ? STYLE_ShOW : STYLE_HIDE); if (this._accountTenantDropdown.values.length > 0) { - selectDefaultDropdownValue( + utils.selectDefaultDropdownValue( this._accountTenantDropdown, this._serviceContext.tenant?.id, false); - this._accountTenantDropdown.loading = false; } } catch (error) { logError(TelemetryViews.SelectMigrationServiceDialog, '_populateTentantsDropdown', error); @@ -376,20 +389,20 @@ export class SelectMigrationServiceDialog { error.message); } finally { this._accountTenantDropdown.loading = false; + await this._populateSubscriptionDropdown(); } } private async _populateSubscriptionDropdown(): Promise { try { this._azureSubscriptionDropdown.loading = true; - this._azureSubscriptionDropdown.values = await this._getSubscriptionDropdownValues( - this._serviceContext.azureAccount); + this._subscriptions = await utils.getAzureSubscriptions(this._serviceContext.azureAccount); + this._azureSubscriptionDropdown.values = await utils.getAzureSubscriptionsDropdownValues(this._subscriptions); if (this._azureSubscriptionDropdown.values.length > 0) { - selectDefaultDropdownValue( + utils.selectDefaultDropdownValue( this._azureSubscriptionDropdown, this._serviceContext.subscription?.id, false); - this._azureSubscriptionDropdown.loading = false; } } catch (error) { logError(TelemetryViews.SelectMigrationServiceDialog, '_populateSubscriptionDropdown', error); @@ -404,15 +417,14 @@ export class SelectMigrationServiceDialog { private async _populateLocationDropdown(): Promise { try { this._azureLocationDropdown.loading = true; - this._azureLocationDropdown.values = await this._getAzureLocationDropdownValues( - this._serviceContext.azureAccount, - this._serviceContext.subscription); + this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this._serviceContext.azureAccount, this._serviceContext.subscription); + this._locations = await utils.getSqlMigrationServiceLocations(this._serviceContext.azureAccount, this._serviceContext.subscription, this._sqlMigrationServices); + this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this._locations); if (this._azureLocationDropdown.values.length > 0) { - selectDefaultDropdownValue( + utils.selectDefaultDropdownValue( this._azureLocationDropdown, this._serviceContext.location?.displayName, true); - this._azureLocationDropdown.loading = false; } } catch (error) { logError(TelemetryViews.SelectMigrationServiceDialog, '_populateLocationDropdown', error); @@ -427,14 +439,13 @@ export class SelectMigrationServiceDialog { private async _populateResourceGroupDropdown(): Promise { try { this._azureResourceGroupDropdown.loading = true; - this._azureResourceGroupDropdown.values = await this._getAzureResourceGroupDropdownValues( - this._serviceContext.location); + this._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this._sqlMigrationServices, this._serviceContext.location!); + this._azureResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups); if (this._azureResourceGroupDropdown.values.length > 0) { - selectDefaultDropdownValue( + utils.selectDefaultDropdownValue( this._azureResourceGroupDropdown, this._serviceContext.resourceGroup?.id, false); - this._azureResourceGroupDropdown.loading = false; } } catch (error) { logError(TelemetryViews.SelectMigrationServiceDialog, '_populateResourceGroupDropdown', error); @@ -449,14 +460,9 @@ export class SelectMigrationServiceDialog { private async _populateMigrationServiceDropdown(): Promise { try { this._azureServiceDropdown.loading = true; - this._azureServiceDropdown.values = await this._getMigrationServiceDropdownValues( - this._serviceContext.azureAccount, - this._serviceContext.subscription, - this._serviceContext.location, - this._serviceContext.resourceGroup); - + this._azureServiceDropdown.values = await utils.getAzureSqlMigrationServicesDropdownValues(this._sqlMigrationServices, this._serviceContext.location!, this._serviceContext.resourceGroup!); if (this._azureServiceDropdown.values.length > 0) { - selectDefaultDropdownValue( + utils.selectDefaultDropdownValue( this._azureServiceDropdown, this._serviceContext?.migrationService?.id, false); @@ -470,127 +476,4 @@ export class SelectMigrationServiceDialog { this._azureServiceDropdown.loading = false; } } - - private async _getAccountDropdownValues(): Promise { - this._azureAccounts = await azdata.accounts.getAllAccounts() || []; - return this._azureAccounts.map(account => { - return { - name: account.displayInfo.userId, - displayName: account.isStale - ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName) - : account.displayInfo.displayName, - }; - }); - } - - private async _getSubscriptionDropdownValues(account?: azdata.Account): Promise { - this._subscriptions = []; - if (account?.isStale === false) { - try { - this._subscriptions = await getSubscriptions(account); - this._subscriptions.sort((a, b) => a.name.localeCompare(b.name)); - } catch (error) { - logError(TelemetryViews.SelectMigrationServiceDialog, '_getSubscriptionDropdownValues', error); - void vscode.window.showErrorMessage( - constants.SELECT_SUBSCRIPTION_ERROR, - error.message); - } - } - - return this._subscriptions.map(subscription => { - return { - name: subscription.id, - displayName: `${subscription.name} - ${subscription.id}`, - }; - }); - } - - private _getTenantDropdownValues(account?: azdata.Account): azdata.CategoryValue[] { - this._accountTenants = account?.isStale === false - ? account?.properties?.tenants ?? [] - : []; - - return this._accountTenants.map(tenant => { - return { - name: tenant.id, - displayName: tenant.displayName, - }; - }); - } - - private async _getAzureLocationDropdownValues( - account?: azdata.Account, - subscription?: azurecore.azureResource.AzureResourceSubscription): Promise { - let locations: azurecore.azureResource.AzureLocation[] = []; - if (account && subscription) { - // get all available locations - locations = await getLocations(account, subscription); - this._sqlMigrationServices = await getSqlMigrationServices( - account, - subscription) || []; - this._sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name)); - } else { - this._sqlMigrationServices = []; - } - - // keep locaitons with services only - this._locations = locations.filter( - (loc, i) => this._sqlMigrationServices.some(service => service.location === loc.name)); - this._locations.sort((a, b) => a.name.localeCompare(b.name)); - return this._locations.map(loc => { - return { - name: loc.name, - displayName: loc.displayName, - }; - }); - } - - private async _getAzureResourceGroupDropdownValues(location?: azurecore.azureResource.AzureLocation): Promise { - this._resourceGroups = location - ? this._getMigrationServicesResourceGroups(location) - : []; - this._resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); - return this._resourceGroups.map(rg => { - return { - name: rg.id, - displayName: rg.name, - }; - }); - } - - private _getMigrationServicesResourceGroups(location?: azurecore.azureResource.AzureLocation): azurecore.azureResource.AzureResourceResourceGroup[] { - const resourceGroups = this._sqlMigrationServices - .filter(service => service.location === location?.name) - .map(service => service.properties.resourceGroup); - - return resourceGroups - .filter((rg, i, arr) => arr.indexOf(rg) === i) - .map(rg => { - return { - id: getFullResourceGroupFromId(rg), - name: rg, - }; - }); - } - - private async _getMigrationServiceDropdownValues( - account?: azdata.Account, - subscription?: azurecore.azureResource.AzureResourceSubscription, - location?: azurecore.azureResource.AzureLocation, - resourceGroup?: azurecore.azureResource.AzureResourceResourceGroup): Promise { - - const locationName = location?.name?.toLowerCase(); - const resourceGroupName = resourceGroup?.name?.toLowerCase(); - - return this._sqlMigrationServices - .filter(service => - service.location?.toLowerCase() === locationName && - service.properties?.resourceGroup?.toLowerCase() === resourceGroupName) - .map(service => { - return ({ - name: service.id, - displayName: `${service.name}`, - }); - }); - } } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index d1ce41fe8e..6ce7879474 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -7,7 +7,7 @@ import * as azdata from 'azdata'; 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, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups, getSqlMigrationServicesByResourceGroup } from '../api/azure'; +import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases } from '../api/azure'; import * as constants from '../constants/strings'; import * as nls from 'vscode-nls'; import { v4 as uuidv4 } from 'uuid'; @@ -180,7 +180,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _sourceDatabaseNames!: string[]; public _targetDatabaseNames!: string[]; - public _sqlMigrationServiceResourceGroup!: string; + public _sqlMigrationServiceResourceGroup!: azurecore.azureResource.AzureResourceResourceGroup; public _sqlMigrationService!: SqlMigrationService | undefined; public _sqlMigrationServices!: SqlMigrationService[]; public _nodeNames!: string[]; @@ -821,51 +821,6 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this.extensionContext.extensionPath; } - public async getAccountValues(): Promise { - let accountValues: azdata.CategoryValue[] = []; - try { - this._azureAccounts = await azdata.accounts.getAllAccounts(); - if (this._azureAccounts.length === 0) { - accountValues = [{ - displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, - name: '' - }]; - } - accountValues = this._azureAccounts.map((account): azdata.CategoryValue => { - return { - name: account.displayInfo.userId, - displayName: account.isStale - ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName) - : account.displayInfo.displayName - }; - }); - } catch (e) { - console.log(e); - accountValues = [{ - displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, - name: '' - }]; - } - return accountValues; - } - - public getAccount(index: number): azdata.Account { - return this._azureAccounts[index]; - } - - public getTenantValues(): azdata.CategoryValue[] { - return this._accountTenants.map(tenant => { - return { - displayName: tenant.displayName, - name: tenant.id - }; - }); - } - - public getTenant(index: number): azurecore.Tenant { - return this._accountTenants[index]; - } - public async getSourceConnectionProfile(): Promise { const sqlConnections = await azdata.connection.getConnections(); return sqlConnections.find((value) => { @@ -877,650 +832,16 @@ export class MigrationStateModel implements Model, vscode.Disposable { })!; } - public async getSubscriptionsDropdownValues(): Promise { - let subscriptionsValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount?.isStale === false) { - this._subscriptions = await getSubscriptions(this._azureAccount); - } else { - this._subscriptions = []; - } - - this._subscriptions.forEach((subscription) => { - subscriptionsValues.push({ - name: subscription.id, - displayName: `${subscription.name} - ${subscription.id}` - }); - }); - - if (subscriptionsValues.length === 0) { - subscriptionsValues = [ - { - displayName: constants.NO_SUBSCRIPTIONS_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - subscriptionsValues = [ - { - displayName: constants.NO_SUBSCRIPTIONS_FOUND, - name: '' - } - ]; - } - - return subscriptionsValues; - } - - public getSubscription(index: number): azurecore.azureResource.AzureResourceSubscription { - return this._subscriptions[index]; - } - - public async getAzureLocationDropdownValues(subscription: azurecore.azureResource.AzureResourceSubscription): Promise { - let locationValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription) { - this._locations = await getLocations(this._azureAccount, subscription); - } else { - this._locations = []; - } - - this._locations.forEach((loc) => { - locationValues.push({ - name: loc.name, - displayName: loc.displayName - }); - }); - - if (locationValues.length === 0) { - locationValues = [ - { - displayName: constants.NO_LOCATION_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - locationValues = [ - { - displayName: constants.NO_LOCATION_FOUND, - name: '' - } - ]; - } - - return locationValues; - } - - public getLocation(index: number): azurecore.azureResource.AzureLocation { - return this._locations[index]; - } - public getLocationDisplayName(location: string): Promise { return getLocationDisplayName(location); } - public async getAzureResourceGroupDropdownValues(subscription: azurecore.azureResource.AzureResourceSubscription): Promise { - let resourceGroupValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription) { - this._resourceGroups = await getResourceGroups(this._azureAccount, subscription); - } else { - this._resourceGroups = []; - } - 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 async getAzureResourceGroupForManagedInstancesDropdownValues(subscription: azurecore.azureResource.AzureResourceSubscription): Promise { - let resourceGroupValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription) { - let managedInstances = await getAvailableManagedInstanceProducts(this._azureAccount, subscription); - this._resourceGroups = managedInstances.map((mi) => { - return { - id: getFullResourceGroupFromId(mi.id), - name: getResourceGroupFromId(mi.id), - subscription: { - id: mi.subscriptionId - }, - tenant: mi.tenantId, - }; - }); - - // remove duplicates - this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); - sortResourceArrayByName(this._resourceGroups); - } else { - this._resourceGroups = []; - } - - 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 async getAzureResourceGroupForVirtualMachinesDropdownValues(subscription: azurecore.azureResource.AzureResourceSubscription): Promise { - let resourceGroupValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription) { - let virtualMachines = await getAvailableSqlVMs(this._azureAccount, subscription); - this._resourceGroups = virtualMachines.map((vm) => { - return { - id: getFullResourceGroupFromId(vm.id), - name: getResourceGroupFromId(vm.id), - subscription: { - id: vm.subscriptionId - }, - tenant: vm.tenantId, - }; - }); - - // remove duplicates - this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); - sortResourceArrayByName(this._resourceGroups); - } else { - this._resourceGroups = []; - } - - 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 async getAzureResourceGroupForStorageAccountsDropdownValues(subscription: azurecore.azureResource.AzureResourceSubscription): Promise { - let resourceGroupValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription) { - let storageAccounts = await getAvailableStorageAccounts(this._azureAccount, subscription); - this._resourceGroups = storageAccounts.map((sa) => { - return { - id: getFullResourceGroupFromId(sa.id), - name: getResourceGroupFromId(sa.id), - subscription: { - id: sa.subscriptionId - }, - tenant: sa.tenantId, - }; - }); - - // remove duplicates - this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); - sortResourceArrayByName(this._resourceGroups); - } else { - this._resourceGroups = []; - } - - 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 async getAzureResourceGroupForSqlMigrationServicesDropdownValues(subscription: azurecore.azureResource.AzureResourceSubscription): Promise { - let resourceGroupValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription) { - let dmsInstances = await getSqlMigrationServices(this._azureAccount, subscription); - this._resourceGroups = dmsInstances.map((dms) => { - return { - id: getFullResourceGroupFromId(dms.id), - name: getResourceGroupFromId(dms.id), - subscription: { - id: dms.properties.subscriptionId - } - }; - }); - - // remove duplicates - this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); - sortResourceArrayByName(this._resourceGroups); - } else { - this._resourceGroups = []; - } - - 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): azurecore.azureResource.AzureResourceResourceGroup { - return this._resourceGroups[index]; - } - - public async getManagedInstanceValues(subscription: azurecore.azureResource.AzureResourceSubscription, location: azurecore.azureResource.AzureLocation, resourceGroup: azurecore.azureResource.AzureResourceResourceGroup): Promise { - let managedInstanceValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription && location && resourceGroup) { - this._targetManagedInstances = (await getAvailableManagedInstanceProducts(this._azureAccount, subscription)).filter((mi) => { - if (mi.location.toLowerCase() === location?.name.toLowerCase() && mi.resourceGroup?.toLowerCase() === resourceGroup?.name.toLowerCase()) { - return true; - } - return false; - }); - } else { - this._targetManagedInstances = []; - } - - this._targetManagedInstances.forEach((managedInstance) => { - let managedInstanceValue: azdata.CategoryValue; - - if (managedInstance.properties.state === 'Ready') { - managedInstanceValue = { - name: managedInstance.id, - displayName: `${managedInstance.name}` - }; - } else { - managedInstanceValue = { - name: managedInstance.id, - displayName: constants.UNAVAILABLE_MANAGED_INSTANCE_PREFIX(managedInstance.name) - }; - } - - managedInstanceValues.push(managedInstanceValue); - }); - - if (managedInstanceValues.length === 0) { - managedInstanceValues = [ - { - displayName: constants.NO_MANAGED_INSTANCE_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - managedInstanceValues = [ - { - displayName: constants.NO_MANAGED_INSTANCE_FOUND, - name: '' - } - ]; - } - return managedInstanceValues; - } - - public getManagedInstance(index: number): SqlManagedInstance { - return this._targetManagedInstances[index]; - } - public async getManagedDatabases(): Promise { return (await getSqlManagedInstanceDatabases(this._azureAccount, this._targetSubscription, this._targetServerInstance)).map(t => t.name); } - public async getSqlVirtualMachineValues(subscription: azurecore.azureResource.AzureResourceSubscription, location: azurecore.azureResource.AzureLocation, resourceGroup: azurecore.azureResource.AzureResourceResourceGroup): Promise { - let virtualMachineValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription && location && resourceGroup) { - this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription)).filter((virtualMachine) => { - if (virtualMachine?.location?.toLowerCase() === location?.name?.toLowerCase() && getResourceGroupFromId(virtualMachine.id).toLowerCase() === resourceGroup?.name.toLowerCase()) { - 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}` - }; - }); - } else { - this._targetSqlVirtualMachines = []; - } - - if (virtualMachineValues.length === 0) { - virtualMachineValues = [ - { - displayName: constants.NO_VIRTUAL_MACHINE_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - virtualMachineValues = [ - { - displayName: constants.NO_VIRTUAL_MACHINE_FOUND, - name: '' - } - ]; - } - return virtualMachineValues; - } - - public getVirtualMachine(index: number): SqlVMServer { - return this._targetSqlVirtualMachines[index]; - } - - public async getStorageAccountValues(subscription: azurecore.azureResource.AzureResourceSubscription, resourceGroup: azurecore.azureResource.AzureResourceResourceGroup): Promise { - let storageAccountValues: azdata.CategoryValue[] = []; - if (!resourceGroup) { - return storageAccountValues; - } - try { - if (this._azureAccount && subscription && resourceGroup) { - const storageAccount = (await getAvailableStorageAccounts(this._azureAccount, subscription)); - this._storageAccounts = storageAccount.filter(sa => { - return sa.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sa.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase(); - }); - } else { - this._storageAccounts = []; - } - - this._storageAccounts.forEach((storageAccount) => { - storageAccountValues.push({ - name: storageAccount.id, - displayName: `${storageAccount.name}` - }); - }); - - if (storageAccountValues.length === 0) { - storageAccountValues = [ - { - displayName: constants.NO_STORAGE_ACCOUNT_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - storageAccountValues = [ - { - displayName: constants.NO_STORAGE_ACCOUNT_FOUND, - name: '' - } - ]; - } - return storageAccountValues; - } - - public getStorageAccount(index: number): StorageAccount { - return this._storageAccounts[index]; - } - - public async getFileShareValues(subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: StorageAccount): Promise { - let fileShareValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription && storageAccount) { - this._fileShares = await getFileShares(this._azureAccount, subscription, storageAccount); - } else { - this._fileShares = []; - } - - this._fileShares.forEach((fileShare) => { - fileShareValues.push({ - name: fileShare.id, - displayName: `${fileShare.name}` - }); - }); - - if (fileShareValues.length === 0) { - fileShareValues = [ - { - displayName: constants.NO_FILESHARES_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - fileShareValues = [ - { - displayName: constants.NO_FILESHARES_FOUND, - name: '' - } - ]; - } - return fileShareValues; - } - - public getFileShare(index: number): azurecore.azureResource.FileShare { - return this._fileShares[index]; - } - - public async getBlobContainerValues(subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: StorageAccount): Promise { - let blobContainerValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription && storageAccount) { - this._blobContainers = await getBlobContainers(this._azureAccount, subscription, storageAccount); - } else { - this._blobContainers = []; - } - - this._blobContainers.forEach((blobContainer) => { - blobContainerValues.push({ - name: blobContainer.id, - displayName: `${blobContainer.name}` - }); - }); - - if (blobContainerValues.length === 0) { - blobContainerValues = [ - { - displayName: constants.NO_BLOBCONTAINERS_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - blobContainerValues = [ - { - displayName: constants.NO_BLOBCONTAINERS_FOUND, - name: '' - } - ]; - } - return blobContainerValues; - } - - public getBlobContainer(index: number): azurecore.azureResource.BlobContainer { - return this._blobContainers[index]; - } - - public async getBlobLastBackupFileNameValues(subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: StorageAccount, blobContainer: azurecore.azureResource.BlobContainer): Promise { - let blobLastBackupFileValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription && storageAccount && blobContainer) { - this._lastFileNames = await getBlobs(this._azureAccount, subscription, storageAccount, blobContainer.name); - } else { - this._lastFileNames = []; - } - - this._lastFileNames.forEach((blob) => { - blobLastBackupFileValues.push({ - name: blob.name, - displayName: `${blob.name}`, - }); - }); - - if (blobLastBackupFileValues.length === 0) { - blobLastBackupFileValues = [ - { - displayName: constants.NO_BLOBFILES_FOUND, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - blobLastBackupFileValues = [ - { - displayName: constants.NO_BLOBFILES_FOUND, - name: '' - } - ]; - } - return blobLastBackupFileValues; - } - - public getBlobLastBackupFileName(index: number): string { - return this._lastFileNames[index]?.name; - } - - public async getSqlMigrationServiceValues(subscription: azurecore.azureResource.AzureResourceSubscription, resourceGroupName: string): Promise { - let sqlMigrationServiceValues: azdata.CategoryValue[] = []; - try { - if (this._azureAccount && subscription && resourceGroupName && this._targetServerInstance) { - const services = await getSqlMigrationServicesByResourceGroup( - this._azureAccount, - subscription, - resourceGroupName?.toLowerCase()); - const targetLoc = this._targetServerInstance.location.toLowerCase(); - this._sqlMigrationServices = services.filter(sms => sms.location.toLowerCase() === targetLoc); - } else { - this._sqlMigrationServices = []; - } - - this._sqlMigrationServices.forEach((sqlMigrationService) => { - sqlMigrationServiceValues.push({ - name: sqlMigrationService.id, - displayName: `${sqlMigrationService.name}` - }); - }); - - if (sqlMigrationServiceValues.length === 0) { - sqlMigrationServiceValues = [ - { - displayName: constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR, - name: '' - } - ]; - } - } catch (e) { - console.log(e); - sqlMigrationServiceValues = [ - { - displayName: constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR, - name: '' - } - ]; - } - return sqlMigrationServiceValues; - } - - public getMigrationService(index: number): SqlMigrationService { - return this._sqlMigrationServices[index]; - } - public async startMigration() { const sqlConnections = await azdata.connection.getConnections(); const currentConnection = sqlConnections.find((value) => { diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts index 7bd85d17a7..4e75520810 100644 --- a/extensions/sql-migration/src/telemtery.ts +++ b/extensions/sql-migration/src/telemtery.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import AdsTelemetryReporter, { TelemetryEventMeasures, TelemetryEventProperties } from '@microsoft/ads-extension-telemetry'; -import { getPackageInfo } from './api/utils'; const packageJson = require('../package.json'); -let packageInfo = getPackageInfo(packageJson)!; +let packageInfo = { + name: packageJson.name, + version: packageJson.version, + aiKey: packageJson.aiKey +}; export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); @@ -19,7 +22,7 @@ export enum TelemetryViews { MigrationCutoverDialog = 'MigrationCutoverDialog', MigrationStatusDialog = 'MigrationStatusDialog', MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage', - MigrationWizardTaSkuRecommendationPage = 'MigrationWizardTaSkuRecommendationPage', + MigrationWizardSkuRecommendationPage = 'MigrationWizardSkuRecommendationPage', MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage', MigrationWizardIntegrationRuntimePage = 'MigrationWizardIntegrationRuntimePage', MigrationWizardSummaryPage = 'MigrationWizardSummaryPage', @@ -30,6 +33,7 @@ export enum TelemetryViews { SkuRecommendationWizard = 'SkuRecommendationWizard', DataCollectionWizard = 'GetAzureRecommendationDialog', SelectMigrationServiceDialog = 'SelectMigrationServiceDialog', + Utils = 'Utils' } export enum TelemetryAction { diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 9a4318ea3d..e83df3d048 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -12,7 +12,7 @@ import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateM import * as constants from '../constants/strings'; import { IconPathHelper } from '../constants/iconPathHelper'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; -import { findDropDownItemIndex, selectDropDownIndex, selectDefaultDropdownValue } from '../api/utils'; +import * as utils from '../api/utils'; import { logError, TelemetryViews } from '../telemtery'; import * as styles from '../constants/styles'; @@ -687,12 +687,14 @@ export class DatabaseBackupPage extends MigrationWizardPage { }, }).component(); this._disposables.push(this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._networkShareStorageAccountResourceGroupDropdown, value); - if (selectedIndex > -1) { - for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { - this.migrationStateModel._databaseBackup.networkShares[i].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); + if (value && value !== 'undefined') { + const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); + if (selectedResourceGroup) { + for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { + this.migrationStateModel._databaseBackup.networkShares[i].resourceGroup = selectedResourceGroup; + } + await this.loadNetworkShareStorageDropdown(); } - await this.loadNetworkShareStorageDropdown(); } })); @@ -714,10 +716,12 @@ export class DatabaseBackupPage extends MigrationWizardPage { fireOnTextChange: true, }).component(); this._disposables.push(this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => { - const selectedIndex = findDropDownItemIndex(this._networkShareContainerStorageAccountDropdown, value); - if (selectedIndex > -1) { - for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { - this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex); + if (value && value !== 'undefined') { + const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); + if (selectedStorageAccount) { + for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { + this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = selectedStorageAccount; + } } } })); @@ -962,48 +966,58 @@ export class DatabaseBackupPage extends MigrationWizardPage { }).component(); this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(blobContainerResourceDropdown, value); - if (selectedIndex > -1 && !blobResourceGroupErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); - await this.loadBlobStorageDropdown(index); - await blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); - } else { - await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); + if (value && value !== 'undefined' && this.migrationStateModel._resourceGroups) { + const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); + if (selectedResourceGroup && !blobResourceGroupErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = selectedResourceGroup; + await this.loadBlobStorageDropdown(index); + await blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); + } else { + await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); + } } })); this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); this._disposables.push(blobContainerStorageAccountDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value); - if (selectedIndex > -1 && !blobStorageAccountErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex); - await this.loadBlobContainerDropdown(index); - await blobContainerDropdown.updateProperties({ enabled: true }); - } else { - await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); + if (value && value !== 'undefined') { + const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); + if (selectedStorageAccount && !blobStorageAccountErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].storageAccount = selectedStorageAccount; + await this.loadBlobContainerDropdown(index); + await blobContainerDropdown.updateProperties({ enabled: true }); + } else { + await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); + } } })); this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); this._disposables.push(blobContainerDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value); - if (selectedIndex > -1 && !blobContainerErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(selectedIndex); - if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { - await this.loadBlobLastBackupFileDropdown(index); - await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); + if (value && value !== 'undefined' && this.migrationStateModel._blobContainers) { + const selectedBlobContainer = this.migrationStateModel._blobContainers.find(blob => blob.name === value); + if (selectedBlobContainer && !blobContainerErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].blobContainer = selectedBlobContainer; + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + await this.loadBlobLastBackupFileDropdown(index); + await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); + } + } else { + await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); } - } else { - await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); } })); this._blobContainerDropdowns.push(blobContainerDropdown); if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { this._disposables.push(blobContainerLastBackupFileDropdown.onValueChanged(value => { - const selectedIndex = findDropDownItemIndex(blobContainerLastBackupFileDropdown, value); - if (selectedIndex > -1 && !blobFileErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = this.migrationStateModel.getBlobLastBackupFileName(selectedIndex); + if (value && value !== 'undefined') { + if (this.migrationStateModel._lastFileNames) { + const selectedLastBackupFile = this.migrationStateModel._lastFileNames.find(fileName => fileName.name === value); + if (selectedLastBackupFile && !blobFileErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = selectedLastBackupFile.name; + } + } } })); this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown); @@ -1276,8 +1290,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadNetworkStorageResourceGroup(): Promise { this._networkShareStorageAccountResourceGroupDropdown.loading = true; try { - this._networkShareStorageAccountResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForStorageAccountsDropdownValues(this.migrationStateModel._databaseBackup.subscription); - selectDefaultDropdownValue(this._networkShareStorageAccountResourceGroupDropdown, this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.id, false); + this.migrationStateModel._storageAccounts = await utils.getStorageAccounts(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription); + this.migrationStateModel._resourceGroups = await utils.getStorageAccountResourceGroups(this.migrationStateModel._storageAccounts, this.migrationStateModel._location); + this._networkShareStorageAccountResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); + utils.selectDefaultDropdownValue(this._networkShareStorageAccountResourceGroupDropdown, this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkStorageResourceGroup', error); } finally { @@ -1290,8 +1306,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._networkShareContainerStorageAccountDropdown.loading = true; this._networkShareStorageAccountResourceGroupDropdown.loading = true; try { - this._networkShareContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.networkShares[0]?.resourceGroup); - selectDefaultDropdownValue(this._networkShareContainerStorageAccountDropdown, this.migrationStateModel?._databaseBackup?.networkShares[0]?.storageAccount?.id, false); + this._networkShareContainerStorageAccountDropdown.values = await utils.getStorageAccountsDropdownValues(this.migrationStateModel._storageAccounts, this.migrationStateModel._location, this.migrationStateModel._databaseBackup.networkShares[0]?.resourceGroup); + utils.selectDefaultDropdownValue(this._networkShareContainerStorageAccountDropdown, this.migrationStateModel?._databaseBackup?.networkShares[0]?.storageAccount?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkShareStorageDropdown', error); } finally { @@ -1303,10 +1319,12 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadBlobResourceGroup(): Promise { this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); try { - const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupForStorageAccountsDropdownValues(this.migrationStateModel._databaseBackup.subscription); + this.migrationStateModel._storageAccounts = await utils.getStorageAccounts(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription); + this.migrationStateModel._resourceGroups = await utils.getStorageAccountResourceGroups(this.migrationStateModel._storageAccounts, this.migrationStateModel._location); + const resourceGroupValues = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); this._blobContainerResourceGroupDropdowns.forEach((dropDown, index) => { dropDown.values = resourceGroupValues; - selectDefaultDropdownValue(dropDown, this.migrationStateModel._databaseBackup?.blobs[index]?.resourceGroup?.id, false); + utils.selectDefaultDropdownValue(dropDown, this.migrationStateModel._databaseBackup?.blobs[index]?.resourceGroup?.id, false); }); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobResourceGroup', error); @@ -1318,8 +1336,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadBlobStorageDropdown(index: number): Promise { this._blobContainerStorageAccountDropdowns[index].loading = true; try { - this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup); - selectDefaultDropdownValue(this._blobContainerStorageAccountDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id, false); + this._blobContainerStorageAccountDropdowns[index].values = await utils.getStorageAccountsDropdownValues(this.migrationStateModel._storageAccounts, this.migrationStateModel._location, this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup); + utils.selectDefaultDropdownValue(this._blobContainerStorageAccountDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobStorageDropdown', error); } finally { @@ -1330,9 +1348,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadBlobContainerDropdown(index: number): Promise { this._blobContainerDropdowns[index].loading = true; try { - const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount); - this._blobContainerDropdowns[index].values = blobContainerValues; - selectDefaultDropdownValue(this._blobContainerDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id, false); + this.migrationStateModel._blobContainers = await utils.getBlobContainer(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount); + this._blobContainerDropdowns[index].values = await utils.getBlobContainersValues(this.migrationStateModel._blobContainers); + utils.selectDefaultDropdownValue(this._blobContainerDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobContainers', error); } finally { @@ -1343,9 +1361,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadBlobLastBackupFileDropdown(index: number): Promise { this._blobContainerLastBackupFileDropdowns[index].loading = true; try { - const blobLastBackupFileValues = await this.migrationStateModel.getBlobLastBackupFileNameValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); - this._blobContainerLastBackupFileDropdowns[index].values = blobLastBackupFileValues; - selectDefaultDropdownValue(this._blobContainerLastBackupFileDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile, false); + this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); + this._blobContainerLastBackupFileDropdowns[index].values = await utils.getBlobLastBackupFileNamesValues(this.migrationStateModel._lastFileNames); + utils.selectDefaultDropdownValue(this._blobContainerLastBackupFileDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobLastBackupFiles', error); } finally { @@ -1363,18 +1381,18 @@ export class DatabaseBackupPage extends MigrationWizardPage { if (this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE) { this._blobContainerLastBackupFileDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER); - selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[rowIndex], 0); + utils.selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[rowIndex], 0); await this._blobContainerLastBackupFileDropdowns[rowIndex]?.updateProperties(dropdownProps); } if (columnName === constants.BLOB_CONTAINER) { return; } this._blobContainerDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_STORAGE_ACCOUNT); - selectDropDownIndex(this._blobContainerDropdowns[rowIndex], 0); + utils.selectDropDownIndex(this._blobContainerDropdowns[rowIndex], 0); await this._blobContainerDropdowns[rowIndex].updateProperties(dropdownProps); if (columnName === constants.STORAGE_ACCOUNT) { return; } this._blobContainerStorageAccountDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_RESOURCE_GROUP_PROMPT); - selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0); + utils.selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0); await this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps); } } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 295b2e6ee3..57297cb5b4 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -13,7 +13,7 @@ import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import { getFullResourceGroupFromId, getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData } from '../api/azure'; import { IconPathHelper } from '../constants/iconPathHelper'; import { logError, TelemetryViews } from '../telemtery'; -import { findDropDownItemIndex, selectDefaultDropdownValue } from '../api/utils'; +import * as utils from '../api/utils'; import * as styles from '../constants/styles'; export class IntergrationRuntimePage extends MigrationWizardPage { @@ -170,14 +170,13 @@ export class IntergrationRuntimePage extends MigrationWizardPage { } }).component(); this._disposables.push(this._resourceGroupDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._resourceGroupDropdown, value); - if (selectedIndex > -1 && - value !== constants.RESOURCE_GROUP_NOT_FOUND) { - this.migrationStateModel._sqlMigrationServiceResourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex).name; - } else { - this.migrationStateModel._sqlMigrationServiceResourceGroup = undefined!; + if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) { + const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); + this.migrationStateModel._sqlMigrationServiceResourceGroup = (selectedResourceGroup) + ? selectedResourceGroup + : undefined!; + await this.populateDms(); } - await this.populateDms(); })); const migrationServiceDropdownLabel = this._view.modelBuilder.text().withProps({ @@ -199,16 +198,16 @@ export class IntergrationRuntimePage extends MigrationWizardPage { } }).component(); this._disposables.push(this._dmsDropdown.onValueChanged(async (value) => { - if (value && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) { + if (value && value !== 'undefined' && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) { if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { this._dmsInfoContainer.display = 'inline'; } this.wizard.message = { text: '' }; - const selectedIndex = findDropDownItemIndex(this._dmsDropdown, value); - if (selectedIndex > -1) { - this.migrationStateModel._sqlMigrationService = this.migrationStateModel.getMigrationService(selectedIndex); + const selectedDms = this.migrationStateModel._sqlMigrationServices.find(dms => dms.name === value && dms.properties.resourceGroup.toLowerCase() === this.migrationStateModel._sqlMigrationServiceResourceGroup.name.toLowerCase()); + if (selectedDms) { + this.migrationStateModel._sqlMigrationService = selectedDms; await this.loadMigrationServiceStatus(); } } else { @@ -373,11 +372,13 @@ export class IntergrationRuntimePage extends MigrationWizardPage { this._resourceGroupDropdown.loading = true; this._dmsDropdown.loading = true; try { - this._resourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForSqlMigrationServicesDropdownValues(this.migrationStateModel._targetSubscription); + this.migrationStateModel._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription); + this.migrationStateModel._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this.migrationStateModel._sqlMigrationServices, this.migrationStateModel._location); + this._resourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); const resourceGroup = (this.migrationStateModel._sqlMigrationService) ? getFullResourceGroupFromId(this.migrationStateModel._sqlMigrationService?.id) : undefined; - selectDefaultDropdownValue(this._resourceGroupDropdown, resourceGroup, false); + utils.selectDefaultDropdownValue(this._resourceGroupDropdown, resourceGroup, false); } finally { this._resourceGroupDropdown.loading = false; this._dmsDropdown.loading = false; @@ -387,8 +388,8 @@ export class IntergrationRuntimePage extends MigrationWizardPage { public async populateDms(): Promise { this._dmsDropdown.loading = true; try { - this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._sqlMigrationServiceResourceGroup); - selectDefaultDropdownValue(this._dmsDropdown, this.migrationStateModel._sqlMigrationService?.id, false); + this._dmsDropdown.values = await utils.getAzureSqlMigrationServicesDropdownValues(this.migrationStateModel._sqlMigrationServices, this.migrationStateModel._location, this.migrationStateModel._sqlMigrationServiceResourceGroup); + utils.selectDefaultDropdownValue(this._dmsDropdown, this.migrationStateModel._sqlMigrationService?.id, false); } finally { this._dmsDropdown.loading = false; } diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index fc6d0bd62e..4dbfb50b55 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -466,7 +466,7 @@ export class SKURecommendationPage extends MigrationWizardPage { } } catch (e) { errors.push(constants.SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR(serverName, e)); - logError(TelemetryViews.MigrationWizardTaSkuRecommendationPage, 'SkuRecommendationUnexpectedError', e); + logError(TelemetryViews.MigrationWizardSkuRecommendationPage, 'SkuRecommendationUnexpectedError', e); } finally { this.migrationStateModel._runAssessments = errors.length > 0; if (errors.length > 0) { diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index 49fefd006c..66016a3523 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -11,8 +11,10 @@ import { MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../m import * as constants from '../constants/strings'; import * as styles from '../constants/styles'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; -import { deepClone, findDropDownItemIndex, selectDropDownIndex, selectDefaultDropdownValue } from '../api/utils'; +import * as utils from '../api/utils'; import { azureResource } from 'azurecore'; +import { SqlVMServer } from '../api/azure'; +import { ProvisioningState } from '../models/migrationLocalStorage'; export class TargetSelectionPage extends MigrationWizardPage { private _view!: azdata.ModelView; @@ -28,6 +30,8 @@ export class TargetSelectionPage extends MigrationWizardPage { private _azureResourceDropdownLabel!: azdata.TextComponent; private _azureResourceDropdown!: azdata.DropDownComponent; + private _migrationTargetPlatform!: MigrationTargetType; + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.AZURE_SQL_TARGET_PAGE_TITLE), migrationStateModel); } @@ -87,8 +91,15 @@ export class TargetSelectionPage extends MigrationWizardPage { break; } - await this.populateResourceInstanceDropdown(); await this.populateAzureAccountsDropdown(); + if (this._migrationTargetPlatform !== this.migrationStateModel._targetType) { + // if the user had previously selected values on this page, then went back to change the migration target platform + // and came back, forcibly reload the location/resource group/resource values since they will now be different + this._migrationTargetPlatform = this.migrationStateModel._targetType; + await this.populateLocationDropdown(); + await this.populateResourceGroupDropdown(); + await this.populateResourceInstanceDropdown(); + } this.wizard.registerNavigationValidator((pageChangeInfo) => { const errors: string[] = []; @@ -121,7 +132,7 @@ export class TargetSelectionPage extends MigrationWizardPage { const resourceDropdownValue = (this._azureResourceDropdown.value)?.displayName; switch (this.migrationStateModel._targetType) { case MigrationTargetType.SQLMI: { - let targetMi = this.migrationStateModel._targetServerInstance as azureResource.AzureSqlManagedInstance; + const targetMi = this.migrationStateModel._targetServerInstance as azureResource.AzureSqlManagedInstance; if (!targetMi || resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) { errors.push(constants.INVALID_MANAGED_INSTANCE_ERROR); break; @@ -133,9 +144,14 @@ export class TargetSelectionPage extends MigrationWizardPage { break; } case MigrationTargetType.SQLVM: { - if (!this.migrationStateModel._targetServerInstance || - resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) { + const targetVm = this.migrationStateModel._targetServerInstance as SqlVMServer; + if (!targetVm || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) { errors.push(constants.INVALID_VIRTUAL_MACHINE_ERROR); + break; + } + if (targetVm.properties.provisioningState !== ProvisioningState.Succeeded) { + errors.push(constants.VM_NOT_READY_ERROR(targetVm.name, targetVm.properties.provisioningState)); + break; } break; } @@ -176,34 +192,20 @@ export class TargetSelectionPage extends MigrationWizardPage { width: WIZARD_INPUT_COMPONENT_WIDTH, editable: true, required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_AN_ACCOUNT, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push(this._azureAccountsDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value); - if (selectedIndex > -1) { - const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex); - // Making a clone of the account object to preserve the original tenants - this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount); - if (selectedAzureAccount.isStale === false && - this.migrationStateModel._azureAccount.properties.tenants.length > 1) { - this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants; - this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues(); - selectDropDownIndex(this._accountTenantDropdown, 0); - await this._accountTenantFlexContainer.updateCssStyles({ - 'display': 'inline' - }); - } else { - await this._accountTenantFlexContainer.updateCssStyles({ - 'display': 'none' - }); - } - await this._azureAccountsDropdown.validate(); - } else { - this.migrationStateModel._azureAccount = undefined!; + if (value && value !== 'undefined') { + const selectedAccount = this.migrationStateModel._azureAccounts.find(account => account.displayInfo.displayName === value); + this.migrationStateModel._azureAccount = (selectedAccount) + ? utils.deepClone(selectedAccount)! + : undefined!; + await this.populateTenantsDropdown(); } - await this.populateSubscriptionDropdown(); })); const linkAccountButton = this._view.modelBuilder.hyperlink() @@ -251,20 +253,22 @@ export class TargetSelectionPage extends MigrationWizardPage { width: WIZARD_INPUT_COMPONENT_WIDTH, editable: true, fireOnTextChange: true, + placeholder: constants.SELECT_A_TENANT }).component(); this._disposables.push(this._accountTenantDropdown.onValueChanged(async (value) => { - /** - * Replacing all the tenants in azure account with the tenant user has selected. - * All azure requests will only run on this tenant from now on - */ - const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value); - const selectedTenant = this.migrationStateModel.getTenant(selectedIndex); - this.migrationStateModel._azureTenant = deepClone(selectedTenant); - if (selectedIndex > -1) { - this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel.getTenant(selectedIndex)]; + if (value && value !== 'undefined') { + /** + * Replacing all the tenants in azure account with the tenant user has selected. + * All azure requests will only run on this tenant from now on + */ + const selectedTenant = this.migrationStateModel._accountTenants.find(tenant => tenant.displayName === value); + if (selectedTenant) { + this.migrationStateModel._azureTenant = utils.deepClone(selectedTenant)!; + this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel._azureTenant]; + } + await this.populateSubscriptionDropdown(); } - await this.populateSubscriptionDropdown(); })); this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer() @@ -300,21 +304,20 @@ export class TargetSelectionPage extends MigrationWizardPage { editable: true, required: true, fireOnTextChange: true, + placeholder: constants.SELECT_A_SUBSCRIPTION, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push(this._azureSubscriptionDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureSubscriptionDropdown, value); - if (selectedIndex > -1 && - value !== constants.NO_SUBSCRIPTIONS_FOUND) { - this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(selectedIndex); - } else { - this.migrationStateModel._targetSubscription = undefined!; + if (value && value !== 'undefined' && value !== constants.NO_SUBSCRIPTIONS_FOUND) { + const selectedSubscription = this.migrationStateModel._subscriptions.find(subscription => `${subscription.name} - ${subscription.id}` === value); + this.migrationStateModel._targetSubscription = (selectedSubscription) + ? utils.deepClone(selectedSubscription)! + : undefined!; + this.migrationStateModel.refreshDatabaseBackupPage = true; + await this.populateLocationDropdown(); } - this.migrationStateModel.refreshDatabaseBackupPage = true; - await this.populateLocationDropdown(); - await this.populateResourceGroupDropdown(); })); const azureLocationLabel = this._view.modelBuilder.text().withProps({ @@ -332,20 +335,21 @@ export class TargetSelectionPage extends MigrationWizardPage { editable: true, required: true, fireOnTextChange: true, + placeholder: constants.SELECT_A_LOCATION, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push(this._azureLocationDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureLocationDropdown, value); - if (selectedIndex > -1 && - value !== constants.NO_LOCATION_FOUND) { - this.migrationStateModel._location = this.migrationStateModel.getLocation(selectedIndex); - } else { - this.migrationStateModel._location = undefined!; + if (value && value !== 'undefined' && value !== constants.NO_LOCATION_FOUND) { + const selectedLocation = this.migrationStateModel._locations.find(location => location.displayName === value); + this.migrationStateModel._location = (selectedLocation) + ? utils.deepClone(selectedLocation)! + : undefined!; + this.migrationStateModel.refreshDatabaseBackupPage = true; + await this.populateResourceGroupDropdown(); + await this.populateResourceInstanceDropdown(); } - this.migrationStateModel.refreshDatabaseBackupPage = true; - await this.populateResourceInstanceDropdown(); })); const azureResourceGroupLabel = this._view.modelBuilder.text().withProps({ @@ -363,19 +367,19 @@ export class TargetSelectionPage extends MigrationWizardPage { editable: true, required: true, fireOnTextChange: true, + placeholder: constants.SELECT_A_RESOURCE_GROUP, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push(this._azureResourceGroupDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureResourceGroupDropdown, value); - if (selectedIndex > -1 && - value !== constants.RESOURCE_GROUP_NOT_FOUND) { - this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); - } else { - this.migrationStateModel._resourceGroup = undefined!; + if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) { + const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); + this.migrationStateModel._resourceGroup = (selectedResourceGroup) + ? utils.deepClone(selectedResourceGroup)! + : undefined!; + await this.populateResourceInstanceDropdown(); } - await this.populateResourceInstanceDropdown(); })); this._azureResourceDropdownLabel = this._view.modelBuilder.text().withProps({ @@ -393,39 +397,54 @@ export class TargetSelectionPage extends MigrationWizardPage { editable: true, required: true, fireOnTextChange: true, + placeholder: constants.SELECT_A_SERVICE, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push(this._azureResourceDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(this._azureResourceDropdown, value); - if (selectedIndex > -1 && - value !== constants.NO_MANAGED_INSTANCE_FOUND && - value !== constants.NO_VIRTUAL_MACHINE_FOUND) { + if (value && value !== 'undefined' && value !== constants.NO_MANAGED_INSTANCE_FOUND && value !== constants.NO_VIRTUAL_MACHINE_FOUND) { this.migrationStateModel._sqlMigrationServices = undefined!; switch (this.migrationStateModel._targetType) { case MigrationTargetType.SQLVM: - this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(selectedIndex); + const selectedVm = this.migrationStateModel._targetSqlVirtualMachines.find(vm => vm.name === value || constants.UNAVAILABLE_TARGET_PREFIX(vm.name) === value); + if (selectedVm) { + this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer; + + if (this.migrationStateModel._targetServerInstance.properties.provisioningState !== ProvisioningState.Succeeded) { + this.wizard.message = { + text: constants.VM_NOT_READY_ERROR(this.migrationStateModel._targetServerInstance.name, this.migrationStateModel._targetServerInstance.properties.provisioningState), + level: azdata.window.MessageLevel.Error + }; + } else { + this.wizard.message = { + text: '', + level: azdata.window.MessageLevel.Error + }; + } + } break; case MigrationTargetType.SQLMI: - this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(selectedIndex); - if (this.migrationStateModel._targetServerInstance.properties.state !== 'Ready') { - this.wizard.message = { - text: constants.MI_NOT_READY_ERROR(this.migrationStateModel._targetServerInstance.name, this.migrationStateModel._targetServerInstance.properties.state), - level: azdata.window.MessageLevel.Error - }; - } else { - this.wizard.message = { - text: '', - level: azdata.window.MessageLevel.Error - }; + const selectedMi = this.migrationStateModel._targetManagedInstances.find(mi => mi.name === value || constants.UNAVAILABLE_TARGET_PREFIX(mi.name) === value); + if (selectedMi) { + this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance; + + if (this.migrationStateModel._targetServerInstance.properties.state !== 'Ready') { + this.wizard.message = { + text: constants.MI_NOT_READY_ERROR(this.migrationStateModel._targetServerInstance.name, this.migrationStateModel._targetServerInstance.properties.state), + level: azdata.window.MessageLevel.Error + }; + } else { + this.wizard.message = { + text: '', + level: azdata.window.MessageLevel.Error + }; + } } break; } - } else { - this.migrationStateModel._targetServerInstance = undefined!; } })); @@ -448,18 +467,40 @@ export class TargetSelectionPage extends MigrationWizardPage { private async populateAzureAccountsDropdown(): Promise { try { this.updateDropdownLoadingStatus(TargetDropDowns.AzureAccount, true); - this._azureAccountsDropdown.values = await this.migrationStateModel.getAccountValues(); - selectDefaultDropdownValue(this._azureAccountsDropdown, this.migrationStateModel._azureAccount?.displayInfo?.userId, false); + this.migrationStateModel._azureAccounts = await utils.getAzureAccounts(); + this._azureAccountsDropdown.values = await utils.getAzureAccountsDropdownValues(this.migrationStateModel._azureAccounts); + utils.selectDefaultDropdownValue(this._azureAccountsDropdown, this.migrationStateModel._azureAccount?.displayInfo?.userId, false); } finally { this.updateDropdownLoadingStatus(TargetDropDowns.AzureAccount, false); } } + private async populateTenantsDropdown(): Promise { + try { + this.updateDropdownLoadingStatus(TargetDropDowns.Tenant, true); + if (this.migrationStateModel._azureAccount && this.migrationStateModel._azureAccount.isStale === false && this.migrationStateModel._azureAccount.properties.tenants.length > 0) { + this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount); + this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants); + utils.selectDefaultDropdownValue(this._accountTenantDropdown, this.migrationStateModel._azureTenant?.id, true); + } + await this._accountTenantFlexContainer.updateCssStyles(this.migrationStateModel._azureAccount.properties.tenants.length > 1 + ? { 'display': 'inline' } + : { 'display': 'none' } + ); + await this._azureAccountsDropdown.validate(); + } finally { + this.updateDropdownLoadingStatus(TargetDropDowns.Tenant, false); + await this.populateSubscriptionDropdown(); + } + } + + private async populateSubscriptionDropdown(): Promise { try { this.updateDropdownLoadingStatus(TargetDropDowns.Subscription, true); - this._azureSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues(); - selectDefaultDropdownValue(this._azureSubscriptionDropdown, this.migrationStateModel._targetSubscription?.id, false); + this.migrationStateModel._subscriptions = await utils.getAzureSubscriptions(this.migrationStateModel._azureAccount); + this._azureSubscriptionDropdown.values = await utils.getAzureSubscriptionsDropdownValues(this.migrationStateModel._subscriptions); + utils.selectDefaultDropdownValue(this._azureSubscriptionDropdown, this.migrationStateModel._targetSubscription?.id, false); } catch (e) { console.log(e); } finally { @@ -470,8 +511,18 @@ export class TargetSelectionPage extends MigrationWizardPage { public async populateLocationDropdown(): Promise { try { this.updateDropdownLoadingStatus(TargetDropDowns.Location, true); - this._azureLocationDropdown.values = await this.migrationStateModel.getAzureLocationDropdownValues(this.migrationStateModel._targetSubscription); - selectDefaultDropdownValue(this._azureLocationDropdown, this.migrationStateModel._location?.displayName, true); + switch (this.migrationStateModel._targetType) { + case MigrationTargetType.SQLMI: + this.migrationStateModel._targetManagedInstances = await utils.getManagedInstances(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription); + this.migrationStateModel._locations = await utils.getSqlManagedInstanceLocations(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription, this.migrationStateModel._targetManagedInstances); + break; + case MigrationTargetType.SQLVM: + this.migrationStateModel._targetSqlVirtualMachines = await utils.getVirtualMachines(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription); + this.migrationStateModel._locations = await utils.getSqlVirtualMachineLocations(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription, this.migrationStateModel._targetSqlVirtualMachines); + break; + } + this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations); + utils.selectDefaultDropdownValue(this._azureLocationDropdown, this.migrationStateModel._location?.displayName, true); } catch (e) { console.log(e); } finally { @@ -484,13 +535,14 @@ export class TargetSelectionPage extends MigrationWizardPage { this.updateDropdownLoadingStatus(TargetDropDowns.ResourceGroup, true); switch (this.migrationStateModel._targetType) { case MigrationTargetType.SQLMI: - this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForManagedInstancesDropdownValues(this.migrationStateModel._targetSubscription); + this.migrationStateModel._resourceGroups = await utils.getSqlManagedInstanceResourceGroups(this.migrationStateModel._targetManagedInstances, this.migrationStateModel._location); break; case MigrationTargetType.SQLVM: - this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForVirtualMachinesDropdownValues(this.migrationStateModel._targetSubscription); + this.migrationStateModel._resourceGroups = await utils.getSqlVirtualMachineResourceGroups(this.migrationStateModel._targetSqlVirtualMachines, this.migrationStateModel._location); break; } - selectDefaultDropdownValue(this._azureResourceGroupDropdown, this.migrationStateModel._resourceGroup?.id, false); + this._azureResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); + utils.selectDefaultDropdownValue(this._azureResourceGroupDropdown, this.migrationStateModel._resourceGroup?.id, false); } catch (e) { console.log(e); } finally { @@ -503,15 +555,15 @@ export class TargetSelectionPage extends MigrationWizardPage { this.updateDropdownLoadingStatus(TargetDropDowns.ResourceInstance, true); switch (this.migrationStateModel._targetType) { case MigrationTargetType.SQLMI: { - this._azureResourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); + this._azureResourceDropdown.values = await utils.getManagedInstancesDropdownValues(this.migrationStateModel._targetManagedInstances, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); break; } case MigrationTargetType.SQLVM: { - this._azureResourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); + this._azureResourceDropdown.values = await utils.getVirtualMachinesDropdownValues(this.migrationStateModel._targetSqlVirtualMachines, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); break; } } - selectDefaultDropdownValue(this._azureResourceDropdown, this.migrationStateModel._targetServerInstance?.name, true); + utils.selectDefaultDropdownValue(this._azureResourceDropdown, this.migrationStateModel._targetServerInstance?.name, true); } catch (e) { console.log(e); } finally { @@ -537,6 +589,7 @@ export class TargetSelectionPage extends MigrationWizardPage { export enum TargetDropDowns { AzureAccount, + Tenant, Subscription, Location, ResourceGroup,