diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 479ed91f3b..97ecc25f31 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -2,7 +2,7 @@ "name": "sql-migration", "displayName": "%displayName%", "description": "%description%", - "version": "0.0.12", + "version": "0.0.13", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 12cf0ae9f0..ef34bbc74a 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -347,11 +347,11 @@ export interface StartDatabaseMigrationRequest { sourceDatabaseName: string, migrationService: string, backupConfiguration: { - targetLocation: { + targetLocation?: { storageAccountResourceId: string, accountKey: string, }, - sourceLocation: SourceLocation + sourceLocation?: SourceLocation }, sourceSqlConnection: { authentication: string, @@ -416,8 +416,8 @@ export interface SqlConnectionInfo { } export interface BackupConfiguration { - sourceLocation: SourceLocation; - targetLocation: TargetLocation; + sourceLocation?: SourceLocation; + targetLocation?: TargetLocation; } export interface AutoCutoverConfiguration { @@ -454,7 +454,7 @@ export interface TargetLocation { export interface BackupFileInfo { fileName: string; - status: string; + status: 'Arrived' | 'Uploading' | 'Uploaded' | 'Restoring' | 'Restored' | 'Cancelled' | 'Ignored'; } export interface DatabaseMigrationFileShare { diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index a277176e53..0cfa75c887 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -51,7 +51,8 @@ export const VIEW_SELECT_BUTTON_LABEL = localize('sql.migration.view.select.butt export function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string { return localize('total.databases.selected', "{0} of {1} Database(s) selected.", selectedDbCount, totalDbCount); } - +export const SELECT_TARGET_TO_CONTINUE = localize('sql.migration.select.target.to.continue', "Please select a target to continue"); +export const SELECT_DATABASE_TO_MIGRATE = localize('sql.migration.select.database.to.migrate', "Please select databases to migrate"); export const ASSESSMENT_COMPLETED = (serverName: string): string => { return localize('sql.migration.generic.congratulations', "We have completed the assessment of your SQL Server Instance '{0}'.", serverName); }; @@ -83,6 +84,8 @@ export const DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL = localize('sql.migrat export const DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL = localize('sql.migration.nc.blob.storage.radio.label', "My database backups are in an Azure Storage Blob Container (Coming soon)"); export const DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL = localize('sql.migration.nc.file.share.radio.label', "My database backups are in an Azure Storage File Share (Coming soon)"); + + export const DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT = localize('sql.migration.network.share.header.text', "Network share details"); export const DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT = localize('sql.migration.network.share.help.text', "Provide the network share location that contains backups and the user credentials that has read access to the share"); export const DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL = localize('sql.migration.network.share.location.label', "Network share location that contains backups."); @@ -97,8 +100,8 @@ export const DATABASE_BACKUP_SUBSCRIPTION_PLACEHOLDER = localize('sql.migration. export const DATABASE_BACKUP_NETWORK_SHARE_NETWORK_STORAGE_ACCOUNT_LABEL = localize('sql.migration.network.share.storage.account.label', "Select the storage account where backup files will be copied."); export const DATABASE_BACKUP_STORAGE_ACCOUNT_PLACEHOLDER = localize('sql.migration.network.share.storage.account.placeholder', "Select account"); export const DUPLICATE_NAME_ERROR = localize('sql.migration.unique.name', "Select a unique name for this target database"); -export function DATABASE_ALREADY_EXISTS_MI(targetName: string): string { - return localize('sql.migration.database.already.exists', "Database with the same name already exists on target Managed Instance '{0}'", targetName); +export function DATABASE_ALREADY_EXISTS_MI(dbName: string, targetName: string): string { + return localize('sql.migration.database.already.exists', "Database '{0}' already exists on target Managed Instance '{1}'.", dbName, targetName); } export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Select the subscription that contains the storage account."); export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_LABEL = localize('sql.migration.blob.storage.account.label', "Select the storage account that contains the backup files."); @@ -128,9 +131,6 @@ export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare export const INVALID_BLOBCONTAINER_ERROR = localize('sql.migration.invalid.blobContainer.error', "Please select a valid blob container to proceed."); export const INVALID_NETWORK_SHARE_LOCATION = localize('sql.migration.invalid.network.share.location', "Invalid network share location format. Example: {0}", '\\\\Servername.domainname.com\\Backupfolder'); export const INVALID_USER_ACCOUNT = localize('sql.migration.invalid.user.account', "Invalid user account format. Example: {0}", 'Domain\\username'); -export function TARGET_NAME_FOR_DATABASE(dbName: string): string { - return localize('sql.migration.target.name.for.database', 'Target name for database ‘{0}’', dbName); -} export function TARGET_NETWORK_SHARE_LOCATION(dbName: string): string { return localize('sql.migration.network.share.location', "Network share location to read backups for database ‘{0}’", dbName); } @@ -236,6 +236,7 @@ export const MODE = localize('sql.migration.mode', "Mode"); export const BACKUP_LOCATION = localize('sql.migration.backup.location', "Backup Location"); export const AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS = localize('sql.migration.azure.storage.account.to.upload.backups', "Azure Storage Account to Upload Backups"); export const SHIR = localize('sql.migration.shir', "Self-hosted Integration Runtime node"); +export const TARGET_NAME = localize('sql.migration.summary.target.name', "Target Databases:"); // Open notebook quick pick string export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pick.placeholder', "Select the operation you'd like to perform"); diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index b7a953f81b..2af17aa4c7 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -538,7 +538,11 @@ export class MigrationCutoverDialog { }); if (migrationStatusTextValue === MigrationStatus.InProgress) { - this._cutoverButton.enabled = tableData.length > 0; + const restoredCount = (this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets.filter(a => a.listOfBackupFiles[0].status === 'Restored'))?.length!; + if (restoredCount > 0) { + this._cutoverButton.enabled = true; + } + this._cancelButton.enabled = true; } else { this._cutoverButton.enabled = false; this._cancelButton.enabled = false; diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index 85f69baccf..663acac08c 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -28,11 +28,13 @@ export class MigrationLocalStorage { if (refreshStatus) { try { const backupConfiguration = migration.migrationContext.properties.backupConfiguration; + const sourceDatabase = migration.migrationContext.properties.sourceDatabaseName; migration.migrationContext = await getMigrationStatus( migration.azureAccount, migration.subscription, migration.migrationContext ); + migration.migrationContext.properties.sourceDatabaseName = sourceDatabase; migration.migrationContext.properties.backupConfiguration = backupConfiguration; if (migration.asyncUrl) { migration.asyncOperationResult = await getMigrationAsyncOperationDetails( diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 2165896c04..c98431f048 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -55,26 +55,29 @@ export enum NetworkContainerType { NETWORK_SHARE } +export interface DatabaseBackupModel { + migrationMode: MigrationMode; + networkContainerType: NetworkContainerType; + storageKey: string; + networkShare: NetworkShare; + subscription: azureResource.AzureResourceSubscription; + blob: Blob; +} + export interface NetworkShare { networkShareLocation: string; windowsUser: string; password: string; -} - -export interface DatabaseBackupModel { - migrationMode: MigrationMode; - networkContainerType: NetworkContainerType; - networkShareLocation: string; - windowsUser: string; - password: string; - subscription: azureResource.AzureResourceSubscription; resourceGroup: azureResource.AzureResourceResourceGroup; storageAccount: StorageAccount; - storageKey: string; - azureSecurityToken: string; - fileShares: azureResource.FileShare[]; - blobContainers: azureResource.BlobContainer[]; } + +export interface Blob { + resourceGroup: azureResource.AzureResourceResourceGroup; + storageAccount: StorageAccount; + blobContainer: azureResource.BlobContainer; +} + export interface Model { readonly sourceConnectionId: string; readonly currentState: State; @@ -140,6 +143,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { ) { this._currentState = State.INIT; this._databaseBackup = {} as DatabaseBackupModel; + this._databaseBackup.networkShare = {} as NetworkShare; + this._databaseBackup.blob = {} as Blob; } public get sourceConnectionId(): string { @@ -490,15 +495,15 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._targetSqlVirtualMachines[index]; } - public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise { + public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { let storageAccountValues: azdata.CategoryValue[] = []; - if (!this._databaseBackup.resourceGroup) { + if (!resourceGroup) { return storageAccountValues; } try { const storageAccount = (await getAvailableStorageAccounts(this._azureAccount, subscription)); this._storageAccounts = storageAccount.filter(sa => { - return sa.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sa.resourceGroup?.toLowerCase() === this._databaseBackup.resourceGroup.name.toLowerCase(); + return sa.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sa.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase(); }); this._storageAccounts.forEach((storageAccount) => { storageAccountValues.push({ @@ -652,19 +657,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { properties: { sourceDatabaseName: '', migrationService: this._sqlMigrationService?.id!, - backupConfiguration: { - targetLocation: { - storageAccountResourceId: this._databaseBackup.storageAccount.id, - accountKey: this._databaseBackup.storageKey, - }, - sourceLocation: { - fileShare: { - path: this._databaseBackup.networkShareLocation, - username: this._databaseBackup.windowsUser, - password: this._databaseBackup.password, - } - }, - }, + backupConfiguration: {}, sourceSqlConnection: { dataSource: currentConnection?.serverName!, authentication: this._authenticationType, @@ -674,11 +667,39 @@ export class MigrationStateModel implements Model, vscode.Disposable { scope: this._targetServerInstance.id } }; + switch (this._databaseBackup.networkContainerType) { + case NetworkContainerType.BLOB_CONTAINER: + requestBody.properties.backupConfiguration = { + targetLocation: undefined!, + sourceLocation: { + azureBlob: { + storageAccountResourceId: this._databaseBackup.blob.storageAccount.id, + accountKey: this._databaseBackup.storageKey, + blobContainerName: this._databaseBackup.blob.blobContainer.name + } + } + }; + break; + case NetworkContainerType.NETWORK_SHARE: + requestBody.properties.backupConfiguration = { + targetLocation: { + storageAccountResourceId: this._databaseBackup.networkShare.storageAccount.id, + accountKey: this._databaseBackup.storageKey, + }, + sourceLocation: { + fileShare: { + path: this._databaseBackup.networkShare.networkShareLocation, + username: this._databaseBackup.networkShare.windowsUser, + password: this._databaseBackup.networkShare.password, + } + } + }; + break; + } for (let i = 0; i < this._migrationDbs.length; i++) { - requestBody.properties.sourceDatabaseName = this._migrationDbs[i]; try { - requestBody.properties.backupConfiguration.sourceLocation.fileShare!.path = this._databaseBackup.networkShareLocation; + requestBody.properties.sourceDatabaseName = this._migrationDbs[i]; const response = await startDatabaseMigration( this._azureAccount, this._targetSubscription, @@ -687,6 +708,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { this._targetDatabaseNames[i], requestBody ); + response.databaseMigration.properties.sourceDatabaseName = this._migrationDbs[i]; response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!; if (response.status === 201 || response.status === 200) { MigrationLocalStorage.saveMigration( diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 197282ef85..82a719759c 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -16,70 +16,93 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _view!: azdata.ModelView; private _networkShareContainer!: azdata.FlexContainer; + private _windowsUserAccountText!: azdata.InputBoxComponent; + private _passwordText!: azdata.InputBoxComponent; + private _networkSharePath!: azdata.InputBoxComponent; + + private _blobContainer!: azdata.FlexContainer; + private _blobContainerSubscription!: azdata.InputBoxComponent; + private _blobContainerLocation!: azdata.InputBoxComponent; + private _blobContainerResourceGroup!: azdata.DropDownComponent; + private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent; + private _blobContainerDropdown!: azdata.DropDownComponent; + + private _fileShareContainer!: azdata.FlexContainer; + private _fileShareSubscription!: azdata.InputBoxComponent; + private _fileShareStorageAccountDropdown!: azdata.DropDownComponent; + + private _networkShareStorageAccountDetails!: azdata.FlexContainer; private _networkShareContainerSubscription!: azdata.InputBoxComponent; private _networkShareContainerLocation!: azdata.InputBoxComponent; private _networkShareStorageAccountResourceGroupDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountRefreshButton!: azdata.ButtonComponent; - private _windowsUserAccountText!: azdata.InputBoxComponent; - private _passwordText!: azdata.InputBoxComponent; - private _networkShareDatabaseConfigContainer!: azdata.FlexContainer; + + private _targetDatabaseContainer!: azdata.FlexContainer; + private _targetDatabaseNamesTable!: azdata.DeclarativeTableComponent; private _targetDatabaseNames: azdata.InputBoxComponent[] = []; - private _blobContainer!: azdata.FlexContainer; - private _blobContainerSubscription!: azdata.InputBoxComponent; - private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent; - private _blobContainerDatabaseConfigContainer!: azdata.FlexContainer; - private _blobContainerDropdowns: azdata.DropDownComponent[] = []; - - private _fileShareContainer!: azdata.FlexContainer; - private _fileShareSubscription!: azdata.InputBoxComponent; - private _fileShareStorageAccountDropdown!: azdata.DropDownComponent; - private _fileShareDatabaseConfigContainer!: azdata.FlexContainer; - private _fileShareDropdowns: azdata.DropDownComponent[] = []; private _existingDatabases: string[] = []; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel); - this.wizardPage.description = constants.DATABASE_BACKUP_PAGE_DESCRIPTION; } protected async registerContent(view: azdata.ModelView): Promise { this._view = view; - this._networkShareContainer = this.createNetworkShareContainer(view); - this._blobContainer = this.createBlobContainer(view); - this._fileShareContainer = this.createFileShareContainer(view); - const networkContainer = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).withItems([ - this._networkShareContainer, - this._blobContainer, - this._fileShareContainer - ]).component(); + const radioButtonContainer = this.createBackupLocationComponent(); - const form = view.modelBuilder.formContainer() + const networkDetailsContainer = this.createNetworkDetailsContainer(); + + this._targetDatabaseContainer = this.createTargetDatabaseContainer(); + + this._networkShareStorageAccountDetails = this.createNetworkShareStorageAccountDetailsContainer(); + + const form = this._view.modelBuilder.formContainer() .withFormItems( [ - this.createBackupLocationComponent(view), { title: '', - component: networkContainer + component: radioButtonContainer }, + { + title: '', + component: networkDetailsContainer + }, + { + title: '', + component: this._targetDatabaseContainer + }, + { + title: '', + component: this._networkShareStorageAccountDetails + } ] ); await view.initializeModel(form.component()); + + } - private createBackupLocationComponent(view: azdata.ModelView): azdata.FormComponent { + private createBackupLocationComponent(): azdata.FlexContainer { const buttonGroup = 'networkContainer'; - const networkShareButton = view.modelBuilder.radioButton() + const selectLocationText = this._view.modelBuilder.text().withProps({ + value: constants.DATABASE_BACKUP_PAGE_DESCRIPTION, + CSSStyles: { + 'font-size': '13px' + } + }).component(); + + const networkShareButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL, - checked: true + CSSStyles: { + 'font-size': '13px' + } }).component(); networkShareButton.onDidChangeCheckedState((e) => { @@ -88,24 +111,30 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }); - const blobContainerButton = view.modelBuilder.radioButton() + const blobContainerButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL, + enabled: false, + CSSStyles: { + 'font-size': '13px' + } }).component(); blobContainerButton.onDidChangeCheckedState((e) => { if (e) { - vscode.window.showInformationMessage('Feature coming soon'); - networkShareButton.checked = true; - //this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); + this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); } }); - const fileShareButton = view.modelBuilder.radioButton() + const fileShareButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL, + enabled: false, + CSSStyles: { + 'font-size': '13px' + } }).component(); fileShareButton.onDidChangeCheckedState((e) => { @@ -116,8 +145,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }); - const flexContainer = view.modelBuilder.flexContainer().withItems( + const flexContainer = this._view.modelBuilder.flexContainer().withItems( [ + selectLocationText, networkShareButton, blobContainerButton, fileShareButton @@ -126,126 +156,26 @@ export class DatabaseBackupPage extends MigrationWizardPage { flexFlow: 'column' }).component(); - return { - title: '', - component: flexContainer - }; - } - - private createFileShareContainer(view: azdata.ModelView): azdata.FlexContainer { - - const subscriptionLabel = view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL, - requiredIndicator: true, - }).component(); - this._fileShareSubscription = view.modelBuilder.inputBox().withProps({ - required: false, - enabled: false - }).component(); - - const storageAccountLabel = view.modelBuilder.text() - .withProps({ - value: constants.DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL, - requiredIndicator: true, - }).component(); - this._fileShareStorageAccountDropdown = view.modelBuilder.dropDown() - .withProps({ - required: false - }).component(); - this._fileShareStorageAccountDropdown.onValueChanged(async (value) => { - if (value.selected) { - this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); - this.migrationStateModel._databaseBackup.fileShares = undefined!; - await this.loadFileShareDropdown(); - } - }); - - const fileShareDatabaseConfigHeader = view.modelBuilder.text().withProps({ - value: constants.ENTER_FILE_SHARE_INFORMATION - }).component(); - - this._fileShareDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).component(); - - const flexContainer = view.modelBuilder.flexContainer() - .withItems( - [ - subscriptionLabel, - this._fileShareSubscription, - storageAccountLabel, - this._fileShareStorageAccountDropdown, - fileShareDatabaseConfigHeader, - this._fileShareDatabaseConfigContainer - ] - ).withLayout({ - flexFlow: 'column' - }).withProps({ - display: 'none' - }).component(); - return flexContainer; } - private createBlobContainer(view: azdata.ModelView): azdata.FlexContainer { - const subscriptionLabel = view.modelBuilder.text() - .withProps({ - value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL, - requiredIndicator: true, - }).component(); - this._blobContainerSubscription = view.modelBuilder.inputBox() - .withProps({ - required: false, - enabled: false - }).component(); + private createNetworkDetailsContainer(): azdata.FlexContainer { + this._networkShareContainer = this.createNetworkShareContainer(); + this._blobContainer = this.createBlobContainer(); + this._fileShareContainer = this.createFileShareContainer(); - const storageAccountLabel = view.modelBuilder.text() - .withProps({ - value: constants.DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_LABEL, - requiredIndicator: true, - }).component(); - this._blobContainerStorageAccountDropdown = view.modelBuilder.dropDown() - .withProps({ - required: false - }).component(); - this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => { - if (value.selected) { - this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); - this.migrationStateModel._databaseBackup.blobContainers = undefined!; - await this.loadBlobContainerDropdown(); - } - }); - - - const blobContainerDatabaseConfigHeader = view.modelBuilder.text().withProps({ - value: constants.ENTER_BLOB_CONTAINER_INFORMATION - }).component(); - - this._blobContainerDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({ + const networkContainer = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' - }).component(); - - const flexContainer = view.modelBuilder.flexContainer() - .withItems( - [ - subscriptionLabel, - this._blobContainerSubscription, - storageAccountLabel, - this._blobContainerStorageAccountDropdown, - blobContainerDatabaseConfigHeader, - this._blobContainerDatabaseConfigContainer - ] - ).withLayout({ - flexFlow: 'column' - }).withProps({ - display: 'none' - }).component(); - - return flexContainer; + }).withItems([ + this._networkShareContainer, + this._blobContainer, + this._fileShareContainer + ]).component(); + return networkContainer; } - private createNetworkShareContainer(view: azdata.ModelView): azdata.FlexContainer { - const networkShareHeading = view.modelBuilder.text().withProps({ + 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: { @@ -253,21 +183,31 @@ export class DatabaseBackupPage extends MigrationWizardPage { 'font-weight': 'bold' } }).component(); - const networkShareHelpText = view.modelBuilder.text().withProps({ + + const networkShareHelpText = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, - width: WIZARD_INPUT_COMPONENT_WIDTH + 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, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); - const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({ + this._networkSharePath = this._view.modelBuilder.inputBox().withProps({ placeHolder: '\\\\Servername.domainname.com\\Backupfolder', - required: true, validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION, - width: WIZARD_INPUT_COMPONENT_WIDTH + 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) { @@ -278,23 +218,31 @@ export class DatabaseBackupPage extends MigrationWizardPage { } return true; }).component(); - networkLocationInputBox.onTextChanged((value) => { + this._networkSharePath.onTextChanged((value) => { this.validateFields(); - this.migrationStateModel._databaseBackup.networkShareLocation = value; + this.migrationStateModel._databaseBackup.networkShare.networkShareLocation = value; }); - const networkShareInfoBox = view.modelBuilder.infoBox().withProps({ + + const networkShareInfoBox = this._view.modelBuilder.infoBox().withProps({ text: constants.DATABASE_SERVICE_ACCOUNT_INFO_TEXT, style: 'information', - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'margin-top': '10px' + } }).component(); - const windowsUserAccountLabel = view.modelBuilder.text() + const windowsUserAccountLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); - this._windowsUserAccountText = view.modelBuilder.inputBox() + this._windowsUserAccountText = this._view.modelBuilder.inputBox() .withProps({ placeHolder: 'Domain\\username', required: true, @@ -312,16 +260,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { return true; }).component(); this._windowsUserAccountText.onTextChanged((value) => { - this.migrationStateModel._databaseBackup.windowsUser = value; + this.migrationStateModel._databaseBackup.networkShare.windowsUser = value; }); - const passwordLabel = view.modelBuilder.text() + const passwordLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); - this._passwordText = view.modelBuilder.inputBox() + this._passwordText = this._view.modelBuilder.inputBox() .withProps({ placeHolder: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER, inputType: 'password', @@ -329,10 +280,223 @@ export class DatabaseBackupPage extends MigrationWizardPage { width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._passwordText.onTextChanged((value) => { - this.migrationStateModel._databaseBackup.password = value; + this.migrationStateModel._databaseBackup.networkShare.password = value; }); - const azureAccountHeader = view.modelBuilder.text() + const flexContainer = this._view.modelBuilder.flexContainer().withItems( + [ + networkShareHeading, + networkShareHelpText, + networkLocationInputBoxLabel, + this._networkSharePath, + networkShareInfoBox, + windowsUserAccountLabel, + this._windowsUserAccountText, + passwordLabel, + this._passwordText, + ] + ).withLayout({ + flexFlow: 'column' + }).withProps({ + display: 'none' + }).component(); + + return flexContainer; + } + + private createFileShareContainer(): azdata.FlexContainer { + + const subscriptionLabel = this._view.modelBuilder.text().withProps({ + value: constants.DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL, + requiredIndicator: true, + }).component(); + this._fileShareSubscription = this._view.modelBuilder.inputBox().withProps({ + enabled: false + }).component(); + + const storageAccountLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL, + }).component(); + this._fileShareStorageAccountDropdown = this._view.modelBuilder.dropDown().component(); + + const flexContainer = this._view.modelBuilder.flexContainer() + .withItems( + [ + subscriptionLabel, + this._fileShareSubscription, + storageAccountLabel, + this._fileShareStorageAccountDropdown + ] + ).withLayout({ + flexFlow: 'column' + }).withProps({ + display: 'none' + }).component(); + + return flexContainer; + } + + private createBlobContainer(): azdata.FlexContainer { + + 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.inputBox() + .withProps({ + enabled: false + }).component(); + + const locationLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.LOCATION, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } + }).component(); + this._blobContainerLocation = this._view.modelBuilder.inputBox() + .withProps({ + enabled: false + }).component(); + + const resourceGroupLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.RESOURCE_GROUP, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } + }).component(); + this._blobContainerResourceGroup = this._view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._blobContainerResourceGroup.onValueChanged(e => { + if (e.selected && e.selected !== constants.RESOURCE_GROUP_NOT_FOUND) { + this.migrationStateModel._databaseBackup.blob.resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); + } + this.loadblobStorageDropdown(); + }); + + const storageAccountLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.STORAGE_ACCOUNT, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } + }).component(); + this._blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() + .withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => { + if (value.selected && value.selected !== constants.NO_STORAGE_ACCOUNT_FOUND) { + this.migrationStateModel._databaseBackup.blob.storageAccount = this.migrationStateModel.getStorageAccount(value.index); + } + await this.loadBlobContainerDropdown(); + }); + + + const blobContainerLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.BLOB_CONTAINER, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } + }).component(); + this._blobContainerDropdown = this._view.modelBuilder.dropDown() + .withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._blobContainerDropdown.onValueChanged(async (value) => { + if (value.selected && value.selected !== constants.NO_BLOBCONTAINERS_FOUND) { + this.migrationStateModel._databaseBackup.blob.blobContainer = this.migrationStateModel.getBlobContainer(value.index); + } + }); + + + + const flexContainer = this._view.modelBuilder.flexContainer() + .withItems( + [ + subscriptionLabel, + this._blobContainerSubscription, + locationLabel, + this._blobContainerLocation, + resourceGroupLabel, + this._blobContainerResourceGroup, + storageAccountLabel, + this._blobContainerStorageAccountDropdown, + blobContainerLabel, + this._blobContainerDropdown, + ] + ).withLayout({ + flexFlow: 'column' + }).withProps({ + display: 'none' + }).component(); + + return flexContainer; + } + + + private createTargetDatabaseContainer(): azdata.FlexContainer { + const rowCssStyle: azdata.CssStyles = { + 'border': 'none', + 'font-size': '13px', + 'border-bottom': '1px solid', + }; + + const headerCssStyles: azdata.CssStyles = { + 'border': 'none', + 'font-size': '13px', + 'font-weight': 'bold', + 'text-align': 'left', + 'border-bottom': '1px solid', + }; + + this._targetDatabaseNamesTable = 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(); + + const container = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).withItems([ + this._targetDatabaseNamesTable + ]).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, @@ -342,72 +506,87 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }).component(); - const azureAccountHelpText = view.modelBuilder.text() + const azureAccountHelpText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + } }).component(); - const subscriptionLabel = view.modelBuilder.text() + const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.SUBSCRIPTION, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); - this._networkShareContainerSubscription = view.modelBuilder.inputBox() + this._networkShareContainerSubscription = this._view.modelBuilder.inputBox() .withProps({ required: true, enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - const locationLabel = view.modelBuilder.text() + const locationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); - this._networkShareContainerLocation = view.modelBuilder.inputBox() + this._networkShareContainerLocation = this._view.modelBuilder.inputBox() .withProps({ required: true, enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); - const resourceGroupLabel = view.modelBuilder.text() + const resourceGroupLabel = this._view.modelBuilder.text() .withProps({ value: constants.RESOURCE_GROUP, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); - this._networkShareStorageAccountResourceGroupDropdown = view.modelBuilder.dropDown().withProps({ + this._networkShareStorageAccountResourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({ width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(e => { if (e.selected) { - this.migrationStateModel._databaseBackup.resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); + this.migrationStateModel._databaseBackup.networkShare.resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); this.loadNetworkShareStorageDropdown(); } }); - const storageAccountLabel = view.modelBuilder.text() + const storageAccountLabel = this._view.modelBuilder.text() .withProps({ value: constants.STORAGE_ACCOUNT, - requiredIndicator: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); - this._networkShareContainerStorageAccountDropdown = view.modelBuilder.dropDown() + this._networkShareContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() .withProps({ required: true, width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => { if (value.selected) { - this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); + this.migrationStateModel._databaseBackup.networkShare.storageAccount = this.migrationStateModel.getStorageAccount(value.index); } }); - this._networkShareContainerStorageAccountRefreshButton = view.modelBuilder.button().withProps({ + this._networkShareContainerStorageAccountRefreshButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.refresh, iconHeight: 18, iconWidth: 18, @@ -418,7 +597,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.loadNetworkShareStorageDropdown(); }); - const storageAccountContainer = view.modelBuilder.flexContainer().component(); + const storageAccountContainer = this._view.modelBuilder.flexContainer().component(); storageAccountContainer.addItem(this._networkShareContainerStorageAccountDropdown, { flex: '0 0 auto' @@ -431,85 +610,50 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }); - const networkShareDatabaseConfigHeader = view.modelBuilder.text().withProps({ - value: constants.ENTER_NETWORK_SHARE_INFORMATION, - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - 'font-size': '14px', - 'font-weight': 'bold' - } - }).component(); - this._networkShareDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({ + const container = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', - }).component(); - - - const flexContainer = view.modelBuilder.flexContainer().withItems( - [ - networkShareHeading, - networkShareHelpText, - networkLocationInputBoxLabel, - networkLocationInputBox, - networkShareInfoBox, - windowsUserAccountLabel, - this._windowsUserAccountText, - passwordLabel, - this._passwordText, - azureAccountHeader, - azureAccountHelpText, - subscriptionLabel, - this._networkShareContainerSubscription, - locationLabel, - this._networkShareContainerLocation, - resourceGroupLabel, - this._networkShareStorageAccountResourceGroupDropdown, - storageAccountLabel, - storageAccountContainer, - networkShareDatabaseConfigHeader, - this._networkShareDatabaseConfigContainer - ] - ).withLayout({ - flexFlow: 'column' - }).withProps({ + }).withItems([ + azureAccountHeader, + azureAccountHelpText, + subscriptionLabel, + this._networkShareContainerSubscription, + locationLabel, + this._networkShareContainerLocation, + resourceGroupLabel, + this._networkShareStorageAccountResourceGroupDropdown, + storageAccountLabel, + storageAccountContainer, + ]).withProps({ display: 'none' }).component(); - return flexContainer; + return container; } + public async onPageEnter(): Promise { if (this.migrationStateModel.refreshDatabaseBackupPage) { this._targetDatabaseNames = []; if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); } - this._fileShareDropdowns = []; - this._blobContainerDropdowns = []; this.migrationStateModel._targetDatabaseNames = []; - this.migrationStateModel._databaseBackup.fileShares = []; - this.migrationStateModel._databaseBackup.blobContainers = []; - this._networkShareDatabaseConfigContainer.clearItems(); - this._fileShareDatabaseConfigContainer.clearItems(); - this._blobContainerDatabaseConfigContainer.clearItems(); + const tableRows: azdata.DeclarativeTableCellValue[][] = []; this.migrationStateModel._migrationDbs.forEach((db, index) => { + const targetRow: azdata.DeclarativeTableCellValue[] = []; this.migrationStateModel._targetDatabaseNames.push(''); - const targetNameLabel = constants.TARGET_NAME_FOR_DATABASE(db); - const targetNameNetworkInputBoxLabel = this._view.modelBuilder.text().withProps({ - value: targetNameLabel, - requiredIndicator: true - }).component(); - const targetNameNetworkInputBox = this._view.modelBuilder.inputBox().withProps({ + const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ required: true, value: db, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: '280px' }).withValidation(c => { if (this._targetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; return false; } if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL - c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(this.migrationStateModel._targetServerInstance.name); + 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!)) { @@ -518,79 +662,21 @@ export class DatabaseBackupPage extends MigrationWizardPage { } return true; }).component(); - targetNameNetworkInputBox.onTextChanged((value) => { + targetDatabaseInput.onTextChanged((value) => { this.migrationStateModel._targetDatabaseNames[index] = value.trim(); }); - this._targetDatabaseNames.push(targetNameNetworkInputBox); + this._targetDatabaseNames.push(targetDatabaseInput); - this._networkShareDatabaseConfigContainer.addItems( - [ - targetNameNetworkInputBoxLabel, - targetNameNetworkInputBox - ] - ); - - const targetNameFileInputBoxLabel = this._view.modelBuilder.text().withProps({ - value: targetNameLabel - }).component(); - const targetNameFileInputBox = this._view.modelBuilder.inputBox().withProps({ - }).component(); - const fileShareLabel = this._view.modelBuilder.text() - .withProps({ - value: constants.TARGET_FILE_SHARE(db), - requiredIndicator: true, - }).component(); - const fileShareDropdown = this._view.modelBuilder.dropDown() - .withProps({ - }).component(); - fileShareDropdown.onValueChanged((value) => { - if (value.selected && value.selected !== constants.NO_FILESHARES_FOUND) { - this.validateFields(); - this.migrationStateModel._databaseBackup.fileShares[index] = this.migrationStateModel.getFileShare(value.index); - } + targetRow.push({ + value: db }); - this.migrationStateModel._databaseBackup.fileShares.push(undefined!); - this._fileShareDropdowns.push(fileShareDropdown); - this._fileShareDatabaseConfigContainer.addItems( - [ - targetNameFileInputBoxLabel, - targetNameFileInputBox, - fileShareLabel, - fileShareDropdown - ] - ); - - const targetNameBlobInputBoxLabel = this._view.modelBuilder.text().withProps({ - value: targetNameLabel - }).component(); - const targetNameBlobInputBox = this._view.modelBuilder.inputBox().withProps({ - }).component(); - const blobContainerLabel = this._view.modelBuilder.text() - .withProps({ - value: constants.TARGET_BLOB_CONTAINER(db), - requiredIndicator: true, - }).component(); - const blobContainerDropdown = this._view.modelBuilder.dropDown() - .withProps({ - }).component(); - blobContainerDropdown.onValueChanged((value) => { - if (value.selected && value.selected !== constants.NO_BLOBCONTAINERS_FOUND) { - this.validateFields(); - this.migrationStateModel._databaseBackup.blobContainers[index] = this.migrationStateModel.getBlobContainer(value.index); - } + targetRow.push({ + value: targetDatabaseInput }); - this.migrationStateModel._databaseBackup.fileShares.push(undefined!); - this._blobContainerDropdowns.push(blobContainerDropdown); - this._blobContainerDatabaseConfigContainer.addItems( - [ - targetNameBlobInputBoxLabel, - targetNameBlobInputBox, - blobContainerLabel, - blobContainerDropdown - ] - ); + tableRows.push(targetRow); }); + this._targetDatabaseNamesTable.dataValues = tableRows; this.migrationStateModel.refreshDatabaseBackupPage = false; } await this.getSubscriptionValues(); @@ -603,37 +689,32 @@ export class DatabaseBackupPage extends MigrationWizardPage { switch (this.migrationStateModel._databaseBackup.networkContainerType) { case NetworkContainerType.NETWORK_SHARE: - if (this._networkShareContainerSubscription.value === constants.NO_SUBSCRIPTIONS_FOUND) { - errors.push(constants.INVALID_SUBSCRIPTION_ERROR); + 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: + if ((this._blobContainerResourceGroup.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) { + errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); + } if ((this._blobContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); } - for (let i = 0; i < this._blobContainerDropdowns.length; i++) { - if ((this._blobContainerDropdowns[i].value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { - errors.push(constants.INVALID_BLOBCONTAINER_ERROR); - break; - } - } - break; - case NetworkContainerType.FILE_SHARE: - if ((this._fileShareStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { - errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); - } - for (let i = 0; i < this._fileShareDropdowns.length; i++) { - if ((this._fileShareDropdowns[i].value).displayName === constants.NO_FILESHARES_FOUND) { - errors.push(constants.INVALID_FILESHARE_ERROR); - break; - } + if ((this._blobContainerDropdown.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { + errors.push(constants.INVALID_BLOBCONTAINER_ERROR); } 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 @@ -647,7 +728,13 @@ export class DatabaseBackupPage extends MigrationWizardPage { public async onPageLeave(): Promise { try { - this.migrationStateModel._databaseBackup.storageKey = (await getStorageAccountAccessKeys(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount)).keyName1; + const storageAccount = (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) ? + this.migrationStateModel._databaseBackup.blob.storageAccount : this.migrationStateModel._databaseBackup.networkShare.storageAccount; + + this.migrationStateModel._databaseBackup.storageKey = (await getStorageAccountAccessKeys( + this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription, + storageAccount)).keyName1; } finally { this.wizard.registerNavigationValidator((pageChangeInfo) => { return true; @@ -659,66 +746,58 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private toggleNetworkContainerFields(containerType: NetworkContainerType): void { + this.wizard.message = { + text: '', + level: azdata.window.MessageLevel.Error + }; + + this.wizard.nextButton.enabled = true; this.migrationStateModel._databaseBackup.networkContainerType = containerType; this._fileShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.FILE_SHARE) ? 'inline' : 'none' }); this._blobContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none' }); this._networkShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' }); + this._networkShareStorageAccountDetails.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' }); + this._targetDatabaseContainer.updateCssStyles({ 'display': 'inline' }); + this._windowsUserAccountText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); this._passwordText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); - this._targetDatabaseNames.forEach((inputBox) => { - inputBox.validate(); - }); - this._windowsUserAccountText.validate(); - this._passwordText.validate(); - this._networkShareContainerSubscription.validate(); - this._networkShareContainerStorageAccountDropdown.validate(); - this._blobContainerSubscription.validate(); - this._blobContainerStorageAccountDropdown.validate(); - this._blobContainerDropdowns.forEach((dropdown) => { - dropdown.validate(); - }); - this._fileShareSubscription.validate(); - this._fileShareStorageAccountDropdown.validate(); - this._fileShareDropdowns.forEach(dropdown => { - dropdown.validate(); - }); - + this.validateFields(); } - private validateFields(): void { - this._targetDatabaseNames.forEach((inputBox) => { + private async validateFields(): Promise { + 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(); + await this._blobContainerResourceGroup.validate(); + await this._blobContainerStorageAccountDropdown.validate(); + await this._blobContainerDropdown.validate(); + await this._targetDatabaseNames.forEach((inputBox) => { inputBox.validate(); }); - this._windowsUserAccountText.validate(); - this._passwordText.validate(); - this._networkShareContainerSubscription.validate(); - this._networkShareContainerStorageAccountDropdown.validate(); - this._blobContainerSubscription.validate(); - this._blobContainerStorageAccountDropdown.validate(); - this._blobContainerDropdowns.forEach((dropdown) => { - dropdown.validate(); - }); - this._fileShareSubscription.validate(); - this._fileShareStorageAccountDropdown.validate(); - this._fileShareDropdowns.forEach((dropdown) => { - dropdown.validate(); - }); } private async getSubscriptionValues(): Promise { - this._fileShareSubscription.value = this.migrationStateModel._targetSubscription.name; + this._networkShareContainerSubscription.value = this.migrationStateModel._targetSubscription.name; this._networkShareContainerLocation.value = await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); + this._blobContainerSubscription.value = this.migrationStateModel._targetSubscription.name; + this._blobContainerLocation.value = await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); + this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel._targetSubscription; + + this.loadNetworkStorageResourceGroup(); - this.loadFileShareStorageDropdown(); - this.loadblobStorageDropdown(); + this.loadblobResourceGroup(); } private async loadNetworkStorageResourceGroup(): Promise { @@ -736,7 +815,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadNetworkShareStorageDropdown(): Promise { this._networkShareContainerStorageAccountDropdown.loading = true; try { - this._networkShareContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription); + this._networkShareContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.networkShare.resourceGroup); } catch (error) { console.log(error); } finally { @@ -744,81 +823,38 @@ export class DatabaseBackupPage extends MigrationWizardPage { } } - private async loadFileShareStorageDropdown(): Promise { - if (!this.migrationStateModel._databaseBackup.storageAccount) { - this._fileShareStorageAccountDropdown.loading = true; - this._fileShareDropdowns.forEach((dropdown) => { - dropdown.loading = true; - }); - try { - this._fileShareStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription); - } catch (error) { - console.log(error); - } finally { - this._fileShareStorageAccountDropdown.loading = false; - this._fileShareDropdowns.forEach((dropdown) => { - dropdown.loading = false; - }); - } + private async loadblobResourceGroup(): Promise { + this._blobContainerResourceGroup.loading = true; + try { + this._blobContainerResourceGroup.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); + } catch (error) { + console.log(error); + } finally { + this._blobContainerResourceGroup.loading = false; } } private async loadblobStorageDropdown(): Promise { - if (!this.migrationStateModel._databaseBackup.storageAccount) { - this._blobContainerStorageAccountDropdown.loading = true; - this._blobContainerDropdowns.forEach((dropdown) => { - dropdown.loading = true; - }); - try { - this._blobContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription); - } catch (error) { - console.log(error); - } finally { - this._blobContainerStorageAccountDropdown.loading = false; - this._blobContainerDropdowns.forEach((dropdown) => { - dropdown.loading = false; - }); - } - } - } - - private async loadFileShareDropdown(): Promise { - if (!this.migrationStateModel._fileShares) { - this._fileShareDropdowns.forEach((dropdown) => { - dropdown.loading = true; - }); - try { - const fileShareValues = await this.migrationStateModel.getFileShareValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount); - this._fileShareDropdowns.forEach((dropdown) => { - dropdown.values = fileShareValues; - }); - } catch (error) { - console.log(error); - } finally { - this._fileShareDropdowns.forEach((dropdown) => { - dropdown.loading = true; - }); - } + this._blobContainerStorageAccountDropdown.loading = true; + try { + this._blobContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blob.resourceGroup); + } catch (error) { + console.log(error); + } finally { + this._blobContainerStorageAccountDropdown.loading = false; } } private async loadBlobContainerDropdown(): Promise { - if (!this.migrationStateModel._blobContainers) { - this._blobContainerDropdowns.forEach((dropdown) => { - dropdown.loading = true; - }); - try { - const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount); - this._blobContainerDropdowns.forEach((dropdown) => { - dropdown.values = blobContainerValues; - }); - } catch (error) { - console.log(error); - } finally { - this._blobContainerDropdowns.forEach((dropdown) => { - dropdown.loading = false; - }); - } + this._blobContainerDropdown.loading = true; + try { + const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blob.storageAccount); + this._blobContainerDropdown.values = blobContainerValues; + } catch (error) { + console.log(error); + } finally { + this._blobContainerDropdown.loading = false; } } + } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 96c0670e5e..e070260b48 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -33,6 +33,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { private _refresh1!: azdata.ButtonComponent; private _refresh2!: azdata.ButtonComponent; + private _firstEnter: boolean = true; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.IR_PAGE_TITLE), migrationStateModel); @@ -75,7 +76,10 @@ export class IntergrationRuntimePage extends MigrationWizardPage { } public async onPageEnter(): Promise { - this.populateMigrationService(); + if (this._firstEnter) { + this.populateMigrationService(); + this._firstEnter = false; + } this.wizard.registerNavigationValidator((pageChangeInfo) => { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { this.wizard.message = { diff --git a/extensions/sql-migration/src/wizard/migrationModePage.ts b/extensions/sql-migration/src/wizard/migrationModePage.ts index d9157708ec..9634d57f3d 100644 --- a/extensions/sql-migration/src/wizard/migrationModePage.ts +++ b/extensions/sql-migration/src/wizard/migrationModePage.ts @@ -45,6 +45,7 @@ export class MigrationModePage extends MigrationWizardPage { label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL, name: buttonGroup, CSSStyles: { + 'font-size': '13px', 'font-weight': 'bold' }, checked: true @@ -53,6 +54,7 @@ export class MigrationModePage extends MigrationWizardPage { const onlineDescription = view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION, CSSStyles: { + 'font-size': '13px', 'margin': '0 0 10px 20px' } }).component(); @@ -69,6 +71,7 @@ export class MigrationModePage extends MigrationWizardPage { label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL, name: buttonGroup, CSSStyles: { + 'font-size': '13px', 'font-weight': 'bold' }, }).component(); @@ -76,6 +79,7 @@ export class MigrationModePage extends MigrationWizardPage { const offlineDescription = view.modelBuilder.text().withProps({ value: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION, CSSStyles: { + 'font-size': '13px', 'margin': '0 0 10px 20px' } }).component(); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 9dc5b38f74..56f45bd0cd 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -477,9 +477,12 @@ export class SKURecommendationPage extends MigrationWizardPage { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { return true; } - if (this.migrationStateModel._migrationDbs.length === 0) { - errors.push('Please select databases to migrate'); + if (this._rbg.selectedCardId === undefined || this._rbg.selectedCardId === '') { + errors.push(constants.SELECT_TARGET_TO_CONTINUE); + } + if (this.migrationStateModel._migrationDbs.length === 0) { + errors.push(constants.SELECT_DATABASE_TO_MIGRATE); } if ((this._managedInstanceSubscriptionDropdown.value)?.displayName === constants.NO_SUBSCRIPTIONS_FOUND) { errors.push(constants.INVALID_SUBSCRIPTION_ERROR); diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 8282520de7..787d102a86 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -86,46 +86,40 @@ export class SummaryPage extends MigrationWizardPage { flexContainer.addItems( [ createInformationRow(this._view, constants.BACKUP_LOCATION, constants.NETWORK_SHARE), - createInformationRow(this._view, constants.NETWORK_SHARE, this.migrationStateModel._databaseBackup.networkShareLocation), - createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.windowsUser), + createInformationRow(this._view, constants.NETWORK_SHARE, this.migrationStateModel._databaseBackup.networkShare.networkShareLocation), + createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.networkShare.windowsUser), createHeadingTextComponent(this._view, constants.AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS), createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name), - createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.storageAccount.location), - createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.storageAccount.resourceGroup!), - createInformationRow(this._view, constants.STORAGE_ACCOUNT, this.migrationStateModel._databaseBackup.storageAccount.name!), - createHeadingTextComponent(this._view, 'Target Databases:') + createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.networkShare.storageAccount.location), + createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.networkShare.storageAccount.resourceGroup!), + createInformationRow(this._view, constants.STORAGE_ACCOUNT, this.migrationStateModel._databaseBackup.networkShare.storageAccount.name!), ] ); - this.migrationStateModel._migrationDbs.forEach((db, index) => { - flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NAME_FOR_DATABASE(db), this.migrationStateModel._targetDatabaseNames[index])); - }); break; case NetworkContainerType.FILE_SHARE: flexContainer.addItems( [ createInformationRow(this._view, constants.TYPE, constants.FILE_SHARE), createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name), - createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.storageAccount.name), ] ); - this.migrationStateModel._migrationDbs.forEach((db, index) => { - flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NAME_FOR_DATABASE(db), this.migrationStateModel._targetDatabaseNames[index])); - flexContainer.addItem(createInformationRow(this._view, constants.TARGET_FILE_SHARE(db), this.migrationStateModel._databaseBackup.fileShares[index].name)); - }); break; case NetworkContainerType.BLOB_CONTAINER: flexContainer.addItems( [ createInformationRow(this._view, constants.TYPE, constants.BLOB_CONTAINER), createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name), - createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.storageAccount.name), + createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.blob.storageAccount.location), + createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.blob.storageAccount.resourceGroup!), + createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.blob.storageAccount.name), + createInformationRow(this._view, constants.BLOB_CONTAINER, this.migrationStateModel._databaseBackup.blob.blobContainer.name) ] ); - this.migrationStateModel._migrationDbs.forEach((db, index) => { - flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NAME_FOR_DATABASE(db), this.migrationStateModel._targetDatabaseNames[index])); - flexContainer.addItem(createInformationRow(this._view, constants.TARGET_FILE_SHARE(db), this.migrationStateModel._databaseBackup.blobContainers[index].name)); - }); } + flexContainer.addItem(createHeadingTextComponent(this._view, constants.TARGET_NAME)); + this.migrationStateModel._migrationDbs.forEach((db, index) => { + flexContainer.addItem(createInformationRow(this._view, db, this.migrationStateModel._targetDatabaseNames[index])); + }); return flexContainer; } }