diff --git a/extensions/sql-migration/images/background.svg b/extensions/sql-migration/images/background.svg index 9bc057df35..02c9c49d34 100644 --- a/extensions/sql-migration/images/background.svg +++ b/extensions/sql-migration/images/background.svg @@ -16,21 +16,7 @@ - - - - - - - - - - - - - - - + diff --git a/extensions/sql-migration/images/discard.svg b/extensions/sql-migration/images/discard.svg new file mode 100644 index 0000000000..356c911744 --- /dev/null +++ b/extensions/sql-migration/images/discard.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 09de559d90..94aeabeecf 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -220,6 +220,16 @@ export async function startMigrationCutover(account: azdata.Account, subscriptio return response.response.data.value; } +export async function stopMigration(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration): Promise { + const api = await getAzureCoreAPI(); + const host = `https://eastus2euap.management.azure.com`; + const path = `${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cancel?api-version=2020-09-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } +} + /** * For now only east us euap is supported. Actual API calls will be added in the public release. */ diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index 15e9de31f7..cebaba14f4 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -12,6 +12,7 @@ export interface IconPath { export class IconPathHelper { public static copy: IconPath; + public static discard: IconPath; public static refresh: IconPath; public static cutover: IconPath; public static sqlMigrationLogo: IconPath; @@ -27,6 +28,10 @@ export class IconPathHelper { light: context.asAbsolutePath('images/copy.svg'), dark: context.asAbsolutePath('images/copy.svg') }; + IconPathHelper.discard = { + light: context.asAbsolutePath('images/discard.svg'), + dark: context.asAbsolutePath('images/discard.svg') + }; IconPathHelper.refresh = { light: context.asAbsolutePath('images/refresh.svg'), dark: context.asAbsolutePath('images/refresh.svg') diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 12ecb9ce42..2bb94050b1 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -58,7 +58,7 @@ export const DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL = localize('sql.migrat export const DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL = localize('sql.migration.network.share.windows.user.label', "Windows user account with read access to the network share location."); export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL = localize('sql.migration.network.share.password.label', "Password"); export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER = localize('sql.migration.network.share.password.placeholder', "Enter password"); -export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Enter Azure storage account information where the backup will be copied"); +export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Select the storage account where backup files will be copied to during migration"); export const DATABASE_BACKUP_NETWORK_SHARE_SUBSCRIPTION_LABEL = localize('sql.migration.network.share.subscription.label', "Select the subscription that contains the storage account."); export const DATABASE_BACKUP_SUBSCRIPTION_PLACEHOLDER = localize('sql.migration.network.share.subscription.placeholder', "Select subscription"); 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."); @@ -87,7 +87,21 @@ 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); +} +export function TARGET_FILE_SHARE(dbName: string): string { + return localize('sql.migration.file.share', "Select the file share that contains the backup files for ‘{0}’", dbName); +} +export function TARGET_BLOB_CONTAINER(dbName: string): string { + return localize('sql.migration.blob.container', "Select the container that contains the backup files for ‘{0}’", dbName); +} +export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter network share path information for selected databases"); +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"); // integration runtime page export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Migration Controller"); @@ -168,8 +182,8 @@ export const SUMMARY_AZURE_STORAGE_SUBSCRIPTION = localize('sql.migration.summar export const SUMMARY_AZURE_STORAGE = localize('sql.migration.summary.azure.storage', "Azure storage"); export const SUMMARY_IR_NODE = localize('sql.migration.ir.node', "Integration Runtime node"); export const NETWORK_SHARE = localize('sql.migration.network.share', "Network Share"); -export const BLOB_CONTAINER = localize('sql.migration.blob.container', "Blob Container"); -export const FILE_SHARE = localize('sql.migration.file.share', "File Share"); +export const BLOB_CONTAINER = localize('sql.migration.blob.container.title', "Blob Container"); +export const FILE_SHARE = localize('sql.migration.file.share.title', "File Share"); export const MIGRATION_STARTED = localize('sql.migration.started.notification', "Migration in progress"); // Open notebook quick pick string @@ -184,8 +198,8 @@ export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.descripti export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL"); export const DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION = localize('sql.migration.dashboard.migrate.task.button.description', "Migrate SQL Server instance to Azure SQL."); export const DATABASE_MIGRATION_STATUS = localize('sql.migration.database.migration.status', "Database Migration Status"); -export const HELP_VIDEO1_TITLE = localize('sql.migration.dashboard.video1.title', "Migrate to SQL Server to SQL Managed Instance"); -export const HELP_VIDEO2_TITLE = localize('sql.migration.dashboard.video2.title', "Migrate to SQL Server to SQL Virtual Machine"); +export const HELP_VIDEO1_TITLE = localize('sql.migration.dashboard.video1.title', "Migrate SQL Server to SQL Managed Instance"); +export const HELP_VIDEO2_TITLE = localize('sql.migration.dashboard.video2.title', "Migrate SQL Server to SQL Virtual Machine"); export const HELP_LINK1_TITLE = localize('sql.migration.dashboard.link1.title', "Migrating your SQL Server to cloud"); export const HELP_LINK1_DESCRIPTION = localize('sql.migration.dashboard.link1.description', "Lorem ipsum dolor sit amet, consectetur adipi. Lorem ipsum dolor sit amet, consectetur adipi. Lorem ipsum."); export const HELP_TITLE = localize('sql.migration.dashboard.help.title', "Help Articles and Video Links"); @@ -212,11 +226,11 @@ export const SOURCE_VERSION = localize('sql.migration.source.version', "Source v export const TARGET_SERVER = localize('sql.migration.target.server', "Target server"); export const TARGET_VERSION = localize('sql.migration.target.version', "Target version"); export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status"); -export const FULL_BACKUP_FILES = localize('sql.migration.full.backup.files', "Full backup files(s)"); +export const FULL_BACKUP_FILES = localize('sql.migration.full.backup.files', "Full backup files"); export const LAST_APPLIED_LSN = localize('sql.migration.last.applied.lsn', "Last applied LSN"); -export const LAST_APPLIED_BACKUP_FILES = localize('sql.migration.last.applied.backup.files', "Last applied backup file(s)"); -export const LAST_APPLIED_BACKUP_FILES_TAKEN_ON = localize('sql.migration.last.applied.files.taken.on', "Last applied backup file(s) taken on"); -export const ACTIVE_BACKUP_FILES = localize('sql.migration.active.backup.files', "Active Backup file(s)"); +export const LAST_APPLIED_BACKUP_FILES = localize('sql.migration.last.applied.backup.files', "Last applied backup files"); +export const LAST_APPLIED_BACKUP_FILES_TAKEN_ON = localize('sql.migration.last.applied.files.taken.on', "Last applied backup files taken on"); +export const ACTIVE_BACKUP_FILES = localize('sql.migration.active.backup.files', "Active Backup files"); export const STATUS = localize('sql.migration.status', "Status"); export const BACKUP_START_TIME = localize('sql.migration.backup.start.time', "Backup start time"); export const FIRST_LSN = localize('sql.migration.first.lsn', "First LSN"); @@ -224,7 +238,7 @@ export const LAST_LSN = localize('sql.migration.last.LSN', "Last LSN"); export const CANNOT_START_CUTOVER_ERROR = localize('sql.migration.cannot.start.cutover.error', "Cannot start the cutover process until all the migrations are done. Click refresh to fetch the latest file status"); export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Database Managed Instance"); export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "Azure SQL Database Virtual Machine"); - +export const CANCEL_MIGRATION = localize('sql.migration.cancel.migration', "Cancel migration"); export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) { if (fileCount === 1) { return localize('sql.migration.active.backup.files.items', "Active Backup files (1 item)"); diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index dd0c5c010f..026108ec1f 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -184,6 +184,9 @@ export class DashboardWidget { label: taskMetaData.title, title: taskMetaData.title, width: maxWidth, + CSSStyles: { + 'border': '1px solid' + } }).component(); buttonContainer.onDidClick(async () => { if (taskMetaData.command) { @@ -267,12 +270,17 @@ export class DashboardWidget { const localMigrations = MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection); for (let i = 0; i < localMigrations.length; i++) { const localMigration = localMigrations[i]; - localMigration.migrationContext = await getDatabaseMigration( - localMigration.azureAccount, - localMigration.subscription, - localMigration.targetManagedInstance.location, - localMigration.migrationContext.id - ); + try { + localMigration.migrationContext = await getDatabaseMigration( + localMigration.azureAccount, + localMigration.subscription, + localMigration.targetManagedInstance.location, + localMigration.migrationContext.id + ); + } catch (e) { + console.log(e); + } + localMigration.sourceConnectionProfile = currentConnection; } return localMigrations; @@ -329,7 +337,7 @@ export class DashboardWidget { 'width': '400px', 'height': '50px', 'margin-top': '10px', - 'box-shadow': '0 1px 2px 0 rgba(0,0,0,0.2)' + 'border': '1px solid' } }).component(); @@ -389,7 +397,7 @@ export class DashboardWidget { justifyContent: 'flex-start', }).withProps({ CSSStyles: { - 'border': '1px solid rgba(0, 0, 0, 0.1)', + 'border': '1px solid', 'padding': '15px' } }).component(); @@ -436,7 +444,7 @@ export class DashboardWidget { buttonContainer.addItem(viewAllButton, { flex: 'auto', CSSStyles: { - 'border-right': '1px solid rgba(0, 0, 0, 0.7)', + 'border-right': '1px solid ', 'width': '40px', } }); @@ -492,7 +500,7 @@ export class DashboardWidget { justifyContent: 'flex-start', }).withProps({ CSSStyles: { - 'border': '1px solid rgba(0, 0, 0, 0.1)', + 'border': '1px solid', 'padding': '15px' } }).component(); @@ -518,7 +526,7 @@ export class DashboardWidget { const links = [{ title: loc.HELP_LINK1_TITLE, description: loc.HELP_LINK1_DESCRIPTION, - link: 'www.microsoft.com' //TODO: add proper link over here. + link: 'https://www.microsoft.com' //TODO: add proper link over here. }]; const styles = { diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index bcd2384adc..fbb5344e51 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -128,6 +128,7 @@ export class AssessmentResultsDialog { protected async execute() { this.model._migrationDbs = this._tree.selectedDbs(); this.skuRecommendationPage.refreshDatabaseCount(this._model._migrationDbs.length); + this.model.refreshDatabaseBackupPage = true; this._isOpen = false; } diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 8346a3e093..e11b9b568e 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -16,6 +16,7 @@ export class MigrationCutoverDialog { private _databaseTitleName!: azdata.TextComponent; private _databaseCutoverButton!: azdata.ButtonComponent; private _refresh!: azdata.ButtonComponent; + private _cancel!: azdata.ButtonComponent; private _serverName!: azdata.TextComponent; private _serverVersion!: azdata.TextComponent; @@ -290,6 +291,27 @@ export class MigrationCutoverDialog { } }); + this._cancel = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.discard, + iconHeight: '16px', + iconWidth: '16px', + label: loc.CANCEL_MIGRATION, + height: '55px', + width: '130px' + }).component(); + + this._cancel.onDidClick((e) => { + this.cancelMigration(); + }); + + header.addItem(this._cancel, { + flex: '0', + CSSStyles: { + 'width': '130px' + } + }); + + this._refresh = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.refresh, iconHeight: '16px', @@ -392,6 +414,7 @@ export class MigrationCutoverDialog { this._databaseCutoverButton.enabled = true; } else { this._databaseCutoverButton.enabled = false; + this._cancel.enabled = false; } } catch (e) { console.log(e); @@ -419,7 +442,10 @@ export class MigrationCutoverDialog { value: value, CSSStyles: { 'margin-top': '5px', - 'margin-bottom': '0' + 'margin-bottom': '0', + 'width': '100%', + 'overflow': 'hidden', + 'text-overflow': 'ellipses' } }).component(); flexContainer.addItem(textComponent); @@ -428,6 +454,11 @@ export class MigrationCutoverDialog { text: textComponent }; } + + private async cancelMigration(): Promise { + await this._model.cancelMigration(); + await this.refreshStatus(); + } } interface ActiveBackupFileSchema { diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts index 66d5a82dee..5a7f7b74c7 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getMigrationStatus, DatabaseMigration, startMigrationCutover } from '../../api/azure'; +import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration } from '../../api/azure'; import { MigrationContext } from '../../models/migrationLocalStorage'; @@ -36,4 +36,19 @@ export class MigrationCutoverDialogModel { } return undefined!; } + + public async cancelMigration(): Promise { + try { + if (this.migrationStatus) { + await stopMigration( + this._migration.azureAccount, + this._migration.subscription, + this.migrationStatus + ); + } + } catch (error) { + console.log(error); + } + return undefined!; + } } diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index 34d014bdb9..d66df04a73 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -10,6 +10,7 @@ import { MigrationContext } from '../../models/migrationLocalStorage'; import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog'; import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel'; import * as loc from '../../constants/strings'; +import { getDatabaseMigration } from '../../api/azure'; export class MigrationStatusDialog { private _model: MigrationStatusDialogModel; private _dialogObject!: azdata.window.Dialog; @@ -84,6 +85,10 @@ export class MigrationStatusDialog { label: 'Refresh', }).component(); + this._refresh.onDidClick((e) => { + this.refreshTable(); + }); + const flexContainer = this._view.modelBuilder.flexContainer().component(); flexContainer.addItem(this._searchBox, { @@ -136,7 +141,7 @@ export class MigrationStatusDialog { height: '20px' }).component(); const sqlMigrationName = this._view.modelBuilder.hyperlink().withProps({ - label: migration.migrationContext.name, + label: migration.targetManagedInstance.name, url: '' }).component(); sqlMigrationName.onDidClick((e) => { @@ -184,6 +189,19 @@ export class MigrationStatusDialog { } } + private refreshTable(): void { + this._model._migrations.forEach(async (migration) => { + migration.migrationContext = await getDatabaseMigration( + migration.azureAccount, + migration.subscription, + migration.targetManagedInstance.location, + migration.migrationContext.id + ); + }); + + this.populateMigrationTable(); + } + private createStatusTable(): azdata.DeclarativeTableComponent { this._statusTable = this._view.modelBuilder.declarativeTable().withProps({ columns: [ diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index e8dcaf8d11..e18c0817d6 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -52,15 +52,15 @@ export interface NetworkShare { export interface DatabaseBackupModel { migrationCutover: MigrationCutover; networkContainerType: NetworkContainerType; - networkShareLocation: string; + networkShareLocations: string[]; windowsUser: string; password: string; subscription: azureResource.AzureResourceSubscription; storageAccount: StorageAccount; storageKey: string; azureSecurityToken: string; - fileShare: azureResource.FileShare; - blobContainer: azureResource.BlobContainer; + fileShares: azureResource.FileShare[]; + blobContainers: azureResource.BlobContainer[]; } export interface Model { readonly sourceConnectionId: string; @@ -91,6 +91,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _storageAccounts!: StorageAccount[]; public _fileShares!: azureResource.FileShare[]; public _blobContainers!: azureResource.BlobContainer[]; + public _refreshNetworkShareLocation!: azureResource.BlobContainer[]; + public _targetDatabaseNames!: string[]; public _migrationController!: SqlMigrationController; public _migrationControllers!: SqlMigrationController[]; @@ -102,7 +104,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { private _skuRecommendations: SKURecommendations | undefined; private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined; - + public refreshDatabaseBackupPage!: boolean; constructor( private readonly _extensionContext: vscode.ExtensionContext, @@ -467,7 +469,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { }, SourceLocation: { FileShare: { - Path: this._databaseBackup.networkShareLocation, + Path: '', Username: this._databaseBackup.windowsUser, Password: this._databaseBackup.password, } @@ -482,19 +484,19 @@ export class MigrationStateModel implements Model, vscode.Disposable { } }; - this._migrationDbs.forEach(async (db) => { + this._migrationDbs.forEach(async (db, index) => { requestBody.properties.SourceDatabaseName = db; try { + requestBody.properties.BackupConfiguration.SourceLocation.FileShare.Path = this._databaseBackup.networkShareLocations[index]; const response = await startDatabaseMigration( this._azureAccount, this._targetSubscription, this._migrationController?.properties.location!, this._targetServerInstance, - db, + this._targetDatabaseNames[index], requestBody ); - if (response.status === 201) { MigrationLocalStorage.saveMigration( currentConnection!, diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 46f8cdac32..f06850d281 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -7,27 +7,31 @@ import * as azdata from 'azdata'; import { EOL } from 'os'; import { getStorageAccountAccessKeys } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationCutover, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; +import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import * as vscode from 'vscode'; export class DatabaseBackupPage extends MigrationWizardPage { + private _view!: azdata.ModelView; private _networkShareContainer!: azdata.FlexContainer; private _networkShareContainerSubscriptionDropdown!: azdata.DropDownComponent; private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent; - private _networkShareLocationText!: azdata.InputBoxComponent; private _windowsUserAccountText!: azdata.InputBoxComponent; private _passwordText!: azdata.InputBoxComponent; + private _networkShareDatabaseConfigContainer!: azdata.FlexContainer; + private _networkShareLocations!: azdata.InputBoxComponent[]; private _blobContainer!: azdata.FlexContainer; private _blobContainerSubscriptionDropdown!: azdata.DropDownComponent; private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent; - private _blobContainerBlobDropdown!: azdata.DropDownComponent; + private _blobContainerDatabaseConfigContainer!: azdata.FlexContainer; + private _blobContainerDropdowns!: azdata.DropDownComponent[]; private _fileShareContainer!: azdata.FlexContainer; private _fileShareSubscriptionDropdown!: azdata.DropDownComponent; private _fileShareStorageAccountDropdown!: azdata.DropDownComponent; - private _fileShareFileShareDropdown!: azdata.DropDownComponent; + private _fileShareDatabaseConfigContainer!: azdata.FlexContainer; + private _fileShareDropdowns!: azdata.DropDownComponent[]; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel); @@ -35,7 +39,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } 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); @@ -56,7 +60,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { title: '', component: networkContainer }, - this.migrationModeContainer(view), ] ); await view.initializeModel(form.component()); @@ -68,7 +71,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { const networkShareButton = view.modelBuilder.radioButton() .withProps({ name: buttonGroup, - label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL + label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL, + checked: true }).component(); networkShareButton.onDidChangeCheckedState((e) => { @@ -150,26 +154,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._fileShareStorageAccountDropdown.onValueChanged(async (value) => { if (value.selected) { this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); - this.migrationStateModel._databaseBackup.fileShare = undefined!; + this.migrationStateModel._databaseBackup.fileShares = undefined!; await this.loadFileShareDropdown(); } }); - const fileShareLabel = view.modelBuilder.text() - .withProps({ - value: constants.DATABASE_BACKUP_FILE_SHARE_LABEL, - requiredIndicator: true, - }).component(); - this._fileShareFileShareDropdown = view.modelBuilder.dropDown() - .withProps({ - required: true - }).component(); - this._fileShareFileShareDropdown.onValueChanged((value) => { - if (value.selected) { - this.migrationStateModel._databaseBackup.fileShare = this.migrationStateModel.getFileShare(value.index); - } - }); + 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( @@ -178,8 +175,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._fileShareSubscriptionDropdown, storageAccountLabel, this._fileShareStorageAccountDropdown, - fileShareLabel, - this._fileShareFileShareDropdown + fileShareDatabaseConfigHeader, + this._fileShareDatabaseConfigContainer ] ).withLayout({ flexFlow: 'column' @@ -220,24 +217,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => { if (value.selected) { this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); - this.migrationStateModel._databaseBackup.blobContainer = undefined!; + this.migrationStateModel._databaseBackup.blobContainers = undefined!; await this.loadBlobContainerDropdown(); } }); - const containerLabel = view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_LABEL, - requiredIndicator: true, + + const blobContainerDatabaseConfigHeader = view.modelBuilder.text().withProps({ + value: constants.ENTER_BLOB_CONTAINER_INFORMATION + }).component(); + + this._blobContainerDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' }).component(); - this._blobContainerBlobDropdown = view.modelBuilder.dropDown() - .withProps({ - required: true - }).component(); - this._blobContainerBlobDropdown.onValueChanged((value) => { - if (value.selected) { - this.migrationStateModel._databaseBackup.blobContainer = this.migrationStateModel.getBlobContainer(value.index); - } - }); const flexContainer = view.modelBuilder.flexContainer() .withItems( @@ -246,8 +238,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerSubscriptionDropdown, storageAccountLabel, this._blobContainerStorageAccountDropdown, - containerLabel, - this._blobContainerBlobDropdown + blobContainerDatabaseConfigHeader, + this._blobContainerDatabaseConfigContainer ] ).withLayout({ flexFlow: 'column' @@ -264,31 +256,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, }).component(); - const networkShareLocationLabel = view.modelBuilder.text() - .withProps({ - value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, - requiredIndicator: true, - }).component(); - this._networkShareLocationText = view.modelBuilder.inputBox() - .withProps({ - placeHolder: '\\\\Servername.domainname.com\\Backupfolder', - required: true, - validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION - }) - .withValidation((component) => { - if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { - if (component.value) { - if (!/(?<=\\\\)[^\\]*/.test(component.value)) { - return false; - } - } - } - return true; - }).component(); - this._networkShareLocationText.onTextChanged((value) => { - this.migrationStateModel._databaseBackup.networkShareLocation = value; - }); - const windowsUserAccountLabel = view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL, @@ -366,20 +333,30 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }); + + const networkShareDatabaseConfigHeader = view.modelBuilder.text().withProps({ + value: constants.ENTER_NETWORK_SHARE_INFORMATION + }).component(); + + this._networkShareDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + + const flexContainer = view.modelBuilder.flexContainer().withItems( [ + azureAccountHelpText, networkShareHelpText, - networkShareLocationLabel, - this._networkShareLocationText, + subscriptionLabel, + this._networkShareContainerSubscriptionDropdown, + storageAccountLabel, + this._networkShareContainerStorageAccountDropdown, windowsUserAccountLabel, this._windowsUserAccountText, passwordLabel, this._passwordText, - azureAccountHelpText, - subscriptionLabel, - this._networkShareContainerSubscriptionDropdown, - storageAccountLabel, - this._networkShareContainerStorageAccountDropdown + networkShareDatabaseConfigHeader, + this._networkShareDatabaseConfigContainer ] ).withLayout({ flexFlow: 'column' @@ -390,57 +367,130 @@ export class DatabaseBackupPage extends MigrationWizardPage { return flexContainer; } - private migrationModeContainer(view: azdata.ModelView): azdata.FormComponent { - const description = view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION - }).component(); - - const buttonGroup = 'cutoverContainer'; - - const onlineButton = view.modelBuilder.radioButton().withProps({ - label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL, - name: buttonGroup, - checked: true - }).component(); - - this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE; - - onlineButton.onDidChangeCheckedState((e) => { - if (e) { - this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE; - } - }); - - const offlineButton = view.modelBuilder.radioButton().withProps({ - label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL, - name: buttonGroup - }).component(); - - offlineButton.onDidChangeCheckedState((e) => { - if (e) { - vscode.window.showInformationMessage('Feature coming soon'); - onlineButton.checked = true; - //this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.OFFLINE; - } - }); - - const flexContainer = view.modelBuilder.flexContainer().withItems( - [ - description, - onlineButton, - offlineButton - ] - ).withLayout({ - flexFlow: 'column' - }).component(); - - return { - title: constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL, - component: flexContainer - }; - } - public async onPageEnter(): Promise { + if (this.migrationStateModel.refreshDatabaseBackupPage) { + + this._networkShareLocations = []; + this._fileShareDropdowns = []; + this._blobContainerDropdowns = []; + this.migrationStateModel._targetDatabaseNames = []; + this.migrationStateModel._databaseBackup.networkShareLocations = []; + this.migrationStateModel._databaseBackup.fileShares = []; + this.migrationStateModel._databaseBackup.blobContainers = []; + this._networkShareDatabaseConfigContainer.clearItems(); + this._fileShareDatabaseConfigContainer.clearItems(); + this._blobContainerDatabaseConfigContainer.clearItems(); + + this.migrationStateModel._migrationDbs.forEach((db, index) => { + 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({ + required: true + }).component(); + targetNameNetworkInputBox.onTextChanged((value) => { + this.migrationStateModel._targetDatabaseNames[index] = value; + }); + + const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({ + value: constants.TARGET_NETWORK_SHARE_LOCATION(db), + requiredIndicator: true + }).component(); + const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({ + placeHolder: '\\\\Servername.domainname.com\\Backupfolder', + required: true, + validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION + }).withValidation((component) => { + if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { + if (component.value) { + if (!/(?<=\\\\)[^\\]*/.test(component.value)) { + return false; + } + } + } + return true; + }).component(); + networkLocationInputBox.onTextChanged((value) => { + this.validateFields(); + this.migrationStateModel._databaseBackup.networkShareLocations[index] = value; + }); + this.migrationStateModel._databaseBackup.networkShareLocations.push(undefined!); + this._networkShareLocations.push(networkLocationInputBox); + this._networkShareDatabaseConfigContainer.addItems( + [ + targetNameNetworkInputBoxLabel, + targetNameNetworkInputBox, + networkLocationInputBoxLabel, + networkLocationInputBox + ] + ); + + 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) { + this.validateFields(); + this.migrationStateModel._databaseBackup.fileShares[index] = this.migrationStateModel.getFileShare(value.index); + } + }); + 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) { + this.validateFields(); + this.migrationStateModel._databaseBackup.blobContainers[index] = this.migrationStateModel.getBlobContainer(value.index); + } + }); + this.migrationStateModel._databaseBackup.fileShares.push(undefined!); + this._blobContainerDropdowns.push(blobContainerDropdown); + this._blobContainerDatabaseConfigContainer.addItems( + [ + targetNameBlobInputBoxLabel, + targetNameBlobInputBox, + blobContainerLabel, + blobContainerDropdown + ] + ); + }); + + this.migrationStateModel.refreshDatabaseBackupPage = false; + } await this.getSubscriptionValues(); this.wizard.registerNavigationValidator((pageChangeInfo) => { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { @@ -465,8 +515,11 @@ export class DatabaseBackupPage extends MigrationWizardPage { if ((this._blobContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); } - if ((this._blobContainerBlobDropdown.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { - errors.push(constants.INVALID_BLOBCONTAINER_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: @@ -476,8 +529,11 @@ export class DatabaseBackupPage extends MigrationWizardPage { if ((this._fileShareStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); } - if ((this._fileShareFileShareDropdown.value).displayName === constants.NO_FILESHARES_FOUND) { - errors.push(constants.INVALID_FILESHARE_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; + } } break; } @@ -508,8 +564,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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._networkShareLocationText.updateProperties({ - required: containerType === NetworkContainerType.NETWORK_SHARE + this._networkShareLocations.forEach((inputBox) => { + inputBox.updateProperties({ + required: containerType === NetworkContainerType.NETWORK_SHARE + }); }); this._windowsUserAccountText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE @@ -517,21 +575,47 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._passwordText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); - - this._networkShareLocationText.validate(); + this._networkShareLocations.forEach((inputBox) => { + inputBox.validate(); + }); this._windowsUserAccountText.validate(); this._passwordText.validate(); this._networkShareContainerSubscriptionDropdown.validate(); this._networkShareContainerStorageAccountDropdown.validate(); this._blobContainerSubscriptionDropdown.validate(); this._blobContainerStorageAccountDropdown.validate(); - this._blobContainerBlobDropdown.validate(); + this._blobContainerDropdowns.forEach((dropdown) => { + dropdown.validate(); + }); this._fileShareSubscriptionDropdown.validate(); this._fileShareStorageAccountDropdown.validate(); - this._fileShareFileShareDropdown.validate(); + this._fileShareDropdowns.forEach(dropdown => { + dropdown.validate(); + }); } + + private validateFields(): void { + this._networkShareLocations.forEach((inputBox) => { + inputBox.validate(); + }); + this._windowsUserAccountText.validate(); + this._passwordText.validate(); + this._networkShareContainerSubscriptionDropdown.validate(); + this._networkShareContainerStorageAccountDropdown.validate(); + this._blobContainerSubscriptionDropdown.validate(); + this._blobContainerStorageAccountDropdown.validate(); + this._blobContainerDropdowns.forEach((dropdown) => { + dropdown.validate(); + }); + this._fileShareSubscriptionDropdown.validate(); + this._fileShareStorageAccountDropdown.validate(); + this._fileShareDropdowns.forEach((dropdown) => { + dropdown.validate(); + }); + } + private async getSubscriptionValues(): Promise { if (!this.migrationStateModel._databaseBackup.subscription) { this._networkShareContainerSubscriptionDropdown.loading = true; @@ -568,14 +652,18 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadFileShareStorageDropdown(): Promise { if (!this.migrationStateModel._databaseBackup.storageAccount) { this._fileShareStorageAccountDropdown.loading = true; - this._fileShareFileShareDropdown.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._fileShareFileShareDropdown.loading = false; + this._fileShareDropdowns.forEach((dropdown) => { + dropdown.loading = false; + }); } } } @@ -583,41 +671,58 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadblobStorageDropdown(): Promise { if (!this.migrationStateModel._databaseBackup.storageAccount) { this._blobContainerStorageAccountDropdown.loading = true; - this._blobContainerBlobDropdown.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._blobContainerBlobDropdown.loading = true; - + this._blobContainerDropdowns.forEach((dropdown) => { + dropdown.loading = false; + }); } } } private async loadFileShareDropdown(): Promise { if (!this.migrationStateModel._fileShares) { - this._fileShareFileShareDropdown.loading = true; + this._fileShareDropdowns.forEach((dropdown) => { + dropdown.loading = true; + }); try { - this._fileShareFileShareDropdown.values = await this.migrationStateModel.getFileShareValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount); + 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._fileShareFileShareDropdown.loading = false; + this._fileShareDropdowns.forEach((dropdown) => { + dropdown.loading = true; + }); } } } private async loadBlobContainerDropdown(): Promise { if (!this.migrationStateModel._blobContainers) { - this._blobContainerBlobDropdown.loading = true; + this._blobContainerDropdowns.forEach((dropdown) => { + dropdown.loading = true; + }); try { - this._blobContainerBlobDropdown.values = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount); + 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._blobContainerBlobDropdown.loading = false; + this._blobContainerDropdowns.forEach((dropdown) => { + dropdown.loading = false; + }); } } } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 59ab2bc86e..b0d7ea3e81 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -132,7 +132,9 @@ export class IntergrationRuntimePage extends MigrationWizardPage { text: '' }; this.migrationStateModel._migrationController = this.migrationStateModel.getMigrationController(value.index); - await this.loadControllerStatus(); + if (value !== constants.MIGRATION_CONTROLLER_NOT_FOUND_ERROR) { + await this.loadControllerStatus(); + } } }); @@ -169,7 +171,6 @@ export class IntergrationRuntimePage extends MigrationWizardPage { private async loadControllerStatus(): Promise { this._statusLoadingComponent.loading = true; - try { this._migrationDetailsContainer.clearItems(); diff --git a/extensions/sql-migration/src/wizard/migrationModePage.ts b/extensions/sql-migration/src/wizard/migrationModePage.ts new file mode 100644 index 0000000000..0ed8a2f913 --- /dev/null +++ b/extensions/sql-migration/src/wizard/migrationModePage.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { MigrationWizardPage } from '../models/migrationWizardPage'; +import { MigrationCutover, MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; +import * as constants from '../constants/strings'; + +export class MigrationModePage extends MigrationWizardPage { + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { + super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL, 'MigrationModePage'), migrationStateModel); + this.wizardPage.description = constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION; + } + + protected async registerContent(view: azdata.ModelView): Promise { + const form = view.modelBuilder.formContainer() + .withFormItems( + [ + this.migrationModeContainer(view), + ] + ); + await view.initializeModel(form.component()); + } + + public async onPageEnter(): Promise { + } + public async onPageLeave(): Promise { + } + protected async handleStateChange(e: StateChangeEvent): Promise { + } + + private migrationModeContainer(view: azdata.ModelView): azdata.FormComponent { + const buttonGroup = 'cutoverContainer'; + + const onlineButton = view.modelBuilder.radioButton().withProps({ + label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL, + name: buttonGroup, + checked: true + }).component(); + + this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE; + + onlineButton.onDidChangeCheckedState((e) => { + if (e) { + this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE; + } + }); + + const offlineButton = view.modelBuilder.radioButton().withProps({ + label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL, + name: buttonGroup + }).component(); + + offlineButton.onDidChangeCheckedState((e) => { + if (e) { + vscode.window.showInformationMessage('Feature coming soon'); + onlineButton.checked = true; + //this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.OFFLINE; TODO: Enable when offline mode is supported. + } + }); + + const flexContainer = view.modelBuilder.flexContainer().withItems( + [ + onlineButton, + offlineButton + ] + ).withLayout({ + flexFlow: 'column' + }).component(); + + return { + component: flexContainer + }; + } +} diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 7d493b7435..40fa81a035 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -300,8 +300,11 @@ export class SKURecommendationPage extends MigrationWizardPage { errors.push(constants.INVALID_SUBSCRIPTION_ERROR); } const resourceDropdownValue = (this._resourceDropdown.value).displayName; - if (resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) { - errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); + if (resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) { + errors.push(constants.NO_MANAGED_INSTANCE_FOUND); + } + else if (resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) { + errors.push(constants.NO_VIRTUAL_MACHINE_FOUND); } if (errors.length > 0) { diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index e519a353cc..0f0dde87ee 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -72,12 +72,16 @@ export class SummaryPage extends MigrationWizardPage { flexContainer.addItems( [ createInformationRow(this._view, constants.TYPE, constants.NETWORK_SHARE), - createInformationRow(this._view, constants.PATH, this.migrationStateModel._databaseBackup.networkShareLocation), createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.windowsUser), 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), + createHeadingTextComponent(this._view, 'Target Databases:') ] ); + 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_NETWORK_SHARE_LOCATION(db), this.migrationStateModel._databaseBackup.networkShareLocations[index])); + }); break; case NetworkContainerType.FILE_SHARE: flexContainer.addItems( @@ -85,19 +89,25 @@ export class SummaryPage extends MigrationWizardPage { 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), - createInformationRow(this._view, constants.FILE_SHARE, this.migrationStateModel._databaseBackup.fileShare.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.blobContainer.subscription.name), + 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.BLOB_CONTAINER, this.migrationStateModel._databaseBackup.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)); + }); } return flexContainer; } diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 4358818c36..7831a6b9ac 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -14,6 +14,7 @@ import { DatabaseBackupPage } from './databaseBackupPage'; import { AccountsSelectionPage } from './accountsSelectionPage'; import { IntergrationRuntimePage } from './integrationRuntimePage'; import { SummaryPage } from './summaryPage'; +import { MigrationModePage } from './migrationModePage'; export const WIZARD_INPUT_COMPONENT_WIDTH = '400px'; export class WizardController { @@ -36,6 +37,7 @@ export class WizardController { wizard.generateScriptButton.hidden = true; const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel); // const subscriptionSelectionPage = new SubscriptionSelectionPage(wizard, stateModel); + const migrationModePage = new MigrationModePage(wizard, stateModel); const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel); const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel); const integrationRuntimePage = new IntergrationRuntimePage(wizard, stateModel); @@ -45,6 +47,7 @@ export class WizardController { // subscriptionSelectionPage, azureAccountsPage, skuRecommendationPage, + migrationModePage, databaseBackupPage, integrationRuntimePage, summaryPage @@ -114,7 +117,7 @@ export function createHeadingTextComponent(view: azdata.ModelView, value: string export function creaetLabelTextComponent(view: azdata.ModelView, value: string): azdata.TextComponent { const component = createTextCompononent(view, value); component.updateCssStyles({ - 'width': '250px' + 'width': '300px' }); return component; }