Fixing bugs for migration extension private preview 1. (#14872)

* Fixing Database backup page target layout

* Filtering out Azure sql db issues from assessment results
Correcting the database count for issued databases in sku rec page.

* Adding copy migration details button to migration status

* Adding start migration button to toolbar

* Fixing a syntax error in package.json

* Adding rg and location to target selection page
Filtering storage account by target location.

* Fixing dashboard title to azure sql migration

* Not making assessment targets selected by default.

* Adding tooltip for database and instance table items.

* Fixing duplicate task widget

* Some fixes mentioned in the PR
Localizing button text
renaming  a var
changing null to undefined.

* Adding enum for Migration target types

* Fixing a critical multi db migration bug because of unhandled race condition

* Adding Azure location api to azure core

* Adding source database info in status
This commit is contained in:
Aasim Khan
2021-03-26 10:32:28 -07:00
committed by GitHub
parent e0f24cc268
commit 4d78aefe57
15 changed files with 336 additions and 198 deletions

View File

@@ -26,6 +26,20 @@ export async function getSubscriptions(account: azdata.Account): Promise<Subscri
return subscriptions.subscriptions;
}
export async function getLocations(account: azdata.Account, subscription: Subscription): Promise<azureResource.AzureLocation[]> {
const api = await getAzureCoreAPI();
const response = await api.getLocations(account, subscription, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
sortResourceArrayByName(response.locations);
const supportedLocations = ['eastus2', 'eastus2euap'];
const filteredLocations = response.locations.filter(loc => {
return supportedLocations.includes(loc.name);
});
return filteredLocations;
}
export type AzureProduct = azureResource.AzureGraphResource;
export async function getResourceGroups(account: azdata.Account, subscription: Subscription): Promise<azureResource.AzureResourceResourceGroup[]> {
@@ -65,9 +79,9 @@ export type SqlVMServer = {
tenantId: string,
subscriptionId: string
};
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> {
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<SqlVMServer[]> {
const api = await getAzureCoreAPI();
const path = `/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`;
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroup.name}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());

View File

@@ -87,10 +87,12 @@ export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migrat
export const DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL = localize('sql.migration.database.backup.email.notification.label', "Email notifications");
export const DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL = localize('sql.migration.database.backup.email.notification.checkbox.label', "Notify me when migration is complete");
export const NO_SUBSCRIPTIONS_FOUND = localize('sql.migration.no.subscription.found', "No subscription found");
export const NO_LOCATION_FOUND = localize('sql.migration.no.location.found', "No location found");
export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage account found");
export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found");
export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found");
export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "Please select a valid subscription to proceed.");
export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "Please select a valid location to proceed.");
export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "Please select a valid storage account to proceed.");
export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare.error', "Please select a valid file share to proceed.");
export const INVALID_BLOBCONTAINER_ERROR = localize('sql.migration.invalid.blobContainer.error', "Please select a valid blob container to proceed.");
@@ -108,7 +110,7 @@ export function TARGET_FILE_SHARE(dbName: string): string {
export function TARGET_BLOB_CONTAINER(dbName: string): string {
return localize('sql.migration.blob.container', "Select the container that contains the backup files for {0}", dbName);
}
export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter network share path information for selected databases");
export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter target names for selected databases");
export const ENTER_BLOB_CONTAINER_INFORMATION = localize('sql.migration.blob.container.information', "Enter the target name and select the blob container location for selected databases");
export const ENTER_FILE_SHARE_INFORMATION = localize('sql.migration.enter.file.share.information', "Enter the target name and select the file share location of selected databases");
@@ -152,7 +154,7 @@ export function SERVICE_NOT_READY(serviceName: string): string {
export function SERVICE_READY(serviceName: string, host: string): string {
return localize('sql.migration.service.ready', "Azure Data Migration Service '{0}' is connected to self-hosted Integration Runtime running on the node - {1}", serviceName, host);
}
export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource Groups found");
export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource groups found");
export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "Please select a valid resource group to proceed.");
export const INVALID_REGION_ERROR = localize('sql.migration.invalid.region.error', "Please select a valid region to proceed.");
export const INVALID_SERVICE_NAME_ERROR = localize('sql.migration.invalid.service.name.error', "Please enter a valid name for the Migration Service.");
@@ -230,6 +232,7 @@ export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP
//Migration cutover dialog
export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database");
export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
export const TARGET_SERVER = localize('sql.migration.target.server', "Target server");
@@ -255,7 +258,7 @@ export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) {
return localize('sql.migration.active.backup.files.multiple.items', "Active Backup files ({0} items)", fileCount);
}
}
export const COPY_MIGRATION_DETAILS = localize('sql.migration.copy.migration.details', "Copy Migration Details");
//Migration status dialog
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");

View File

@@ -166,14 +166,14 @@ export class SqlDatabaseTree {
{
displayName: constants.INSTANCE,
valueType: azdata.DeclarativeDataType.component,
width: 150,
width: 130,
isReadOnly: true,
headerCssStyles: headerLeft
},
{
displayName: constants.WARNINGS,
valueType: azdata.DeclarativeDataType.string,
width: 50,
width: 30,
isReadOnly: true,
headerCssStyles: headerRight
}
@@ -595,9 +595,9 @@ export class SqlDatabaseTree {
public selectedDbs(): string[] {
let result: string[] = [];
this._databaseTable.dataValues?.forEach((arr) => {
this._databaseTable.dataValues?.forEach((arr, index) => {
if (arr[0].value === true) {
result.push(arr[1].value.toString());
result.push(this._dbNames[index]);
}
});
return result;
@@ -615,8 +615,6 @@ export class SqlDatabaseTree {
);
});
this._assessmentResultsTable.dataValues = assessmentResults;
this._selectedIssue = this._activeIssues[0];
this.refreshAssessmentDetails();
}
public refreshAssessmentDetails(): void {
@@ -734,14 +732,8 @@ export class SqlDatabaseTree {
);
});
}
this._dbName.value = this._serverName;
this._instanceTable.dataValues = instanceTableValues;
this._databaseTable.dataValues = databaseTableValues;
if (this._targetType === MigrationTargetType.SQLMI) {
this._activeIssues = this._model._assessmentResults.issues;
this._selectedIssue = this._model._assessmentResults?.issues[0];
this.refreshResults();
}
}
private createIconTextCell(icon: IconPath, text: string): azdata.FlexContainer {
@@ -755,8 +747,10 @@ export class SqlDatabaseTree {
}).component();
const textComponent = this._view.modelBuilder.text().withProps({
value: text,
title: text,
CSSStyles: {
'margin': '0px'
'margin': '0px',
'width': '110px'
}
}).component();

View File

@@ -10,6 +10,8 @@ import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as loc from '../../constants/strings';
import { getSqlServerName } from '../../api/utils';
import { EOL } from 'os';
import * as vscode from 'vscode';
export class MigrationCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -20,9 +22,11 @@ export class MigrationCutoverDialog {
private _refreshButton!: azdata.ButtonComponent;
private _cancelButton!: azdata.ButtonComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _copyDatabaseMigrationDetails!: azdata.ButtonComponent;
private _serverName!: azdata.TextComponent;
private _serverVersion!: azdata.TextComponent;
private _sourceDatabase!: azdata.TextComponent;
private _targetServer!: azdata.TextComponent;
private _targetVersion!: azdata.TextComponent;
private _migrationStatus!: azdata.TextComponent;
@@ -46,9 +50,11 @@ export class MigrationCutoverDialog {
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
const sourceDatabase = this.createInfoField(loc.SOURCE_DATABASE, '');
const sourceDetails = this.createInfoField(loc.SOURCE_SERVER, '');
const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, '');
this._sourceDatabase = sourceDatabase.text;
this._serverName = sourceDetails.text;
this._serverVersion = sourceVersion.text;
@@ -56,6 +62,11 @@ export class MigrationCutoverDialog {
flexFlow: 'column'
}).component();
flexServer.addItem(sourceDatabase.flexContainer, {
CSSStyles: {
'width': '150px'
}
});
flexServer.addItem(sourceDetails.flexContainer, {
CSSStyles: {
'width': '150px'
@@ -336,6 +347,27 @@ export class MigrationCutoverDialog {
}
});
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.copy,
iconHeight: '16px',
iconWidth: '16px',
label: loc.COPY_MIGRATION_DETAILS,
height: '55px',
width: '100px'
}).component();
this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
await this.refreshStatus();
vscode.env.clipboard.writeText(JSON.stringify(this._model.migrationStatus, undefined, 2));
});
header.addItem(this._copyDatabaseMigrationDetails, {
flex: '0',
CSSStyles: {
'width': '100px'
}
});
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
height: '55px'
@@ -344,6 +376,7 @@ export class MigrationCutoverDialog {
header.addItem(this._refreshLoader, {
flex: '0'
});
return header;
}
@@ -363,6 +396,7 @@ export class MigrationCutoverDialog {
};
const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId);
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName;
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetServerName = this._model._migration.targetManagedInstance.name;
@@ -402,6 +436,7 @@ export class MigrationCutoverDialog {
}
});
this._sourceDatabase.value = sourceDatabaseName;
this._serverName.value = sqlServerName;
this._serverVersion.value = `${sqlServerVersion}
${sqlServerInfo.serverVersion}`;
@@ -414,7 +449,7 @@ export class MigrationCutoverDialog {
this._lastAppliedLSN.value = lastAppliedSSN!;
this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
this._lastAppliedBackupTakenOn.value = new Date(lastAppliedBackupFileTakenOn!).toLocaleString();
this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? new Date(lastAppliedBackupFileTakenOn).toLocaleString() : '';
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);

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 } from '../api/azure';
import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups } from '../api/azure';
import { SKURecommendations } from './externalContract';
import * as constants from '../constants/strings';
import { MigrationLocalStorage } from './migrationLocalStorage';
@@ -34,8 +34,9 @@ export enum State {
}
export enum MigrationTargetType {
SQLVM = 'sqlvm',
SQLMI = 'sqlmi'
SQLVM = 'AzureSqlVirtualMachine',
SQLMI = 'AzureSqlManagedInstance',
SQLDB = 'AzureSqlDatabase'
}
export enum MigrationSourceAuthenticationType {
@@ -63,7 +64,7 @@ export interface NetworkShare {
export interface DatabaseBackupModel {
migrationCutover: MigrationCutover;
networkContainerType: NetworkContainerType;
networkShareLocations: string[];
networkShareLocation: string;
windowsUser: string;
password: string;
subscription: azureResource.AzureResourceSubscription;
@@ -100,6 +101,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _subscriptions!: azureResource.AzureResourceSubscription[];
public _targetSubscription!: azureResource.AzureResourceSubscription;
public _locations!: azureResource.AzureLocation[];
public _location!: azureResource.AzureLocation;
public _resourceGroups!: azureResource.AzureResourceResourceGroup[];
public _resourceGroup!: azureResource.AzureResourceResourceGroup;
public _targetManagedInstances!: SqlManagedInstance[];
public _targetSqlVirtualMachines!: SqlVMServer[];
public _targetServerInstance!: SqlManagedInstance;
@@ -173,11 +178,13 @@ export class MigrationStateModel implements Model, vscode.Disposable {
});
assessmentResults?.items.forEach((item) => {
const dbIndex = serverDatabases.indexOf(item.databaseName);
if (dbIndex === -1) {
serverLevelAssessments.push(item);
} else {
databaseLevelAssessments[dbIndex].issues.push(item);
if (item.appliesToMigrationTargetPlatform === MigrationTargetType.SQLMI) {
const dbIndex = serverDatabases.indexOf(item.databaseName);
if (dbIndex === -1) {
serverLevelAssessments.push(item);
} else {
databaseLevelAssessments[dbIndex].issues.push(item);
}
}
});
@@ -313,10 +320,88 @@ export class MigrationStateModel implements Model, vscode.Disposable {
return this._subscriptions[index];
}
public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
public async getAzureLocationDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
let locationValues: azdata.CategoryValue[] = [];
try {
this._locations = await getLocations(this._azureAccount, subscription);
this._locations.forEach((loc) => {
locationValues.push({
name: loc.name,
displayName: loc.displayName
});
});
if (locationValues.length === 0) {
locationValues = [
{
displayName: constants.INVALID_LOCATION_ERROR,
name: ''
}
];
}
} catch (e) {
console.log(e);
locationValues = [
{
displayName: constants.INVALID_LOCATION_ERROR,
name: ''
}
];
}
return locationValues;
}
public getLocation(index: number): azureResource.AzureLocation {
return this._locations[index];
}
public async getAzureResourceGroupDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
let resourceGroupValues: azdata.CategoryValue[] = [];
try {
this._resourceGroups = await getResourceGroups(this._azureAccount, subscription);
this._resourceGroups.forEach((rg) => {
resourceGroupValues.push({
name: rg.id,
displayName: rg.name
});
});
if (resourceGroupValues.length === 0) {
resourceGroupValues = [
{
displayName: constants.RESOURCE_GROUP_NOT_FOUND,
name: ''
}
];
}
} catch (e) {
console.log(e);
resourceGroupValues = [
{
displayName: constants.RESOURCE_GROUP_NOT_FOUND,
name: ''
}
];
}
return resourceGroupValues;
}
public getAzureResourceGroup(index: number): azureResource.AzureResourceResourceGroup {
return this._resourceGroups[index];
}
public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
let managedInstanceValues: azdata.CategoryValue[] = [];
try {
this._targetManagedInstances = await getAvailableManagedInstanceProducts(this._azureAccount, subscription);
this._targetManagedInstances = (await getAvailableManagedInstanceProducts(this._azureAccount, subscription)).filter((mi) => {
if (mi.location === location.name && mi.resourceGroup?.toLocaleLowerCase() === resourceGroup.name.toLowerCase()) {
return true;
}
return false;
});
this._targetManagedInstances.forEach((managedInstance) => {
managedInstanceValues.push({
name: managedInstance.id,
@@ -348,13 +433,19 @@ export class MigrationStateModel implements Model, vscode.Disposable {
return this._targetManagedInstances[index];
}
public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
let virtualMachineValues: azdata.CategoryValue[] = [];
try {
this._targetSqlVirtualMachines = await getAvailableSqlVMs(this._azureAccount, subscription);
virtualMachineValues = this._targetSqlVirtualMachines.filter((virtualMachine) => {
return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms.
}).map((virtualMachine) => {
this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription, resourceGroup)).filter((virtualMachine) => {
if (virtualMachine.location === location.name) {
if (virtualMachine.properties.sqlImageOffer) {
return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms.
}
return true; // Returning all VMs that don't have this property as we don't want to accidentally skip valid vms.
}
return false;
});
virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => {
return {
name: virtualMachine.id,
displayName: `${virtualMachine.name}`
@@ -388,7 +479,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
let storageAccountValues: azdata.CategoryValue[] = [];
try {
this._storageAccounts = await getAvailableStorageAccounts(this._azureAccount, subscription);
this._storageAccounts = (await getAvailableStorageAccounts(this._azureAccount, subscription)).filter(sa => sa.location === this._targetServerInstance.location);
this._storageAccounts.forEach((storageAccount) => {
storageAccountValues.push({
name: storageAccount.id,
@@ -564,17 +655,16 @@ export class MigrationStateModel implements Model, vscode.Disposable {
}
};
this._migrationDbs.forEach(async (db, index) => {
requestBody.properties.sourceDatabaseName = db;
for (let i = 0; i < this._migrationDbs.length; i++) {
requestBody.properties.sourceDatabaseName = this._migrationDbs[i];
try {
requestBody.properties.backupConfiguration.sourceLocation.fileShare!.path = this._databaseBackup.networkShareLocations[index];
requestBody.properties.backupConfiguration.sourceLocation.fileShare!.path = this._databaseBackup.networkShareLocation;
const response = await startDatabaseMigration(
this._azureAccount,
this._targetSubscription,
this._sqlMigrationService?.properties.location!,
this._targetServerInstance,
this._targetDatabaseNames[index],
this._targetDatabaseNames[i],
requestBody
);
if (response.status === 201) {
@@ -586,14 +676,13 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._targetSubscription,
this._sqlMigrationService
);
vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', db, this._targetServerInstance.name, this._targetDatabaseNames[index]));
vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._migrationDbs[i], this._targetServerInstance.name, this._targetDatabaseNames[i]));
}
} catch (e) {
console.log(e);
vscode.window.showInformationMessage(e);
}
});
}
}
}

View File

@@ -332,12 +332,32 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index);
}
});
const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL,
requiredIndicator: true
}).component();
const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({
placeHolder: '\\\\Servername.domainname.com\\Backupfolder',
required: true,
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION
}).withValidation((component) => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (component.value) {
if (!/(?<=\\\\)[^\\]*/.test(component.value)) {
return false;
}
}
}
return true;
}).component();
networkLocationInputBox.onTextChanged((value) => {
this.validateFields();
this.migrationStateModel._databaseBackup.networkShareLocation = value;
});
const networkShareDatabaseConfigHeader = view.modelBuilder.text().withProps({
value: constants.ENTER_NETWORK_SHARE_INFORMATION
}).component();
this._networkShareDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
@@ -347,14 +367,16 @@ export class DatabaseBackupPage extends MigrationWizardPage {
[
azureAccountHelpText,
networkShareHelpText,
subscriptionLabel,
this._networkShareContainerSubscriptionDropdown,
storageAccountLabel,
this._networkShareContainerStorageAccountDropdown,
networkLocationInputBoxLabel,
networkLocationInputBox,
windowsUserAccountLabel,
this._windowsUserAccountText,
passwordLabel,
this._passwordText,
subscriptionLabel,
this._networkShareContainerSubscriptionDropdown,
storageAccountLabel,
this._networkShareContainerStorageAccountDropdown,
networkShareDatabaseConfigHeader,
this._networkShareDatabaseConfigContainer
]
@@ -374,7 +396,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._fileShareDropdowns = [];
this._blobContainerDropdowns = [];
this.migrationStateModel._targetDatabaseNames = [];
this.migrationStateModel._databaseBackup.networkShareLocations = [];
this.migrationStateModel._databaseBackup.fileShares = [];
this.migrationStateModel._databaseBackup.blobContainers = [];
this._networkShareDatabaseConfigContainer.clearItems();
@@ -389,42 +410,16 @@ export class DatabaseBackupPage extends MigrationWizardPage {
requiredIndicator: true
}).component();
const targetNameNetworkInputBox = this._view.modelBuilder.inputBox().withProps({
required: true
required: true,
value: db
}).component();
targetNameNetworkInputBox.onTextChanged((value) => {
this.migrationStateModel._targetDatabaseNames[index] = value;
});
const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({
value: constants.TARGET_NETWORK_SHARE_LOCATION(db),
requiredIndicator: true
}).component();
const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({
placeHolder: '\\\\Servername.domainname.com\\Backupfolder',
required: true,
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION
}).withValidation((component) => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (component.value) {
if (!/(?<=\\\\)[^\\]*/.test(component.value)) {
return false;
}
}
}
return true;
}).component();
networkLocationInputBox.onTextChanged((value) => {
this.validateFields();
this.migrationStateModel._databaseBackup.networkShareLocations[index] = value;
});
this.migrationStateModel._databaseBackup.networkShareLocations.push(undefined!);
this._networkShareLocations.push(networkLocationInputBox);
this._networkShareDatabaseConfigContainer.addItems(
[
targetNameNetworkInputBoxLabel,
targetNameNetworkInputBox,
networkLocationInputBoxLabel,
networkLocationInputBox
targetNameNetworkInputBox
]
);

View File

@@ -26,6 +26,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
private _chooseTargetComponent!: azdata.DivContainer;
private _azureSubscriptionText!: azdata.TextComponent;
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
private _azureLocationDropdown!: azdata.DropDownComponent;
private _azureResourceGroupDropdown!: azdata.DropDownComponent;
private _resourceDropdownLabel!: azdata.TextComponent;
private _resourceDropdown!: azdata.DropDownComponent;
private _rbg!: azdata.RadioCardGroupComponent;
@@ -66,6 +68,35 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index);
this.migrationStateModel._targetServerInstance = undefined!;
this.migrationStateModel._sqlMigrationService = undefined!;
this.populateLocationAndResourceGroupDropdown();
}
});
this._resourceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE
}).component();
const azureLocationLabel = view.modelBuilder.text().withProps({
value: constants.LOCATION
}).component();
this._azureLocationDropdown = view.modelBuilder.dropDown().component();
this._azureLocationDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._location = this.migrationStateModel.getLocation(e.index);
this.populateResourceInstanceDropdown();
}
});
this._resourceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE
}).component();
const azureResourceGroupLabel = view.modelBuilder.text().withProps({
value: constants.RESOURCE_GROUP
}).component();
this._azureResourceGroupDropdown = view.modelBuilder.dropDown().component();
this._azureResourceGroupDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index);
this.populateResourceInstanceDropdown();
}
});
@@ -91,6 +122,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
[
managedInstanceSubscriptionDropdownLabel,
this._managedInstanceSubscriptionDropdown,
azureLocationLabel,
this._azureLocationDropdown,
azureResourceGroupLabel,
this._azureResourceGroupDropdown,
this._resourceDropdownLabel,
this._resourceDropdown
]
@@ -284,16 +319,30 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}
public async populateLocationAndResourceGroupDropdown(): Promise<void> {
this._azureResourceGroupDropdown.loading = true;
this._azureLocationDropdown.loading = true;
try {
this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription);
this._azureLocationDropdown.values = await this.migrationStateModel.getAzureLocationDropdownValues(this.migrationStateModel._targetSubscription);
} catch (e) {
console.log(e);
} finally {
this._azureResourceGroupDropdown.loading = false;
this._azureLocationDropdown.loading = false;
}
}
private async populateResourceInstanceDropdown(): Promise<void> {
this._resourceDropdown.loading = true;
try {
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) {
this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription);
this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup);
} else {
this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
this._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription);
this._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup);
}
} catch (e) {
console.log(e);
@@ -322,6 +371,13 @@ export class SKURecommendationPage extends MigrationWizardPage {
if ((<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>this._azureLocationDropdown.value).displayName === constants.NO_LOCATION_FOUND) {
errors.push(constants.INVALID_LOCATION_ERROR);
}
if ((<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
const resourceDropdownValue = (<azdata.CategoryValue>this._resourceDropdown.value).displayName;
if (resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) {
errors.push(constants.NO_MANAGED_INSTANCE_FOUND);
@@ -377,8 +433,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
if (this.migrationStateModel._assessmentResults) {
const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments.length;
const dbWithIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length > 0).length;
const miCardText = `${dbWithIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`;
const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length === 0).length;
const miCardText = `${dbWithoutIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`;
this._rbg.cards[0].descriptions[1].textValue = miCardText;
const vmCardText = `${dbCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._vmDbs.length} selected)`;

View File

@@ -72,6 +72,7 @@ export class SummaryPage extends MigrationWizardPage {
flexContainer.addItems(
[
createInformationRow(this._view, constants.TYPE, constants.NETWORK_SHARE),
createInformationRow(this._view, constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, this.migrationStateModel._databaseBackup.networkShareLocation),
createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.windowsUser),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.storageAccount.name),
@@ -80,7 +81,6 @@ export class SummaryPage extends MigrationWizardPage {
);
this.migrationStateModel._migrationDbs.forEach((db, index) => {
flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NAME_FOR_DATABASE(db), this.migrationStateModel._targetDatabaseNames[index]));
flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NETWORK_SHARE_LOCATION(db), this.migrationStateModel._databaseBackup.networkShareLocations[index]));
});
break;
case NetworkContainerType.FILE_SHARE:

View File

@@ -1,111 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
export class TempTargetSelectionPage extends MigrationWizardPage {
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
private _managedInstanceDropdown!: azdata.DropDownComponent;
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.TARGET_SELECTION_PAGE_TITLE), migrationStateModel);
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text()
.withProps({
value: constants.SUBSCRIPTION
}).component();
this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown()
.withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._managedInstanceSubscriptionDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index);
this.migrationStateModel._targetServerInstance = undefined!;
this.migrationStateModel._sqlMigrationService = undefined!;
this.populateManagedInstanceDropdown();
}
});
const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE
}).component();
this._managedInstanceDropdown = view.modelBuilder.dropDown()
.withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._managedInstanceDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._sqlMigrationServices = undefined!;
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index);
}
});
const targetContainer = view.modelBuilder.flexContainer().withItems(
[
managedInstanceSubscriptionDropdownLabel,
this._managedInstanceSubscriptionDropdown,
managedInstanceDropdownLabel,
this._managedInstanceDropdown
]
).withLayout({
flexFlow: 'column'
}).component();
const form = view.modelBuilder.formContainer()
.withFormItems(
[
{
component: targetContainer
}
]
);
await view.initializeModel(form.component());
}
public async onPageEnter(): Promise<void> {
this.populateSubscriptionDropdown();
}
public async onPageLeave(): Promise<void> {
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private async populateSubscriptionDropdown(): Promise<void> {
if (!this.migrationStateModel._targetSubscription) {
this._managedInstanceSubscriptionDropdown.loading = true;
this._managedInstanceDropdown.loading = true;
try {
this._managedInstanceSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues();
} catch (e) {
console.log(e);
} finally {
this._managedInstanceSubscriptionDropdown.loading = false;
}
}
}
private async populateManagedInstanceDropdown(): Promise<void> {
if (!this.migrationStateModel._targetServerInstance) {
this._managedInstanceDropdown.loading = true;
try {
this._managedInstanceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription);
} catch (e) {
console.log(e);
} finally {
this._managedInstanceDropdown.loading = false;
}
}
}
}