diff --git a/extensions/sql-migration/images/completingCutover.svg b/extensions/sql-migration/images/completingCutover.svg new file mode 100644 index 0000000000..9a6e44f584 --- /dev/null +++ b/extensions/sql-migration/images/completingCutover.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/extensions/sql-migration/images/error.svg b/extensions/sql-migration/images/error.svg new file mode 100644 index 0000000000..6eba0b225a --- /dev/null +++ b/extensions/sql-migration/images/error.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index d9e5414a8c..92c5d83f54 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.1.0", + "version": "0.1.1", "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 ef34bbc74a..3d7ce1f28d 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -32,7 +32,12 @@ export async function getLocations(account: azdata.Account, subscription: Subscr throw new Error(response.errors.toString()); } sortResourceArrayByName(response.locations); - const supportedLocations = ['eastus2', 'eastus2euap']; + const supportedLocations = [ + 'eastus2', + 'eastus2euap', + 'eastus', + 'canadacentral' + ]; const filteredLocations = response.locations.filter(loc => { return supportedLocations.includes(loc.name); }); @@ -377,8 +382,8 @@ export interface DatabaseMigration { } export interface DatabaseMigrationProperties { scope: string; - provisioningState: string; - migrationStatus: string; + provisioningState: 'Succeeded' | 'Failed' | 'Creating'; + migrationStatus: 'InProgress' | 'Failed' | 'Succeeded' | 'Creating' | 'Completing' | 'Cancelling'; migrationStatusDetails?: MigrationStatusDetails; startedOn: string; endedOn: string; diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index 058605a5eb..0cd2a2b378 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { DAYS, HRS, MINUTE, SEC } from '../constants/strings'; +import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; +import { MigrationContext } from '../models/migrationLocalStorage'; export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -83,3 +85,38 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date): return DAYS(parseFloat(days)); } } + +export function filterMigrations(databaseMigrations: MigrationContext[], statusFilter: string, databaseNameFilter?: string): MigrationContext[] { + let filteredMigration: MigrationContext[] = []; + if (statusFilter === AdsMigrationStatus.ALL) { + filteredMigration = databaseMigrations; + } else if (statusFilter === AdsMigrationStatus.ONGOING) { + filteredMigration = databaseMigrations.filter((value) => { + const status = value.migrationContext.properties.migrationStatus; + const provisioning = value.migrationContext.properties.provisioningState; + return status === 'InProgress' || status === 'Creating' || provisioning === 'Creating'; + }); + } else if (statusFilter === AdsMigrationStatus.SUCCEEDED) { + filteredMigration = databaseMigrations.filter((value) => { + const status = value.migrationContext.properties.migrationStatus; + return status === 'Succeeded'; + }); + } else if (statusFilter === AdsMigrationStatus.FAILED) { + filteredMigration = databaseMigrations.filter((value) => { + const status = value.migrationContext.properties.migrationStatus; + const provisioning = value.migrationContext.properties.provisioningState; + return status === 'Failed' || provisioning === 'Failed'; + }); + } else if (statusFilter === AdsMigrationStatus.COMPLETING) { + filteredMigration = databaseMigrations.filter((value) => { + const status = value.migrationContext.properties.migrationStatus; + return status === 'Completing'; + }); + } + if (databaseNameFilter) { + filteredMigration = filteredMigration.filter((value) => { + return value.migrationContext.name.toLowerCase().includes(databaseNameFilter.toLowerCase()); + }); + } + return filteredMigration; +} diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index fc348b100a..c0c62a58e3 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -30,6 +30,8 @@ export class IconPathHelper { public static cancel: IconPath; public static warning: IconPath; public static info: IconPath; + public static error: IconPath; + public static completingCutover: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.copy = { @@ -108,5 +110,13 @@ export class IconPathHelper { light: context.asAbsolutePath('images/info.svg'), dark: context.asAbsolutePath('images/infoBox.svg') }; + IconPathHelper.error = { + light: context.asAbsolutePath('images/error.svg'), + dark: context.asAbsolutePath('images/error.svg') + }; + IconPathHelper.completingCutover = { + light: context.asAbsolutePath('images/completingCutover.svg'), + dark: context.asAbsolutePath('images/completingCutover.svg') + }; } } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 1ed2bafb86..1de7080822 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -5,6 +5,7 @@ import { AzureAccount } from 'azurecore'; import * as nls from 'vscode-nls'; +import { MigrationSourceAuthenticationType } from '../models/stateMachine'; const localize = nls.loadMessageBundle(); @@ -144,6 +145,15 @@ export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.net export const ENTER_BLOB_CONTAINER_INFORMATION = localize('sql.migration.blob.container.information', "Enter the target name and select the blob container location for selected databases"); export const ENTER_FILE_SHARE_INFORMATION = localize('sql.migration.enter.file.share.information', "Enter the target name and select the file share location of selected databases"); export const INVALID_TARGET_NAME_ERROR = localize('sql.migration.invalid.target.name.error', "Please enter a valid name for the target database."); +export const PROVIDE_UNIQUE_CONTAINERS = localize('sql.migration.provide.unique.containers', "Please provide unique containers for target databases. Databases affected: "); +export function SQL_SOURCE_DETAILS(authMethod: MigrationSourceAuthenticationType, serverName: string): string { + switch (authMethod) { + case MigrationSourceAuthenticationType.Integrated: + return localize('sql.migration.source.details.windowAuth', "Enter the Windows Authentication credential used for connecting to SQL Server Instance {0}. ​ This credential will be used to for connecting to SQL Server instance and identifying valid backup file(s)", serverName); + case MigrationSourceAuthenticationType.Sql: + return localize('sql.migration.source.details.sqlAuth', "Enter the SQL Authentication credential used for connecting to SQL Server Instance {0}. ​ This credential will be used to for connecting to SQL Server instance and identifying valid backup file(s)", serverName); + } +} // integration runtime page export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service"); @@ -261,8 +271,10 @@ export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account deta export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine"); export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details"); export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migration in progress"); +export const MIGRATION_FAILED = localize('sql.migration.failed', "Migration failed"); export const LOG_SHIPPING_IN_PROGRESS = localize('sql.migration.log.shipping.in.progress', "Log shipping in progress"); -export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migration completed"); +export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migration completed"); +export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Completing cutover"); export const SUCCESSFULLY_MIGRATED_TO_AZURE_SQL = localize('sql.migration.successfully.migrated.to.azure.sql', "Successfully migrated to Azure SQL"); export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migration not started"); export const CHOOSE_TO_MIGRATE_TO_AZURE_SQL = localize('sql.migration.choose.to.migrate.to.azure.sql', "Choose to migrate to Azure SQL"); diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index 8be02d8868..7d5af19b5a 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -9,7 +9,8 @@ import { MigrationContext, MigrationLocalStorage } from '../models/migrationLoca import * as loc from '../constants/strings'; import { IconPath, IconPathHelper } from '../constants/iconPathHelper'; import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog'; -import { MigrationCategory } from '../dialog/migrationStatus/migrationStatusDialogModel'; +import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; +import { filterMigrations } from '../api/utils'; interface IActionMetadata { title?: string, @@ -39,6 +40,8 @@ export class DashboardWidget { private _inProgressMigrationButton!: StatusCard; private _inProgressWarningMigrationButton!: StatusCard; private _successfulMigrationButton!: StatusCard; + private _failedMigrationButton!: StatusCard; + private _completingMigrationButton!: StatusCard; private _notStartedMigrationCard!: StatusCard; private _migrationStatusMap: Map = new Map(); private _viewAllMigrationsButton!: azdata.ButtonComponent; @@ -233,15 +236,9 @@ export class DashboardWidget { this._migrationStatusCardLoadingContainer.loading = true; try { this.setCurrentMigrations(await this.getMigrations()); - const migrationStatus = await this.getCurrentMigrations(); - const inProgressMigrations = migrationStatus.filter((value) => { - const status = value.migrationContext.properties.migrationStatus; - const provisioning = value.migrationContext.properties.provisioningState; - return status === 'InProgress' || status === 'Creating' || status === 'Completing' || provisioning === 'Creating'; - }); - + const migrations = await this.getCurrentMigrations(); + const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING); let warningCount = 0; - for (let i = 0; i < inProgressMigrations.length; i++) { if ( inProgressMigrations[i].asyncOperationResult?.error?.message || @@ -252,7 +249,6 @@ export class DashboardWidget { warningCount += 1; } } - if (warningCount > 0) { this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount); this._inProgressMigrationButton.container.display = 'none'; @@ -261,22 +257,32 @@ export class DashboardWidget { this._inProgressMigrationButton.container.display = 'inline'; this._inProgressWarningMigrationButton.container.display = 'none'; } + this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString(); this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString(); - const successfulMigration = migrationStatus.filter((value) => { - const status = value.migrationContext.properties.migrationStatus; - return status === 'Succeeded'; - }); + const successfulMigration = filterMigrations(migrations, AdsMigrationStatus.SUCCEEDED); this._successfulMigrationButton.count.value = successfulMigration.length.toString(); - const currentConnection = (await azdata.connection.getCurrentConnection()); - const migrationDatabases = new Set( - migrationStatus.map((value) => { - return value.migrationContext.properties.sourceDatabaseName; - })); - const serverDatabases = await azdata.connection.listDatabases(currentConnection.connectionId); - this._notStartedMigrationCard.count.value = (serverDatabases.length - migrationDatabases.size).toString(); + + const failedMigrations = filterMigrations(migrations, AdsMigrationStatus.FAILED); + const failedCount = failedMigrations.length; + if (failedCount > 0) { + this._failedMigrationButton.container.display = 'inline'; + this._failedMigrationButton.count.value = failedMigrations.length.toString(); + } else { + this._failedMigrationButton.container.display = 'none'; + } + + const completingCutoverMigrations = filterMigrations(migrations, AdsMigrationStatus.COMPLETING); + const cutoverCount = completingCutoverMigrations.length; + if (cutoverCount > 0) { + this._completingMigrationButton.container.display = 'inline'; + this._completingMigrationButton.count.value = cutoverCount.toString(); + } else { + this._completingMigrationButton.container.display = 'none'; + } + } catch (error) { console.log(error); } finally { @@ -498,7 +504,7 @@ export class DashboardWidget { const statusContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '400px', - height: '280px', + height: '350px', justifyContent: 'flex-start', }).withProps({ CSSStyles: { @@ -527,7 +533,7 @@ export class DashboardWidget { this._viewAllMigrationsButton.onDidClick(async (e) => { const migrationStatus = await this.getCurrentMigrations(); - new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), MigrationCategory.ALL).initialize(); + new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize(); }); const refreshButton = view.modelBuilder.hyperlink().withProps({ @@ -581,7 +587,7 @@ export class DashboardWidget { loc.MIGRATION_IN_PROGRESS ); this._inProgressMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING); + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); dialog.initialize(); }); @@ -595,7 +601,7 @@ export class DashboardWidget { '' ); this._inProgressWarningMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING); + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); dialog.initialize(); }); @@ -608,13 +614,38 @@ export class DashboardWidget { loc.MIGRATION_COMPLETED ); this._successfulMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.SUCCEEDED); + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED); dialog.initialize(); }); this._migrationStatusCardsContainer.addItem( this._successfulMigrationButton.container ); + + this._completingMigrationButton = this.createStatusCard( + IconPathHelper.completingCutover, + loc.MIGRATION_CUTOVER_CARD + ); + this._completingMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING); + dialog.initialize(); + }); + this._migrationStatusCardsContainer.addItem( + this._completingMigrationButton.container + ); + + this._failedMigrationButton = this.createStatusCard( + IconPathHelper.error, + loc.MIGRATION_FAILED + ); + this._failedMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.FAILED); + dialog.initialize(); + }); + this._migrationStatusCardsContainer.addItem( + this._failedMigrationButton.container + ); + this._notStartedMigrationCard = this.createStatusCard( IconPathHelper.notStartedMigration, loc.MIGRATION_NOT_STARTED @@ -650,7 +681,7 @@ export class DashboardWidget { const linksContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '400px', - height: '280px', + height: '350px', justifyContent: 'flex-start', }).withProps({ CSSStyles: { diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts index 879ffa8acc..88ab74c262 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts @@ -72,7 +72,8 @@ export class ConfirmCutoverDialog { } }).component(); - const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length; + + const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length ?? 0; const pendingText = this._view.modelBuilder.text().withProps({ CSSStyles: { 'font-size': '13px', diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 918a432778..391385251d 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -517,9 +517,14 @@ export class MigrationCutoverDialog { this._fullBackupFile.value = fullBackupFileName! ?? '-'; let backupLocation; + const isBlobMigration = this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined; // Displaying storage accounts and blob container for azure blob backups. - if (this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob) { - backupLocation = `${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation.azureBlob.storageAccountResourceId.split('/').pop()} - ${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation.azureBlob.blobContainerName}`; + if (isBlobMigration) { + backupLocation = `${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.storageAccountResourceId.split('/').pop()} - ${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.blobContainerName}`; + this._fileCount.display = 'none'; + this.fileTable.updateCssStyles({ + 'display': 'none' + }); } else { backupLocation = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare?.path! ?? '-'; } @@ -547,7 +552,7 @@ export class MigrationCutoverDialog { if (migrationStatusTextValue === MigrationStatus.InProgress) { const restoredCount = (this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(a => a.listOfBackupFiles[0].status === 'Restored'))?.length ?? 0; - if (restoredCount > 0) { + if (restoredCount > 0 || isBlobMigration) { this._cutoverButton.enabled = true; } this._cancelButton.enabled = true; diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index 55a92916a5..b90960ae12 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -8,9 +8,9 @@ import * as vscode from 'vscode'; import { IconPathHelper } from '../../constants/iconPathHelper'; import { MigrationContext, MigrationLocalStorage } from '../../models/migrationLocalStorage'; import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog'; -import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel'; +import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel'; import * as loc from '../../constants/strings'; -import { convertTimeDifferenceToDuration } from '../../api/utils'; +import { convertTimeDifferenceToDuration, filterMigrations } from '../../api/utils'; export class MigrationStatusDialog { private _model: MigrationStatusDialogModel; private _dialogObject!: azdata.window.Dialog; @@ -21,7 +21,7 @@ export class MigrationStatusDialog { private _statusTable!: azdata.DeclarativeTableComponent; private _refreshLoader!: azdata.LoadingComponent; - constructor(migrations: MigrationContext[], private _filter: MigrationCategory) { + constructor(migrations: MigrationContext[], private _filter: AdsMigrationStatus) { this._model = new MigrationStatusDialogModel(migrations); this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide'); } @@ -40,7 +40,11 @@ export class MigrationStatusDialog { this.populateMigrationTable(); }); - this._statusDropdown.value = this._statusDropdown.values![this._filter]; + if (this._filter) { + this._statusDropdown.value = (this._statusDropdown.values).find((value) => { + return value.name === this._filter; + }); + } const formBuilder = view.modelBuilder.formContainer().withFormItems( [ @@ -124,10 +128,7 @@ export class MigrationStatusDialog { private populateMigrationTable(): void { try { - const migrations = this._model.filterMigration( - this._searchBox.value!, - (this._statusDropdown.value).name - ); + const migrations = filterMigrations(this._model._migrations, (this._statusDropdown.value).name, this._searchBox.value!); const data: azdata.DeclarativeTableCellValue[][] = []; diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts index 6b6c9d59af..56542fde03 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts @@ -10,47 +10,34 @@ export class MigrationStatusDialogModel { public statusDropdownValues: azdata.CategoryValue[] = [ { displayName: 'Status: All', - name: 'All', + name: AdsMigrationStatus.ALL, }, { displayName: 'Status: Ongoing', - name: 'Ongoing', + name: AdsMigrationStatus.ONGOING, + }, { + displayName: 'Status: Completing', + name: AdsMigrationStatus.COMPLETING }, { displayName: 'Status: Succeeded', - name: 'Succeeded', + name: AdsMigrationStatus.SUCCEEDED, + }, { + displayName: 'Status: Failed', + name: AdsMigrationStatus.FAILED } ]; constructor(public _migrations: MigrationContext[]) { } - public filterMigration(databaseName: string, category: string): MigrationContext[] { - let filteredMigration: MigrationContext[] = []; - if (category === 'All') { - filteredMigration = this._migrations; - } else if (category === 'Ongoing') { - filteredMigration = this._migrations.filter((value) => { - const status = value.migrationContext.properties.migrationStatus; - const provisioning = value.migrationContext.properties.provisioningState; - return status === 'InProgress' || status === 'Creating' || status === 'Completing' || provisioning === 'Creating'; - }); - } else if (category === 'Succeeded') { - filteredMigration = this._migrations.filter((value) => { - const status = value.migrationContext.properties.migrationStatus; - return status === 'Succeeded'; - }); - } - if (databaseName) { - filteredMigration = filteredMigration.filter((value) => { - return value.migrationContext.name.toLowerCase().includes(databaseName.toLowerCase()); - }); - } - - return filteredMigration; - } } -export enum MigrationCategory { - ALL, - ONGOING, - SUCCEEDED +/** + * This enum is used to categorize migrations internally in ADS. A migration has 2 statuses: Provisioning Status and Migration Status. The values from both the statuses are mapped to different values in this enum + */ +export enum AdsMigrationStatus { + ALL = 'all', + ONGOING = 'ongoing', + SUCCEEDED = 'succeeded', + FAILED = 'failed', + COMPLETING = 'completing' } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index c98431f048..29542ed20a 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -58,10 +58,9 @@ export enum NetworkContainerType { export interface DatabaseBackupModel { migrationMode: MigrationMode; networkContainerType: NetworkContainerType; - storageKey: string; networkShare: NetworkShare; subscription: azureResource.AzureResourceSubscription; - blob: Blob; + blobs: Blob[]; } export interface NetworkShare { @@ -70,12 +69,14 @@ export interface NetworkShare { password: string; resourceGroup: azureResource.AzureResourceResourceGroup; storageAccount: StorageAccount; + storageKey: string; } export interface Blob { resourceGroup: azureResource.AzureResourceResourceGroup; storageAccount: StorageAccount; blobContainer: azureResource.BlobContainer; + storageKey: string; } export interface Model { @@ -144,7 +145,7 @@ 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; + this._databaseBackup.blobs = []; } public get sourceConnectionId(): string { @@ -667,38 +668,38 @@ 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++) { try { + switch (this._databaseBackup.networkContainerType) { + case NetworkContainerType.BLOB_CONTAINER: + requestBody.properties.backupConfiguration = { + targetLocation: undefined!, + sourceLocation: { + azureBlob: { + storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id, + accountKey: this._databaseBackup.blobs[i].storageKey, + blobContainerName: this._databaseBackup.blobs[i].blobContainer.name + } + } + }; + break; + case NetworkContainerType.NETWORK_SHARE: + requestBody.properties.backupConfiguration = { + targetLocation: { + storageAccountResourceId: this._databaseBackup.networkShare.storageAccount.id, + accountKey: this._databaseBackup.networkShare.storageKey, + }, + sourceLocation: { + fileShare: { + path: this._databaseBackup.networkShare.networkShareLocation, + username: this._databaseBackup.networkShare.windowsUser, + password: this._databaseBackup.networkShare.password, + } + } + }; + break; + } requestBody.properties.sourceDatabaseName = this._migrationDbs[i]; const response = await startDatabaseMigration( this._azureAccount, diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 9685c9e69b..45a7b43b9e 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -7,9 +7,8 @@ import * as azdata from 'azdata'; import { EOL } from 'os'; import { getStorageAccountAccessKeys } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; +import { Blob, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; -import * as vscode from 'vscode'; import { IconPathHelper } from '../constants/iconPathHelper'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; export class DatabaseBackupPage extends MigrationWizardPage { @@ -19,17 +18,16 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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.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 _blobContainerResourceGroupDropdowns!: azdata.DropDownComponent[]; + private _blobContainerStorageAccountDropdowns!: azdata.DropDownComponent[]; + private _blobContainerDropdowns!: azdata.DropDownComponent[]; private _networkShareStorageAccountDetails!: azdata.FlexContainer; private _networkShareContainerSubscription!: azdata.InputBoxComponent; @@ -39,8 +37,12 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _networkShareContainerStorageAccountRefreshButton!: azdata.ButtonComponent; private _targetDatabaseContainer!: azdata.FlexContainer; - private _targetDatabaseNamesTable!: azdata.DeclarativeTableComponent; - private _targetDatabaseNames: azdata.InputBoxComponent[] = []; + 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[] = []; @@ -107,7 +109,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { networkShareButton.onDidChangeCheckedState((e) => { if (e) { - this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE); + this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE); } }); @@ -122,25 +124,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { blobContainerButton.onDidChangeCheckedState((e) => { if (e) { - this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); - } - }); - - 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) => { - if (e) { - vscode.window.showInformationMessage('Feature coming soon'); - networkShareButton.checked = true; - //this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE); + this.switchNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); } }); @@ -148,8 +132,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { [ selectLocationText, networkShareButton, - blobContainerButton, - fileShareButton + blobContainerButton ] ).withLayout({ flexFlow: 'column' @@ -161,19 +144,69 @@ export class DatabaseBackupPage extends MigrationWizardPage { private createNetworkDetailsContainer(): azdata.FlexContainer { this._networkShareContainer = this.createNetworkShareContainer(); this._blobContainer = this.createBlobContainer(); - this._fileShareContainer = this.createFileShareContainer(); const networkContainer = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).withItems([ this._networkShareContainer, this._blobContainer, - this._fileShareContainer ]).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 usernameLable = this._view.modelBuilder.text().withProps({ + value: constants.USERNAME, + width: WIZARD_INPUT_COMPONENT_WIDTH, + 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._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, + 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._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, @@ -282,8 +315,17 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.migrationStateModel._databaseBackup.networkShare.password = value; }); + + + const flexContainer = this._view.modelBuilder.flexContainer().withItems( [ + sqlSourceHeader, + this._sourceHelpText, + usernameLable, + this._sqlSourceUsernameInput, + sqlPasswordLabel, + this._sqlSourcepassword, networkShareHeading, networkShareHelpText, networkLocationInputBoxLabel, @@ -303,39 +345,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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() @@ -364,66 +373,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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( [ @@ -431,12 +380,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerSubscription, locationLabel, this._blobContainerLocation, - resourceGroupLabel, - this._blobContainerResourceGroup, - storageAccountLabel, - this._blobContainerStorageAccountDropdown, - blobContainerLabel, - this._blobContainerDropdown, ] ).withLayout({ flexFlow: 'column' @@ -449,12 +392,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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', @@ -462,8 +399,13 @@ export class DatabaseBackupPage extends MigrationWizardPage { 'text-align': 'left', 'border-bottom': '1px solid', }; + const rowCssStyle: azdata.CssStyles = { + 'border': 'none', + 'font-size': '13px', + 'border-bottom': '1px solid', + }; - this._targetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({ + this._newtworkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({ columns: [ { displayName: constants.SOURCE_DATABASE, @@ -483,11 +425,64 @@ export class DatabaseBackupPage extends MigrationWizardPage { } ] }).component(); + this._blobContainerTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({ + columns: [ + { + displayName: constants.SOURCE_DATABASE, + valueType: azdata.DeclarativeDataType.string, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: '200px' + }, + { + displayName: constants.TARGET_DATABASE_NAME, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: '200px' + }, + { + displayName: constants.RESOURCE_GROUP, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: '200px' + }, + { + displayName: constants.STORAGE_ACCOUNT, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: '200px' + }, + { + displayName: constants.BLOB_CONTAINER, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: '200px' + } + ] + }).component(); + + this._networkTableContainer = this._view.modelBuilder.flexContainer().withItems([ + this._newtworkShareTargetDatabaseNamesTable + ]).component(); + + this._blobTableContainer = this._view.modelBuilder.flexContainer().withItems([ + this._blobContainerTargetDatabaseNamesTable + ]).component(); const container = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).withItems([ - this._targetDatabaseNamesTable + this._networkTableContainer, + this._blobTableContainer ]).withProps({ display: 'none' }).component(); @@ -631,23 +626,39 @@ export class DatabaseBackupPage extends MigrationWizardPage { public async onPageEnter(): Promise { + if (this.migrationStateModel.refreshDatabaseBackupPage) { - this._targetDatabaseNames = []; + 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 = []; + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); } this.migrationStateModel._targetDatabaseNames = []; - - const tableRows: azdata.DeclarativeTableCellValue[][] = []; + this.migrationStateModel._databaseBackup.blobs = []; this.migrationStateModel._migrationDbs.forEach((db, index) => { - const targetRow: azdata.DeclarativeTableCellValue[] = []; + this.migrationStateModel._targetDatabaseNames.push(''); + this.migrationStateModel._databaseBackup.blobs.push({}); const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ required: true, value: db, - width: '280px' + width: '200px' }).withValidation(c => { - if (this._targetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. + 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; } @@ -664,18 +675,105 @@ export class DatabaseBackupPage extends MigrationWizardPage { targetDatabaseInput.onTextChanged((value) => { this.migrationStateModel._targetDatabaseNames[index] = value.trim(); }); - this._targetDatabaseNames.push(targetDatabaseInput); + this._networkShareTargetDatabaseNames.push(targetDatabaseInput); + const blobtargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ + required: true, + value: db, + width: '200px' + }).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(); + blobtargetDatabaseInput.onTextChanged((value) => { + this.migrationStateModel._targetDatabaseNames[index] = value.trim(); + }); + this._blobContainerTargetDatabaseNames.push(blobtargetDatabaseInput); + + const blobContainerResourceDropdown = this._view.modelBuilder.dropDown().withProps({ + width: '200px' + }).component(); + blobContainerResourceDropdown.onValueChanged(e => { + if (e.selected && e.selected !== constants.RESOURCE_GROUP_NOT_FOUND) { + this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index); + } + this.loadblobStorageDropdown(index); + }); + this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); + + const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() + .withProps({ + width: '200px' + }).component(); + + blobContainerStorageAccountDropdown.onValueChanged(async (value) => { + if (value.selected && value.selected !== constants.NO_STORAGE_ACCOUNT_FOUND) { + this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(value.index); + } + await this.loadBlobContainerDropdown(index); + }); + this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); + + const blobContainerDropdown = this._view.modelBuilder.dropDown() + .withProps({ + width: '200px' + }).component(); + blobContainerDropdown.onValueChanged(async (value) => { + if (value.selected && value.selected !== constants.NO_BLOBCONTAINERS_FOUND) { + this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(value.index); + } + }); + this._blobContainerDropdowns.push(blobContainerDropdown); + }); + + + let data: azdata.DeclarativeTableCellValue[][] = []; + this.migrationStateModel._migrationDbs.forEach((db, index) => { + const targetRow: azdata.DeclarativeTableCellValue[] = []; targetRow.push({ value: db }); targetRow.push({ - value: targetDatabaseInput + value: this._networkShareTargetDatabaseNames[index] }); - tableRows.push(targetRow); + 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] + }); + data.push(targetRow); + }); + this._blobContainerTargetDatabaseNamesTable.dataValues = data; - this._targetDatabaseNamesTable.dataValues = tableRows; this.migrationStateModel.refreshDatabaseBackupPage = false; } await this.getSubscriptionValues(); @@ -696,15 +794,39 @@ export class DatabaseBackupPage extends MigrationWizardPage { } 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); - } - if ((this._blobContainerDropdown.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { - errors.push(constants.INVALID_BLOBCONTAINER_ERROR); + this._blobContainerResourceGroupDropdowns.forEach(v => { + if ((v.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) { + errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); + } + }); + this._blobContainerStorageAccountDropdowns.forEach(v => { + if ((v.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { + errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); + } + }); + this._blobContainerDropdowns.forEach(v => { + if ((v.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { + errors.push(constants.INVALID_BLOBCONTAINER_ERROR); + } + }); + + 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; } @@ -727,13 +849,25 @@ export class DatabaseBackupPage extends MigrationWizardPage { public async onPageLeave(): Promise { try { - const storageAccount = (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) ? - this.migrationStateModel._databaseBackup.blob.storageAccount : this.migrationStateModel._databaseBackup.networkShare.storageAccount; + 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.storageKey = (await getStorageAccountAccessKeys( - this.migrationStateModel._azureAccount, - this.migrationStateModel._databaseBackup.subscription, - storageAccount)).keyName1; + this.migrationStateModel._databaseBackup.networkShare.storageKey = (await getStorageAccountAccessKeys( + this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription, + storageAccount)).keyName1; + break; + } } finally { this.wizard.registerNavigationValidator((pageChangeInfo) => { return true; @@ -744,7 +878,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { protected async handleStateChange(e: StateChangeEvent): Promise { } - private toggleNetworkContainerFields(containerType: NetworkContainerType): void { + private switchNetworkContainerFields(containerType: NetworkContainerType): void { this.wizard.message = { text: '', level: azdata.window.MessageLevel.Error @@ -752,11 +886,18 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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._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; + }); this._windowsUserAccountText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE @@ -764,11 +905,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._passwordText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); + this._sqlSourceUsernameInput.updateProperties({ + required: containerType === NetworkContainerType.NETWORK_SHARE + }); + this._sqlSourcepassword.updateProperties({ + required: containerType === NetworkContainerType.NETWORK_SHARE + }); 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(); @@ -776,12 +925,13 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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(); - }); + 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(); + } } private async getSubscriptionValues(): Promise { @@ -823,36 +973,37 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private async loadblobResourceGroup(): Promise { - this._blobContainerResourceGroup.loading = true; + this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); try { - this._blobContainerResourceGroup.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); + const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); + this._blobContainerResourceGroupDropdowns.forEach(v => v.values = resourceGroupValues); } catch (error) { console.log(error); } finally { - this._blobContainerResourceGroup.loading = false; + this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = false); } } - private async loadblobStorageDropdown(): Promise { - this._blobContainerStorageAccountDropdown.loading = true; + private async loadblobStorageDropdown(index: number): Promise { + this._blobContainerStorageAccountDropdowns[index].loading = true; try { - this._blobContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blob.resourceGroup); + this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].resourceGroup); } catch (error) { console.log(error); } finally { - this._blobContainerStorageAccountDropdown.loading = false; + this._blobContainerStorageAccountDropdowns[index].loading = false; } } - private async loadBlobContainerDropdown(): Promise { - this._blobContainerDropdown.loading = true; + 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.blob.storageAccount); - this._blobContainerDropdown.values = blobContainerValues; + const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount); + this._blobContainerDropdowns[index].values = blobContainerValues; } catch (error) { console.log(error); } finally { - this._blobContainerDropdown.loading = false; + this._blobContainerDropdowns[index].loading = false; } } diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 787d102a86..45cffa0156 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -96,29 +96,25 @@ export class SummaryPage extends MigrationWizardPage { ] ); 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), - ] - ); - 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.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) + createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.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])); + if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { + flexContainer.addItems([ + createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.blobs[index].storageAccount.location), + createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.blobs[index].storageAccount.resourceGroup!), + createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.blobs[index].storageAccount.name), + createInformationRow(this._view, constants.BLOB_CONTAINER, this.migrationStateModel._databaseBackup.blobs[index].blobContainer.name) + ]); + } }); return flexContainer; } diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 514e50f439..a0dff13bb8 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -15,7 +15,6 @@ import { AccountsSelectionPage } from './accountsSelectionPage'; import { IntergrationRuntimePage } from './integrationRuntimePage'; import { SummaryPage } from './summaryPage'; import { MigrationModePage } from './migrationModePage'; -import { SqlSourceConfigurationPage } from './sqlSourceConfigurationPage'; export const WIZARD_INPUT_COMPONENT_WIDTH = '600px'; export class WizardController { @@ -40,14 +39,12 @@ export class WizardController { const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel); const migrationModePage = new MigrationModePage(wizard, stateModel); const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel); - const sourceConfigurationPage = new SqlSourceConfigurationPage(wizard, stateModel); const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel); const integrationRuntimePage = new IntergrationRuntimePage(wizard, stateModel); const summaryPage = new SummaryPage(wizard, stateModel); const pages: MigrationWizardPage[] = [ azureAccountsPage, - sourceConfigurationPage, skuRecommendationPage, migrationModePage, databaseBackupPage,