Add IR Migration configuration Validation to SQL Migration extension (#21386)

* re-factor and consolidate wizard pages

* validation WIP 11/10

* validate ir dialog

* navigation fixes

* bump version to 1.2.0

* add resource strings and fix navigatin issue

* map validation state to resource string clean up

* address review comments

* fix typos, address review comments

* address review feedback, readability

* fix res string, validation check, col width

* bug fixes, nav, sqldb migration

* fix nav/refresh/visibility issues

* fix nav issues, cancel pending validation items

* update error text / position

* fix localization bug
This commit is contained in:
brian-harris
2022-12-16 14:52:24 -08:00
committed by GitHub
parent 754d70d654
commit 2e240729af
29 changed files with 1993 additions and 692 deletions

View File

@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import { EOL } from 'os';
import { getStorageAccountAccessKeys } from '../api/azure';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, NetworkShare, StateChangeEvent } from '../models/stateMachine';
import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, NetworkShare, StateChangeEvent, ValidateIrState, ValidationResult } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { IconPathHelper } from '../constants/iconPathHelper';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
@@ -16,9 +16,11 @@ import * as utils from '../api/utils';
import { logError, TelemetryViews } from '../telemtery';
import * as styles from '../constants/styles';
import { TableMigrationSelectionDialog } from '../dialog/tableMigrationSelection/tableMigrationSelectionDialog';
import { ValidateIrDialog } from '../dialog/validationResults/validateIrDialog';
const WIZARD_TABLE_COLUMN_WIDTH = '200px';
const WIZARD_TABLE_COLUMN_WIDTH_SMALL = '170px';
const VALIDATE_IR_CUSTOM_BUTTON_INDEX = 0;
const blobResourceGroupErrorStrings = [constants.RESOURCE_GROUP_NOT_FOUND];
const blobStorageAccountErrorStrings = [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT];
@@ -28,9 +30,6 @@ const blobFileErrorStrings = [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLO
export class DatabaseBackupPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _networkShareButton!: azdata.RadioButtonComponent;
private _blobContainerButton!: azdata.RadioButtonComponent;
private _sourceConnectionContainer!: azdata.FlexContainer;
private _networkShareContainer!: azdata.FlexContainer;
private _windowsUserAccountText!: azdata.InputBoxComponent;
@@ -62,7 +61,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private _networkShareTargetDatabaseNames: azdata.InputBoxComponent[] = [];
private _blobContainerTargetDatabaseNames: azdata.InputBoxComponent[] = [];
private _networkShareLocations: azdata.InputBoxComponent[] = [];
private _radioButtonContainer!: azdata.FlexContainer;
private _networkDetailsContainer!: azdata.FlexContainer;
private _existingDatabases: string[] = [];
@@ -81,21 +79,24 @@ export class DatabaseBackupPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
this._radioButtonContainer = this.createBackupLocationComponent();
this._sourceConnectionContainer = this.createSourceCredentialsContainer();
this._networkDetailsContainer = this.createNetworkDetailsContainer();
this._targetDatabaseContainer = this.createTargetDatabaseContainer();
this._networkShareStorageAccountDetails = this.createNetworkShareStorageAccountDetailsContainer();
this._migrationTableSection = this._migrationTableSelectionContainer();
this._disposables.push(
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].onClick(
async e => await this._validateIr()));
const form = this._view.modelBuilder.formContainer()
.withFormItems([
{ title: '', component: this._radioButtonContainer },
{ title: '', component: this._sourceConnectionContainer },
{ title: '', component: this._networkDetailsContainer },
{ title: '', component: this._migrationTableSection },
{ title: '', component: this._networkShareStorageAccountDetails },
{ title: '', component: this._targetDatabaseContainer },
{ title: '', component: this._networkShareStorageAccountDetails }])
{ title: '', component: this._migrationTableSection },
])
.withProps({ CSSStyles: { 'padding-top': '0' } })
.component();
@@ -108,56 +109,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
await view.initializeModel(form);
}
private createBackupLocationComponent(): azdata.FlexContainer {
const buttonGroup = 'networkContainer';
const selectLocationText = this._view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_PAGE_DESCRIPTION,
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._networkShareButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL,
checked: this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
this._disposables.push(
this._networkShareButton.onDidChangeCheckedState(async checked => {
if (checked) {
await this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE);
}
}));
this._blobContainerButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL,
checked: this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
this._disposables.push(
this._blobContainerButton.onDidChangeCheckedState(async checked => {
if (checked) {
await this.switchNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
}
}));
const flexContainer = this._view.modelBuilder.flexContainer()
.withItems([
selectLocationText,
this._networkShareButton,
this._blobContainerButton])
.withLayout({ flexFlow: 'column' })
.component();
return flexContainer;
}
private createNetworkDetailsContainer(): azdata.FlexContainer {
this._networkShareContainer = this.createNetworkShareContainer();
this._blobContainer = this.createBlobContainer();
@@ -200,7 +151,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).component();
this._disposables.push(
this._sqlSourceUsernameInput.onTextChanged(
value => this.migrationStateModel._sqlServerUsername = value));
value => {
this.migrationStateModel._sqlServerUsername = value;
this._resetValidationUI();
}));
const sqlPasswordLabel = this._view.modelBuilder.text()
.withProps({
@@ -218,7 +172,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).component();
this._disposables.push(
this._sqlSourcePassword.onTextChanged(
value => this.migrationStateModel._sqlServerPassword = value));
value => {
this.migrationStateModel._sqlServerPassword = value;
this._resetValidationUI();
}));
return this._view.modelBuilder.flexContainer()
.withItems([
@@ -273,7 +230,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' }
})
.withValidation((component) => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (this.migrationStateModel.isBackupContainerNetworkShare) {
if (component.value) {
if (!/^[A-Za-z0-9\\\._-]{7,}$/.test(component.value)) {
return false;
@@ -287,6 +244,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].windowsUser = value;
}
this._resetValidationUI();
}));
const passwordLabel = this._view.modelBuilder.text()
@@ -309,6 +267,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].password = value;
}
this._resetValidationUI();
}));
return this._view.modelBuilder.flexContainer()
@@ -325,6 +284,13 @@ export class DatabaseBackupPage extends MigrationWizardPage {
.component();
}
private _resetValidationUI(): void {
if (this.wizard.message.level === azdata.window.MessageLevel.Information) {
this.wizard.message = { text: '' };
}
this.migrationStateModel.resetIrValidationResults();
}
private createBlobContainer(): azdata.FlexContainer {
const blobHeading = this._view.modelBuilder.text()
.withProps({
@@ -612,6 +578,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = selectedStorageAccount;
}
this.migrationStateModel.resetIrValidationResults();
}
}
}));
@@ -658,40 +625,130 @@ export class DatabaseBackupPage extends MigrationWizardPage {
.component();
}
private async _updatePageControlsVisibility(containerType: NetworkContainerType): Promise<void> {
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
const isNetworkShare = containerType === NetworkContainerType.NETWORK_SHARE;
const isBlobContainer = containerType === NetworkContainerType.BLOB_CONTAINER;
private async _updatePageControlsVisibility(): Promise<void> {
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
const isNetworkShare = this.migrationStateModel.isBackupContainerNetworkShare;
const isBlobContainer = this.migrationStateModel.isBackupContainerBlobContainer;
await utils.updateControlDisplay(this._sourceConnectionContainer, isSqlDbTarget || isNetworkShare);
await utils.updateControlDisplay(this._migrationTableSection, isSqlDbTarget);
await utils.updateControlDisplay(this._radioButtonContainer, !isSqlDbTarget);
await utils.updateControlDisplay(this._networkDetailsContainer, !isSqlDbTarget);
await utils.updateControlDisplay(this._targetDatabaseContainer, !isSqlDbTarget);
await utils.updateControlDisplay(this._networkShareStorageAccountDetails, !isSqlDbTarget);
await utils.updateControlDisplay(this._networkShareContainer, isNetworkShare);
await utils.updateControlDisplay(this._networkShareStorageAccountDetails, isNetworkShare);
await utils.updateControlDisplay(this._networkTableContainer, isNetworkShare);
await utils.updateControlDisplay(this._blobContainer, isBlobContainer);
await utils.updateControlDisplay(this._blobTableContainer, isBlobContainer);
await utils.updateControlDisplay(this._networkShareContainer, isNetworkShare && !isSqlDbTarget);
await utils.updateControlDisplay(this._networkShareStorageAccountDetails, isNetworkShare && !isSqlDbTarget);
await utils.updateControlDisplay(this._networkTableContainer, isNetworkShare && !isSqlDbTarget);
await utils.updateControlDisplay(this._blobContainer, isBlobContainer && !isSqlDbTarget);
await utils.updateControlDisplay(this._blobTableContainer, isBlobContainer && !isSqlDbTarget);
await this._windowsUserAccountText.updateProperties({ required: isNetworkShare });
await this._passwordText.updateProperties({ required: isNetworkShare });
await this._windowsUserAccountText.updateProperties({ required: isNetworkShare && !isSqlDbTarget });
await this._passwordText.updateProperties({ required: isNetworkShare && !isSqlDbTarget });
await this._sqlSourceUsernameInput.updateProperties({ required: isNetworkShare || isSqlDbTarget });
await this._sqlSourcePassword.updateProperties({ required: isNetworkShare || isSqlDbTarget });
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
if (this.migrationStateModel.refreshDatabaseBackupPage) {
this._networkShareButton.checked = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE;
this._blobContainerButton.checked = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER;
await this._updatePageControlsVisibility(this.migrationStateModel._databaseBackup.networkContainerType);
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
this.wizard.message = { text: '' };
const errors: string[] = [];
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
if (isSqlDbTarget) {
if (!this._validateTableSelection()) {
errors.push(constants.DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE);
}
} else {
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.NETWORK_SHARE:
if ((<azdata.CategoryValue>this._networkShareStorageAccountResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
if ((<azdata.CategoryValue>this._networkShareContainerStorageAccountDropdown.value)?.displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
break;
case NetworkContainerType.BLOB_CONTAINER:
this._blobContainerResourceGroupDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.RESOURCE_GROUP_NOT_FOUND])) {
errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerStorageAccountDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT])) {
errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT])) {
errors.push(constants.INVALID_BLOB_CONTAINER_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
this._blobContainerLastBackupFileDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER])) {
errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
}
if (errors.length > 0) {
const duplicates: Map<string, number[]> = new Map();
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer?.id;
if (duplicates.has(blobContainerId)) {
duplicates.get(blobContainerId)?.push(i);
} else {
duplicates.set(blobContainerId, [i]);
}
}
duplicates.forEach((d) => {
if (d.length > 1) {
const dupString = `${d.map(index => this.migrationStateModel._databasesForMigration[index]).join(', ')}`;
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
}
});
}
break;
}
}
if (this.migrationStateModel.isSqlMiTarget) {
this.migrationStateModel._targetDatabaseNames.forEach(t => {
// Making sure if database with same name is not present on the target Azure SQL
if (this._existingDatabases.includes(t)) {
errors.push(constants.DATABASE_ALREADY_EXISTS_MI(t, this.migrationStateModel._targetServerInstance.name));
}
});
}
this.wizard.message = {
text: errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
if (errors.length > 0) {
return false;
}
if (this.migrationStateModel.isIrMigration) {
this.wizard.nextButton.enabled = this.migrationStateModel.isIrTargetValidated;
this.updateValidationResultUI();
return this.migrationStateModel.isIrTargetValidated;
} else {
return true;
}
});
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].hidden = !this.migrationStateModel.isIrMigration;
await this._updatePageControlsVisibility();
if (this.migrationStateModel.refreshDatabaseBackupPage) {
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
if (isSqlDbTarget) {
this.wizardPage.title = constants.DATABASE_TABLE_SELECTION_LABEL;
this.wizardPage.description = constants.DATABASE_TABLE_SELECTION_LABEL;
@@ -700,7 +757,15 @@ export class DatabaseBackupPage extends MigrationWizardPage {
try {
const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE;
const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1;
this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration;
const oldHidden = this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden;
const newHidden = !isOfflineMigration;
if (oldHidden !== newHidden) {
// clear values prior to hiding columns if changing column visibility
// to prevent null DeclarativeTableComponent - exception / _view null
await this._blobContainerTargetDatabaseNamesTable.setDataValues([]);
}
this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = newHidden;
this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => {
column.width = isOfflineMigration
? WIZARD_TABLE_COLUMN_WIDTH_SMALL
@@ -712,26 +777,37 @@ export class DatabaseBackupPage extends MigrationWizardPage {
(await this.migrationStateModel.getSourceConnectionProfile()).providerId,
azdata.DataProviderType.QueryProvider);
const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(
await (azdata.connection.getUriForConnection(
this.migrationStateModel.sourceConnectionId)), query);
let username = '';
try {
const query = 'select SUSER_NAME()';
const ownerUri = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
const results = await queryProvider.runQueryAndReturn(ownerUri, query);
username = results.rows[0][0]?.displayValue;
} catch (e) {
username = connectionProfile.userName;
}
const username = results.rows[0][0].displayValue;
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin'
? MigrationSourceAuthenticationType.Sql
: connectionProfile.authenticationType === 'Integrated' // TODO: use azdata.connection.AuthenticationType.Integrated after next ADS release
? MigrationSourceAuthenticationType.Integrated
: undefined!;
this.migrationStateModel._authenticationType =
connectionProfile.authenticationType === azdata.connection.AuthenticationType.SqlLogin
? MigrationSourceAuthenticationType.Sql
: connectionProfile.authenticationType === azdata.connection.AuthenticationType.Integrated
? MigrationSourceAuthenticationType.Integrated
: undefined!;
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(
this.migrationStateModel._authenticationType,
connectionProfile.serverName);
this._sqlSourceUsernameInput.value = username;
this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password;
this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares
? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser
: '';
this._windowsUserAccountText.value =
this.migrationStateModel._databaseBackup.networkShares[0]?.windowsUser
?? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser
?? '';
this._passwordText.value =
this.migrationStateModel._databaseBackup.networkShares[0]?.password
?? this.migrationStateModel.savedInfo?.networkShares[0]?.password
?? '';
this._networkShareTargetDatabaseNames = [];
this._networkShareLocations = [];
@@ -741,7 +817,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._blobContainerDropdowns = [];
this._blobContainerLastBackupFileDropdowns = [];
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) {
if (this.migrationStateModel.isSqlMiTarget) {
this._existingDatabases = await this.migrationStateModel.getManagedDatabases();
}
@@ -803,7 +879,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return false;
}
// Making sure if database with same name is not present on the target Azure SQL
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) {
if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) {
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
@@ -816,7 +892,8 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._disposables.push(
targetDatabaseInput.onTextChanged(async (value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
await this.validateFields();
this._resetValidationUI();
await this.validateFields(targetDatabaseInput);
}));
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
this._networkShareTargetDatabaseNames.push(targetDatabaseInput);
@@ -828,7 +905,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION,
width: '300px'
}).withValidation(c => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (this.migrationStateModel.isBackupContainerNetworkShare) {
if (c.value) {
if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(c.value)) {
return false;
@@ -840,7 +917,8 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._disposables.push(
networkShareLocationInput.onTextChanged(async (value) => {
this.migrationStateModel._databaseBackup.networkShares[index].networkShareLocation = value.trim();
await this.validateFields();
this._resetValidationUI();
await this.validateFields(networkShareLocationInput);
}));
networkShareLocationInput.value = this.migrationStateModel._databaseBackup.networkShares[index]?.networkShareLocation;
this._networkShareLocations.push(networkShareLocationInput);
@@ -856,7 +934,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return false;
}
// Making sure if database with same name is not present on the target Azure SQL
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) {
if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) {
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
@@ -868,7 +946,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).component();
this._disposables.push(
blobTargetDatabaseInput.onTextChanged(
(value) => { this.migrationStateModel._targetDatabaseNames[index] = value.trim(); }));
(value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
this._resetValidationUI();
}));
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput);
@@ -987,13 +1068,17 @@ export class DatabaseBackupPage extends MigrationWizardPage {
{ value: this._blobContainerStorageAccountDropdowns[index] },
{ value: this._blobContainerDropdowns[index] },
{ value: this._blobContainerLastBackupFileDropdowns[index] }]);
await this._blobContainerTargetDatabaseNamesTable.setDataValues([]);
await this._blobContainerTargetDatabaseNamesTable.setDataValues(blobContainerTargetData);
await this.getSubscriptionValues();
// clear change tracking flags
this.migrationStateModel.refreshDatabaseBackupPage = false;
this.migrationStateModel._didUpdateDatabasesForMigration = false;
this.migrationStateModel._didDatabaseMappingChange = false;
this.migrationStateModel._validateIrSqlDb = [];
this.migrationStateModel._validateIrSqlMi = [];
this.migrationStateModel._validateIrSqlVm = [];
} catch (error) {
console.log(error);
let errorText = error?.message;
@@ -1006,101 +1091,76 @@ export class DatabaseBackupPage extends MigrationWizardPage {
level: azdata.window.MessageLevel.Error
};
}
await this.validateFields();
this.updateValidationResultUI(true);
}
}
private async _validateIr(): Promise<void> {
this.wizard.message = { text: '' };
const dialog = new ValidateIrDialog(
this.migrationStateModel,
() => this.updateValidationResultUI());
let results: ValidationResult[] = [];
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLDB:
results = this.migrationStateModel._validateIrSqlDb;
break;
case MigrationTargetType.SQLMI:
results = this.migrationStateModel._validateIrSqlMi;
break;
case MigrationTargetType.SQLVM:
results = this.migrationStateModel._validateIrSqlVm;
break;
}
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
await dialog.openDialog(constants.VALIDATION_DIALOG_TITLE, results);
}
public updateValidationResultUI(initializing?: boolean): void {
if (this.migrationStateModel.isIrMigration) {
const succeeded = this.migrationStateModel.isIrTargetValidated;
if (succeeded) {
this.wizard.message = {
level: azdata.window.MessageLevel.Information,
text: constants.VALIDATION_MESSAGE_SUCCESS,
};
} else {
const results = this.migrationStateModel.validationTargetResults;
const hasResults = results.length > 0;
if (initializing && !hasResults) {
return;
}
const canceled = results.some(result => result.state === ValidateIrState.Canceled);
const errors: string[] = results.flatMap(result => result.errors) ?? [];
const errorsMessage: string = errors.join(EOL);
const hasErrors = errors.length > 0;
const msg = hasResults
? hasErrors
? canceled
? constants.VALIDATION_MESSAGE_CANCELED_ERRORS(errorsMessage)
: constants.VALIDATE_IR_VALIDATION_COMPLETED_ERRORS(errorsMessage)
: constants.VALIDATION_MESSAGE_CANCELED
: constants.VALIDATION_MESSAGE_NOT_RUN;
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: msg,
};
}
this.wizard.message = { text: '' };
const errors: string[] = [];
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.NETWORK_SHARE:
if ((<azdata.CategoryValue>this._networkShareStorageAccountResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
if ((<azdata.CategoryValue>this._networkShareContainerStorageAccountDropdown.value)?.displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
break;
case NetworkContainerType.BLOB_CONTAINER:
this._blobContainerResourceGroupDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.RESOURCE_GROUP_NOT_FOUND])) {
errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerStorageAccountDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT])) {
errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT])) {
errors.push(constants.INVALID_BLOB_CONTAINER_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
this._blobContainerLastBackupFileDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER])) {
errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
}
if (errors.length > 0) {
const duplicates: Map<string, number[]> = new Map();
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer?.id;
if (duplicates.has(blobContainerId)) {
duplicates.get(blobContainerId)?.push(i);
} else {
duplicates.set(blobContainerId, [i]);
}
}
duplicates.forEach((d) => {
if (d.length > 1) {
const dupString = `${d.map(index => this.migrationStateModel._databasesForMigration[index]).join(', ')}`;
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
}
});
}
break;
default:
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
if (isSqlDbTarget) {
if (!this._validateTableSelection()) {
errors.push(constants.DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE);
}
break;
}
return false;
}
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) {
this.migrationStateModel._targetDatabaseNames.forEach(t => {
// Making sure if database with same name is not present on the target Azure SQL
if (this._existingDatabases.includes(t)) {
errors.push(constants.DATABASE_ALREADY_EXISTS_MI(t, this.migrationStateModel._targetServerInstance.name));
}
});
}
this.wizard.message = {
text: errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
if (errors.length > 0) {
return false;
}
return true;
});
}
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
try {
if (pageChangeInfo.newPage > pageChangeInfo.lastPage) {
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].hidden = true;
if (pageChangeInfo.newPage > pageChangeInfo.lastPage) {
if (!this.migrationStateModel.isSqlDbTarget) {
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.BLOB_CONTAINER:
for (let i = 0; i < this.migrationStateModel._databaseBackup.blobs.length; i++) {
@@ -1124,26 +1184,12 @@ export class DatabaseBackupPage extends MigrationWizardPage {
break;
}
}
} finally {
this.wizard.registerNavigationValidator((pageChangeInfo) => true);
}
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private async switchNetworkContainerFields(containerType: NetworkContainerType): Promise<void> {
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
this.wizard.nextButton.enabled = true;
this.migrationStateModel._databaseBackup.networkContainerType = containerType;
await this._updatePageControlsVisibility(containerType);
await this.validateFields();
}
private _validateTableSelection(): boolean {
for (const targetDatabaseInfo of this.migrationStateModel._sourceTargetMapping) {
const databaseInfo = targetDatabaseInfo[1];
@@ -1159,27 +1205,36 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return false;
}
private async validateFields(): Promise<void> {
await this._sqlSourceUsernameInput.validate();
await this._sqlSourcePassword.validate();
await this._windowsUserAccountText.validate();
await this._passwordText.validate();
await this._networkShareContainerSubscription.validate();
await this._networkShareStorageAccountResourceGroupDropdown.validate();
await this._networkShareContainerStorageAccountDropdown.validate();
await this._blobContainerSubscription.validate();
private async validateFields(component?: azdata.Component): Promise<void> {
await this._sqlSourceUsernameInput?.validate();
await this._sqlSourcePassword?.validate();
await this._windowsUserAccountText?.validate();
await this._passwordText?.validate();
await this._networkShareContainerSubscription?.validate();
await this._networkShareStorageAccountResourceGroupDropdown?.validate();
await this._networkShareContainerStorageAccountDropdown?.validate();
await this._blobContainerSubscription?.validate();
for (let i = 0; i < this._networkShareTargetDatabaseNames.length; i++) {
await this._networkShareTargetDatabaseNames[i].validate();
await this._networkShareLocations[i].validate();
await this._blobContainerTargetDatabaseNames[i].validate();
await this._blobContainerResourceGroupDropdowns[i].validate();
await this._blobContainerStorageAccountDropdowns[i].validate();
await this._blobContainerDropdowns[i].validate();
await this._networkShareTargetDatabaseNames[i]?.validate();
await this._networkShareLocations[i]?.validate();
await this._blobContainerTargetDatabaseNames[i]?.validate();
await this._blobContainerResourceGroupDropdowns[i]?.validate();
await this._blobContainerStorageAccountDropdowns[i]?.validate();
await this._blobContainerDropdowns[i]?.validate();
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
await this._blobContainerLastBackupFileDropdowns[i]?.validate();
}
}
if (this.migrationStateModel.isIrMigration) {
if (this.migrationStateModel.isSqlDbTarget) {
await this._databaseTable?.validate();
}
}
if (this.migrationStateModel.isBackupContainerNetworkShare) {
await this._networkShareTargetDatabaseNamesTable.validate();
}
await component?.validate();
}
private async getSubscriptionValues(): Promise<void> {
@@ -1265,66 +1320,75 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
private loadBlobStorageDropdown(index: number): void {
try {
this._blobContainerStorageAccountDropdowns[index].loading = true;
this._blobContainerStorageAccountDropdowns[index].values = utils.getAzureResourceDropdownValues(
this.migrationStateModel._storageAccounts,
this.migrationStateModel._location,
this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup?.name,
constants.NO_STORAGE_ACCOUNT_FOUND);
const dropDown = this._blobContainerStorageAccountDropdowns[index];
if (dropDown) {
try {
dropDown.loading = true;
dropDown.values = utils.getAzureResourceDropdownValues(
this.migrationStateModel._storageAccounts,
this.migrationStateModel._location,
this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup?.name,
constants.NO_STORAGE_ACCOUNT_FOUND);
utils.selectDefaultDropdownValue(
this._blobContainerStorageAccountDropdowns[index],
this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobStorageDropdown', error);
} finally {
this._blobContainerStorageAccountDropdowns[index].loading = false;
utils.selectDefaultDropdownValue(
dropDown,
this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobStorageDropdown', error);
} finally {
dropDown.loading = false;
}
}
}
private async loadBlobContainerDropdown(index: number): Promise<void> {
try {
this._blobContainerDropdowns[index].loading = true;
this.migrationStateModel._blobContainers = await utils.getBlobContainer(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount);
const dropDown = this._blobContainerDropdowns[index];
if (dropDown) {
try {
dropDown.loading = true;
this.migrationStateModel._blobContainers = await utils.getBlobContainer(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount);
this._blobContainerDropdowns[index].values = utils.getResourceDropdownValues(
this.migrationStateModel._blobContainers,
constants.NO_BLOBCONTAINERS_FOUND);
dropDown.values = utils.getResourceDropdownValues(
this.migrationStateModel._blobContainers,
constants.NO_BLOBCONTAINERS_FOUND);
utils.selectDefaultDropdownValue(
this._blobContainerDropdowns[index],
this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobContainers', error);
} finally {
this._blobContainerDropdowns[index].loading = false;
utils.selectDefaultDropdownValue(
dropDown,
this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobContainers', error);
} finally {
dropDown.loading = false;
}
}
}
private async loadBlobLastBackupFileDropdown(index: number): Promise<void> {
try {
this._blobContainerLastBackupFileDropdowns[index].loading = true;
this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount,
this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
this._blobContainerLastBackupFileDropdowns[index].values = await utils.getBlobLastBackupFileNamesValues(
this.migrationStateModel._lastFileNames);
utils.selectDefaultDropdownValue(
this._blobContainerLastBackupFileDropdowns[index],
this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobLastBackupFiles', error);
} finally {
this._blobContainerLastBackupFileDropdowns[index].loading = false;
const dropDown = this._blobContainerLastBackupFileDropdowns[index];
if (dropDown) {
try {
dropDown.loading = true;
this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount,
this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
dropDown.values = await utils.getBlobLastBackupFileNamesValues(
this.migrationStateModel._lastFileNames);
utils.selectDefaultDropdownValue(
dropDown,
this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobLastBackupFiles', error);
} finally {
dropDown.loading = false;
}
}
}
@@ -1409,6 +1473,12 @@ export class DatabaseBackupPage extends MigrationWizardPage {
},
],
})
.withValidation(table => {
if (this.migrationStateModel.isSqlDbTarget) {
return this._validateTableSelection();
}
return true;
})
.component();
this._disposables.push(
@@ -1479,7 +1549,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
});
await this._databaseTable.updateProperty('data', data);
this._refreshLoading.loading = false;
}
}