diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index bc8080891c..5eca8ce26a 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -90,6 +90,20 @@ declare module 'azureResource' { }, } + export interface AzureSqlManagedInstance extends AzureGraphResource { + } + + export interface ManagedDatabase { + id: string, + location: string, + name: string, + properties: { + sourceDatabaseId: string, + status: string + }, + type: string + } + export interface AzureResourceDatabase extends AzureSqlResource { serverName: string; serverFullName: string; diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts index 56e0c2a235..aeb48ace86 100644 --- a/extensions/azurecore/src/azureResource/utils.ts +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -7,7 +7,7 @@ import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { TokenCredentials } from '@azure/ms-rest-js'; import axios, { AxiosRequestConfig } from 'axios'; import * as azdata from 'azdata'; -import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult } from 'azurecore'; +import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult } from 'azurecore'; import { azureResource } from 'azureResource'; import { EOL } from 'os'; import * as nls from 'vscode-nls'; @@ -429,6 +429,15 @@ export async function makeHttpRequest(account: azdata.Account, subscription: azu return result; } +export async function getManagedDatabases(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, managedInstance: azureResource.AzureSqlManagedInstance, ignoreErrors: boolean): Promise { + const path = `/subscriptions/${subscription.id}/resourceGroups/${managedInstance.resourceGroup}/providers/Microsoft.Sql/managedInstances/${managedInstance.name}/databases?api-version=2020-02-02-preview`; + const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors); + return { + databases: response?.response?.data?.value ?? [], + errors: response.errors ? response.errors : [] + }; +} + export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise { const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/blobServices/default/containers?api-version=2019-06-01`; const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors); diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index 1a5d5de7d7..764b13142f 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -247,6 +247,7 @@ declare module 'azurecore' { getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise; getLocations(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise; getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; + getManagedDatabases(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, managedInstance: azureResource.AzureSqlManagedInstance, ignoreErrors?: boolean): Promise; getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; @@ -276,8 +277,9 @@ declare module 'azurecore' { export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] }; export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] }; - export type GetLocationsResult = {locations: azureResource.AzureLocation[], errors: Error[] }; - export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; + export type GetLocationsResult = { locations: azureResource.AzureLocation[], errors: Error[] }; + export type GetSqlManagedInstancesResult = { resources: azureResource.AzureSqlManagedInstance[], errors: Error[] }; + export type GetManagedDatabasesResult = { databases: azureResource.ManagedDatabase[], errors: Error[] }; export type GetSqlServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; export type GetSqlVMServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; export type GetStorageAccountResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index d5c1e69024..769f16f1d2 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -174,6 +174,12 @@ export async function activate(context: vscode.ExtensionContext): Promise { return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`); }, + getManagedDatabases(account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + managedInstance: azureResource.AzureSqlManagedInstance, + ignoreErrors: boolean): Promise { + return azureResourceUtils.getManagedDatabases(account, subscription, managedInstance, ignoreErrors); + }, getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors: boolean): Promise { diff --git a/extensions/machine-learning/src/test/stubs.ts b/extensions/machine-learning/src/test/stubs.ts index 7eca3bbebb..d68daa237a 100644 --- a/extensions/machine-learning/src/test/stubs.ts +++ b/extensions/machine-learning/src/test/stubs.ts @@ -8,6 +8,9 @@ import * as azurecore from 'azurecore'; import { azureResource } from 'azureResource'; export class AzurecoreApiStub implements azurecore.IExtension { + getManagedDatabases(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _managedInstance: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise { + throw new Error('Method not implemented.'); + } getLocations(_account?: azdata.Account, _subscription?: azureResource.AzureResourceSubscription, _ignoreErrors?: boolean): Promise { throw new Error('Method not implemented.'); } diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 745ad26f7b..5af176b140 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -2,7 +2,7 @@ "name": "sql-migration", "displayName": "%displayName%", "description": "%description%", - "version": "0.0.5", + "version": "0.0.6", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/package.nls.json b/extensions/sql-migration/package.nls.json index 6b81e61a4a..02d8257a51 100644 --- a/extensions/sql-migration/package.nls.json +++ b/extensions/sql-migration/package.nls.json @@ -1,9 +1,9 @@ { - "displayName": "SQL Migration", - "description": "SQL migration description", - "migration-notebook-command-title": "Open SQL migration notebooks", - "migration-dashboard-title": "SQL Migration", + "displayName": "Azure SQL Migration", + "description": "Azure SQL migration description", + "migration-notebook-command-title": "Open Azure SQL migration notebooks", + "migration-dashboard-title": "Azure SQL Migration", "migration-dashboard-tasks": "Migration Tasks", - "migration-command-category": "SQL Migration", - "start-migration-command": "Start SQL Migration" + "migration-command-category": "Azure SQL Migration", + "start-migration-command": "Start Azure SQL Migration" } diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index e6a4973c95..d0a40868ee 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as azurecore from 'azurecore'; import { azureResource } from 'azureResource'; -import * as loc from '../constants/strings'; async function getAzureCoreAPI(): Promise { const api = (await vscode.extensions.getExtension(azurecore.extension.name)?.activate()) as azurecore.IExtension; @@ -49,7 +48,7 @@ export async function getResourceGroups(account: azdata.Account, subscription: S return result.resourceGroups; } -export type SqlManagedInstance = AzureProduct; +export type SqlManagedInstance = azureResource.AzureSqlManagedInstance; export async function getAvailableManagedInstanceProducts(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); const result = await api.getSqlManagedInstances(account, [subscription], false); @@ -57,6 +56,13 @@ export async function getAvailableManagedInstanceProducts(account: azdata.Accoun return result.resources; } +export async function getSqlManagedInstanceDatabases(account: azdata.Account, subscription: Subscription, managedInstance: SqlManagedInstance): Promise { + const api = await getAzureCoreAPI(); + const result = await api.getManagedDatabases(account, subscription, managedInstance, false); + sortResourceArrayByName(result.databases); + return result.databases; +} + export type SqlServer = AzureProduct; export async function getAvailableSqlServers(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); @@ -116,9 +122,8 @@ export async function getBlobContainers(account: azdata.Account, subscription: S export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise { const api = await getAzureCoreAPI(); - const host = `https://${regionName}.management.azure.com`; const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -127,9 +132,8 @@ export async function getSqlMigrationService(account: azdata.Account, subscripti export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription, regionName: string): Promise { const api = await getAzureCoreAPI(); - const host = `https://${regionName}.management.azure.com`; const path = `/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -139,12 +143,11 @@ export async function getSqlMigrationServices(account: azdata.Account, subscript export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise { const api = await getAzureCoreAPI(); - const host = `https://${regionName}.management.azure.com`; const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`; const requestBody = { 'location': regionName }; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -153,9 +156,8 @@ export async function createSqlMigrationService(account: azdata.Account, subscri export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise { const api = await getAzureCoreAPI(); - const host = `https://${regionName}.management.azure.com`; const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -180,9 +182,8 @@ export async function getStorageAccountAccessKeys(account: azdata.Account, subsc export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string): Promise { const api = await getAzureCoreAPI(); - const host = `https://${regionName}.management.azure.com`; const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -192,9 +193,8 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest): Promise { const api = await getAzureCoreAPI(); - const host = `https://${regionName}.management.azure.com`; const path = `${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -206,9 +206,8 @@ export async function startDatabaseMigration(account: azdata.Account, subscripti export async function getDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, migrationId: string): Promise { const api = await getAzureCoreAPI(); - const host = `https://${regionName}.management.azure.com`; const path = `${migrationId}?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { if (response.response.status === 404 && response.response.data.error.code === 'ResourceDoesNotExist') { throw new Error(response.response.data.error.code); @@ -220,9 +219,8 @@ export async function getDatabaseMigration(account: azdata.Account, subscription export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise { const api = await getAzureCoreAPI(); - const host = `https://eastus2euap.management.azure.com`; const path = `${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -231,9 +229,8 @@ export async function getMigrationStatus(account: azdata.Account, subscription: export async function listMigrationsBySqlMigrationService(account: azdata.Account, subscription: Subscription, sqlMigrationService: SqlMigrationService): Promise { const api = await getAzureCoreAPI(); - const host = `https://eastus2euap.management.azure.com`; const path = `${sqlMigrationService.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -242,9 +239,8 @@ export async function listMigrationsBySqlMigrationService(account: azdata.Accoun export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration): Promise { const api = await getAzureCoreAPI(); - const host = `https://eastus2euap.management.azure.com`; const path = `${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cutover?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -253,24 +249,16 @@ export async function startMigrationCutover(account: azdata.Account, subscriptio export async function stopMigration(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration): Promise { const api = await getAzureCoreAPI(); - const host = `https://eastus2euap.management.azure.com`; const path = `${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cancel?api-version=2020-09-01-preview`; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } } -/** - * For now only east us euap is supported. Actual API calls will be added in the public release. - */ -export function getSqlMigrationServiceRegions(): azdata.CategoryValue[] { - return [ - { - displayName: loc.EASTUS2EUAP, - name: 'eastus2euap' - } - ]; +export async function getLocationDisplayName(location: string): Promise { + const api = await getAzureCoreAPI(); + return await api.getRegionDisplayName(location); } type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription | SqlMigrationService; diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 12e92d528b..3d3bfac4f4 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -58,20 +58,29 @@ export function ACCOUNT_STALE_ERROR(account: AzureAccount) { // database backup page export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database Backup"); -export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location where we can find your database backups (Full, Differential and Log) to use for migration."); +export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location of your database backups to use for migration."); + export const DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL = localize('sql.migration.nc.network.share.radio.label', "My database backups are on a network share"); -export const DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT = localize('sql.migration.network.share.help.text', "Enter network share information"); export const DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL = localize('sql.migration.nc.blob.storage.radio.label', "My database backups are in an Azure Storage Blob Container"); export const DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL = localize('sql.migration.nc.file.share.radio.label', "My database backups are in an Azure Storage File Share"); -export const DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL = localize('sql.migration.network.share.location.label', "Network share location to read backups from."); + +export const DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT = localize('sql.migration.network.share.header.text', "Network share details"); +export const DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT = localize('sql.migration.network.share.help.text', "Provide the network share location that contains backups and the user credentials that has read access to the share"); +export const DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL = localize('sql.migration.network.share.location.label', "Network share location that contains backups."); +export const DATABASE_SERVICE_ACCOUNT_INFO_TEXT = localize('sql.migration.service.account.info.text', "Ensure that the service account running the source SQL Server instance has read privileges on the network share."); export const DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL = localize('sql.migration.network.share.windows.user.label', "Windows user account with read access to the network share location."); export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL = localize('sql.migration.network.share.password.label', "Password"); export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER = localize('sql.migration.network.share.password.placeholder', "Enter password"); -export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Select the storage account where backup files will be copied to during migration"); +export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HEADER = localize('sql.migration.network.share.azure.header', "Storage account details"); +export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Provide the Azure storage account where backups will be uploaded to."); export const DATABASE_BACKUP_NETWORK_SHARE_SUBSCRIPTION_LABEL = localize('sql.migration.network.share.subscription.label', "Select the subscription that contains the storage account."); export const DATABASE_BACKUP_SUBSCRIPTION_PLACEHOLDER = localize('sql.migration.network.share.subscription.placeholder', "Select subscription"); export const DATABASE_BACKUP_NETWORK_SHARE_NETWORK_STORAGE_ACCOUNT_LABEL = localize('sql.migration.network.share.storage.account.label', "Select the storage account where backup files will be copied."); export const DATABASE_BACKUP_STORAGE_ACCOUNT_PLACEHOLDER = localize('sql.migration.network.share.storage.account.placeholder', "Select account"); +export const DUPLICATE_NAME_ERROR = localize('sql.migration.unique.name', "Select a unique name for this target database"); +export function DATABASE_ALREADY_EXISTS_MI(targetName: string): string { + return localize('sql.migration.database.already.exists', "Database with the same name already exists on target Managed Instance '{0}'", targetName); +} export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Select the subscription that contains the storage account."); export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_LABEL = localize('sql.migration.blob.storage.account.label', "Select the storage account that contains the backup files."); export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_LABEL = localize('sql.migration.blob.storage.container.label', "Select the container that contains the backup files."); @@ -156,7 +165,7 @@ export function SERVICE_READY(serviceName: string, host: string): string { } export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource groups found"); export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "Please select a valid resource group to proceed."); -export const INVALID_REGION_ERROR = localize('sql.migration.invalid.region.error', "Please select a valid region to proceed."); +export const INVALID_REGION_ERROR = localize('sql.migration.invalid.region.error', "Please select a valid location to proceed."); export const INVALID_SERVICE_NAME_ERROR = localize('sql.migration.invalid.service.name.error', "Please enter a valid name for the Migration Service."); export const SERVICE_NOT_FOUND = localize('sql.migration.service.not.found', "No Migration Services found. Please create a new one."); export const SERVICE_NOT_SETUP_ERROR = localize('sql.migration.service.not.setup', "Please add a Migration Service to proceed."); @@ -168,6 +177,7 @@ export const TARGET_SELECTION_PAGE_TITLE = localize('sql.migration.target.page.t // common strings export const LEARN_MORE = localize('sql.migration.learn.more', "Learn more"); export const SUBSCRIPTION = localize('sql.migration.subscription', "Subscription"); +export const STORAGE_ACCOUNT = localize('sql.migration.storage.account', "Storage Account"); export const RESOURCE_GROUP = localize('sql.migration.resourceGroups', "Resource group"); export const REGION = localize('sql.migration.region', "Region"); export const NAME = localize('sql.migration.name', "Name"); @@ -259,6 +269,8 @@ export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) { } } export const COPY_MIGRATION_DETAILS = localize('sql.migration.copy.migration.details', "Copy Migration Details"); +export const DETAILS_COPIED = localize('sql.migration.details.copied', "Details copied"); + //Migration status dialog export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations"); @@ -273,9 +285,8 @@ export const FINISH_TIME = localize('sql.migration.finish.time', "Finish Time"); //Source Credentials page. export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration', "Source Configuration"); export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source Credentials"); -export function ENTER_YOUR_SQL_CREDS(sqlServerName: string) { - return localize('sql.migration.enter.your.sql.creds', "Enter the credentials for source SQL server instance ‘{0}’", sqlServerName); -} +export const ENTER_YOUR_SQL_CREDS = localize('sql.migration.enter.your.sql.cred', "Enter the credential for source SQL Server instance. This credential will be used while migrating database(s) to Azure SQL."); +export const SERVER = localize('sql.migration.server', "Server"); export const USERNAME = localize('sql.migration.username', "Username"); //Assessment Dialog diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index 933f553fad..57147e7787 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -161,7 +161,7 @@ export class SqlDatabaseTree { this._instanceTable = this._view.modelBuilder.declarativeTable().withProps( { enableRowSelection: true, - width: 200, + width: 170, columns: [ { displayName: constants.INSTANCE, @@ -182,7 +182,6 @@ export class SqlDatabaseTree { const instanceContainer = this._view.modelBuilder.divContainer().withItems([this._instanceTable]).withProps({ CSSStyles: { - 'width': '200px', 'margin': '19px 8px 0px 34px' } }).component(); diff --git a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts index bed802753e..357d161b13 100644 --- a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts @@ -5,7 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { createSqlMigrationService, getSqlMigrationServiceRegions, getSqlMigrationService, getResourceGroups, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../../api/azure'; +import { createSqlMigrationService, getSqlMigrationService, getResourceGroups, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../../api/azure'; import { MigrationStateModel } from '../../models/stateMachine'; import * as constants from '../../constants/strings'; import * as os from 'os'; @@ -15,9 +15,9 @@ import { IconPathHelper } from '../../constants/iconPathHelper'; export class CreateSqlMigrationServiceDialog { - private migrationServiceSubscriptionDropdown!: azdata.DropDownComponent; + private migrationServiceSubscription!: azdata.TextComponent; private migrationServiceResourceGroupDropdown!: azdata.DropDownComponent; - private migrationServiceRegionDropdown!: azdata.DropDownComponent; + private migrationServiceLocation!: azdata.InputBoxComponent; private migrationServiceNameText!: azdata.InputBoxComponent; private _formSubmitButton!: azdata.ButtonComponent; @@ -45,7 +45,7 @@ export class CreateSqlMigrationServiceDialog { this._dialogObject.registerCloseValidator(async () => { return true; }); - tab.registerContent((view: azdata.ModelView) => { + tab.registerContent(async (view: azdata.ModelView) => { this._view = view; this._formSubmitButton = view.modelBuilder.button().withProps({ @@ -62,10 +62,10 @@ export class CreateSqlMigrationServiceDialog { const subscription = this.migrationStateModel._targetSubscription; const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name; - const region = (this.migrationServiceRegionDropdown.value as azdata.CategoryValue).name; + const location = this.migrationStateModel._targetServerInstance.location; const serviceName = this.migrationServiceNameText.value; - const formValidationErrors = this.validateCreateServiceForm(subscription, resourceGroup, region, serviceName); + const formValidationErrors = this.validateCreateServiceForm(subscription, resourceGroup, location, serviceName); if (formValidationErrors.length > 0) { this.setDialogMessage(formValidationErrors); @@ -75,7 +75,7 @@ export class CreateSqlMigrationServiceDialog { } try { - this.createdMigrationService = await createSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, region, serviceName!); + this.createdMigrationService = await createSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, serviceName!); if (this.createdMigrationService.error) { this.setDialogMessage(`${this.createdMigrationService.error.code} : ${this.createdMigrationService.error.message}`); this._statusLoadingComponent.loading = false; @@ -108,7 +108,7 @@ export class CreateSqlMigrationServiceDialog { const formBuilder = view.modelBuilder.formContainer().withFormItems( [ { - component: this.migrationServiceDropdownContainer() + component: (await this.migrationServiceDropdownContainer()) }, { component: this._formSubmitButton @@ -142,7 +142,7 @@ export class CreateSqlMigrationServiceDialog { }); } - private migrationServiceDropdownContainer(): azdata.FlexContainer { + private async migrationServiceDropdownContainer(): Promise { const dialogDescription = this._view.modelBuilder.text().withProps({ value: constants.MIGRATION_SERVICE_DIALOG_DESCRIPTION }).component(); @@ -155,17 +155,11 @@ export class CreateSqlMigrationServiceDialog { value: constants.SUBSCRIPTION }).component(); - this.migrationServiceSubscriptionDropdown = this._view.modelBuilder.dropDown().withProps({ + this.migrationServiceSubscription = this._view.modelBuilder.inputBox().withProps({ required: true, enabled: false }).component(); - this.migrationServiceSubscriptionDropdown.onValueChanged((e) => { - if (this.migrationServiceSubscriptionDropdown.value) { - this.populateResourceGroups(); - } - }); - const resourceGroupDropdownLabel = this._view.modelBuilder.text().withProps({ value: constants.RESOURCE_GROUP }).component(); @@ -180,33 +174,34 @@ export class CreateSqlMigrationServiceDialog { this.migrationServiceNameText = this._view.modelBuilder.inputBox().component(); - const regionsDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.REGION + const locationDropdownLabel = this._view.modelBuilder.text().withProps({ + value: constants.LOCATION }).component(); - this.migrationServiceRegionDropdown = this._view.modelBuilder.dropDown().withProps({ + this.migrationServiceLocation = this._view.modelBuilder.inputBox().withProps({ required: true, - values: getSqlMigrationServiceRegions() + enabled: false, + value: await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location) }).component(); const flexContainer = this._view.modelBuilder.flexContainer().withItems([ dialogDescription, formHeading, subscriptionDropdownLabel, - this.migrationServiceSubscriptionDropdown, + this.migrationServiceSubscription, + locationDropdownLabel, + this.migrationServiceLocation, resourceGroupDropdownLabel, this.migrationServiceResourceGroupDropdown, migrationServiceNameLabel, - this.migrationServiceNameText, - regionsDropdownLabel, - this.migrationServiceRegionDropdown + this.migrationServiceNameText ]).withLayout({ flexFlow: 'column' }).component(); return flexContainer; } - private validateCreateServiceForm(subscription: azureResource.AzureResourceSubscription, resourceGroup: string | undefined, region: string | undefined, migrationServiceName: string | undefined): string { + private validateCreateServiceForm(subscription: azureResource.AzureResourceSubscription, resourceGroup: string | undefined, location: string | undefined, migrationServiceName: string | undefined): string { const errors: string[] = []; if (!subscription) { errors.push(constants.INVALID_SUBSCRIPTION_ERROR); @@ -214,7 +209,7 @@ export class CreateSqlMigrationServiceDialog { if (!resourceGroup) { errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); } - if (!region) { + if (!location) { errors.push(constants.INVALID_REGION_ERROR); } if (!migrationServiceName || migrationServiceName.length === 0) { @@ -224,17 +219,8 @@ export class CreateSqlMigrationServiceDialog { } private async populateSubscriptions(): Promise { - this.migrationServiceSubscriptionDropdown.loading = true; this.migrationServiceResourceGroupDropdown.loading = true; - - - this.migrationServiceSubscriptionDropdown.values = [ - { - displayName: this.migrationStateModel._targetSubscription.name, - name: '' - } - ]; - this.migrationServiceSubscriptionDropdown.loading = false; + this.migrationServiceSubscription.value = this.migrationStateModel._targetSubscription.name; this.populateResourceGroups(); } @@ -390,9 +376,9 @@ export class CreateSqlMigrationServiceDialog { private async refreshStatus(): Promise { const subscription = this.migrationStateModel._targetSubscription; const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name; - const region = (this.migrationServiceRegionDropdown.value as azdata.CategoryValue).name; - const migrationServiceStatus = await getSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, region, this.createdMigrationService!.name); - const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this.migrationStateModel._azureAccount, subscription, resourceGroup, region, this.createdMigrationService!.name); + const location = this.migrationStateModel._targetServerInstance.location; + const migrationServiceStatus = await getSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name); + const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name); this.createdMigrationServiceNodeNames = migrationServiceMonitoringStatus.nodes.map((node) => { return node.nodeName; }); @@ -419,8 +405,8 @@ export class CreateSqlMigrationServiceDialog { private async refreshAuthTable(): Promise { const subscription = this.migrationStateModel._targetSubscription; const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name; - const region = (this.migrationServiceRegionDropdown.value as azdata.CategoryValue).name; - const keys = await getSqlMigrationServiceAuthKeys(this.migrationStateModel._azureAccount, subscription, resourceGroup, region, this.createdMigrationService!.name); + const location = this.migrationStateModel._targetServerInstance.location; + const keys = await getSqlMigrationServiceAuthKeys(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name); this._copyKey1Button = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.copy diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 802f0aaecc..05753a1c0b 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import { IconPathHelper } from '../../constants/iconPathHelper'; import { MigrationContext } from '../../models/migrationLocalStorage'; -import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel'; +import { MigrationCutoverDialogModel, MigrationStatus } from './migrationCutoverDialogModel'; import * as loc from '../../constants/strings'; import { getSqlServerName } from '../../api/utils'; import { EOL } from 'os'; @@ -359,6 +359,7 @@ export class MigrationCutoverDialog { this._copyDatabaseMigrationDetails.onDidClick(async (e) => { await this.refreshStatus(); vscode.env.clipboard.writeText(JSON.stringify(this._model.migrationStatus, undefined, 2)); + vscode.window.showInformationMessage(loc.DETAILS_COPIED); }); header.addItem(this._copyDatabaseMigrationDetails, { @@ -392,9 +393,10 @@ export class MigrationCutoverDialog { errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []); errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.restoreBlockingReason); this._dialogObject.message = { - text: errors.filter(e => e !== undefined).join(EOL) + text: errors.filter(e => e !== undefined).join(EOL), + level: this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress ? azdata.window.MessageLevel.Warning : azdata.window.MessageLevel.Error }; - const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId); + const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId); const sqlServerName = this._model._migration.sourceConnectionProfile.serverName; const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName; const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!); @@ -470,7 +472,7 @@ export class MigrationCutoverDialog { this._startCutover = true; } - if (migrationStatusTextValue === 'InProgress') { + if (migrationStatusTextValue === MigrationStatus.InProgress) { const fileNotRestored = await tableData.some(file => file.status !== 'Restored'); this._cutoverButton.enabled = !fileNotRestored; this._cancelButton.enabled = true; diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts index 88ed758d1f..751f56f7f9 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts @@ -6,6 +6,12 @@ import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration } from '../../api/azure'; import { MigrationContext } from '../../models/migrationLocalStorage'; +export enum MigrationStatus { + Failed = 'Failed', + Succeeded = 'Succeeded', + InProgress = 'InProgress', + Canceled = 'Canceled' +} export class MigrationCutoverDialogModel { diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index aaad81063c..93af61f08e 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -8,7 +8,7 @@ import { azureResource } from 'azureResource'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; -import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups } from '../api/azure'; +import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups, getLocationDisplayName, getSqlManagedInstanceDatabases } from '../api/azure'; import { SKURecommendations } from './externalContract'; import * as constants from '../constants/strings'; import { MigrationLocalStorage } from './migrationLocalStorage'; @@ -68,6 +68,7 @@ export interface DatabaseBackupModel { windowsUser: string; password: string; subscription: azureResource.AzureResourceSubscription; + resourceGroup: azureResource.AzureResourceResourceGroup; storageAccount: StorageAccount; storageKey: string; azureSecurityToken: string; @@ -356,6 +357,10 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._locations[index]; } + public getLocationDisplayName(location: string): Promise { + return getLocationDisplayName(location); + } + public async getAzureResourceGroupDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise { let resourceGroupValues: azdata.CategoryValue[] = []; try { @@ -384,7 +389,6 @@ export class MigrationStateModel implements Model, vscode.Disposable { } ]; } - return resourceGroupValues; } @@ -433,6 +437,12 @@ export class MigrationStateModel implements Model, vscode.Disposable { 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: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { let virtualMachineValues: azdata.CategoryValue[] = []; try { @@ -479,7 +489,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise { let storageAccountValues: azdata.CategoryValue[] = []; try { - this._storageAccounts = (await getAvailableStorageAccounts(this._azureAccount, subscription)).filter(sa => sa.location === this._targetServerInstance.location); + this._storageAccounts = (await getAvailableStorageAccounts(this._azureAccount, subscription)).filter(sa => sa.location === this._targetServerInstance.location && sa.resourceGroup === this._databaseBackup.resourceGroup.name); this._storageAccounts.forEach((storageAccount) => { storageAccountValues.push({ name: storageAccount.id, @@ -585,7 +595,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getSqlMigrationServiceValues(subscription: azureResource.AzureResourceSubscription, managedInstance: SqlManagedInstance): Promise { let sqlMigrationServiceValues: azdata.CategoryValue[] = []; try { - this._sqlMigrationServices = await getSqlMigrationServices(this._azureAccount, subscription, managedInstance.location); + this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription, managedInstance.location)).filter(sms => sms.location === this._targetServerInstance.location); this._sqlMigrationServices.forEach((sqlMigrationService) => { sqlMigrationServiceValues.push({ name: sqlMigrationService.id, diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index 56007de0e0..a0402f75e5 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -35,6 +35,14 @@ export class AccountsSelectionPage extends MigrationWizardPage { private createAzureAccountsDropdown(view: azdata.ModelView): azdata.FormComponent { + const azureAccountLabel = view.modelBuilder.text().withProps({ + value: constants.ACCOUNTS_SELECTION_PAGE_TITLE, + requiredIndicator: true, + CSSStyles: { + 'margin': '0px' + } + }).component(); + this._azureAccountsDropdown = view.modelBuilder.dropDown() .withProps({ width: WIZARD_INPUT_COMPONENT_WIDTH @@ -101,7 +109,11 @@ export class AccountsSelectionPage extends MigrationWizardPage { .withLayout({ flexFlow: 'column' }) - .withItems([this._azureAccountsDropdown, linkAccountButton]) + .withItems([ + azureAccountLabel, + this._azureAccountsDropdown, + linkAccountButton + ]) .component(); return { @@ -114,6 +126,7 @@ export class AccountsSelectionPage extends MigrationWizardPage { const azureTenantDropdownLabel = view.modelBuilder.text().withProps({ value: constants.AZURE_TENANT, + requiredIndicator: true, CSSStyles: { 'margin': '0px' } diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 7058bc1d5e..f637b48158 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -7,32 +7,39 @@ import * as azdata from 'azdata'; import { EOL } from 'os'; import { getStorageAccountAccessKeys } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; +import { MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import * as vscode from 'vscode'; +import { IconPathHelper } from '../constants/iconPathHelper'; +import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; export class DatabaseBackupPage extends MigrationWizardPage { private _view!: azdata.ModelView; private _networkShareContainer!: azdata.FlexContainer; - private _networkShareContainerSubscriptionDropdown!: azdata.DropDownComponent; + private _networkShareContainerSubscription!: azdata.InputBoxComponent; + private _networkShareContainerLocation!: azdata.InputBoxComponent; + private _networkShareStorageAccountResourceGroupDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent; + private _networkShareContainerStorageAccountRefreshButton!: azdata.ButtonComponent; private _windowsUserAccountText!: azdata.InputBoxComponent; private _passwordText!: azdata.InputBoxComponent; private _networkShareDatabaseConfigContainer!: azdata.FlexContainer; - private _networkShareLocations: azdata.InputBoxComponent[] = []; + private _targetDatabaseNames: azdata.InputBoxComponent[] = []; private _blobContainer!: azdata.FlexContainer; - private _blobContainerSubscriptionDropdown!: azdata.DropDownComponent; + private _blobContainerSubscription!: azdata.InputBoxComponent; private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent; private _blobContainerDatabaseConfigContainer!: azdata.FlexContainer; private _blobContainerDropdowns: azdata.DropDownComponent[] = []; private _fileShareContainer!: azdata.FlexContainer; - private _fileShareSubscriptionDropdown!: azdata.DropDownComponent; + private _fileShareSubscription!: azdata.InputBoxComponent; private _fileShareStorageAccountDropdown!: azdata.DropDownComponent; private _fileShareDatabaseConfigContainer!: azdata.FlexContainer; private _fileShareDropdowns: azdata.DropDownComponent[] = []; + private _existingDatabases: string[] = []; + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel); this.wizardPage.description = constants.DATABASE_BACKUP_PAGE_DESCRIPTION; @@ -131,16 +138,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { value: constants.DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL, requiredIndicator: true, }).component(); - this._fileShareSubscriptionDropdown = view.modelBuilder.dropDown().withProps({ + this._fileShareSubscription = view.modelBuilder.inputBox().withProps({ required: true, + enabled: false }).component(); - this._fileShareSubscriptionDropdown.onValueChanged(async (value) => { - if (value.selected) { - this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel.getSubscription(value.index); - this.migrationStateModel._databaseBackup.storageAccount = undefined!; - await this.loadFileShareStorageDropdown(); - } - }); const storageAccountLabel = view.modelBuilder.text() .withProps({ @@ -159,7 +160,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }); - const fileShareDatabaseConfigHeader = view.modelBuilder.text().withProps({ value: constants.ENTER_FILE_SHARE_INFORMATION }).component(); @@ -172,7 +172,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withItems( [ subscriptionLabel, - this._fileShareSubscriptionDropdown, + this._fileShareSubscription, storageAccountLabel, this._fileShareStorageAccountDropdown, fileShareDatabaseConfigHeader, @@ -193,17 +193,11 @@ export class DatabaseBackupPage extends MigrationWizardPage { value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL, requiredIndicator: true, }).component(); - this._blobContainerSubscriptionDropdown = view.modelBuilder.dropDown() + this._blobContainerSubscription = view.modelBuilder.inputBox() .withProps({ - required: true + required: true, + enabled: false }).component(); - this._blobContainerSubscriptionDropdown.onValueChanged(async (value) => { - if (value.selected) { - this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel.getSubscription(value.index); - this.migrationStateModel._databaseBackup.storageAccount = undefined!; - await this.loadblobStorageDropdown(); - } - }); const storageAccountLabel = view.modelBuilder.text() .withProps({ @@ -235,7 +229,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withItems( [ subscriptionLabel, - this._blobContainerSubscriptionDropdown, + this._blobContainerSubscription, storageAccountLabel, this._blobContainerStorageAccountDropdown, blobContainerDatabaseConfigHeader, @@ -251,21 +245,61 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private createNetworkShareContainer(view: azdata.ModelView): azdata.FlexContainer { - const networkShareHelpText = view.modelBuilder.text() - .withProps({ - value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, - }).component(); + const networkShareHeading = view.modelBuilder.text().withProps({ + value: constants.DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '14px', + 'font-weight': 'bold' + } + }).component(); + const networkShareHelpText = view.modelBuilder.text().withProps({ + value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + + const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({ + value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, + requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({ + placeHolder: '\\\\Servername.domainname.com\\Backupfolder', + required: true, + validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).withValidation((component) => { + if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { + if (component.value) { + if (!/(?<=\\\\)[^\\]*/.test(component.value)) { + return false; + } + } + } + return true; + }).component(); + networkLocationInputBox.onTextChanged((value) => { + this.validateFields(); + this.migrationStateModel._databaseBackup.networkShareLocation = value; + }); + const networkShareInfoBox = view.modelBuilder.infoBox().withProps({ + text: constants.DATABASE_SERVICE_ACCOUNT_INFO_TEXT, + style: 'information', + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); const windowsUserAccountLabel = view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL, requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._windowsUserAccountText = view.modelBuilder.inputBox() .withProps({ placeHolder: 'Domain\\username', required: true, - validationErrorMessage: constants.INVALID_USER_ACCOUNT + validationErrorMessage: constants.INVALID_USER_ACCOUNT, + width: WIZARD_INPUT_COMPONENT_WIDTH }) .withValidation((component) => { if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { @@ -285,98 +319,144 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._passwordText = view.modelBuilder.inputBox() .withProps({ placeHolder: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER, inputType: 'password', - required: true + required: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._passwordText.onTextChanged((value) => { this.migrationStateModel._databaseBackup.password = value; }); + const azureAccountHeader = view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HEADER, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + const azureAccountHelpText = view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); const subscriptionLabel = view.modelBuilder.text() .withProps({ - value: constants.DATABASE_BACKUP_NETWORK_SHARE_SUBSCRIPTION_LABEL, + value: constants.SUBSCRIPTION, requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - this._networkShareContainerSubscriptionDropdown = view.modelBuilder.dropDown() + this._networkShareContainerSubscription = view.modelBuilder.inputBox() .withProps({ - required: true + required: true, + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - this._networkShareContainerSubscriptionDropdown.onValueChanged(async (value) => { - if (value.selected) { - this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel.getSubscription(value.index); - this.migrationStateModel._databaseBackup.storageAccount = undefined!; - await this.loadNetworkShareStorageDropdown(); + + const locationLabel = view.modelBuilder.text() + .withProps({ + value: constants.LOCATION, + requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._networkShareContainerLocation = view.modelBuilder.inputBox() + .withProps({ + required: true, + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + + const resourceGroupLabel = view.modelBuilder.text() + .withProps({ + value: constants.RESOURCE_GROUP, + requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._networkShareStorageAccountResourceGroupDropdown = view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(e => { + if (e.selected) { + this.migrationStateModel._databaseBackup.resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); + this.loadNetworkShareStorageDropdown(); } }); const storageAccountLabel = view.modelBuilder.text() .withProps({ - value: constants.DATABASE_BACKUP_NETWORK_SHARE_NETWORK_STORAGE_ACCOUNT_LABEL, + value: constants.STORAGE_ACCOUNT, requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._networkShareContainerStorageAccountDropdown = view.modelBuilder.dropDown() .withProps({ - required: true + required: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => { if (value.selected) { this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); } }); - const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, - requiredIndicator: true + + this._networkShareContainerStorageAccountRefreshButton = view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.refresh, + iconHeight: 18, + iconWidth: 18, + height: 25 }).component(); - const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({ - placeHolder: '\\\\Servername.domainname.com\\Backupfolder', - required: true, - validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION - }).withValidation((component) => { - if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { - if (component.value) { - if (!/(?<=\\\\)[^\\]*/.test(component.value)) { - return false; - } - } + + this._networkShareContainerStorageAccountRefreshButton.onDidClick((e) => { + this.loadNetworkShareStorageDropdown(); + }); + + const storageAccountContainer = view.modelBuilder.flexContainer().component(); + + storageAccountContainer.addItem(this._networkShareContainerStorageAccountDropdown, { + flex: '0 0 auto' + }); + + storageAccountContainer.addItem(this._networkShareContainerStorageAccountRefreshButton, { + flex: '0 0 auto', + CSSStyles: { + 'margin-left': '5px' } - return true; - }).component(); - networkLocationInputBox.onTextChanged((value) => { - this.validateFields(); - this.migrationStateModel._databaseBackup.networkShareLocation = value; }); const networkShareDatabaseConfigHeader = view.modelBuilder.text().withProps({ - value: constants.ENTER_NETWORK_SHARE_INFORMATION + value: constants.ENTER_NETWORK_SHARE_INFORMATION, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._networkShareDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' + flexFlow: 'column', }).component(); const flexContainer = view.modelBuilder.flexContainer().withItems( [ - azureAccountHelpText, + networkShareHeading, networkShareHelpText, networkLocationInputBoxLabel, networkLocationInputBox, + networkShareInfoBox, windowsUserAccountLabel, this._windowsUserAccountText, passwordLabel, this._passwordText, + azureAccountHeader, + azureAccountHelpText, subscriptionLabel, - this._networkShareContainerSubscriptionDropdown, + this._networkShareContainerSubscription, + locationLabel, + this._networkShareContainerLocation, + resourceGroupLabel, + this._networkShareStorageAccountResourceGroupDropdown, storageAccountLabel, - this._networkShareContainerStorageAccountDropdown, + storageAccountContainer, networkShareDatabaseConfigHeader, this._networkShareDatabaseConfigContainer ] @@ -391,8 +471,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { public async onPageEnter(): Promise { if (this.migrationStateModel.refreshDatabaseBackupPage) { - - this._networkShareLocations = []; + this._targetDatabaseNames = []; + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { + this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); + } this._fileShareDropdowns = []; this._blobContainerDropdowns = []; this.migrationStateModel._targetDatabaseNames = []; @@ -411,11 +493,24 @@ export class DatabaseBackupPage extends MigrationWizardPage { }).component(); const targetNameNetworkInputBox = this._view.modelBuilder.inputBox().withProps({ required: true, - value: db + value: db, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).withValidation(c => { + if (this._targetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. + c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; + return false; + } + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL + c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(this.migrationStateModel._targetServerInstance.name); + return false; + } + return true; }).component(); targetNameNetworkInputBox.onTextChanged((value) => { this.migrationStateModel._targetDatabaseNames[index] = value; }); + this._targetDatabaseNames.push(targetNameNetworkInputBox); + this._networkShareDatabaseConfigContainer.addItems( [ targetNameNetworkInputBoxLabel, @@ -437,7 +532,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withProps({ }).component(); fileShareDropdown.onValueChanged((value) => { - if (value.selected) { + if (value.selected && value.selected !== constants.NO_FILESHARES_FOUND) { this.validateFields(); this.migrationStateModel._databaseBackup.fileShares[index] = this.migrationStateModel.getFileShare(value.index); } @@ -467,7 +562,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withProps({ }).component(); blobContainerDropdown.onValueChanged((value) => { - if (value.selected) { + if (value.selected && value.selected !== constants.NO_BLOBCONTAINERS_FOUND) { this.validateFields(); this.migrationStateModel._databaseBackup.blobContainers[index] = this.migrationStateModel.getBlobContainer(value.index); } @@ -496,7 +591,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { switch (this.migrationStateModel._databaseBackup.networkContainerType) { case NetworkContainerType.NETWORK_SHARE: - if ((this._networkShareContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { + if (this._networkShareContainerSubscription.value === constants.NO_SUBSCRIPTIONS_FOUND) { errors.push(constants.INVALID_SUBSCRIPTION_ERROR); } if ((this._networkShareContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { @@ -504,9 +599,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { } break; case NetworkContainerType.BLOB_CONTAINER: - if ((this._blobContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { - errors.push(constants.INVALID_SUBSCRIPTION_ERROR); - } if ((this._blobContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); } @@ -518,9 +610,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { } break; case NetworkContainerType.FILE_SHARE: - if ((this._fileShareSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { - errors.push(constants.INVALID_SUBSCRIPTION_ERROR); - } if ((this._fileShareStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); } @@ -545,10 +634,13 @@ export class DatabaseBackupPage extends MigrationWizardPage { } public async onPageLeave(): Promise { - this.migrationStateModel._databaseBackup.storageKey = (await getStorageAccountAccessKeys(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount)).keyName1; - this.wizard.registerNavigationValidator((pageChangeInfo) => { - return true; - }); + try { + this.migrationStateModel._databaseBackup.storageKey = (await getStorageAccountAccessKeys(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount)).keyName1; + } finally { + this.wizard.registerNavigationValidator((pageChangeInfo) => { + return true; + }); + } } protected async handleStateChange(e: StateChangeEvent): Promise { @@ -559,30 +651,25 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._fileShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.FILE_SHARE) ? 'inline' : 'none' }); this._blobContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none' }); this._networkShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' }); - this._networkShareLocations.forEach((inputBox) => { - inputBox.updateProperties({ - required: containerType === NetworkContainerType.NETWORK_SHARE - }); - }); this._windowsUserAccountText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); this._passwordText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); - this._networkShareLocations.forEach((inputBox) => { + this._targetDatabaseNames.forEach((inputBox) => { inputBox.validate(); }); this._windowsUserAccountText.validate(); this._passwordText.validate(); - this._networkShareContainerSubscriptionDropdown.validate(); + this._networkShareContainerSubscription.validate(); this._networkShareContainerStorageAccountDropdown.validate(); - this._blobContainerSubscriptionDropdown.validate(); + this._blobContainerSubscription.validate(); this._blobContainerStorageAccountDropdown.validate(); this._blobContainerDropdowns.forEach((dropdown) => { dropdown.validate(); }); - this._fileShareSubscriptionDropdown.validate(); + this._fileShareSubscription.validate(); this._fileShareStorageAccountDropdown.validate(); this._fileShareDropdowns.forEach(dropdown => { dropdown.validate(); @@ -592,19 +679,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { private validateFields(): void { - this._networkShareLocations.forEach((inputBox) => { + this._targetDatabaseNames.forEach((inputBox) => { inputBox.validate(); }); this._windowsUserAccountText.validate(); this._passwordText.validate(); - this._networkShareContainerSubscriptionDropdown.validate(); + this._networkShareContainerSubscription.validate(); this._networkShareContainerStorageAccountDropdown.validate(); - this._blobContainerSubscriptionDropdown.validate(); + this._blobContainerSubscription.validate(); this._blobContainerStorageAccountDropdown.validate(); this._blobContainerDropdowns.forEach((dropdown) => { dropdown.validate(); }); - this._fileShareSubscriptionDropdown.validate(); + this._fileShareSubscription.validate(); this._fileShareStorageAccountDropdown.validate(); this._fileShareDropdowns.forEach((dropdown) => { dropdown.validate(); @@ -612,35 +699,36 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private async getSubscriptionValues(): Promise { - if (!this.migrationStateModel._databaseBackup.subscription) { - this._networkShareContainerSubscriptionDropdown.loading = true; - this._fileShareSubscriptionDropdown.loading = true; - this._blobContainerSubscriptionDropdown.loading = true; - try { - const subscriptionDropdownValues = await this.migrationStateModel.getSubscriptionsDropdownValues(); - this._fileShareSubscriptionDropdown.values = subscriptionDropdownValues; - this._networkShareContainerSubscriptionDropdown.values = subscriptionDropdownValues; - this._blobContainerSubscriptionDropdown.values = subscriptionDropdownValues; - } catch (error) { - console.log(error); - } finally { - this._networkShareContainerSubscriptionDropdown.loading = false; - this._fileShareSubscriptionDropdown.loading = false; - this._blobContainerSubscriptionDropdown.loading = false; - } + this._fileShareSubscription.value = this.migrationStateModel._targetSubscription.name; + this._networkShareContainerSubscription.value = this.migrationStateModel._targetSubscription.name; + this._networkShareContainerLocation.value = await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); + this._blobContainerSubscription.value = this.migrationStateModel._targetSubscription.name; + this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel._targetSubscription; + this.loadNetworkStorageResourceGroup(); + this.loadFileShareStorageDropdown(); + this.loadblobStorageDropdown(); + } + + private async loadNetworkStorageResourceGroup(): Promise { + this._networkShareStorageAccountResourceGroupDropdown.loading = true; + try { + this._networkShareStorageAccountResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); + } catch (error) { + console.log(error); + } finally { + this._networkShareStorageAccountResourceGroupDropdown.loading = false; + this.loadNetworkShareStorageDropdown(); } } private async loadNetworkShareStorageDropdown(): Promise { - if (!this.migrationStateModel._databaseBackup.storageAccount) { - this._networkShareContainerStorageAccountDropdown.loading = true; - try { - this._networkShareContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription); - } catch (error) { - console.log(error); - } finally { - this._networkShareContainerStorageAccountDropdown.loading = false; - } + this._networkShareContainerStorageAccountDropdown.loading = true; + try { + this._networkShareContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription); + } catch (error) { + console.log(error); + } finally { + this._networkShareContainerStorageAccountDropdown.loading = false; } } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 3762b09325..f87dd2e746 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -410,7 +410,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { migrationServiceTitle, createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), createInformationRow(this._view, constants.RESOURCE_GROUP, migrationService.properties.resourceGroup), - createInformationRow(this._view, constants.LOCATION, migrationService.properties.location), + createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(migrationService.properties.location)), connectionLabelContainer, connectionStatusLoader, authenticationKeysLabel, diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index b302f04ab3..34d30aa698 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -11,6 +11,7 @@ import * as constants from '../constants/strings'; import * as vscode from 'vscode'; import { EOL } from 'os'; import { IconPath, IconPathHelper } from '../constants/iconPathHelper'; +import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; export interface Product { type: MigrationTargetType; @@ -60,9 +61,12 @@ export class SKURecommendationPage extends MigrationWizardPage { const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.SUBSCRIPTION + value: constants.SUBSCRIPTION, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().component(); this._managedInstanceSubscriptionDropdown.onValueChanged((e) => { if (e.selected) { this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index); @@ -72,13 +76,17 @@ export class SKURecommendationPage extends MigrationWizardPage { } }); this._resourceDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.MANAGED_INSTANCE + value: constants.MANAGED_INSTANCE, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); const azureLocationLabel = view.modelBuilder.text().withProps({ - value: constants.LOCATION + value: constants.LOCATION, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._azureLocationDropdown = view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - this._azureLocationDropdown = view.modelBuilder.dropDown().component(); this._azureLocationDropdown.onValueChanged((e) => { if (e.selected) { this.migrationStateModel._location = this.migrationStateModel.getLocation(e.index); @@ -86,14 +94,18 @@ export class SKURecommendationPage extends MigrationWizardPage { } }); this._resourceDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.MANAGED_INSTANCE + value: constants.MANAGED_INSTANCE, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); const azureResourceGroupLabel = view.modelBuilder.text().withProps({ - value: constants.RESOURCE_GROUP + value: constants.RESOURCE_GROUP, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._azureResourceGroupDropdown = view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - this._azureResourceGroupDropdown = view.modelBuilder.dropDown().component(); this._azureResourceGroupDropdown.onValueChanged((e) => { if (e.selected) { this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); @@ -101,10 +113,13 @@ export class SKURecommendationPage extends MigrationWizardPage { } }); this._resourceDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.MANAGED_INSTANCE + value: constants.MANAGED_INSTANCE, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - this._resourceDropdown = view.modelBuilder.dropDown().component(); + this._resourceDropdown = view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); this._resourceDropdown.onValueChanged((e) => { if (e.selected && e.selected !== constants.NO_MANAGED_INSTANCE_FOUND && @@ -272,9 +287,11 @@ export class SKURecommendationPage extends MigrationWizardPage { private changeTargetType(newTargetType: string) { if (newTargetType === MigrationTargetType.SQLMI) { + this.migrationStateModel._targetType = MigrationTargetType.SQLMI; this._azureSubscriptionText.value = constants.SELECT_AZURE_MI; this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs; } else { + this.migrationStateModel._targetType = MigrationTargetType.SQLVM; this._azureSubscriptionText.value = constants.SELECT_AZURE_VM; this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs; } diff --git a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts index 905b84fa2b..780897d1a8 100644 --- a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts +++ b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts @@ -8,7 +8,7 @@ import * as os from 'os'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationSourceAuthenticationType, MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; -import { createLabelTextComponent, createHeadingTextComponent } from './wizardController'; +import { createLabelTextComponent, createHeadingTextComponent, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; export class SqlSourceConfigurationPage extends MigrationWizardPage { private _view!: azdata.ModelView; @@ -70,29 +70,46 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { const enterYourCredText = createLabelTextComponent( this._view, - constants.ENTER_YOUR_SQL_CREDS(connectionProfile.serverName), + constants.ENTER_YOUR_SQL_CREDS, { - 'width': '400px' + 'width': '600px' } ); + const serverLabel = this._view.modelBuilder.text().withProps({ + value: constants.SERVER, + requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + + const server = this._view.modelBuilder.inputBox().withProps({ + value: connectionProfile.serverName, + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + const authenticationTypeLable = this._view.modelBuilder.text().withProps({ - value: constants.AUTHENTICATION_TYPE + value: constants.AUTHENTICATION_TYPE, + requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); const authenticationTypeInput = this._view.modelBuilder.inputBox().withProps({ value: this.migrationStateModel._authenticationType === MigrationSourceAuthenticationType.Sql ? 'SQL Login' : 'Windows Authentication', - enabled: false + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); const usernameLable = this._view.modelBuilder.text().withProps({ value: constants.USERNAME, - requiredIndicator: true + requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._usernameInput = this._view.modelBuilder.inputBox().withProps({ value: username, required: true, - enabled: false + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._usernameInput.onTextChanged(value => { this.migrationStateModel._sqlServerUsername = value; @@ -100,12 +117,14 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { const passwordLabel = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, - requiredIndicator: true + requiredIndicator: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._password = this._view.modelBuilder.inputBox().withProps({ value: (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password, required: true, - inputType: 'password' + inputType: 'password', + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._password.onTextChanged(value => { this.migrationStateModel._sqlServerPassword = value; @@ -115,6 +134,8 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { [ sourceCredText, enterYourCredText, + serverLabel, + server, authenticationTypeLable, authenticationTypeInput, usernameLable, diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index e816d83585..e97efd6e06 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -17,7 +17,7 @@ import { SummaryPage } from './summaryPage'; import { MigrationModePage } from './migrationModePage'; import { SqlSourceConfigurationPage } from './sqlSourceConfigurationPage'; -export const WIZARD_INPUT_COMPONENT_WIDTH = '400px'; +export const WIZARD_INPUT_COMPONENT_WIDTH = '600px'; export class WizardController { constructor(private readonly extensionContext: vscode.ExtensionContext) {