/*--------------------------------------------------------------------------------------------- * 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 * as vscode from 'vscode'; import { EOL } from 'os'; import { getStorageAccountAccessKeys, SqlVMServer } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, 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'; import * as utils from '../api/utils'; import { MigrationTargetType } from '../api/utils'; import { logError, TelemetryViews } from '../telemetry'; import * as styles from '../constants/styles'; import { TableMigrationSelectionDialog } from '../dialog/tableMigrationSelection/tableMigrationSelectionDialog'; import { ValidateIrDialog } from '../dialog/validationResults/validateIrDialog'; import { canTargetConnectToStorageAccount, getSourceConnectionCredentials, getSourceConnectionProfile, getSourceConnectionQueryProvider, getSourceConnectionUri } from '../api/sqlUtils'; 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]; const blobContainerErrorStrings = [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT]; const blobFileErrorStrings = [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER]; const blobFolderErrorStrings = [constants.NO_BLOBFOLDERS_FOUND, constants.SELECT_BLOB_CONTAINER]; export class DatabaseBackupPage extends MigrationWizardPage { private _view!: azdata.ModelView; private _sourceConnectionContainer!: azdata.FlexContainer; private _networkShareContainer!: azdata.FlexContainer; private _windowsUserAccountText!: azdata.InputBoxComponent; private _passwordText!: azdata.InputBoxComponent; private _sourceHelpText!: azdata.TextComponent; private _sqlSourceUsernameInput!: azdata.InputBoxComponent; private _sqlSourcePassword!: azdata.InputBoxComponent; private _blobContainer!: azdata.FlexContainer; private _blobContainerSubscription!: azdata.TextComponent; private _blobContainerLocation!: azdata.TextComponent; private _blobContainerResourceGroupDropdowns!: azdata.DropDownComponent[]; private _blobContainerStorageAccountDropdowns!: azdata.DropDownComponent[]; private _blobContainerDropdowns!: azdata.DropDownComponent[]; private _blobContainerLastBackupFileDropdowns!: azdata.DropDownComponent[]; private _blobContainerFolderDropdowns!: azdata.DropDownComponent[]; private _blobContainerFolderStructureInfoBox!: azdata.TextComponent; private _blobContainerVmDatabaseAlreadyExistsInfoBox!: azdata.TextComponent; private _networkShareStorageAccountDetails!: azdata.FlexContainer; private _networkShareContainerSubscription!: azdata.TextComponent; private _networkShareContainerLocation!: azdata.TextComponent; private _networkShareStorageAccountResourceGroupDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountRefreshButton!: azdata.ButtonComponent; private _networkShareVmDatabaseAlreadyExistsInfoBox!: azdata.TextComponent; private _targetDatabaseContainer!: azdata.FlexContainer; private _networkShareTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent; private _blobContainerTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent; private _networkTableContainer!: azdata.FlexContainer; private _blobTableContainer!: azdata.FlexContainer; private _networkShareTargetDatabaseNames: azdata.InputBoxComponent[] = []; private _blobContainerTargetDatabaseNames: azdata.InputBoxComponent[] = []; private _networkShareLocations: azdata.InputBoxComponent[] = []; private _networkDetailsContainer!: azdata.FlexContainer; private _existingDatabases: string[] = []; private _nonPageBlobErrors: string[] = []; private _inaccessibleStorageAccounts: string[] = []; private _disposables: vscode.Disposable[] = []; // SQL DB table selection private _refreshLoading!: azdata.LoadingComponent; private _refreshButton!: azdata.ButtonComponent; private _databaseTable!: azdata.TableComponent; private _migrationTableSection!: azdata.FlexContainer; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATA_SOURCE_CONFIGURATION_PAGE_TITLE), migrationStateModel); } protected async registerContent(view: azdata.ModelView): Promise { this._view = view; 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._sourceConnectionContainer }, { title: '', component: this._networkDetailsContainer }, { title: '', component: this._networkShareStorageAccountDetails }, { title: '', component: this._targetDatabaseContainer }, { title: '', component: this._migrationTableSection }, ]) .withProps({ CSSStyles: { 'padding-top': '0' } }) .component(); this._disposables.push( this._view.onClosed(e => { this._disposables.forEach( d => { try { d.dispose(); } catch { } }); })); await view.initializeModel(form); } private createNetworkDetailsContainer(): azdata.FlexContainer { this._networkShareContainer = this.createNetworkShareContainer(); this._blobContainer = this.createBlobContainer(); return this._view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column' }) .withItems([ this._networkShareContainer, this._blobContainer]) .component(); } private createSourceCredentialsContainer(): azdata.FlexContainer { const sqlSourceHeader = this._view.modelBuilder.text() .withProps({ value: constants.SOURCE_CREDENTIALS, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '4px' } }).component(); this._sourceHelpText = this._view.modelBuilder.text() .withProps({ width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS } }).component(); const usernameLabel = this._view.modelBuilder.text() .withProps({ value: constants.USERNAME, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { ...styles.LABEL_CSS } }).component(); this._sqlSourceUsernameInput = this._view.modelBuilder.inputBox() .withProps({ required: true, enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push( this._sqlSourceUsernameInput.onTextChanged( value => { this.migrationStateModel._sqlServerUsername = value; this._resetValidationUI(); })); const sqlPasswordLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { ...styles.LABEL_CSS } }).component(); this._sqlSourcePassword = this._view.modelBuilder.inputBox() .withProps({ required: true, inputType: 'password', width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push( this._sqlSourcePassword.onTextChanged( value => { this.migrationStateModel._sqlServerPassword = value; this._resetValidationUI(); })); return this._view.modelBuilder.flexContainer() .withItems([ sqlSourceHeader, this._sourceHelpText, usernameLabel, this._sqlSourceUsernameInput, sqlPasswordLabel, this._sqlSourcePassword]) .withLayout({ flexFlow: 'column' }) .withProps({ display: 'none' }) .component(); } private createNetworkShareContainer(): azdata.FlexContainer { const networkShareHeading = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '24px' } }).component(); const networkShareHelpText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS } }).component(); const networkShareInfoBox = this._view.modelBuilder.infoBox() .withProps({ text: constants.DATABASE_SERVICE_ACCOUNT_INFO_TEXT, style: 'information', width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS } }).component(); const windowsUserAccountLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL, description: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_INFO, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { ...styles.LABEL_CSS }, }).component(); this._windowsUserAccountText = this._view.modelBuilder.inputBox() .withProps({ placeHolder: constants.WINDOWS_USER_ACCOUNT, required: true, validationErrorMessage: constants.INVALID_USER_ACCOUNT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' } }) .withValidation((component) => { if (this.migrationStateModel.isBackupContainerNetworkShare) { if (component.value) { if (!/^[A-Za-z0-9\\\._-]{7,}$/.test(component.value)) { return false; } } } return true; }).component(); this._disposables.push( this._windowsUserAccountText.onTextChanged((value) => { 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() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { ...styles.LABEL_CSS, } }).component(); this._passwordText = this._view.modelBuilder.inputBox() .withProps({ placeHolder: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER, inputType: 'password', required: true, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' } }).component(); this._disposables.push( this._passwordText.onTextChanged((value) => { 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() .withItems([ networkShareHeading, networkShareHelpText, networkShareInfoBox, windowsUserAccountLabel, this._windowsUserAccountText, passwordLabel, this._passwordText]) .withLayout({ flexFlow: 'column' }) .withProps({ display: 'none' }) .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({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_HEADER_TEXT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.SECTION_HEADER_CSS } }).component(); const blobHelpText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_HELP_TEXT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' } }).component(); const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL, CSSStyles: { ...styles.LABEL_CSS } }).component(); this._blobContainerSubscription = this._view.modelBuilder.text() .withProps({ enabled: false, CSSStyles: { ...styles.BODY_CSS, 'margin': '0 0 12px 0' } }).component(); const locationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, CSSStyles: { ...styles.LABEL_CSS } }).component(); this._blobContainerLocation = this._view.modelBuilder.text() .withProps({ enabled: false, CSSStyles: { ...styles.BODY_CSS, 'margin': '0px' } }).component(); const flexContainer = this._view.modelBuilder.flexContainer() .withItems([ blobHeading, blobHelpText, subscriptionLabel, this._blobContainerSubscription, locationLabel, this._blobContainerLocation]) .withLayout({ flexFlow: 'column' }) .withProps({ display: 'none' }) .component(); return flexContainer; } private createTargetDatabaseContainer(): azdata.FlexContainer { const headerCssStyles: azdata.CssStyles = { ...styles.LABEL_CSS, 'border': 'none', 'text-align': 'left', 'box-shadow': 'inset 0px -1px 0px #F3F2F1', }; const rowCssStyle: azdata.CssStyles = { ...styles.BODY_CSS, 'border': 'none', 'font-size': '13px', 'box-shadow': 'inset 0px -1px 0px #F3F2F1', }; const networkShareTableText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_TABLE_HELP_TEXT, CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '8px' } }).component(); const blobTableText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_TABLE_HELP_TEXT, CSSStyles: { ...styles.SECTION_HEADER_CSS } }).component(); this._networkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable() .withProps({ columns: [ { displayName: constants.SOURCE_DATABASE, valueType: azdata.DeclarativeDataType.string, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: '250px' }, { displayName: constants.TARGET_DATABASE_NAME, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.NETWORK_SHARE_PATH, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: '300px' } ] }).component(); this._blobContainerTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable() .withProps({ columns: [ { displayName: constants.SOURCE_DATABASE, valueType: azdata.DeclarativeDataType.string, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL, }, { displayName: constants.TARGET_DATABASE_NAME, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.RESOURCE_GROUP, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.STORAGE_ACCOUNT, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.BLOB_CONTAINER, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL, hidden: true }, { displayName: constants.BLOB_CONTAINER_FOLDER, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH_SMALL, hidden: true }, ] }).component(); this._networkShareVmDatabaseAlreadyExistsInfoBox = this._view.modelBuilder.infoBox() .withProps({ text: constants.DATABASE_ALREADY_EXISTS_VM_INFO, style: 'information', width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS, 'display': 'none' } }).component(); this._networkTableContainer = this._view.modelBuilder.flexContainer() .withItems([ networkShareTableText, this._networkShareVmDatabaseAlreadyExistsInfoBox, this._networkShareTargetDatabaseNamesTable]) .withProps({ CSSStyles: { 'display': 'none', } }) .component(); const allFieldsRequiredLabel = this._view.modelBuilder.text() .withProps({ value: constants.ALL_FIELDS_REQUIRED, CSSStyles: { ...styles.BODY_CSS } }).component(); this._blobContainerFolderStructureInfoBox = this._view.modelBuilder.infoBox() .withProps({ text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_INFO, style: 'information', width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS } }).component(); this._blobContainerVmDatabaseAlreadyExistsInfoBox = this._view.modelBuilder.infoBox() .withProps({ text: constants.DATABASE_ALREADY_EXISTS_VM_INFO, style: 'information', width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS, 'display': 'none' } }).component(); this._blobTableContainer = this._view.modelBuilder.flexContainer() .withItems([ blobTableText, allFieldsRequiredLabel, this._blobContainerFolderStructureInfoBox, this._blobContainerVmDatabaseAlreadyExistsInfoBox, this._blobContainerTargetDatabaseNamesTable]) .withProps({ CSSStyles: { 'display': 'none', } }) .component(); const container = this._view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column' }) .withItems([ this._networkTableContainer, this._blobTableContainer]) .withProps({ CSSStyles: { 'display': 'none' } }) .component(); return container; } private createNetworkShareStorageAccountDetailsContainer(): azdata.FlexContainer { const azureAccountHeader = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HEADER, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' } }).component(); const azureAccountHelpText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' } }).component(); const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.SUBSCRIPTION, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.LABEL_CSS, 'margin': '0' } }).component(); this._networkShareContainerSubscription = this._view.modelBuilder.text() .withProps({ enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'margin': '0' } }).component(); const locationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0' } }).component(); this._networkShareContainerLocation = this._view.modelBuilder.text() .withProps({ enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'margin': '0' } }).component(); const resourceGroupLabel = this._view.modelBuilder.text() .withProps({ value: constants.RESOURCE_GROUP, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { ...styles.LABEL_CSS } }).component(); this._networkShareStorageAccountResourceGroupDropdown = this._view.modelBuilder.dropDown() .withProps({ required: true, ariaLabel: constants.RESOURCE_GROUP, width: WIZARD_INPUT_COMPONENT_WIDTH, editable: true, fireOnTextChange: true, CSSStyles: { 'margin-top': '-1em' }, }).component(); this._disposables.push( this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(async (value) => { if (value && value !== 'undefined') { const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); if (selectedResourceGroup) { for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { this.migrationStateModel._databaseBackup.networkShares[i].resourceGroup = selectedResourceGroup; } this.loadNetworkShareStorageDropdown(); } } })); const storageAccountLabel = this._view.modelBuilder.text() .withProps({ value: constants.STORAGE_ACCOUNT, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { ...styles.LABEL_CSS } }).component(); this._networkShareContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() .withProps({ ariaLabel: constants.STORAGE_ACCOUNT, required: true, width: WIZARD_INPUT_COMPONENT_WIDTH, editable: true, fireOnTextChange: true, }).component(); this._disposables.push( this._networkShareContainerStorageAccountDropdown.onValueChanged(async (value) => { if (value && value !== 'undefined') { const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); if (selectedStorageAccount) { for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = selectedStorageAccount; } this.migrationStateModel.resetIrValidationResults(); // check for storage account connectivity if ((this.migrationStateModel.isSqlMiTarget || this.migrationStateModel.isSqlVmTarget)) { if (!(await canTargetConnectToStorageAccount( this.migrationStateModel._targetType, this.migrationStateModel._targetServerInstance, selectedStorageAccount, this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription))) { this._inaccessibleStorageAccounts = [selectedStorageAccount.name]; } else { this._inaccessibleStorageAccounts = []; } this.wizard.message = { text: this._inaccessibleStorageAccounts.length > 0 ? constants.STORAGE_ACCOUNT_CONNECTIVITY_WARNING(this.migrationStateModel._targetServerInstance.name, this._inaccessibleStorageAccounts, this.migrationStateModel.isSqlMiTarget) : '', level: azdata.window.MessageLevel.Warning } } } } })); this._networkShareContainerStorageAccountRefreshButton = this._view.modelBuilder.button() .withProps({ iconPath: IconPathHelper.refresh, iconHeight: 18, iconWidth: 18, height: 25, ariaLabel: constants.REFRESH, }).component(); this._disposables.push( this._networkShareContainerStorageAccountRefreshButton.onDidClick( async () => { this.loadNetworkShareStorageDropdown(); // check for storage account connectivity const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === (this._networkShareContainerStorageAccountDropdown.value as azdata.CategoryValue).displayName); if ((this.migrationStateModel.isSqlMiTarget || this.migrationStateModel.isSqlVmTarget) && selectedStorageAccount) { if (!(await canTargetConnectToStorageAccount( this.migrationStateModel._targetType, this.migrationStateModel._targetServerInstance, selectedStorageAccount, this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription))) { this._inaccessibleStorageAccounts = [selectedStorageAccount.name]; } else { this._inaccessibleStorageAccounts = []; } this.wizard.message = { text: this._inaccessibleStorageAccounts.length > 0 ? constants.STORAGE_ACCOUNT_CONNECTIVITY_WARNING(this.migrationStateModel._targetServerInstance.name, this._inaccessibleStorageAccounts, this.migrationStateModel.isSqlMiTarget) : '', level: azdata.window.MessageLevel.Warning } } })); const storageAccountContainer = this._view.modelBuilder.flexContainer() .withProps({ CSSStyles: { 'margin-top': '-1em' } }) .component(); storageAccountContainer.addItem( this._networkShareContainerStorageAccountDropdown, { flex: '0 0 auto' }); storageAccountContainer.addItem( this._networkShareContainerStorageAccountRefreshButton, { flex: '0 0 auto', CSSStyles: { 'margin-left': '5px' } }); return this._view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column' }) .withItems([ azureAccountHeader, azureAccountHelpText, subscriptionLabel, this._networkShareContainerSubscription, locationLabel, this._networkShareContainerLocation, resourceGroupLabel, this._networkShareStorageAccountResourceGroupDropdown, storageAccountLabel, storageAccountContainer]) .withProps({ display: 'none' }) .component(); } private async _updatePageControlsVisibility(): Promise { const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget; const isSqlVmTarget = this.migrationStateModel.isSqlVmTarget; 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._networkDetailsContainer, !isSqlDbTarget); await utils.updateControlDisplay(this._targetDatabaseContainer, !isSqlDbTarget); await utils.updateControlDisplay(this._networkShareContainer, isNetworkShare && !isSqlDbTarget); await utils.updateControlDisplay(this._networkShareStorageAccountDetails, isNetworkShare && !isSqlDbTarget); await utils.updateControlDisplay(this._networkShareVmDatabaseAlreadyExistsInfoBox, isSqlVmTarget); await utils.updateControlDisplay(this._networkTableContainer, isNetworkShare && !isSqlDbTarget); await utils.updateControlDisplay(this._blobContainer, isBlobContainer && !isSqlDbTarget); await utils.updateControlDisplay(this._blobContainerFolderStructureInfoBox, isBlobContainer && !isSqlDbTarget); await utils.updateControlDisplay(this._blobContainerVmDatabaseAlreadyExistsInfoBox, isSqlVmTarget); await utils.updateControlDisplay(this._blobTableContainer, isBlobContainer && !isSqlDbTarget); 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 { this.wizard.registerNavigationValidator((pageChangeInfo) => { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { return true; } 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 ((this._networkShareStorageAccountResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) { errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); } if ((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, blobResourceGroupErrorStrings)) { errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._databasesForMigration[index])); } }); this._blobContainerStorageAccountDropdowns.forEach((v, index) => { if (this.shouldDisplayBlobDropdownError(v, blobStorageAccountErrorStrings)) { errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._databasesForMigration[index])); } }); this._blobContainerDropdowns.forEach((v, index) => { if (this.shouldDisplayBlobDropdownError(v, blobContainerErrorStrings)) { 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, blobFileErrorStrings)) { errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index])); } }); } else { this._blobContainerFolderDropdowns.forEach((v, index) => { if (this.shouldDisplayBlobDropdownError(v, blobFolderErrorStrings)) { errors.push(constants.INVALID_BLOB_LAST_BACKUP_FOLDER_ERROR(this.migrationStateModel._databasesForMigration[index])); } }); } if (this.migrationStateModel.isSqlVmTarget && utils.isTargetSqlVm2014OrBelow(this.migrationStateModel._targetServerInstance as SqlVMServer)) { errors.push(...this._nonPageBlobErrors); } if (errors.length > 0) { const duplicates: Map = 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; await this._loadTableData(); } try { const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE; // for offline migrations, show last backup file column const lastBackupFileColumnIndex = 5; const lastBackupFileColumnOldHidden = this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden; const lastBackupFileColumnNewHidden = !isOfflineMigration; if (lastBackupFileColumnOldHidden !== lastBackupFileColumnNewHidden) { // 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 = lastBackupFileColumnNewHidden; // for online migrations, show folder column const folderColumnIndex = 6; const folderColumnOldHidden = this._blobContainerTargetDatabaseNamesTable.columns[folderColumnIndex].hidden; const folderColumnNewHidden = isOfflineMigration; if (folderColumnOldHidden !== folderColumnNewHidden) { // clear values prior to hiding columns if changing column visibility // to prevent null DeclarativeTableComponent - exception / _view null await this._blobContainerTargetDatabaseNamesTable.setDataValues([]); } this._blobContainerTargetDatabaseNamesTable.columns[folderColumnIndex].hidden = folderColumnNewHidden; const connectionProfile = await getSourceConnectionProfile(); const queryProvider = await getSourceConnectionQueryProvider(); let username = ''; try { const query = 'select SUSER_NAME()'; const ownerUri = await getSourceConnectionUri(); const results = await queryProvider.runQueryAndReturn(ownerUri, query); username = results.rows[0][0]?.displayValue; } catch (e) { username = connectionProfile.userName; } 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, isSqlDbTarget); this._sqlSourceUsernameInput.value = username; this._sqlSourcePassword.value = (await getSourceConnectionCredentials()).password; const networkShares = this.migrationStateModel._databaseBackup?.networkShares?.length > 0 ? this.migrationStateModel._databaseBackup?.networkShares : this.migrationStateModel.savedInfo?.networkShares ?? []; const networkShare = networkShares?.length > 0 ? networkShares[0] : undefined; this._windowsUserAccountText.value = networkShare?.windowsUser ?? ''; this._passwordText.value = networkShare?.password ?? ''; this._networkShareTargetDatabaseNames = []; this._networkShareLocations = []; this._blobContainerTargetDatabaseNames = []; this._blobContainerResourceGroupDropdowns = []; this._blobContainerStorageAccountDropdowns = []; this._blobContainerDropdowns = []; this._blobContainerLastBackupFileDropdowns = []; this._blobContainerFolderDropdowns = []; this._inaccessibleStorageAccounts = []; if (this.migrationStateModel.isSqlMiTarget) { this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); } const originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames; const originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || []; const originalBlobs = this.migrationStateModel._databaseBackup.blobs; if (this.migrationStateModel._didUpdateDatabasesForMigration || this.migrationStateModel._didDatabaseMappingChange) { this.migrationStateModel._targetDatabaseNames = []; this.migrationStateModel._databaseBackup.networkShares = []; this.migrationStateModel._databaseBackup.blobs = []; } this.migrationStateModel._databasesForMigration.forEach((sourceDatabaseName, index) => { let targetDatabaseName = isSqlDbTarget ? this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? sourceDatabaseName : sourceDatabaseName; let networkShare = {}; let blob = {}; if (this.migrationStateModel._didUpdateDatabasesForMigration || this.migrationStateModel._didDatabaseMappingChange) { const dbIndex = this.migrationStateModel._sourceDatabaseNames?.indexOf(sourceDatabaseName); if (dbIndex > -1) { targetDatabaseName = isSqlDbTarget ? targetDatabaseName : originalTargetDatabaseNames[dbIndex] ?? targetDatabaseName; networkShare = originalNetworkShares[dbIndex] ?? networkShare; blob = originalBlobs[dbIndex] ?? blob; } else { // network share values are uniform for all dbs in the same migration, except for networkShareLocation const previouslySelectedNetworkShare = originalNetworkShares.length > 0 ? originalNetworkShares[0] : ''; if (previouslySelectedNetworkShare) { networkShare = { ...previouslySelectedNetworkShare, networkShareLocation: '', }; } } } this.migrationStateModel._targetDatabaseNames[index] = targetDatabaseName; this.migrationStateModel._databaseBackup.networkShares[index] = networkShare; this.migrationStateModel._databaseBackup.blobs[index] = blob; const targetDatabaseInput = this._view.modelBuilder.inputBox() .withProps({ required: true, value: targetDatabaseName, width: WIZARD_TABLE_COLUMN_WIDTH }).withValidation(c => { //Making sure no databases have duplicate values. if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; return false; } // Making sure if database with same name is not present on the target Azure SQL if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) { c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); return false; } if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; return false; } return true; }).component(); this._disposables.push( targetDatabaseInput.onTextChanged(async (value) => { this.migrationStateModel._targetDatabaseNames[index] = value.trim(); this._resetValidationUI(); await this.validateFields(targetDatabaseInput); })); targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; this._networkShareTargetDatabaseNames.push(targetDatabaseInput); const networkShareLocationInput = this._view.modelBuilder.inputBox() .withProps({ required: true, placeHolder: constants.NETWORK_SHARE_PATH_FORMAT, validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION, width: '300px' }).withValidation(c => { if (this.migrationStateModel.isBackupContainerNetworkShare) { if (c.value) { if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(c.value)) { return false; } } } return true; }).component(); this._disposables.push( networkShareLocationInput.onTextChanged(async (value) => { this.migrationStateModel._databaseBackup.networkShares[index].networkShareLocation = value.trim(); this._resetValidationUI(); await this.validateFields(networkShareLocationInput); })); networkShareLocationInput.value = this.migrationStateModel._databaseBackup.networkShares[index]?.networkShareLocation; this._networkShareLocations.push(networkShareLocationInput); const blobTargetDatabaseInput = this._view.modelBuilder.inputBox() .withProps({ required: true, value: targetDatabaseName, }).withValidation(c => { //Making sure no databases have duplicate values. if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; return false; } // Making sure if database with same name is not present on the target Azure SQL if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) { c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); return false; } if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; return false; } return true; }).component(); this._disposables.push( blobTargetDatabaseInput.onTextChanged( (value) => { this.migrationStateModel._targetDatabaseNames[index] = value.trim(); this._resetValidationUI(); })); targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput); const blobContainerResourceDropdown = this._view.modelBuilder.dropDown() .withProps({ ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP, editable: true, fireOnTextChange: true, required: true, }).component(); const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() .withProps({ ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT, editable: true, fireOnTextChange: true, required: true, enabled: false, }).component(); const blobContainerDropdown = this._view.modelBuilder.dropDown() .withProps({ ariaLabel: constants.BLOB_CONTAINER, editable: true, fireOnTextChange: true, required: true, enabled: false, }).component(); const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown() .withProps({ ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, editable: true, fireOnTextChange: true, required: true, enabled: false, }).component(); const blobContainerFolderDropdown = this._view.modelBuilder.dropDown() .withProps({ ariaLabel: constants.BLOB_CONTAINER_FOLDER, editable: true, fireOnTextChange: true, required: true, enabled: false, }).component(); this._disposables.push( blobContainerResourceDropdown.onValueChanged(async (value) => { if (value && value !== 'undefined' && this.migrationStateModel._resourceGroups) { const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); if (selectedResourceGroup && !blobResourceGroupErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = selectedResourceGroup; this.loadBlobStorageDropdown(index); await blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); } else { await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); } } })); this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); this._disposables.push( blobContainerStorageAccountDropdown.onValueChanged(async (value) => { if (value && value !== 'undefined') { const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); if (selectedStorageAccount && !blobStorageAccountErrorStrings.includes(value)) { const oldSelectedStorageAccount = this.migrationStateModel._databaseBackup.blobs[index].storageAccount ? this.migrationStateModel._databaseBackup.blobs[index].storageAccount.name : ''; this.migrationStateModel._databaseBackup.blobs[index].storageAccount = selectedStorageAccount; // check for storage account connectivity if ((this.migrationStateModel.isSqlMiTarget || this.migrationStateModel.isSqlVmTarget)) { if (this.migrationStateModel._databaseBackup.blobs.filter((e, i) => i !== index).every(blob => blob.storageAccount && blob.storageAccount.name.toLowerCase() !== oldSelectedStorageAccount.toLowerCase())) { this._inaccessibleStorageAccounts = this._inaccessibleStorageAccounts.filter(storageAccountName => storageAccountName.toLowerCase() !== oldSelectedStorageAccount.toLowerCase()); } if (!(await canTargetConnectToStorageAccount( this.migrationStateModel._targetType, this.migrationStateModel._targetServerInstance, selectedStorageAccount, this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription))) { this._inaccessibleStorageAccounts = this._inaccessibleStorageAccounts.filter(storageAccountName => storageAccountName.toLowerCase() !== selectedStorageAccount.name.toLowerCase()); this._inaccessibleStorageAccounts.push(selectedStorageAccount.name); } this.wizard.message = { text: this._inaccessibleStorageAccounts.length > 0 ? constants.STORAGE_ACCOUNT_CONNECTIVITY_WARNING(this.migrationStateModel._targetServerInstance.name, this._inaccessibleStorageAccounts, this.migrationStateModel.isSqlMiTarget) : '', level: azdata.window.MessageLevel.Warning } } await this.loadBlobContainerDropdown(index); await blobContainerDropdown.updateProperties({ enabled: true }); } else { await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); } } })); this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); this._disposables.push( blobContainerDropdown.onValueChanged(async (value) => { if (value && value !== 'undefined' && this.migrationStateModel._blobContainers) { const selectedBlobContainer = this.migrationStateModel._blobContainers.find(blob => blob.name === value); if (selectedBlobContainer && !blobContainerErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].blobContainer = selectedBlobContainer; // check for block blobs for SQL VM <= 2014 if (this.migrationStateModel.isSqlVmTarget && utils.isTargetSqlVm2014OrBelow(this.migrationStateModel._targetServerInstance as SqlVMServer)) { const backups = await utils.getBlobLastBackupFileNames( this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); const errorMessage = constants.INVALID_NON_PAGE_BLOB_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index]); this._nonPageBlobErrors = this._nonPageBlobErrors.filter(err => err !== errorMessage); const allBackupsPageBlob = backups.every(backup => backup.properties.blobType === 'PageBlob') if (!allBackupsPageBlob) { this._nonPageBlobErrors.push(errorMessage); } } if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { await this.loadBlobLastBackupFileDropdown(index); await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); } else { await this.loadBlobFolderDropdown(index); await blobContainerFolderDropdown.updateProperties({ enabled: true }); } } else { await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); } } })); this._blobContainerDropdowns.push(blobContainerDropdown); if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { this._disposables.push( blobContainerLastBackupFileDropdown.onValueChanged(value => { if (value && value !== 'undefined') { if (this.migrationStateModel._lastFileNames) { const selectedLastBackupFile = this.migrationStateModel._lastFileNames.find(fileName => fileName.name === value); if (selectedLastBackupFile && !blobFileErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = selectedLastBackupFile.name; } // check for duplicate storage account/blob container/last backup file combination if migrating multiple databases, // as they should all be unique - backups for multiple databases in the same location are not supported let backupLocations: string[] = []; backupLocations = this.migrationStateModel._databaseBackup.blobs.map(blob => { return blob && blob.storageAccount ? (blob.storageAccount.id + '/' + utils.getBlobContainerNameWithFolder(blob, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE)).toLowerCase() : ''; }).filter(backupLocation => backupLocation !== ''); let uniqueBackupLocations = [...new Set(backupLocations)]; if (!isSqlDbTarget && uniqueBackupLocations.length !== backupLocations.length) { this.wizard.message = { level: azdata.window.MessageLevel.Warning, text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_WARNING, }; } else { this.wizard.message = { text: '' }; } } } })); this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown); } else { this._disposables.push( blobContainerFolderDropdown.onValueChanged(value => { if (value && value !== 'undefined') { if (this.migrationStateModel._blobContainerFolders && this.migrationStateModel._blobContainerFolders.includes(value) && !blobFolderErrorStrings.includes(value)) { const selectedFolder = value; this.migrationStateModel._databaseBackup.blobs[index].folderName = selectedFolder; // check for duplicate storage account/blob container/folder combination if migrating multiple databases, // as they should all be unique - backups for multiple databases in the same location are not supported let backupLocations: string[] = []; backupLocations = this.migrationStateModel._databaseBackup.blobs.map(blob => { return blob && blob.storageAccount ? (blob.storageAccount.id + '/' + utils.getBlobContainerNameWithFolder(blob, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE)).toLowerCase() : ''; }).filter(backupLocation => backupLocation !== ''); let uniqueBackupLocations = [...new Set(backupLocations)]; if (!isSqlDbTarget && uniqueBackupLocations.length !== backupLocations.length) { this.wizard.message = { level: azdata.window.MessageLevel.Warning, text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_WARNING, }; } else { this.wizard.message = { text: '' }; } } } })); this._blobContainerFolderDropdowns.push(blobContainerFolderDropdown); } }); this.migrationStateModel._sourceDatabaseNames = this.migrationStateModel._databasesForMigration; const networkShareTargetData = this.migrationStateModel._databasesForMigration .map((db, index) => [ { value: db }, { value: this._networkShareTargetDatabaseNames[index] }, { value: this._networkShareLocations[index] }]); await this._networkShareTargetDatabaseNamesTable.setDataValues(networkShareTargetData); const blobContainerTargetData = this.migrationStateModel._databasesForMigration .map((db, index) => [ { value: db }, { value: this._blobContainerTargetDatabaseNames[index] }, { value: this._blobContainerResourceGroupDropdowns[index] }, { value: this._blobContainerStorageAccountDropdowns[index] }, { value: this._blobContainerDropdowns[index] }, { value: this._blobContainerLastBackupFileDropdowns[index] }, { value: this._blobContainerFolderDropdowns[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; if (errorText === constants.INVALID_OWNER_URI) { errorText = constants.DATABASE_BACKUP_PAGE_LOAD_ERROR; } this.wizard.message = { text: errorText, description: error?.stack, level: azdata.window.MessageLevel.Error }; } await this.validateFields(); this.updateValidationResultUI(true); } } private async _validateIr(): Promise { 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; } 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, }; } } } public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { 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++) { const storageAccount = this.migrationStateModel._databaseBackup.blobs[i].storageAccount; this.migrationStateModel._databaseBackup.blobs[i].storageKey = (await getStorageAccountAccessKeys( this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, storageAccount)).keyName1; } break; case NetworkContainerType.NETWORK_SHARE: // All network share migrations use the same storage account if (this.migrationStateModel._databaseBackup.networkShares?.length > 0) { const storageAccount = this.migrationStateModel._databaseBackup.networkShares[0]?.storageAccount; const storageKey = (await getStorageAccountAccessKeys( this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, storageAccount)).keyName1; for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { this.migrationStateModel._databaseBackup.networkShares[i].storageKey = storageKey; } } break; } } } } protected async handleStateChange(e: StateChangeEvent): Promise { } private _validateTableSelection(): boolean { for (const targetDatabaseInfo of this.migrationStateModel._sourceTargetMapping) { const databaseInfo = targetDatabaseInfo[1]; if (databaseInfo) { for (const sourceTable of databaseInfo.sourceTables) { const tableInfo = sourceTable[1]; if (tableInfo.selectedForMigration === true) { return true; } } } } return false; } private async validateFields(component?: azdata.Component): Promise { 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(); if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { await this._blobContainerLastBackupFileDropdowns[i]?.validate(); } else { await this._blobContainerFolderDropdowns[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 { this._networkShareContainerSubscription.value = this.migrationStateModel._targetSubscription.name; this._networkShareContainerLocation.value = this.migrationStateModel._location.displayName; this._blobContainerSubscription.value = this.migrationStateModel._targetSubscription.name; this._blobContainerLocation.value = this.migrationStateModel._location.displayName; this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel._targetSubscription; await this.loadNetworkStorageResourceGroup(); await this.loadBlobResourceGroup(); } private async loadNetworkStorageResourceGroup(): Promise { try { this._networkShareStorageAccountResourceGroupDropdown.loading = true; this.migrationStateModel._storageAccounts = await utils.getStorageAccounts( this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription); this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation( this.migrationStateModel._storageAccounts, this.migrationStateModel._location); this._networkShareStorageAccountResourceGroupDropdown.values = utils.getResourceDropdownValues( this.migrationStateModel._resourceGroups, constants.RESOURCE_GROUP_NOT_FOUND); utils.selectDefaultDropdownValue( this._networkShareStorageAccountResourceGroupDropdown, this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkStorageResourceGroup', error); } finally { this._networkShareStorageAccountResourceGroupDropdown.loading = false; this.loadNetworkShareStorageDropdown(); } } private loadNetworkShareStorageDropdown(): void { try { this._networkShareContainerStorageAccountDropdown.loading = true; this._networkShareStorageAccountResourceGroupDropdown.loading = true; this._networkShareContainerStorageAccountDropdown.values = utils.getAzureResourceDropdownValues( this.migrationStateModel._storageAccounts, this.migrationStateModel._location, this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.name, constants.NO_STORAGE_ACCOUNT_FOUND); utils.selectDefaultDropdownValue(this._networkShareContainerStorageAccountDropdown, this.migrationStateModel?._databaseBackup?.networkShares[0]?.storageAccount?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkShareStorageDropdown', error); } finally { this._networkShareContainerStorageAccountDropdown.loading = false; this._networkShareStorageAccountResourceGroupDropdown.loading = false; } } private async loadBlobResourceGroup(): Promise { try { this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); this.migrationStateModel._storageAccounts = await utils.getStorageAccounts( this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription); this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation( this.migrationStateModel._storageAccounts, this.migrationStateModel._location); const resourceGroupValues = utils.getResourceDropdownValues( this.migrationStateModel._resourceGroups, constants.RESOURCE_GROUP_NOT_FOUND); this._blobContainerResourceGroupDropdowns.forEach((dropDown, index) => { dropDown.values = resourceGroupValues; utils.selectDefaultDropdownValue(dropDown, this.migrationStateModel._databaseBackup?.blobs[index]?.resourceGroup?.id, false); }); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobResourceGroup', error); } finally { this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = false); } } private loadBlobStorageDropdown(index: number): void { 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( 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 { 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); dropDown.values = utils.getResourceDropdownValues( this.migrationStateModel._blobContainers, constants.NO_BLOBCONTAINERS_FOUND); 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 { 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 = 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; } } } private async loadBlobFolderDropdown(index: number): Promise { const dropDown = this._blobContainerFolderDropdowns[index]; if (dropDown) { try { dropDown.loading = true; this.migrationStateModel._blobContainerFolders = await utils.getBlobFolders(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); dropDown.values = utils.getBlobFolderValues(this.migrationStateModel._blobContainerFolders); utils.selectDefaultDropdownValue( dropDown, this.migrationStateModel._blobContainerFolders[0], false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobFolders', error); } finally { dropDown.loading = false; } } } private shouldDisplayBlobDropdownError(v: azdata.DropDownComponent, errorStrings: string[]) { return v.value === undefined || errorStrings.includes((v.value)?.displayName); } private async disableBlobTableDropdowns(rowIndex: number, columnName: string): Promise { const dropdownProps = { enabled: false, loading: false }; const createDropdownValuesWithPrereq = (displayName: string, name: string = '') => [{ displayName, name }]; if (this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE) { this._blobContainerLastBackupFileDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER); utils.selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[rowIndex], 0); await this._blobContainerLastBackupFileDropdowns[rowIndex]?.updateProperties(dropdownProps); } else { this._blobContainerFolderDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER); utils.selectDropDownIndex(this._blobContainerFolderDropdowns[rowIndex], 0); await this._blobContainerFolderDropdowns[rowIndex]?.updateProperties(dropdownProps); } if (columnName === constants.BLOB_CONTAINER) { return; } this._blobContainerDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_STORAGE_ACCOUNT); utils.selectDropDownIndex(this._blobContainerDropdowns[rowIndex], 0); await this._blobContainerDropdowns[rowIndex].updateProperties(dropdownProps); if (columnName === constants.STORAGE_ACCOUNT) { return; } this._blobContainerStorageAccountDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_RESOURCE_GROUP_PROMPT); utils.selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0); await this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps); } private _migrationTableSelectionContainer(): azdata.FlexContainer { const tableMappingHeader = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_TABLE_SELECTION_LABEL, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.SECTION_HEADER_CSS } }).component(); const tableMappingText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_TABLE_SELECTION_DESCRIPTION, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS } }).component(); const tableMappingInfoBox = this._view.modelBuilder.infoBox() .withProps({ text: constants.DATABASE_SCHEMA_MIGRATION_HELP, style: 'information', width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { ...styles.BODY_CSS, 'margin': '5px 0 0 0' }, links: [ { text: constants.DATABASE_SCHEMA_MIGRATION_DACPAC_EXTENSION, url: 'https://learn.microsoft.com/sql/azure-data-studio/extensions/sql-server-dacpac-extension' }, { text: constants.DATABASE_SCHEMA_MIGRATION_PROJECTS_EXTENSION, url: 'https://learn.microsoft.com/sql/azure-data-studio/extensions/sql-database-project-extension' }, ] }).component(); this._refreshButton = this._view.modelBuilder.button() .withProps({ buttonType: azdata.ButtonType.Normal, iconHeight: 16, iconWidth: 16, iconPath: IconPathHelper.refresh, label: constants.DATABASE_TABLE_REFRESH_LABEL, width: 70, CSSStyles: { 'margin': '15px 0 0 0' }, }) .component(); this._disposables.push( this._refreshButton.onDidClick( async e => await this._loadTableData())); this._refreshLoading = this._view.modelBuilder.loadingComponent() .withItem(this._refreshButton) .withProps({ loading: false }) .component(); const cssClass = undefined; this._databaseTable = this._view.modelBuilder.table() .withProps({ forceFitColumns: azdata.ColumnSizingMode.AutoFit, height: '600px', CSSStyles: { 'margin': '15px 0 0 0' }, data: [], columns: [ { name: constants.DATABASE_TABLE_SOURCE_DATABASE_COLUMN_LABEL, value: 'sourceDatabase', width: 200, type: azdata.ColumnType.text, cssClass: cssClass, headerCssClass: cssClass, }, { name: constants.DATABASE_TABLE_TARGET_DATABASE_COLUMN_LABEL, value: 'targetDataase', width: 200, type: azdata.ColumnType.text, cssClass: cssClass, headerCssClass: cssClass, }, { name: constants.DATABASE_TABLE_SELECTED_TABLES_COLUMN_LABEL, value: 'selectedTables', width: 160, type: azdata.ColumnType.hyperlink, cssClass: cssClass, headerCssClass: cssClass, icon: IconPathHelper.edit, }, ], }) .withValidation(table => { if (this.migrationStateModel.isSqlDbTarget) { return this._validateTableSelection(); } return true; }) .component(); this._disposables.push( this._databaseTable.onCellAction!( async (rowState: azdata.ICellActionEventArgs) => { const buttonState = rowState; if (buttonState?.column === 2) { // open table selection dialog const sourceDatabaseName = this._databaseTable.data[rowState.row][0]; const targetDatabaseInfo = this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName); const targetDatabaseName = targetDatabaseInfo?.databaseName; if (sourceDatabaseName && targetDatabaseName) { await this._openTableSelectionDialog( sourceDatabaseName, targetDatabaseName, () => this._loadTableData()); } } })); return this._view.modelBuilder.flexContainer() .withItems([ tableMappingHeader, tableMappingText, tableMappingInfoBox, this._refreshLoading, this._databaseTable]) .withLayout({ flexFlow: 'column' }) .component(); } private async _openTableSelectionDialog( sourceDatabaseName: string, targetDatabaseName: string, onSaveCallback: () => Promise): Promise { const dialog = new TableMigrationSelectionDialog( this.migrationStateModel, sourceDatabaseName, onSaveCallback); await dialog.openDialog( constants.SELECT_DATABASE_TABLES_TITLE(targetDatabaseName)); } private async _loadTableData(): Promise { this._refreshLoading.loading = true; this.wizard.message = { text: '' }; const data: any[][] = []; this.migrationStateModel._sourceTargetMapping.forEach((targetDatabaseInfo, sourceDatabaseName) => { if (sourceDatabaseName) { const tableCount = targetDatabaseInfo?.sourceTables.size ?? 0; const hasTables = tableCount > 0; let selectedCount = 0; targetDatabaseInfo?.sourceTables.forEach( tableInfo => selectedCount += tableInfo.selectedForMigration ? 1 : 0); const hasSelectedTables = hasTables && selectedCount > 0; data.push([ sourceDatabaseName, targetDatabaseInfo?.databaseName, { // table selection icon: hasSelectedTables ? IconPathHelper.completedMigration : IconPathHelper.edit, title: hasTables ? constants.TABLE_SELECTION_COUNT(selectedCount, tableCount) : constants.TABLE_SELECTION_EDIT, }]); } }); await this._databaseTable.updateProperty('data', data); this._refreshLoading.loading = false; } }