Migration Private preview 1 fixes 2 (#14898)

* Removing canary host

* Rebranding extension name to Azure SQL Migration

* stopping instance table overflow in assessment dialog

* Added info message for details copied

* Limiting storage account and DMS to the same subscription as target

* making accounts page look like figma mockups

* converting error messages to warnings in cutover dialog

* making source config page look like figma mockups

* adding more filters for storage account and dms

* Adding validations for target database names.

* Fixing branding in other strings

* Adding types for SQL Managed Instance
This commit is contained in:
Aasim Khan
2021-03-29 17:43:19 -07:00
committed by GitHub
parent 69361b5c97
commit a231d2aa82
20 changed files with 419 additions and 244 deletions

View File

@@ -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<azurecore.IExtension> {
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<SqlManagedInstance[]> {
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<azureResource.ManagedDatabase[]> {
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<SqlServer[]> {
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<SqlMigrationService> {
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<SqlMigrationService[]> {
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<SqlMigrationService> {
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<SqlMigrationServiceAuthenticationKeys> {
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<IntegrationRuntimeMonitoringData> {
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<StartDatabaseMigrationResponse> {
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<DatabaseMigration> {
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<DatabaseMigration> {
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<DatabaseMigration[]> {
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<any> {
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<void> {
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<string> {
const api = await getAzureCoreAPI();
return await api.getRegionDisplayName(location);
}
type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription | SqlMigrationService;

View File

@@ -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

View File

@@ -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();

View File

@@ -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<azdata.FlexContainer> {
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<void> {
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<void> {
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<void> {
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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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<string> {
return getLocationDisplayName(location);
}
public async getAzureResourceGroupDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
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<string[]> {
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<azdata.CategoryValue[]> {
let virtualMachineValues: azdata.CategoryValue[] = [];
try {
@@ -479,7 +489,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
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<azdata.CategoryValue[]> {
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,

View File

@@ -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'
}

View File

@@ -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<void> {
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 ((<azdata.CategoryValue>this._networkShareContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
if (this._networkShareContainerSubscription.value === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>this._networkShareContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
@@ -504,9 +599,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
break;
case NetworkContainerType.BLOB_CONTAINER:
if ((<azdata.CategoryValue>this._blobContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>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 ((<azdata.CategoryValue>this._fileShareSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>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<void> {
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<void> {
@@ -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<void> {
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<void> {
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<void> {
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;
}
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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) {