/*--------------------------------------------------------------------------------------------- * 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 } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { IconPathHelper } from '../constants/iconPathHelper'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import { findDropDownItemIndex, selectDropDownIndex } from '../api/utils'; const WIZARD_TABLE_COLUMN_WIDTH = '200px'; const WIZARD_TABLE_COLUMN_WIDTH_SMALL = '170px'; const blobResourceGroupErrorStrings = [constants.RESOURCE_GROUP_NOT_FOUND]; const blobStorageAccountErrorStrings = [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP]; const blobContainerErrorStrings = [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT]; const blobFileErrorStrings = [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER]; export class DatabaseBackupPage extends MigrationWizardPage { private _view!: azdata.ModelView; private _networkShareButton!: azdata.RadioButtonComponent; private _blobContainerButton!: azdata.RadioButtonComponent; private _networkShareContainer!: azdata.FlexContainer; private _windowsUserAccountText!: azdata.InputBoxComponent; private _passwordText!: azdata.InputBoxComponent; private _networkSharePath!: 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 _networkShareStorageAccountDetails!: azdata.FlexContainer; private _networkShareContainerSubscription!: azdata.TextComponent; private _networkShareContainerLocation!: azdata.TextComponent; private _networkShareStorageAccountResourceGroupDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountRefreshButton!: azdata.ButtonComponent; private _targetDatabaseContainer!: azdata.FlexContainer; private _newtworkShareTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent; private _blobContainerTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent; private _networkTableContainer!: azdata.FlexContainer; private _blobTableContainer!: azdata.FlexContainer; private _networkShareTargetDatabaseNames: azdata.InputBoxComponent[] = []; private _blobContainerTargetDatabaseNames: azdata.InputBoxComponent[] = []; private _existingDatabases: string[] = []; private _disposables: vscode.Disposable[] = []; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel); } protected async registerContent(view: azdata.ModelView): Promise { this._view = view; const radioButtonContainer = this.createBackupLocationComponent(); const networkDetailsContainer = this.createNetworkDetailsContainer(); this._targetDatabaseContainer = this.createTargetDatabaseContainer(); this._networkShareStorageAccountDetails = this.createNetworkShareStorageAccountDetailsContainer(); const form = this._view.modelBuilder.formContainer() .withFormItems( [ { title: '', component: radioButtonContainer }, { title: '', component: networkDetailsContainer }, { title: '', component: this._targetDatabaseContainer }, { title: '', component: this._networkShareStorageAccountDetails } ] ); this._disposables.push(this._view.onClosed(e => { this._disposables.forEach( d => { try { d.dispose(); } catch { } }); })); await view.initializeModel(form.component()); } private createBackupLocationComponent(): azdata.FlexContainer { const buttonGroup = 'networkContainer'; const selectLocationText = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_PAGE_DESCRIPTION, CSSStyles: { 'font-size': '13px' } }).component(); this._networkShareButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL, CSSStyles: { 'font-size': '13px' } }).component(); this._disposables.push(this._networkShareButton.onDidChangeCheckedState(async (e) => { if (e) { await this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE); } })); this._blobContainerButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL, CSSStyles: { 'font-size': '13px' } }).component(); this._disposables.push(this._blobContainerButton.onDidChangeCheckedState(async (e) => { if (e) { 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(); const networkContainer = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).withItems([ this._networkShareContainer, this._blobContainer, ]).component(); return networkContainer; } private createNetworkShareContainer(): azdata.FlexContainer { const sqlSourceHeader = this._view.modelBuilder.text().withProps({ value: constants.SOURCE_CREDENTIALS, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '14px', 'font-weight': 'bold' } }).component(); this._sourceHelpText = this._view.modelBuilder.text().withProps({ width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', } }).component(); const usernameLabel = this._view.modelBuilder.text().withProps({ value: constants.USERNAME, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold', } }).component(); this._sqlSourceUsernameInput = this._view.modelBuilder.inputBox().withProps({ required: true, enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, }).component(); this._disposables.push(this._sqlSourceUsernameInput.onTextChanged(value => { this.migrationStateModel._sqlServerUsername = value; })); const sqlPasswordLabel = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold', } }).component(); this._sqlSourcepassword = this._view.modelBuilder.inputBox().withProps({ required: true, inputType: 'password', width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._disposables.push(this._sqlSourcepassword.onTextChanged(value => { this.migrationStateModel._sqlServerPassword = value; })); const networkShareHeading = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '14px', 'font-weight': 'bold' } }).component(); const networkShareHelpText = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', } }).component(); const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, description: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_INFO, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); this._networkSharePath = this._view.modelBuilder.inputBox().withProps({ placeHolder: constants.NETWORK_SHARE_PATH, validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).withValidation((component) => { if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { if (component.value) { if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(component.value)) { return false; } } } return true; }).component(); this._disposables.push(this._networkSharePath.onTextChanged(async (value) => { await this.validateFields(); this.migrationStateModel._databaseBackup.networkShare.networkShareLocation = value; })); const networkShareInfoBox = this._view.modelBuilder.infoBox().withProps({ text: constants.DATABASE_SERVICE_ACCOUNT_INFO_TEXT, style: 'information', width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', 'margin-top': '10px' } }).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: { 'font-size': '13px', 'font-weight': 'bold' } }).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 }) .withValidation((component) => { if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { if (component.value) { if (!/^[A-Za-z0-9\\\._-]{7,}$/.test(component.value)) { return false; } } } return true; }).component(); this._disposables.push(this._windowsUserAccountText.onTextChanged((value) => { this.migrationStateModel._databaseBackup.networkShare.windowsUser = value; })); const passwordLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).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 }).component(); this._disposables.push(this._passwordText.onTextChanged((value) => { this.migrationStateModel._databaseBackup.networkShare.password = value; })); const flexContainer = this._view.modelBuilder.flexContainer().withItems( [ sqlSourceHeader, this._sourceHelpText, usernameLabel, this._sqlSourceUsernameInput, sqlPasswordLabel, this._sqlSourcepassword, networkShareHeading, networkShareHelpText, networkLocationInputBoxLabel, this._networkSharePath, networkShareInfoBox, windowsUserAccountLabel, this._windowsUserAccountText, passwordLabel, this._passwordText, ] ).withLayout({ flexFlow: 'column' }).withProps({ display: 'none' }).component(); return flexContainer; } 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: { 'font-size': '14px', 'font-weight': 'bold' } }).component(); const blobHelpText = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_HELP_TEXT, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', } }).component(); const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); this._blobContainerSubscription = this._view.modelBuilder.text() .withProps({ enabled: false, CSSStyles: { 'font-size': '13px', 'margin': '0px' } }).component(); const locationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); this._blobContainerLocation = this._view.modelBuilder.text() .withProps({ enabled: false, CSSStyles: { 'font-size': '13px', 'margin': '0px 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 = { 'border': 'none', 'font-size': '13px', 'font-weight': 'bold', 'text-align': 'left', 'box-shadow': 'inset 0px -1px 0px #F3F2F1', }; const rowCssStyle: azdata.CssStyles = { '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: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); const blobTableText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_TABLE_HELP_TEXT, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); this._newtworkShareTargetDatabaseNamesTable = 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: '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, }, { displayName: constants.TARGET_DATABASE_NAME, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH }, { displayName: constants.RESOURCE_GROUP, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH }, { displayName: constants.STORAGE_ACCOUNT, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH }, { displayName: constants.BLOB_CONTAINER, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH }, { displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, valueType: azdata.DeclarativeDataType.component, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH, hidden: true } ] }).component(); this._networkTableContainer = this._view.modelBuilder.flexContainer().withItems([ networkShareTableText, this._newtworkShareTargetDatabaseNamesTable ]).component(); const allFieldsRequiredLabel = this._view.modelBuilder.text() .withProps({ value: constants.ALL_FIELDS_REQUIRED, CSSStyles: { 'font-size': '13px', } }).component(); this._blobTableContainer = this._view.modelBuilder.flexContainer().withItems([ blobTableText, allFieldsRequiredLabel, this._blobContainerTargetDatabaseNamesTable ]).component(); const container = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).withItems([ this._networkTableContainer, this._blobTableContainer ]).withProps({ 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: { 'font-size': '14px', 'font-weight': 'bold' } }).component(); const azureAccountHelpText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', } }).component(); const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.SUBSCRIPTION, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); this._networkShareContainerSubscription = this._view.modelBuilder.text() .withProps({ enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', 'margin': '0px 0px' } }).component(); const locationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); this._networkShareContainerLocation = this._view.modelBuilder.text() .withProps({ enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, CSSStyles: { 'font-size': '13px', 'margin': '0px 0px' } }).component(); const resourceGroupLabel = this._view.modelBuilder.text() .withProps({ value: constants.RESOURCE_GROUP, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).component(); this._networkShareStorageAccountResourceGroupDropdown = this._view.modelBuilder.dropDown() .withProps({ required: true, ariaLabel: constants.RESOURCE_GROUP, width: WIZARD_INPUT_COMPONENT_WIDTH, editable: true, fireOnTextChange: true, }).component(); this._disposables.push(this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(async (value) => { const selectedIndex = findDropDownItemIndex(this._networkShareStorageAccountResourceGroupDropdown, value); if (selectedIndex > -1) { this.migrationStateModel._databaseBackup.networkShare.resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); await this.loadNetworkShareStorageDropdown(); } })); const storageAccountLabel = this._view.modelBuilder.text() .withProps({ value: constants.STORAGE_ACCOUNT, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, CSSStyles: { 'font-size': '13px', 'font-weight': 'bold' } }).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((value) => { const selectedIndex = findDropDownItemIndex(this._networkShareContainerStorageAccountDropdown, value); if (selectedIndex > -1) { this.migrationStateModel._databaseBackup.networkShare.storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex); } })); 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 (value) => { await this.loadNetworkShareStorageDropdown(); })); const storageAccountContainer = this._view.modelBuilder.flexContainer().component(); storageAccountContainer.addItem(this._networkShareContainerStorageAccountDropdown, { flex: '0 0 auto' }); storageAccountContainer.addItem(this._networkShareContainerStorageAccountRefreshButton, { flex: '0 0 auto', CSSStyles: { 'margin-left': '5px' } }); const container = 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(); return container; } public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { if (this.migrationStateModel.refreshDatabaseBackupPage) { const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE; const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1; this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration; this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => { column.width = isOfflineMigration ? WIZARD_TABLE_COLUMN_WIDTH_SMALL : WIZARD_TABLE_COLUMN_WIDTH; }); this._networkShareButton.checked = false; this._networkTableContainer.display = 'none'; await this._networkShareContainer.updateCssStyles({ 'display': 'none' }); this._blobContainerButton.checked = false; this._blobTableContainer.display = 'none'; await this._blobContainer.updateCssStyles({ 'display': 'none' }); await this._targetDatabaseContainer.updateCssStyles({ 'display': 'none' }); await this._networkShareStorageAccountDetails.updateCssStyles({ 'display': 'none' }); const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile(); const queryProvider = azdata.dataprotocol.getProvider((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); const username = results.rows[0][0].displayValue; this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.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._networkShareTargetDatabaseNames = []; this._blobContainerTargetDatabaseNames = []; this._blobContainerResourceGroupDropdowns = []; this._blobContainerStorageAccountDropdowns = []; this._blobContainerDropdowns = []; this._blobContainerLastBackupFileDropdowns = []; if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); } this.migrationStateModel._targetDatabaseNames = []; this.migrationStateModel._databaseBackup.blobs = []; this.migrationStateModel._migrationDbs.forEach((db, index) => { this.migrationStateModel._targetDatabaseNames.push(''); this.migrationStateModel._databaseBackup.blobs.push({}); const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ required: true, value: db, width: WIZARD_TABLE_COLUMN_WIDTH }).withValidation(c => { if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; return false; } if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(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(); await this.validateFields(); })); this._networkShareTargetDatabaseNames.push(targetDatabaseInput); const blobtargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ required: true, value: db, }).withValidation(c => { if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; return false; } if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(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._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(); this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => { const selectedIndex = findDropDownItemIndex(blobContainerResourceDropdown, value); if (selectedIndex > -1 && !blobResourceGroupErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); await 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) => { const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value); if (selectedIndex > -1 && !blobStorageAccountErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex); 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) => { const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value); if (selectedIndex > -1 && !blobContainerErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(selectedIndex); if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { await this.loadBlobLastBackupFileDropdown(index); await blobContainerLastBackupFileDropdown.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 => { const selectedIndex = findDropDownItemIndex(blobContainerLastBackupFileDropdown, value); if (selectedIndex > -1 && !blobFileErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = this.migrationStateModel.getBlobLastBackupFileName(selectedIndex); } })); this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown); } }); let data: azdata.DeclarativeTableCellValue[][] = []; this.migrationStateModel._migrationDbs.forEach((db, index) => { const targetRow: azdata.DeclarativeTableCellValue[] = []; targetRow.push({ value: db }); targetRow.push({ value: this._networkShareTargetDatabaseNames[index] }); data.push(targetRow); }); this._newtworkShareTargetDatabaseNamesTable.dataValues = data; data = []; this.migrationStateModel._migrationDbs.forEach((db, index) => { const targetRow: azdata.DeclarativeTableCellValue[] = []; targetRow.push({ value: db }); targetRow.push({ value: this._blobContainerTargetDatabaseNames[index] }); targetRow.push({ value: this._blobContainerResourceGroupDropdowns[index] }); targetRow.push({ value: this._blobContainerStorageAccountDropdowns[index] }); targetRow.push({ value: this._blobContainerDropdowns[index] }); targetRow.push({ value: this._blobContainerLastBackupFileDropdowns[index] }); data.push(targetRow); }); await this._blobContainerTargetDatabaseNamesTable.setDataValues(data); await this.getSubscriptionValues(); this.migrationStateModel.refreshDatabaseBackupPage = false; } this.wizard.registerNavigationValidator((pageChangeInfo) => { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { return true; } const errors: string[] = []; 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, [constants.RESOURCE_GROUP_NOT_FOUND])) { errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._migrationDbs[index])); } }); this._blobContainerStorageAccountDropdowns.forEach((v, index) => { if (this.shouldDisplayBlobDropdownError(v, [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP])) { errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._migrationDbs[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._migrationDbs[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._migrationDbs[index])); } }); } 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._migrationDbs[index]).join(', ')}`; errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString); } }); } break; } this.migrationStateModel._targetDatabaseNames.forEach(t => { if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(t)) { // Making sure if database with same name is not present on the target Azure SQL 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 { try { if (pageChangeInfo.newPage > pageChangeInfo.lastPage) { 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: const storageAccount = this.migrationStateModel._databaseBackup.networkShare.storageAccount; this.migrationStateModel._databaseBackup.networkShare.storageKey = (await getStorageAccountAccessKeys( this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, storageAccount)).keyName1; break; } } } finally { this.wizard.registerNavigationValidator((pageChangeInfo) => { return true; }); } } protected async handleStateChange(e: StateChangeEvent): Promise { } private async switchNetworkContainerFields(containerType: NetworkContainerType): Promise { this.wizard.message = { text: '', level: azdata.window.MessageLevel.Error }; this.wizard.nextButton.enabled = true; this.migrationStateModel._databaseBackup.networkContainerType = containerType; await this._blobContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none' }); await this._networkShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' }); await this._networkShareStorageAccountDetails.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' }); await this._targetDatabaseContainer.updateCssStyles({ 'display': 'inline' }); this._networkTableContainer.display = (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none'; this._blobTableContainer.display = (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none'; //Preserving the database Names between the 2 tables. this.migrationStateModel._targetDatabaseNames?.forEach((v, index) => { this._networkShareTargetDatabaseNames[index].value = v; this._blobContainerTargetDatabaseNames[index].value = v; }); await this._windowsUserAccountText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); await this._passwordText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); await this._sqlSourceUsernameInput.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); await this._sqlSourcepassword.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); await this.validateFields(); } private async validateFields(): Promise { await this._sqlSourceUsernameInput.validate(); await this._sqlSourcepassword.validate(); await this._networkSharePath.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._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(); } } } private async getSubscriptionValues(): Promise { this._networkShareContainerSubscription.value = this.migrationStateModel._targetSubscription.name; this._networkShareContainerLocation.value = await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); this._blobContainerSubscription.value = this.migrationStateModel._targetSubscription.name; this._blobContainerLocation.value = await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel._targetSubscription; await this.loadNetworkStorageResourceGroup(); await this.loadBlobResourceGroup(); } private async loadNetworkStorageResourceGroup(): Promise { this._networkShareStorageAccountResourceGroupDropdown.loading = true; try { this._networkShareStorageAccountResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); selectDropDownIndex(this._networkShareStorageAccountResourceGroupDropdown, 0); } catch (error) { console.log(error); } finally { this._networkShareStorageAccountResourceGroupDropdown.loading = false; await this.loadNetworkShareStorageDropdown(); } } private async loadNetworkShareStorageDropdown(): Promise { this._networkShareContainerStorageAccountDropdown.loading = true; try { this._networkShareContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.networkShare.resourceGroup); selectDropDownIndex(this._networkShareContainerStorageAccountDropdown, 0); } catch (error) { console.log(error); } finally { this._networkShareContainerStorageAccountDropdown.loading = false; } } private async loadBlobResourceGroup(): Promise { this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); try { const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); this._blobContainerResourceGroupDropdowns.forEach(dropDown => { dropDown.values = resourceGroupValues; selectDropDownIndex(dropDown, 0); }); } catch (error) { console.log(error); } finally { this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = false); } } private async loadBlobStorageDropdown(index: number): Promise { this._blobContainerStorageAccountDropdowns[index].loading = true; try { this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].resourceGroup); selectDropDownIndex(this._blobContainerStorageAccountDropdowns[index], 0); } catch (error) { console.log(error); } finally { this._blobContainerStorageAccountDropdowns[index].loading = false; } } private async loadBlobContainerDropdown(index: number): Promise { this._blobContainerDropdowns[index].loading = true; try { const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount); this._blobContainerDropdowns[index].values = blobContainerValues; selectDropDownIndex(this._blobContainerDropdowns[index], 0); } catch (error) { console.log(error); } finally { this._blobContainerDropdowns[index].loading = false; } } private async loadBlobLastBackupFileDropdown(index: number): Promise { this._blobContainerLastBackupFileDropdowns[index].loading = true; try { const blobLastBackupFileValues = await this.migrationStateModel.getBlobLastBackupFileNameValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount, this.migrationStateModel._databaseBackup.blobs[index].blobContainer); this._blobContainerLastBackupFileDropdowns[index].values = blobLastBackupFileValues; selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[index], 0); } catch (error) { console.log(error); } finally { this._blobContainerLastBackupFileDropdowns[index].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); selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[rowIndex], 0); await this._blobContainerLastBackupFileDropdowns[rowIndex]?.updateProperties(dropdownProps); } if (columnName === constants.BLOB_CONTAINER) { return; } this._blobContainerDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_STORAGE_ACCOUNT); 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); selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0); await this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps); } }