diff --git a/extensions/sql-migration/images/copy.svg b/extensions/sql-migration/images/copy.svg new file mode 100644 index 0000000000..330289da52 --- /dev/null +++ b/extensions/sql-migration/images/copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/images/refresh.svg b/extensions/sql-migration/images/refresh.svg new file mode 100644 index 0000000000..f03579110b --- /dev/null +++ b/extensions/sql-migration/images/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 27a0da4226..0687d962c6 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -88,9 +88,19 @@ export async function getMigrationController(account: azdata.Account, subscripti const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); if (response.errors.length > 0) { - throw response.errors.toString(); + throw new Error(response.errors.toString()); } + return response.response.data; +} +export async function getMigrationControllers(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string): Promise { + const api = await getAzureCoreAPI(); + const host = `https://${regionName}.management.azure.com`; + const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers?api-version=2020-09-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } return response.response.data; } @@ -103,7 +113,7 @@ export async function createMigrationController(account: azdata.Account, subscri }; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host); if (response.errors.length > 0) { - throw response.errors.toString(); + throw new Error(response.errors.toString()); } return response.response.data; } @@ -114,7 +124,7 @@ export async function getMigrationControllerAuthKeys(account: azdata.Account, su const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}/ListAuthKeys?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 response.errors.toString(); + throw new Error(response.errors.toString()); } return { keyName1: response?.response?.data?.keyName1 ?? '', @@ -122,6 +132,47 @@ export async function getMigrationControllerAuthKeys(account: azdata.Account, su }; } +export async function getStorageAccountAccessKeys(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise { + const api = await getAzureCoreAPI(); + const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/listKeys?api-version=2019-06-01`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + return { + keyName1: response?.response?.data?.keys[0].value ?? '', + keyName2: response?.response?.data?.keys[0].value ?? '', + }; +} + +export async function getMigrationControllerMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { + const api = await getAzureCoreAPI(); + const host = `https://${regionName}.management.azure.com`; + const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}/monitoringData?api-version=2020-09-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + console.log(response); + return response.response.data; +} + +export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, migrationControllerName: string, requestBody: StartDatabaseMigrationRequest): Promise { + const api = await getAzureCoreAPI(); + const host = `https://${regionName}.management.azure.com`; + const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/managedInstances/${managedInstance}/providers/Microsoft.DataMigration/databaseMigrations/${migrationControllerName}?api-version=2020-09-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + return { + errors: response.errors, + status: response.response.status, + databaseMigration: response.response.data + }; +} + + /** * For now only east us euap is supported. Actual API calls will be added in the public release. */ @@ -175,3 +226,62 @@ export interface GetMigrationControllerAuthKeysResult { keyName1: string, keyName2: string } + +export interface GetStorageAccountAccessKeysResult { + keyName1: string, + keyName2: string +} + +export interface GetMigrationControllerMonitoringData { + name: string, + nodes: MigrationControllerNode[]; +} + +export interface MigrationControllerNode { + availableMemoryInMB: number, + concurrentJobsLimit: number + concurrentJobsRunning: number, + cpuUtilization: number, + nodeName: string + receivedBytes: number + sentBytes: number +} + +export interface StartDatabaseMigrationRequest { + location: string, + properties: { + SourceDatabaseName: string, + MigrationController: string, + BackupConfiguration: { + TargetLocation: { + StorageAccountResourceId: string, + AccountKey: string, + } + SourceLocation: { + FileShare: { + Path: string, + Username: string, + Password: string, + } + }, + }, + SourceSqlConnection: { + DataSource: string, + Username: string, + Password: string + }, + Scope: string + } +} + +export interface DatabaseMigration { + properties: { + name: string, + provisioningState: string, + sourceDatabaseName: string, + migrationOperationId: string, + }, + id: string, + name: string, + type: string +} diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts new file mode 100644 index 0000000000..2495353572 --- /dev/null +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export interface IconPath { + dark: string; + light: string; +} + +export class IconPathHelper { + private static context: vscode.ExtensionContext; + + public static copy: IconPath; + public static refresh: IconPath; + + public static setExtensionContext(context: vscode.ExtensionContext) { + IconPathHelper.context = context; + IconPathHelper.copy = { + light: IconPathHelper.context.asAbsolutePath('images/copy.svg'), + dark: IconPathHelper.context.asAbsolutePath('images/copy.svg') + }; + IconPathHelper.refresh = { + light: context.asAbsolutePath('images/refresh.svg'), + dark: context.asAbsolutePath('images/refresh.svg') + }; + + } +} diff --git a/extensions/sql-migration/src/contants.ts b/extensions/sql-migration/src/constants/notebookPathHelper.ts similarity index 97% rename from extensions/sql-migration/src/contants.ts rename to extensions/sql-migration/src/constants/notebookPathHelper.ts index f9c10c62bb..7b1e84da08 100644 --- a/extensions/sql-migration/src/contants.ts +++ b/extensions/sql-migration/src/constants/notebookPathHelper.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as loc from './models/strings'; +import * as loc from '../models/strings'; export class NotebookPathHelper { private static context: vscode.ExtensionContext; diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts index 53c1758f91..9a34d42995 100644 --- a/extensions/sql-migration/src/main.ts +++ b/extensions/sql-migration/src/main.ts @@ -7,15 +7,16 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import { WizardController } from './wizard/wizardController'; import { AssessmentResultsDialog } from './dialog/assessmentResults/assessmentResultsDialog'; -import { MigrationNotebookInfo, NotebookPathHelper } from './contants'; import { promises as fs } from 'fs'; import * as loc from './models/strings'; - +import { MigrationNotebookInfo, NotebookPathHelper } from './constants/notebookPathHelper'; +import { IconPathHelper } from './constants/iconPathHelper'; class SQLMigration { constructor(private readonly context: vscode.ExtensionContext) { NotebookPathHelper.setExtensionContext(context); + IconPathHelper.setExtensionContext(context); } async start(): Promise { diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts new file mode 100644 index 0000000000..fd04fd6773 --- /dev/null +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import { azureResource } from 'azureResource'; +import { DatabaseMigration, SqlManagedInstance } from '../api/azure'; +import * as azdata from 'azdata'; + + +export class MigrationLocalStorage { + private static context: vscode.ExtensionContext; + private static mementoToken: string = 'sqlmigration.databaseMigrations'; + + public static setExtensionContext(context: vscode.ExtensionContext): void { + MigrationLocalStorage.context = context; + } + + public static getMigrations(connectionProfile: azdata.connection.ConnectionProfile): MigrationContext[] { + + let dataBaseMigrations: MigrationContext[] = []; + try { + const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; + + dataBaseMigrations = migrationMementos.filter((memento) => { + return memento.connection.serverName === connectionProfile.serverName; + }).map((memento) => { + return memento; + }); + } catch (e) { + console.log(e); + } + + + return dataBaseMigrations; + } + + public static saveMigration(connection: azdata.connection.ConnectionProfile, migration: DatabaseMigration, targetMI: SqlManagedInstance, azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription): void { + try { + const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; + migrationMementos.push({ + connection: connection, + migration: migration, + targetMI: targetMI, + subscription: subscription, + azureAccount: azureAccount + }); + this.context.globalState.update(this.mementoToken, migrationMementos); + } catch (e) { + console.log(e); + } + } + + public static clearMigrations() { + this.context.globalState.update(this.mementoToken, ([] as MigrationContext[])); + } +} + +export interface MigrationContext { + connection: azdata.connection.ConnectionProfile, + migration: DatabaseMigration, + targetMI: SqlManagedInstance, + azureAccount: azdata.Account, + subscription: azureResource.AzureResourceSubscription +} diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 3f74cd6221..e8d03a4c3b 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { azureResource } from 'azureResource'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; -import { MigrationController } from '../api/azure'; +import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, MigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount } from '../api/azure'; import { SKURecommendations } from './externalContract'; +import * as constants from '../models/strings'; +import { MigrationLocalStorage } from './migrationLocalStorage'; export enum State { INIT, @@ -28,8 +31,8 @@ export enum State { } export enum MigrationCutover { - MANUAL, - AUTOMATIC + ONLINE, + OFFLINE } export enum NetworkContainerType { @@ -42,28 +45,20 @@ export interface NetworkShare { networkShareLocation: string; windowsUser: string; password: string; - storageSubscriptionId: string; - storageAccountId: string; } -export interface BlobContainer { - subscriptionId: string; - storageAccountId: string; - containerId: string; -} - -export interface FileShare { - subscriptionId: string; - storageAccountId: string; - fileShareId: string; - resourceGroupId: string; -} export interface DatabaseBackupModel { - emailNotification: boolean; migrationCutover: MigrationCutover; networkContainerType: NetworkContainerType; - networkContainer: NetworkShare | BlobContainer | FileShare; + networkShareLocation: string; + windowsUser: string; + password: string; + subscription: azureResource.AzureResourceSubscription; + storageAccount: StorageAccount; + storageKey: string; azureSecurityToken: string; + fileShare: azureResource.FileShare; + blobContainer: azureResource.BlobContainer; } export interface Model { readonly sourceConnectionId: string; @@ -80,14 +75,31 @@ export interface StateChangeEvent { } export class MigrationStateModel implements Model, vscode.Disposable { + public azureAccounts!: azdata.Account[]; + public azureAccount!: azdata.Account; + + public subscriptions!: azureResource.AzureResourceSubscription[]; + + public _targetSubscription!: azureResource.AzureResourceSubscription; + public _targetManagedInstances!: SqlManagedInstance[]; + public _targetManagedInstance!: SqlManagedInstance; + + public databaseBackup!: DatabaseBackupModel; + public _storageAccounts!: StorageAccount[]; + public _fileShares!: azureResource.FileShare[]; + public _blobContainers!: azureResource.BlobContainer[]; + + public migrationController!: MigrationController; + public migrationControllers!: MigrationController[]; + public _nodeNames!: string[]; + private _stateChangeEventEmitter = new vscode.EventEmitter(); private _currentState: State; private _gatheringInformationError: string | undefined; private _skuRecommendations: SKURecommendations | undefined; private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined; - private _azureAccount!: azdata.Account; - private _databaseBackup!: DatabaseBackupModel; - private _migrationController!: MigrationController | undefined; + + constructor( private readonly _extensionContext: vscode.ExtensionContext, @@ -98,22 +110,6 @@ export class MigrationStateModel implements Model, vscode.Disposable { this.databaseBackup = {} as DatabaseBackupModel; } - public get azureAccount(): azdata.Account { - return this._azureAccount; - } - - public set azureAccount(account: azdata.Account) { - this._azureAccount = account; - } - - public get databaseBackup(): DatabaseBackupModel { - return this._databaseBackup; - } - - public set databaseBackup(dbBackup: DatabaseBackupModel) { - this._databaseBackup = dbBackup; - } - public get sourceConnectionId(): string { return this._sourceConnectionId; } @@ -158,14 +154,6 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._stateChangeEventEmitter.event; } - public set migrationController(controller: MigrationController | undefined) { - this._migrationController = controller; - } - - public get migrationController(): MigrationController | undefined { - return this._migrationController; - } - dispose() { this._stateChangeEventEmitter.dispose(); } @@ -173,4 +161,314 @@ export class MigrationStateModel implements Model, vscode.Disposable { public getExtensionPath(): string { return this._extensionContext.extensionPath; } + + public async getAccountValues(): Promise { + let accountValues: azdata.CategoryValue[] = []; + try { + this.azureAccounts = await azdata.accounts.getAllAccounts(); + if (this.azureAccounts.length === 0) { + accountValues = [{ + displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, + name: '' + }]; + } + accountValues = this.azureAccounts.map((account): azdata.CategoryValue => { + return { + displayName: account.displayInfo.displayName, + name: account.displayInfo.userId + }; + }); + } catch (e) { + console.log(e); + accountValues = [{ + displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, + name: '' + }]; + } + return accountValues; + } + + public getAccount(index: number): azdata.Account { + return this.azureAccounts[index]; + } + + public async getSubscriptionsDropdownValues(): Promise { + let subscriptionsValues: azdata.CategoryValue[] = []; + try { + if (!this.subscriptions) { + this.subscriptions = await getSubscriptions(this.azureAccount); + } + this.subscriptions.forEach((subscription) => { + subscriptionsValues.push({ + name: subscription.id, + displayName: `${subscription.name} - ${subscription.id}` + }); + }); + + if (subscriptionsValues.length === 0) { + subscriptionsValues = [ + { + displayName: constants.NO_SUBSCRIPTIONS_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + subscriptionsValues = [ + { + displayName: constants.NO_SUBSCRIPTIONS_FOUND, + name: '' + } + ]; + } + + return subscriptionsValues; + } + + public getSubscription(index: number): azureResource.AzureResourceSubscription { + return this.subscriptions[index]; + } + + public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription): Promise { + let managedInstanceValues: azdata.CategoryValue[] = []; + try { + if (!this._targetManagedInstances) { + this._targetManagedInstances = await getAvailableManagedInstanceProducts(this.azureAccount, subscription); + } + this._targetManagedInstances.forEach((managedInstance) => { + managedInstanceValues.push({ + name: managedInstance.id, + displayName: `${managedInstance.name}` + }); + }); + + if (managedInstanceValues.length === 0) { + managedInstanceValues = [ + { + displayName: constants.NO_MANAGED_INSTANCE_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + managedInstanceValues = [ + { + displayName: constants.NO_MANAGED_INSTANCE_FOUND, + name: '' + } + ]; + } + return managedInstanceValues; + } + + public getManagedInstance(index: number): SqlManagedInstance { + return this._targetManagedInstances[index]; + } + + public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise { + let storageAccountValues: azdata.CategoryValue[] = []; + try { + if (!this._storageAccounts) { + this._storageAccounts = await getAvailableStorageAccounts(this.azureAccount, subscription); + } + this._storageAccounts.forEach((storageAccount) => { + storageAccountValues.push({ + name: storageAccount.id, + displayName: `${storageAccount.name}` + }); + }); + + if (storageAccountValues.length === 0) { + storageAccountValues = [ + { + displayName: constants.NO_STORAGE_ACCOUNT_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + storageAccountValues = [ + { + displayName: constants.NO_STORAGE_ACCOUNT_FOUND, + name: '' + } + ]; + } + return storageAccountValues; + } + + public getStorageAccount(index: number): StorageAccount { + return this._storageAccounts[index]; + } + + public async getFileShareValues(subscription: azureResource.AzureResourceSubscription, storageAccount: StorageAccount): Promise { + let fileShareValues: azdata.CategoryValue[] = []; + try { + if (!this._fileShares) { + this._fileShares = await getFileShares(this.azureAccount, subscription, storageAccount); + } + this._fileShares.forEach((fileShare) => { + fileShareValues.push({ + name: fileShare.id, + displayName: `${fileShare.name}` + }); + }); + + if (fileShareValues.length === 0) { + fileShareValues = [ + { + displayName: constants.NO_FILESHARES_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + fileShareValues = [ + { + displayName: constants.NO_FILESHARES_FOUND, + name: '' + } + ]; + } + return fileShareValues; + } + + public getFileShare(index: number): azureResource.FileShare { + return this._fileShares[index]; + } + + public async getBlobContainerValues(subscription: azureResource.AzureResourceSubscription, storageAccount: StorageAccount): Promise { + let blobContainerValues: azdata.CategoryValue[] = []; + try { + if (!this._blobContainers) { + this._blobContainers = await getBlobContainers(this.azureAccount, subscription, storageAccount); + } + this._blobContainers.forEach((blobContainer) => { + blobContainerValues.push({ + name: blobContainer.id, + displayName: `${blobContainer.name}` + }); + }); + + if (blobContainerValues.length === 0) { + blobContainerValues = [ + { + displayName: constants.NO_BLOBCONTAINERS_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + blobContainerValues = [ + { + displayName: constants.NO_BLOBCONTAINERS_FOUND, + name: '' + } + ]; + } + return blobContainerValues; + } + + public getBlobContainer(index: number): azureResource.BlobContainer { + return this._blobContainers[index]; + } + + + public async getMigrationControllerValues(subscription: azureResource.AzureResourceSubscription, managedInstance: SqlManagedInstance): Promise { + let migrationControllerValues: azdata.CategoryValue[] = []; + try { + if (!this.migrationControllers) { + this.migrationControllers = await getMigrationControllers(this.azureAccount, subscription, managedInstance.resourceGroup!, managedInstance.location); + } + this.migrationControllers.forEach((migrationController) => { + migrationControllerValues.push({ + name: migrationController.id, + displayName: `${migrationController.name}` + }); + }); + + if (migrationControllerValues.length === 0) { + migrationControllerValues = [ + { + displayName: constants.MIGRATION_CONTROLLER_NOT_FOUND_ERROR, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + migrationControllerValues = [ + { + displayName: constants.MIGRATION_CONTROLLER_NOT_FOUND_ERROR, + name: '' + } + ]; + } + return migrationControllerValues; + } + + public getMigrationController(index: number): MigrationController { + return this.migrationControllers[index]; + } + + public async startMigration() { + const sqlConnections = await azdata.connection.getConnections(); + const currentConnection = sqlConnections.find((value) => { + if (value.connectionId === this.sourceConnectionId) { + return true; + } else { + return false; + } + }); + const connectionPassword = await azdata.connection.getCredentials(this.sourceConnectionId); + + const requestBody: StartDatabaseMigrationRequest = { + location: this.migrationController?.properties.location!, + properties: { + SourceDatabaseName: currentConnection?.databaseName!, + MigrationController: this.migrationController?.id!, + BackupConfiguration: { + TargetLocation: { + StorageAccountResourceId: this.databaseBackup.storageAccount.id, + AccountKey: this.databaseBackup.storageKey, + }, + SourceLocation: { + FileShare: { + Path: this.databaseBackup.networkShareLocation, + Username: this.databaseBackup.windowsUser, + Password: this.databaseBackup.password, + } + }, + }, + SourceSqlConnection: { + DataSource: currentConnection?.serverName!, + Username: currentConnection?.userName!, + Password: connectionPassword.password + }, + Scope: this._targetManagedInstance.id + } + }; + console.log(requestBody); + const response = await startDatabaseMigration( + this.azureAccount, + this._targetSubscription, + this._targetManagedInstance.resourceGroup!, + this.migrationController?.properties.location!, + this._targetManagedInstance.name, + this.migrationController?.name!, + requestBody + ); + + console.log(response); + if (!response.error) { + MigrationLocalStorage.saveMigration(currentConnection!, response, this._targetManagedInstance, this.azureAccount, this._targetSubscription); + } + + vscode.window.showInformationMessage(constants.MIGRATION_STARTED); + } } diff --git a/extensions/sql-migration/src/models/strings.ts b/extensions/sql-migration/src/models/strings.ts index 767c40db88..5b4728115f 100644 --- a/extensions/sql-migration/src/models/strings.ts +++ b/extensions/sql-migration/src/models/strings.ts @@ -38,9 +38,10 @@ export const CONGRATULATIONS = localize('sql.migration.generic.congratulations', // Accounts page -export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Select your Azure account"); +export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Azure Account"); +export const ACCOUNTS_SELECTION_PAGE_DESCRIPTION = localize('sql.migration.wizard.account.description', "Select an Azure account linked to Azure Data Studio or link one now."); export const ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR = localize('sql.migration.wizard.account.noaccount.error', "There is no linked account. Please add an account."); -export const ACCOUNT_ADD_BUTTON_LABEL = localize('sql.migration.wizard.account.add.button.label', "Add account"); +export const ACCOUNT_LINK_BUTTON_LABEL = localize('sql.migration.wizard.account.add.button.label', "Link account"); export function accountLinkedMessage(count: number): string { return count === 1 ? localize('sql.migration.wizard.account.count.single.message', '{0} account linked', count) : localize('sql.migration.wizard.account.count.multiple.message', '{0} accounts linked', count); } @@ -70,10 +71,10 @@ export const DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL = localize('sql.migra export const DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL = localize('sql.migration.file.share.storage.account.label', "Select the storage account that contains the file share."); export const DATABASE_BACKUP_FILE_SHARE_LABEL = localize('sql.migration.file.share.label', "Select the file share that contains the backup files."); export const DATABASE_BACKUP_FILE_SHARE_PLACEHOLDER = localize('sql.migration.file.share.placeholder', "Select share"); -export const DATABASE_BACKUP_MIGRATION_CUTOVER_LABEL = localize('sql.migration.database.migration.cutover.label', "Migration Cutover"); -export const DATABASE_BACKUP_MIGRATION_CUTOVER_DESCRIPTION = localize('sql.migration.database.migration.cutover.description', "Select how you want to cutover when the migration is complete."); -export const DATABASE_BACKUP_MIGRATION_CUTOVER_AUTOMATIC_LABEL = localize('sql.migration.database.migration.cutover.automatic.label', "Automatically cutover when migration is complete"); -export const DATABASE_BACKUP_MIGRATION_CUTOVER_MANUAL_LABEL = localize('sql.migration.database.migration.cutover.manual.label', "Manually cutover when migration is complete"); +export const DATABASE_BACKUP_MIGRATION_MODE_LABEL = localize('sql.migration.database.migration.mode.label', "Migration mode"); +export const DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION = localize('sql.migration.database.migration.mode.description', "Choose from the following migration modes to migrate to your Azure SQL target based on your downtime requirements."); +export const DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL = localize('sql.migration.database.migration.mode.online.label', "Online migration: Application downtime is limited to cut over at the end of migration."); +export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migration.database.migration.mode.offline.label', "Offline migration: Application downtime will start when the migration starts."); export const DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL = localize('sql.migration.database.backup.email.notification.label', "Email notifications"); export const DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL = localize('sql.migration.database.backup.email.notification.checkbox.label', "Notify me when migration is complete"); export const NO_SUBSCRIPTIONS_FOUND = localize('sql.migration.no.subscription.found', "No subscription found"); @@ -81,7 +82,7 @@ export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccoun export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found"); export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found"); export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "Please select a valid subscription to proceed."); -export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccout.error', "Please select a valid storage account to proceed."); +export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "Please select a valid storage account to proceed."); export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare.error', "Please select a valid file share to proceed."); 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'); @@ -91,33 +92,35 @@ export const INVALID_USER_ACCOUNT = localize('sql.migration.invalid.user.account // integration runtime page export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Migration Controller"); export const IR_PAGE_DESCRIPTION = localize('sql.migration.ir.page.description', "A migration controller is an ARM (Azure Resource Manager) resource created in your Azure subscription and it is needed to coordinate and monitor data migration activities. If one already exists in your subscription, you can reuse it here. Alternatively you can create a new one by clicking New. {0}"); +export const IR_PAGE_NOTE = localize('sql.migration.ir.page.note', "Note: Migration Controller will run in your Azure subscription in the chosen resource group and does not incur any cost for running it."); export const SELECT_A_MIGRATION_CONTROLLER = localize('sql.migration.controller', "Select a migration controller"); export const DEFAULT_SETUP_BUTTON = localize('sql.migration.default.setup.button', "Setup with defaults: Add migration controller with one click express setup using default options."); export const CUSTOM_SETUP_BUTTON = localize('sql.migration.custom.setup.button', "Custom setup: Add migration controller after customizing most options."); export const MIGRATION_CONTROLLER_NOT_FOUND_ERROR = localize('sql.migration.ir.page.migration.controller.not.found', "No Migration Controllers found. Please create a new one"); +export const CREATE_NEW = localize('sql.migration.create.new', "Create new"); // create migration controller dialog export const CONTROLLER_DIALOG_DESCRIPTION = localize('sql.migration.controller.container.description', "A migration controller is an ARM (Azure Resource Manager) resource created in your Azure subscription and it is needed to coordinate and monitor data migration activities. {0}"); export const CONTROLLER_DIALOG_CONTROLLER_CONTAINER_LOADING_HELP = localize('sql.migration.controller.container.loading.help', "Loading Controller"); export const CONTROLLER_DIALOG_CREATE_CONTROLLER_FORM_HEADING = localize('sql.migration.controller.dialog.create.controller.form.heading', "Enter the information below to add a new migration controller."); -export const CONTROLLER_DIALOG_CONTROLLER_CONTAINER_DESCRIPTION = localize('sql.migration.contoller.container.description', "Migration Controller uses self-hosted Integration Runtime offered by Azure Data Factory for data movement and other migration activities. Follow the instructions below to setup self-hosted Integration Runtime."); -export const CONTROLLER_OPTION1_HEADING = localize('sql.migration.controller.setup.option1.heading', "Option 1: Express setup"); -export const CONTROLLER_OPTION1_SETUP_LINK_TEXT = localize('sql.migration.controller.setup.option1.link.text', "Open the express setup for this computer"); -export const CONTROLLER_OPTION2_HEADING = localize('sql.migration.controller.setup.option2.heading', "Option 2: Express setup"); -export const CONTROLLER_OPTION2_STEP1 = localize('sql.migration.option2.step1', "Step 1: Download and install integration runtime"); -export const CONTROLLER_OPTION2_STEP2 = localize('sql.migration.option2.step2', "Step 2: Use this key to register your integration runtime"); +export const CONTROLLER_DIALOG_CONTROLLER_CONTAINER_HEADING = localize('sql.migration.controller.container.heading', "Setup Integration Runtime"); +export const CONTROLLER_DIALOG_CONTROLLER_CONTAINER_DESCRIPTION = localize('sql.migration.controller.container.container.description', "Follow the instructions below to setup self-hosted Integration Runtime."); +export const CONTROLLER_STEP1 = localize('sql.migration.ir.setup.step1', "Step 1: {0}"); +export const CONTROLLER_STEP1_LINK = localize('sql.migration.option', "Download and install integration runtime"); +export const CONTROLLER_STEP2 = localize('sql.migration.ir.setup.step2', "Step 2: Use this key to register your integration runtime"); +export const CONTROLLER_STEP3 = localize('sql.migration.ir.setup.step3', "Step 3: Check connection"); export const CONTROLLER_CONNECTION_STATUS = localize('sql.migration.connection.status', "Connection Status"); -export const CONTROLELR_KEY1_LABEL = localize('sql.migration.key1.label', "Key 1"); -export const CONTROLELR_KEY2_LABEL = localize('sql.migration.key2.label', "Key 2"); +export const CONTROLLER_KEY1_LABEL = localize('sql.migration.key1.label', "Key 1"); +export const CONTROLLER_KEY2_LABEL = localize('sql.migration.key2.label', "Key 2"); export const CONTROLLER_KEY_COPIED_HELP = localize('sql.migration.key.copied', "Key copied"); export const REFRESH_KEYS = localize('sql.migration.refresh.keys', "Refresh keys"); export const COPY_KEY = localize('sql.migration.copy.key', "Copy key"); export const AUTH_KEY_COLUMN_HEADER = localize('sql.migration.authkeys.header', "Authentication key"); -export function CONTRLLER_NOT_READY(controllerName: string): string { - return localize('sql.migration.controller.not.ready', "Migration Controller {0} is not connected to self-hosted Integration Runtime on any node. Click Refresh", controllerName); +export function CONTROLLER_NOT_READY(controllerName: string): string { + return localize('sql.migration.controller.not.ready', "Migration Controller {0} is not connected to self-hosted Integration Runtime on any node.", controllerName); } -export function CONTRLLER_READY(controllerName: string, host: string): string { - return localize('sql.migration.controller.ready', "Migration Controller '{0}' is connected to self-hosted Integration Runtime on the node - '{1}'.`", controllerName, host); +export function CONTROLLER_READY(controllerName: string, host: string): string { + return localize('sql.migration.controller.ready', "Migration Controller '{0}' is connected to self-hosted Integration Runtime on the node - {1}", controllerName, host); } export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource Groups found"); export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "Please select a valid resource group to proceed."); @@ -143,8 +146,26 @@ export const REFRESH = localize('sql.migration.refresh', "Refresh"); export const SUBMIT = localize('sql.migration.submit', "Submit"); export const CREATE = localize('sql.migration.create', "Create"); export const CANCEL = localize('sql.migration.cancel', "Cancel"); +export const TYPE = localize('sql.migration.type', "Type"); +export const PATH = localize('sql.migration.path', "Path"); +export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User Account"); +//Summary Page +export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', "Summary"); +export const AZURE_ACCOUNT_LINKED = localize('sql.migration.summary.azure.account.linked', "Azure account linked"); +export const MIGRATION_TARGET = localize('sql.migration.summary.migration.target', "Migration target"); +export const SUMMARY_MI_TYPE = localize('sql.migration.summary.mi.type', "Azure SQL Managed Instance"); +export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "Azure SQL Virtual Machine"); +export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Number of database to be migrated"); +export const SUMMARY_AZURE_STORAGE_SUBSCRIPTION = localize('sql.migration.summary.azure.storage.subscription', "Azure storage subscription"); +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 MIGRATION_STARTED = localize('sql.migration.started.notification', "Migration in progress"); + // Open notebook quick pick string export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pick.placeholder', "Select the operation you'd like to perform"); export const NOTEBOOK_INLINE_MIGRATION_TITLE = localize('sql.migration.inline.migration.notebook.title', "Inline migration"); diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index c304ec47ba..db9d9ca253 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -8,13 +8,14 @@ import * as vscode from 'vscode'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../models/strings'; +import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; export class AccountsSelectionPage extends MigrationWizardPage { private _azureAccountsDropdown!: azdata.DropDownComponent; - private _accountsMap: Map = new Map(); constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.ACCOUNTS_SELECTION_PAGE_TITLE), migrationStateModel); + this.wizardPage.description = constants.ACCOUNTS_SELECTION_PAGE_DESCRIPTION; } protected async registerContent(view: azdata.ModelView): Promise { @@ -30,32 +31,36 @@ export class AccountsSelectionPage extends MigrationWizardPage { private createAzureAccountsDropdown(view: azdata.ModelView): azdata.FormComponent { - this._azureAccountsDropdown = view.modelBuilder.dropDown().withValidation((c) => { - if ((c.value).displayName === constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR) { - this.wizard.message = { - text: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, - level: azdata.window.MessageLevel.Error - }; - return false; - } - return true; - }).component(); + this._azureAccountsDropdown = view.modelBuilder.dropDown() + .withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }) + .withValidation((c) => { + if ((c.value).displayName === constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR) { + this.wizard.message = { + text: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, + level: azdata.window.MessageLevel.Error + }; + return false; + } + return true; + }).component(); this._azureAccountsDropdown.onValueChanged(async (value) => { - if (this._azureAccountsDropdown.value) { - const selectedAccount = (this._azureAccountsDropdown.value as azdata.CategoryValue).name; - this.migrationStateModel.azureAccount = this._accountsMap.get(selectedAccount)!; + if (value.selected) { + this.migrationStateModel.azureAccount = this.migrationStateModel.getAccount(value.index); + this.migrationStateModel.subscriptions = undefined!; } }); - const addAccountButton = view.modelBuilder.button() - .withProperties({ - label: constants.ACCOUNT_ADD_BUTTON_LABEL, - width: '100px' + const linkAccountButton = view.modelBuilder.hyperlink() + .withProps({ + label: constants.ACCOUNT_LINK_BUTTON_LABEL, + url: '' }) .component(); - addAccountButton.onDidClick(async (event) => { + linkAccountButton.onDidClick(async (event) => { await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount'); await this.populateAzureAccountsDropdown(); }); @@ -64,7 +69,7 @@ export class AccountsSelectionPage extends MigrationWizardPage { .withLayout({ flexFlow: 'column' }) - .withItems([this._azureAccountsDropdown, addAccountButton], { CSSStyles: { 'margin': '10px', } }) + .withItems([this._azureAccountsDropdown, linkAccountButton], { CSSStyles: { 'margin': '2px', } }) .component(); return { @@ -75,27 +80,12 @@ export class AccountsSelectionPage extends MigrationWizardPage { private async populateAzureAccountsDropdown(): Promise { this._azureAccountsDropdown.loading = true; - let accounts = await azdata.accounts.getAllAccounts(); - - if (accounts.length === 0) { - this._azureAccountsDropdown.value = { - displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, - name: '' - }; - return; + try { + this._azureAccountsDropdown.values = await this.migrationStateModel.getAccountValues(); + this.migrationStateModel.azureAccount = this.migrationStateModel.getAccount(0); + } finally { + this._azureAccountsDropdown.loading = false; } - - this._azureAccountsDropdown.values = accounts.map((account): azdata.CategoryValue => { - let accountCategoryValue = { - displayName: account.displayInfo.displayName, - name: account.displayInfo.userId - }; - this._accountsMap.set(accountCategoryValue.name, account); - return accountCategoryValue; - }); - - this.migrationStateModel.azureAccount = accounts[0]; - this._azureAccountsDropdown.loading = false; } public async onPageEnter(): Promise { diff --git a/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts b/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts index 818448cb9e..07012cd9f7 100644 --- a/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts +++ b/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts @@ -5,12 +5,13 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { createMigrationController, getMigrationControllerRegions, getMigrationController, getResourceGroups, getSubscriptions, Subscription, getMigrationControllerAuthKeys } from '../api/azure'; +import { createMigrationController, getMigrationControllerRegions, getMigrationController, getResourceGroups, getMigrationControllerAuthKeys, getMigrationControllerMonitoringData } from '../api/azure'; import { MigrationStateModel } from '../models/stateMachine'; import * as constants from '../models/strings'; import * as os from 'os'; import { azureResource } from 'azureResource'; import { IntergrationRuntimePage } from './integrationRuntimePage'; +import { IconPathHelper } from '../constants/iconPathHelper'; export class CreateMigrationControllerDialog { @@ -22,17 +23,18 @@ export class CreateMigrationControllerDialog { private _statusLoadingComponent!: azdata.LoadingComponent; private migrationControllerAuthKeyTable!: azdata.DeclarativeTableComponent; - private _connectionStatus!: azdata.TextComponent; + private _connectionStatus!: azdata.InfoBoxComponent; private _copyKey1Button!: azdata.ButtonComponent; private _copyKey2Button!: azdata.ButtonComponent; + private _refreshKey1Button!: azdata.ButtonComponent; + private _refreshKey2Button!: azdata.ButtonComponent; private _setupContainer!: azdata.FlexContainer; private _dialogObject!: azdata.window.Dialog; private _view!: azdata.ModelView; - private _subscriptionMap: Map = new Map(); constructor(private migrationStateModel: MigrationStateModel, private irPage: IntergrationRuntimePage) { - this._dialogObject = azdata.window.createModelViewDialog(constants.IR_PAGE_TITLE, 'MigrationControllerDialog', 'wide'); + this._dialogObject = azdata.window.createModelViewDialog(constants.IR_PAGE_TITLE, 'MigrationControllerDialog', 'medium'); } initialize() { @@ -44,7 +46,7 @@ export class CreateMigrationControllerDialog { this._view = view; this._formSubmitButton = view.modelBuilder.button().withProps({ - label: constants.SUBMIT, + label: constants.CREATE, width: '80px' }).component(); @@ -52,7 +54,7 @@ export class CreateMigrationControllerDialog { this._statusLoadingComponent.loading = true; this._formSubmitButton.enabled = false; - const subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + const subscription = this.migrationStateModel._targetSubscription; const resourceGroup = (this.migrationControllerResourceGroupDropdown.value as azdata.CategoryValue).name; const region = (this.migrationControllerRegionDropdown.value as azdata.CategoryValue).name; const controllerName = this.migrationControllerNameText.value; @@ -128,7 +130,7 @@ export class CreateMigrationControllerDialog { this._dialogObject.okButton.enabled = false; azdata.window.openDialog(this._dialogObject); this._dialogObject.cancelButton.onClick((e) => { - this.migrationStateModel.migrationController = undefined; + this.migrationStateModel.migrationController = undefined!; }); this._dialogObject.okButton.onClick((e) => { this.irPage.populateMigrationController(); @@ -155,7 +157,8 @@ export class CreateMigrationControllerDialog { }).component(); this.migrationControllerSubscriptionDropdown = this._view.modelBuilder.dropDown().withProps({ - required: true + required: true, + enabled: false }).component(); this.migrationControllerSubscriptionDropdown.onValueChanged((e) => { @@ -224,37 +227,21 @@ export class CreateMigrationControllerDialog { private async populateSubscriptions(): Promise { this.migrationControllerSubscriptionDropdown.loading = true; this.migrationControllerResourceGroupDropdown.loading = true; - const subscriptions = await getSubscriptions(this.migrationStateModel.azureAccount); - - let subscriptionDropdownValues: azdata.CategoryValue[] = []; - if (subscriptions && subscriptions.length > 0) { - - subscriptions.forEach((subscription) => { - this._subscriptionMap.set(subscription.id, subscription); - subscriptionDropdownValues.push({ - name: subscription.id, - displayName: subscription.name + ' - ' + subscription.id, - }); - }); - } else { - subscriptionDropdownValues = [ - { - displayName: constants.NO_SUBSCRIPTIONS_FOUND, - name: '' - } - ]; - } - - this.migrationControllerSubscriptionDropdown.values = subscriptionDropdownValues; + this.migrationControllerSubscriptionDropdown.values = [ + { + displayName: this.migrationStateModel._targetSubscription.name, + name: '' + } + ]; this.migrationControllerSubscriptionDropdown.loading = false; this.populateResourceGroups(); } private async populateResourceGroups(): Promise { this.migrationControllerResourceGroupDropdown.loading = true; - let subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + let subscription = this.migrationStateModel._targetSubscription; const resourceGroups = await getResourceGroups(this.migrationStateModel.azureAccount, subscription); let resourceGroupDropdownValues: azdata.CategoryValue[] = []; if (resourceGroups && resourceGroups.length > 0) { @@ -278,79 +265,66 @@ export class CreateMigrationControllerDialog { private createControllerStatus(): azdata.FlexContainer { - const informationTextBox = this._view.modelBuilder.text().withProps({ - value: constants.CONTROLLER_DIALOG_CONTROLLER_CONTAINER_DESCRIPTION - }).component(); - - const expressSetupTitle = this._view.modelBuilder.text().withProps({ - value: constants.CONTROLLER_OPTION1_HEADING, + const setupIRHeadingText = this._view.modelBuilder.text().withProps({ + value: constants.CONTROLLER_DIALOG_CONTROLLER_CONTAINER_HEADING, CSSStyles: { 'font-weight': 'bold' } }).component(); - const expressSetupLink = this._view.modelBuilder.hyperlink().withProps({ - label: constants.CONTROLLER_OPTION1_SETUP_LINK_TEXT, - url: '' + const setupIRdescription = this._view.modelBuilder.text().withProps({ + value: constants.CONTROLLER_DIALOG_CONTROLLER_CONTAINER_DESCRIPTION, }).component(); - expressSetupLink.onDidClick((e) => { - vscode.window.showInformationMessage(constants.FEATURE_NOT_AVAILABLE); - }); + const irSetupStep1Text = this._view.modelBuilder.text().withProps({ + value: constants.CONTROLLER_STEP1, + links: [ + { + text: constants.CONTROLLER_STEP1_LINK, + url: 'https://www.microsoft.com/download/details.aspx?id=39717' + } + ] + }).component(); - const manualSetupTitle = this._view.modelBuilder.text().withProps({ - value: constants.CONTROLLER_OPTION2_HEADING, + const irSetupStep2Text = this._view.modelBuilder.text().withProps({ + value: constants.CONTROLLER_STEP2 + }).component(); + + const irSetupStep3Text = this._view.modelBuilder.hyperlink().withProps({ + label: constants.CONTROLLER_STEP3, + url: '', CSSStyles: { - 'font-weight': 'bold' + 'margin-top': '10px', + 'margin-bottom': '10px' } }).component(); - const manualSetupButton = this._view.modelBuilder.hyperlink().withProps({ - label: constants.CONTROLLER_OPTION2_STEP1, - url: 'https://www.microsoft.com/download/details.aspx?id=39717' - }).component(); - - const manualSetupSecondDescription = this._view.modelBuilder.text().withProps({ - value: constants.CONTROLLER_OPTION2_STEP2 - }).component(); - - const connectionStatusTitle = this._view.modelBuilder.text().withProps({ - value: constants.CONTROLLER_CONNECTION_STATUS, - CSSStyles: { - 'font-weight': 'bold' - } - }).component(); - - this._connectionStatus = this._view.modelBuilder.text().withProps({ - value: '' - }).component(); - - const refreshButton = this._view.modelBuilder.button().withProps({ - label: constants.REFRESH, - secondary: true - }).component(); - - const refreshLoadingIndicator = this._view.modelBuilder.loadingComponent().withProps({ - loading: false - }).component(); - - refreshButton.onDidClick(async (e) => { + irSetupStep3Text.onDidClick(async (e) => { refreshLoadingIndicator.loading = true; + this._connectionStatus.updateCssStyles({ + 'display': 'none' + }); try { await this.refreshStatus(); } catch (e) { console.log(e); } + this._connectionStatus.updateCssStyles({ + 'display': 'inline' + }); refreshLoadingIndicator.loading = false; }); - const connectionStatusContainer = this._view.modelBuilder.flexContainer().withItems( - [ - this._connectionStatus, - refreshButton, - refreshLoadingIndicator - ] - ).component(); + + this._connectionStatus = this._view.modelBuilder.infoBox().component(); + + this._connectionStatus.CSSStyles = { + 'width': '350px' + }; + + const refreshLoadingIndicator = this._view.modelBuilder.loadingComponent().withProps({ + loading: false + }).component(); this.migrationControllerAuthKeyTable = this._view.modelBuilder.declarativeTable().withProps({ @@ -358,54 +332,54 @@ export class CreateMigrationControllerDialog { { displayName: constants.NAME, valueType: azdata.DeclarativeDataType.string, - width: '100px', + width: '50px', isReadOnly: true, + rowCssStyles: { + 'text-align': 'center' + } }, { displayName: constants.AUTH_KEY_COLUMN_HEADER, valueType: azdata.DeclarativeDataType.string, - width: '300px', + width: '500px', + isReadOnly: true, + rowCssStyles: { + overflow: 'scroll' + } + }, + { + displayName: '', + valueType: azdata.DeclarativeDataType.component, + width: '15px', isReadOnly: true, }, { displayName: '', valueType: azdata.DeclarativeDataType.component, - width: '100px', + width: '15px', isReadOnly: true, } ], CSSStyles: { - 'margin-top': '25px' + 'margin-top': '5px' } }).component(); - const refreshKeyButton = this._view.modelBuilder.button().withProps({ - label: constants.REFRESH_KEYS, - CSSStyles: { - 'margin-top': '10px' - }, - width: '100px', - secondary: true - }).component(); - - refreshKeyButton.onDidClick(async (e) => { - this.refreshAuthTable(); - - }); - this._setupContainer = this._view.modelBuilder.flexContainer().withItems( [ - informationTextBox, - expressSetupTitle, - expressSetupLink, - manualSetupTitle, - manualSetupButton, - manualSetupSecondDescription, - refreshKeyButton, + setupIRHeadingText, + setupIRdescription, + irSetupStep1Text, + irSetupStep2Text, this.migrationControllerAuthKeyTable, - connectionStatusTitle, - connectionStatusContainer - ] + irSetupStep3Text, + this._connectionStatus, + refreshLoadingIndicator + ], { + CSSStyles: { + 'margin-bottom': '5px' + } + } ).withLayout({ flexFlow: 'column' }).component(); @@ -415,32 +389,42 @@ export class CreateMigrationControllerDialog { } private async refreshStatus(): Promise { - const subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + const subscription = this.migrationStateModel._targetSubscription; const resourceGroup = (this.migrationControllerResourceGroupDropdown.value as azdata.CategoryValue).name; const region = (this.migrationControllerRegionDropdown.value as azdata.CategoryValue).name; const controllerStatus = await getMigrationController(this.migrationStateModel.azureAccount, subscription, resourceGroup, region, this.migrationStateModel.migrationController!.name); + const controllerMonitoringStatus = await getMigrationControllerMonitoringData(this.migrationStateModel.azureAccount, subscription, resourceGroup, region, this.migrationStateModel.migrationController!.name); + this.migrationStateModel._nodeNames = controllerMonitoringStatus.nodes.map((node) => { + return node.nodeName; + }); if (controllerStatus) { const state = controllerStatus.properties.integrationRuntimeState; if (state === 'Online') { - this._connectionStatus.value = constants.CONTRLLER_READY(this.migrationStateModel.migrationController!.name, os.hostname()); + this._connectionStatus.updateProperties({ + text: constants.CONTROLLER_READY(this.migrationStateModel.migrationController!.name, this.migrationStateModel._nodeNames.join(', ')), + style: 'success' + }); this._dialogObject.okButton.enabled = true; } else { - this._connectionStatus.value = constants.CONTRLLER_NOT_READY(this.migrationStateModel.migrationController!.name); + this._connectionStatus.text = constants.CONTROLLER_NOT_READY(this.migrationStateModel.migrationController!.name); + this._connectionStatus.updateProperties({ + text: constants.CONTROLLER_NOT_READY(this.migrationStateModel.migrationController!.name), + style: 'warning' + }); this._dialogObject.okButton.enabled = false; } } } private async refreshAuthTable(): Promise { - const subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + const subscription = this.migrationStateModel._targetSubscription; const resourceGroup = (this.migrationControllerResourceGroupDropdown.value as azdata.CategoryValue).name; const region = (this.migrationControllerRegionDropdown.value as azdata.CategoryValue).name; const keys = await getMigrationControllerAuthKeys(this.migrationStateModel.azureAccount, subscription, resourceGroup, region, this.migrationStateModel.migrationController!.name); this._copyKey1Button = this._view.modelBuilder.button().withProps({ - label: constants.COPY_KEY, - secondary: true + iconPath: IconPathHelper.copy }).component(); this._copyKey1Button.onDidClick((e) => { @@ -449,8 +433,7 @@ export class CreateMigrationControllerDialog { }); this._copyKey2Button = this._view.modelBuilder.button().withProps({ - label: constants.COPY_KEY, - secondary: true + iconPath: IconPathHelper.copy }).component(); this._copyKey2Button.onDidClick((e) => { @@ -458,28 +441,50 @@ export class CreateMigrationControllerDialog { vscode.window.showInformationMessage(constants.CONTROLLER_KEY_COPIED_HELP); }); + this._refreshKey1Button = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.refresh + }).component(); + + this._refreshKey1Button.onDidClick((e) => { + this.refreshAuthTable(); + }); + + this._refreshKey2Button = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.refresh + }).component(); + + this._refreshKey2Button.onDidClick((e) => { + this.refreshAuthTable(); + }); + this.migrationControllerAuthKeyTable.updateProperties({ dataValues: [ [ { - value: constants.CONTROLELR_KEY1_LABEL + value: constants.CONTROLLER_KEY1_LABEL }, { value: keys.keyName1 }, { value: this._copyKey1Button + }, + { + value: this._refreshKey1Button } ], [ { - value: constants.CONTROLELR_KEY2_LABEL + value: constants.CONTROLLER_KEY2_LABEL }, { value: keys.keyName2 }, { value: this._copyKey2Button + }, + { + value: this._refreshKey2Button } ] ] diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 00ad163931..968380d32f 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { azureResource } from 'azureResource'; import { EOL } from 'os'; -import { getAvailableStorageAccounts, getBlobContainers, getFileShares, getSubscriptions, StorageAccount, Subscription } from '../api/azure'; +import { getStorageAccountAccessKeys } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { BlobContainer, FileShare, MigrationCutover, MigrationStateModel, NetworkContainerType, NetworkShare, StateChangeEvent } from '../models/stateMachine'; +import { MigrationCutover, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../models/strings'; export class DatabaseBackupPage extends MigrationWizardPage { @@ -30,14 +29,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _fileShareStorageAccountDropdown!: azdata.DropDownComponent; private _fileShareFileShareDropdown!: azdata.DropDownComponent; - private _networkShare = {} as NetworkShare; - private _fileShare = {} as FileShare; - private _blob = {} as BlobContainer; - - private _subscriptionDropdownValues: azdata.CategoryValue[] = []; - private _subscriptionMap: Map = new Map(); - private _storageAccountMap: Map = new Map(); - constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel); this.wizardPage.description = constants.DATABASE_BACKUP_PAGE_DESCRIPTION; @@ -65,12 +56,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { title: '', component: networkContainer }, - this.migrationCutoverContainer(view), - this.emailNotificationContainer(view), + this.migrationModeContainer(view), ] ); await view.initializeModel(form.component()); - this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE, this._networkShare); } private createBackupLocationComponent(view: azdata.ModelView): azdata.FormComponent { @@ -79,13 +68,12 @@ export class DatabaseBackupPage extends MigrationWizardPage { const networkShareButton = view.modelBuilder.radioButton() .withProps({ name: buttonGroup, - label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL, - checked: true + label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL }).component(); networkShareButton.onDidChangeCheckedState((e) => { if (e) { - this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE, this._networkShare); + this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE); } }); @@ -97,7 +85,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { blobContainerButton.onDidChangeCheckedState((e) => { if (e) { - this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER, this._blob); + this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); } }); @@ -109,7 +97,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { fileShareButton.onDidChangeCheckedState((e) => { if (e) { - this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE, this._fileShare); + this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE); } }); @@ -139,8 +127,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true, }).component(); this._fileShareSubscriptionDropdown.onValueChanged(async (value) => { - if (this._fileShareSubscriptionDropdown.value) { - this._fileShare.subscriptionId = (this._fileShareSubscriptionDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.subscription = this.migrationStateModel.getSubscription(value.index); + this.migrationStateModel._storageAccounts = undefined!; await this.loadFileShareStorageDropdown(); } }); @@ -155,8 +144,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._fileShareStorageAccountDropdown.onValueChanged(async (value) => { - if (this._fileShareStorageAccountDropdown.value) { - this._fileShare.storageAccountId = (this._fileShareStorageAccountDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); + this.migrationStateModel._fileShares = undefined!; await this.loadFileShareDropdown(); } }); @@ -171,8 +161,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._fileShareFileShareDropdown.onValueChanged((value) => { - if (this._fileShareFileShareDropdown.value) { - this._fileShare.fileShareId = (this._fileShareFileShareDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.fileShare = this.migrationStateModel.getFileShare(value.index); } }); @@ -189,6 +179,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { ] ).withLayout({ flexFlow: 'column' + }).withProps({ + display: 'none' }).component(); return flexContainer; @@ -205,8 +197,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._blobContainerSubscriptionDropdown.onValueChanged(async (value) => { - if (this._blobContainerSubscriptionDropdown.value) { - this._blob.subscriptionId = (this._blobContainerSubscriptionDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.subscription = this.migrationStateModel.getSubscription(value.index); + this.migrationStateModel._storageAccounts = undefined!; await this.loadblobStorageDropdown(); } }); @@ -221,8 +214,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => { - if (this._blobContainerStorageAccountDropdown.value) { - this._blob.storageAccountId = (this._blobContainerStorageAccountDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); + this.migrationStateModel._blobContainers = undefined!; await this.loadBlobContainerDropdown(); } }); @@ -236,8 +230,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._blobContainerBlobDropdown.onValueChanged((value) => { - if (this._blobContainerBlobDropdown.value) { - this._blob.containerId = (this._blobContainerBlobDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.blobContainer = this.migrationStateModel.getBlobContainer(value.index); } }); @@ -253,6 +247,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { ] ).withLayout({ flexFlow: 'column' + }).withProps({ + display: 'none' }).component(); return flexContainer; @@ -278,7 +274,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withValidation((component) => { if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { if (component.value) { - if (!/^(\\)(\\[\w\.-_]+){2,}(\\?)$/.test(component.value)) { + if (!/(?<=\\\\)[^\\]*/.test(component.value)) { return false; } } @@ -286,7 +282,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { return true; }).component(); this._networkShareLocationText.onTextChanged((value) => { - this._networkShare.networkShareLocation = value; + this.migrationStateModel.databaseBackup.networkShareLocation = value; }); const windowsUserAccountLabel = view.modelBuilder.text() @@ -303,7 +299,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withValidation((component) => { if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { if (component.value) { - if (!/^[a-zA-Z][a-zA-Z0-9\-\.]{0,61}[a-zA-Z]\\\w[\w\.\- ]*$/.test(component.value)) { + if (!/(?<=\\).*$/.test(component.value)) { return false; } } @@ -311,7 +307,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { return true; }).component(); this._windowsUserAccountText.onTextChanged((value) => { - this._networkShare.windowsUser = value; + this.migrationStateModel.databaseBackup.windowsUser = value; }); const passwordLabel = view.modelBuilder.text() @@ -326,7 +322,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._passwordText.onTextChanged((value) => { - this._networkShare.password = value; + this.migrationStateModel.databaseBackup.password = value; }); const azureAccountHelpText = view.modelBuilder.text() @@ -344,8 +340,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._networkShareContainerSubscriptionDropdown.onValueChanged(async (value) => { - if (this._networkShareContainerSubscriptionDropdown.value) { - this._networkShare.storageSubscriptionId = (this._networkShareContainerSubscriptionDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.subscription = this.migrationStateModel.getSubscription(value.index); + this.migrationStateModel._storageAccounts = undefined!; await this.loadNetworkShareStorageDropdown(); } }); @@ -360,8 +357,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true }).component(); this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => { - if (this._networkShareContainerStorageAccountDropdown.value) { - this._networkShare.storageAccountId = (this._networkShareContainerStorageAccountDropdown.value as azdata.CategoryValue).name; + if (value.selected) { + this.migrationStateModel.databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index); } }); @@ -382,72 +379,57 @@ export class DatabaseBackupPage extends MigrationWizardPage { ] ).withLayout({ flexFlow: 'column' + }).withProps({ + display: 'none' }).component(); return flexContainer; } - private emailNotificationContainer(view: azdata.ModelView): azdata.FormComponent { - const emailCheckbox = view.modelBuilder.checkBox().withProps({ - label: constants.DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL - }).component(); - - emailCheckbox.onChanged((value) => { - if (value !== undefined) { - this.migrationStateModel.databaseBackup.emailNotification = value; - } - }); - - return { - title: constants.DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL, - component: emailCheckbox - }; - } - - private migrationCutoverContainer(view: azdata.ModelView): azdata.FormComponent { + private migrationModeContainer(view: azdata.ModelView): azdata.FormComponent { const description = view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_DESCRIPTION + value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION }).component(); const buttonGroup = 'cutoverContainer'; - const automaticButton = view.modelBuilder.radioButton().withProps({ - label: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_AUTOMATIC_LABEL, + const onlineButton = view.modelBuilder.radioButton().withProps({ + label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL, name: buttonGroup, checked: true }).component(); - this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.AUTOMATIC; + this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.ONLINE; - automaticButton.onDidChangeCheckedState((e) => { + onlineButton.onDidChangeCheckedState((e) => { if (e) { - this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.AUTOMATIC; + this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.ONLINE; } }); - const manualButton = view.modelBuilder.radioButton().withProps({ - label: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_MANUAL_LABEL, + const offlineButton = view.modelBuilder.radioButton().withProps({ + label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL, name: buttonGroup }).component(); - manualButton.onDidChangeCheckedState((e) => { + offlineButton.onDidChangeCheckedState((e) => { if (e) { - this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.MANUAL; + this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.OFFLINE; } }); const flexContainer = view.modelBuilder.flexContainer().withItems( [ description, - automaticButton, - manualButton + onlineButton, + offlineButton ] ).withLayout({ flexFlow: 'column' }).component(); return { - title: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_LABEL, + title: constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL, component: flexContainer }; } @@ -506,13 +488,14 @@ export class DatabaseBackupPage extends MigrationWizardPage { } public async onPageLeave(): Promise { + this.migrationStateModel.databaseBackup.storageKey = (await getStorageAccountAccessKeys(this.migrationStateModel.azureAccount, this.migrationStateModel.databaseBackup.subscription, this.migrationStateModel.databaseBackup.storageAccount)).keyName1; + console.log(this.migrationStateModel.databaseBackup); } protected async handleStateChange(e: StateChangeEvent): Promise { } - private toggleNetworkContainerFields(containerType: NetworkContainerType, networkContainer: NetworkShare | BlobContainer | FileShare): void { - this.migrationStateModel.databaseBackup.networkContainer = networkContainer; + private toggleNetworkContainerFields(containerType: NetworkContainerType): void { 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' }); @@ -526,169 +509,105 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._passwordText.updateProperties({ required: containerType === NetworkContainerType.NETWORK_SHARE }); + + this._networkShareLocationText.validate(); + this._windowsUserAccountText.validate(); + this._passwordText.validate(); + this._networkShareContainerSubscriptionDropdown.validate(); + this._networkShareContainerStorageAccountDropdown.validate(); + this._blobContainerSubscriptionDropdown.validate(); + this._blobContainerStorageAccountDropdown.validate(); + this._blobContainerBlobDropdown.validate(); + this._fileShareSubscriptionDropdown.validate(); + this._fileShareStorageAccountDropdown.validate(); + this._fileShareFileShareDropdown.validate(); + } private async getSubscriptionValues(): Promise { this._networkShareContainerSubscriptionDropdown.loading = true; this._fileShareSubscriptionDropdown.loading = true; this._blobContainerSubscriptionDropdown.loading = true; - - let subscriptions: azureResource.AzureResourceSubscription[] = []; - try { - subscriptions = await getSubscriptions(this.migrationStateModel.azureAccount); - subscriptions.forEach((subscription) => { - this._subscriptionMap.set(subscription.id, subscription); - this._subscriptionDropdownValues.push({ - name: subscription.id, - displayName: subscription.name + ' - ' + subscription.id, - }); - }); - - if (!this._subscriptionDropdownValues) { - this._subscriptionDropdownValues = [ - { - displayName: constants.NO_SUBSCRIPTIONS_FOUND, - name: '' - } - ]; - } - - this._fileShareSubscriptionDropdown.values = this._subscriptionDropdownValues; - this._networkShareContainerSubscriptionDropdown.values = this._subscriptionDropdownValues; - this._blobContainerSubscriptionDropdown.values = this._subscriptionDropdownValues; - - this._networkShare.storageSubscriptionId = this._subscriptionDropdownValues[0].name; - this._fileShare.subscriptionId = this._subscriptionDropdownValues[0].name; - this._blob.subscriptionId = this._subscriptionDropdownValues[0].name; - + this._fileShareSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues(); + this._networkShareContainerSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues(); + this._blobContainerSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues(); + this.migrationStateModel.databaseBackup.subscription = this.migrationStateModel.getSubscription(0); } catch (error) { - - console.log(error); - this.setEmptyDropdownPlaceHolder(this._fileShareSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND); - this.setEmptyDropdownPlaceHolder(this._networkShareContainerSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND); - this.setEmptyDropdownPlaceHolder(this._blobContainerSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND); + this.migrationStateModel._storageAccounts = undefined!; + } finally { + await this.loadNetworkShareStorageDropdown(); + await this.loadFileShareStorageDropdown(); + await this.loadblobStorageDropdown(); + this._networkShareContainerSubscriptionDropdown.loading = false; + this._fileShareSubscriptionDropdown.loading = false; + this._blobContainerSubscriptionDropdown.loading = false; } - - this._networkShareContainerSubscriptionDropdown.loading = false; - this._fileShareSubscriptionDropdown.loading = false; - this._blobContainerSubscriptionDropdown.loading = false; - - await this.loadNetworkShareStorageDropdown(); - await this.loadFileShareStorageDropdown(); - await this.loadblobStorageDropdown(); - this._networkShareContainerSubscriptionDropdown.validate(); - this._networkShareContainerStorageAccountDropdown.validate(); } private async loadNetworkShareStorageDropdown(): Promise { this._networkShareContainerStorageAccountDropdown.loading = true; - - const subscriptionId = (this._networkShareContainerSubscriptionDropdown.value).name; - if (!subscriptionId.length) { - this.setEmptyDropdownPlaceHolder(this._networkShareContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND); - } else { - const storageAccounts = await this.loadStorageAccounts(this._networkShare.storageSubscriptionId); - - if (storageAccounts && storageAccounts.length) { - this._networkShareContainerStorageAccountDropdown.values = storageAccounts.map(s => { name: s.id, displayName: s.name }); - this._networkShare.storageAccountId = storageAccounts[0].id; - } - else { - this.setEmptyDropdownPlaceHolder(this._networkShareContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND); - } + try { + this._networkShareContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel.databaseBackup.subscription); + this.migrationStateModel.databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(0); + } finally { + this._networkShareContainerStorageAccountDropdown.loading = false; } - this._networkShareContainerStorageAccountDropdown.loading = false; } private async loadFileShareStorageDropdown(): Promise { this._fileShareStorageAccountDropdown.loading = true; this._fileShareFileShareDropdown.loading = true; + try { + this._fileShareStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel.databaseBackup.subscription); + this.migrationStateModel.databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(0); + } catch (error) { + this.migrationStateModel._fileShares = undefined!; + } finally { + await this.loadFileShareDropdown(); + this._fileShareStorageAccountDropdown.loading = false; + this._fileShareFileShareDropdown.loading = false; - const subscriptionId = (this._fileShareSubscriptionDropdown.value).name; - if (!subscriptionId.length) { - this.setEmptyDropdownPlaceHolder(this._fileShareStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND); - } else { - const storageAccounts = await this.loadStorageAccounts(this._fileShare.subscriptionId); - if (storageAccounts && storageAccounts.length) { - this._fileShareStorageAccountDropdown.values = storageAccounts.map(s => { name: s.id, displayName: s.name }); - this._fileShare.storageAccountId = storageAccounts[0].id; - } - else { - this.setEmptyDropdownPlaceHolder(this._fileShareStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND); - this._fileShareStorageAccountDropdown.loading = false; - } } - this._fileShareStorageAccountDropdown.loading = false; - await this.loadFileShareDropdown(); } private async loadblobStorageDropdown(): Promise { this._blobContainerStorageAccountDropdown.loading = true; this._blobContainerBlobDropdown.loading = true; + try { + this._blobContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel.databaseBackup.subscription); + this.migrationStateModel.databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(0); + } catch (error) { + this.migrationStateModel._blobContainers = undefined!; + } finally { + await this.loadBlobContainerDropdown(); + this._blobContainerStorageAccountDropdown.loading = false; + this._blobContainerBlobDropdown.loading = true; - const subscriptionId = (this._blobContainerSubscriptionDropdown.value).name; - if (!subscriptionId.length) { - this.setEmptyDropdownPlaceHolder(this._blobContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND); - } else { - const storageAccounts = await this.loadStorageAccounts(this._blob.subscriptionId); - if (storageAccounts.length) { - this._blobContainerStorageAccountDropdown.values = storageAccounts.map(s => { name: s.id, displayName: s.name }); - this._blob.storageAccountId = storageAccounts[0].id; - } else { - this.setEmptyDropdownPlaceHolder(this._blobContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND); - } } - this._blobContainerStorageAccountDropdown.loading = false; - await this.loadBlobContainerDropdown(); - } - - private async loadStorageAccounts(subscriptionId: string): Promise { - const storageAccounts = await getAvailableStorageAccounts(this.migrationStateModel.azureAccount, this._subscriptionMap.get(subscriptionId)!); - storageAccounts.forEach(s => { - this._storageAccountMap.set(s.id, s); - }); - return storageAccounts; } private async loadFileShareDropdown(): Promise { this._fileShareFileShareDropdown.loading = true; - const storageAccountId = (this._fileShareStorageAccountDropdown.value).name; - if (!storageAccountId.length) { - this.setEmptyDropdownPlaceHolder(this._fileShareFileShareDropdown, constants.NO_FILESHARES_FOUND); - } else { - const fileShares = await getFileShares(this.migrationStateModel.azureAccount, this._subscriptionMap.get(this._fileShare.subscriptionId)!, this._storageAccountMap.get(storageAccountId)!); - if (fileShares && fileShares.length) { - this._fileShareFileShareDropdown.values = fileShares.map(f => { name: f.id, displayName: f.name }); - this._fileShare.fileShareId = fileShares[0].id!; - } else { - this.setEmptyDropdownPlaceHolder(this._fileShareFileShareDropdown, constants.NO_FILESHARES_FOUND); - } + try { + this._fileShareFileShareDropdown.values = await this.migrationStateModel.getFileShareValues(this.migrationStateModel.databaseBackup.subscription, this.migrationStateModel.databaseBackup.storageAccount); + this.migrationStateModel.databaseBackup.fileShare = this.migrationStateModel.getFileShare(0); + } catch (error) { + console.log(error); + } finally { + this._fileShareFileShareDropdown.loading = false; } - this._fileShareFileShareDropdown.loading = false; } private async loadBlobContainerDropdown(): Promise { this._blobContainerBlobDropdown.loading = true; - const storageAccountId = (this._blobContainerStorageAccountDropdown.value).name; - if (!storageAccountId.length) { - this.setEmptyDropdownPlaceHolder(this._blobContainerBlobDropdown, constants.NO_BLOBCONTAINERS_FOUND); - } else { - const blobContainers = await getBlobContainers(this.migrationStateModel.azureAccount, this._subscriptionMap.get(this._blob.subscriptionId)!, this._storageAccountMap.get(storageAccountId)!); - if (blobContainers && blobContainers.length) { - this._blobContainerBlobDropdown.values = blobContainers.map(f => { name: f.id, displayName: f.name }); - this._blob.containerId = blobContainers[0].id!; - } else { - this.setEmptyDropdownPlaceHolder(this._blobContainerBlobDropdown, constants.NO_BLOBCONTAINERS_FOUND); - } + try { + this._blobContainerBlobDropdown.values = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel.databaseBackup.subscription, this.migrationStateModel.databaseBackup.storageAccount); + this.migrationStateModel.databaseBackup.blobContainer = this.migrationStateModel.getBlobContainer(0); + } catch (error) { + console.log(error); + } finally { + this._blobContainerBlobDropdown.loading = false; } - this._blobContainerBlobDropdown.loading = false; - } - - private setEmptyDropdownPlaceHolder(dropDown: azdata.DropDownComponent, placeholder: string): void { - dropDown.values = [{ - displayName: placeholder, - name: '' - }]; } } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index dfb5aa2a8c..00d72d1620 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -4,23 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import * as vscode from 'vscode'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import { CreateMigrationControllerDialog } from './createMigrationControllerDialog'; import * as constants from '../models/strings'; import * as os from 'os'; +import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; export class IntergrationRuntimePage extends MigrationWizardPage { private migrationControllerDropdown!: azdata.DropDownComponent; - private defaultSetupRadioButton!: azdata.RadioButtonComponent; - private customSetupRadioButton!: azdata.RadioButtonComponent; - private startSetupButton!: azdata.ButtonComponent; - private cancelSetupButton!: azdata.ButtonComponent; - private _connectionStatus!: azdata.TextComponent; - private createMigrationContainer!: azdata.FlexContainer; + private _connectionStatus!: azdata.InfoBoxComponent; private _view!: azdata.ModelView; + private _form!: azdata.FormBuilder; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.IR_PAGE_TITLE), migrationStateModel); @@ -29,79 +25,19 @@ export class IntergrationRuntimePage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView): Promise { this._view = view; - const createNewController = view.modelBuilder.button().withProps({ - label: constants.NEW, - width: '100px', - secondary: true + const createNewController = view.modelBuilder.hyperlink().withProps({ + label: constants.CREATE_NEW, + url: '' }).component(); createNewController.onDidClick((e) => { - this.createMigrationContainer.display = 'inline'; + const dialog = new CreateMigrationControllerDialog(this.migrationStateModel, this); + dialog.initialize(); }); - const setupButtonGroup = 'setupOptions'; + this._connectionStatus = view.modelBuilder.infoBox().component(); - this.defaultSetupRadioButton = view.modelBuilder.radioButton().withProps({ - label: constants.DEFAULT_SETUP_BUTTON, - name: setupButtonGroup - }).component(); - this.defaultSetupRadioButton.checked = true; - - this.customSetupRadioButton = view.modelBuilder.radioButton().withProps({ - label: constants.CUSTOM_SETUP_BUTTON, - name: setupButtonGroup - }).component(); - - this.startSetupButton = view.modelBuilder.button().withProps({ - label: constants.CREATE, - width: '100px', - secondary: true - }).component(); - - this.startSetupButton.onDidClick((e) => { - if (this.defaultSetupRadioButton.checked) { - vscode.window.showInformationMessage(constants.FEATURE_NOT_AVAILABLE); - } else { - this.createMigrationContainer.display = 'none'; - const dialog = new CreateMigrationControllerDialog(this.migrationStateModel, this); - dialog.initialize(); - } - }); - - this.cancelSetupButton = view.modelBuilder.button().withProps({ - label: constants.CANCEL, - width: '100px', - secondary: true - }).component(); - - this.cancelSetupButton.onDidClick((e) => { - this.createMigrationContainer.display = 'none'; - }); - - const setupButtonsContainer = view.modelBuilder.flexContainer().withItems([ - this.startSetupButton, - this.cancelSetupButton - ], - { CSSStyles: { 'margin': '10px', } } - ).withLayout({ - flexFlow: 'row' - }).component(); - - this.createMigrationContainer = view.modelBuilder.flexContainer().withItems( - [ - this.defaultSetupRadioButton, - this.customSetupRadioButton, - setupButtonsContainer - ] - ).withLayout({ - flexFlow: 'column' - }).component(); - - this._connectionStatus = view.modelBuilder.text().component(); - - this.createMigrationContainer.display = 'none'; - - const form = view.modelBuilder.formContainer() + this._form = view.modelBuilder.formContainer() .withFormItems( [ { @@ -110,16 +46,13 @@ export class IntergrationRuntimePage extends MigrationWizardPage { { component: createNewController }, - { - component: this.createMigrationContainer - }, { component: this._connectionStatus } ] ); - await view.initializeModel(form.component()); + await view.initializeModel(this._form.component()); } public async onPageEnter(): Promise { @@ -148,6 +81,9 @@ export class IntergrationRuntimePage extends MigrationWizardPage { } public async onPageLeave(): Promise { + this.wizard.registerNavigationValidator((pageChangeInfo) => { + return true; + }); } protected async handleStateChange(e: StateChangeEvent): Promise { @@ -164,16 +100,22 @@ export class IntergrationRuntimePage extends MigrationWizardPage { ] }).component(); + const noteText = this._view.modelBuilder.text().withProps({ + value: constants.IR_PAGE_NOTE + }).component(); + const migrationControllerDropdownLabel = this._view.modelBuilder.text().withProps({ value: constants.SELECT_A_MIGRATION_CONTROLLER }).component(); this.migrationControllerDropdown = this._view.modelBuilder.dropDown().withProps({ required: true, + width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); const flexContainer = this._view.modelBuilder.flexContainer().withItems([ descriptionText, + noteText, migrationControllerDropdownLabel, this.migrationControllerDropdown ]).withLayout({ @@ -183,16 +125,34 @@ export class IntergrationRuntimePage extends MigrationWizardPage { } public async populateMigrationController(controllerStatus?: string): Promise { + this.migrationControllerDropdown.loading = true; let migrationContollerValues: azdata.CategoryValue[] = []; + + // TODO: Replace with this code when APIs are deployed. + // try{ + // this.migrationControllerDropdown.values = await this.migrationStateModel.getMigrationControllerValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetManagedInstance); + // this.migrationStateModel.migrationController = this.migrationStateModel.getMigrationController(0); + // } catch (e) { + + // } finally { + // this.migrationControllerDropdown.loading = false; + // } + if (this.migrationStateModel.migrationController) { + + this._connectionStatus.updateProperties({ + text: constants.CONTROLLER_READY(this.migrationStateModel.migrationController!.name, this.migrationStateModel._nodeNames.join(', ')), + style: 'success' + }); + this._form.addFormItem({ + component: this._connectionStatus + }); migrationContollerValues = [ { displayName: this.migrationStateModel.migrationController.name, - name: this.migrationStateModel.migrationController.name + name: '' } ]; - - this._connectionStatus.value = constants.CONTRLLER_READY(this.migrationStateModel.migrationController!.name, os.hostname()); } else { migrationContollerValues = [ @@ -201,7 +161,9 @@ export class IntergrationRuntimePage extends MigrationWizardPage { name: '' } ]; - this._connectionStatus.value = ''; + this._form.removeFormItem({ + component: this._connectionStatus + }); } this.migrationControllerDropdown.values = migrationContollerValues; this.migrationControllerDropdown.loading = false; diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts new file mode 100644 index 0000000000..47ef130831 --- /dev/null +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MigrationWizardPage } from '../models/migrationWizardPage'; +import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; +import * as constants from '../models/strings'; + +export class SummaryPage extends MigrationWizardPage { + private _view!: azdata.ModelView; + private _flexContainer!: azdata.FlexContainer; + + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { + super(wizard, azdata.window.createWizardPage(constants.SUMMARY_PAGE_TITLE), migrationStateModel); + } + + protected async registerContent(view: azdata.ModelView): Promise { + this._view = view; + this._flexContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + const form = view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this._flexContainer + } + ] + ); + await view.initializeModel(form.component()); + } + + public async onPageEnter(): Promise { + this._flexContainer.addItems( + [ + this.createHeadingTextComponent(constants.AZURE_ACCOUNT_LINKED), + this.createHeadingTextComponent(this.migrationStateModel.azureAccount.displayInfo.displayName), + + + this.createHeadingTextComponent(constants.MIGRATION_TARGET), + this.createInformationRow(constants.TYPE, constants.SUMMARY_MI_TYPE), + this.createInformationRow(constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), + this.createInformationRow(constants.SUMMARY_MI_TYPE, this.migrationStateModel._targetManagedInstance.name), + this.createInformationRow(constants.SUMMARY_DATABASE_COUNT_LABEL, '1'), + + this.createHeadingTextComponent(constants.DATABASE_BACKUP_PAGE_TITLE), + this.createNetworkContainerRows(), + + this.createHeadingTextComponent(constants.IR_PAGE_TITLE), + this.createInformationRow(constants.IR_PAGE_TITLE, this.migrationStateModel.migrationController?.name!), + this.createInformationRow(constants.SUMMARY_IR_NODE, this.migrationStateModel._nodeNames.join(', ')), + + ] + ); + } + + public async onPageLeave(): Promise { + this._flexContainer.clearItems(); + this.wizard.registerNavigationValidator(async (pageChangeInfo) => { + return true; + }); + } + + protected async handleStateChange(e: StateChangeEvent): Promise { + } + + private createInformationRow(label: string, value: string): azdata.FlexContainer { + return this._view.modelBuilder.flexContainer() + .withLayout( + { + flexFlow: 'row', + alignItems: 'center', + }) + .withItems( + [ + this.creaetLabelTextComponent(label), + this.createTextCompononent(value) + ], + { + CSSStyles: { 'margin-right': '5px' } + }) + .component(); + } + + private createHeadingTextComponent(value: string): azdata.TextComponent { + const component = this.createTextCompononent(value); + component.updateCssStyles({ + 'font-size': '13px', + 'font-weight': 'bold' + }); + return component; + } + + + private creaetLabelTextComponent(value: string): azdata.TextComponent { + const component = this.createTextCompononent(value); + component.updateCssStyles({ + 'color': '#595959', + 'width': '250px' + }); + return component; + } + + private createTextCompononent(value: string): azdata.TextComponent { + return this._view.modelBuilder.text().withProps({ + value: value + }).component(); + } + + private createNetworkContainerRows(): azdata.FlexContainer { + const flexContainer = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + switch (this.migrationStateModel.databaseBackup.networkContainerType) { + case NetworkContainerType.NETWORK_SHARE: + flexContainer.addItems( + [ + this.createInformationRow(constants.TYPE, constants.NETWORK_SHARE), + this.createInformationRow(constants.PATH, this.migrationStateModel.databaseBackup.networkShareLocation), + this.createInformationRow(constants.USER_ACCOUNT, this.migrationStateModel.databaseBackup.windowsUser), + this.createInformationRow(constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel.databaseBackup.subscription.name), + this.createInformationRow(constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel.databaseBackup.storageAccount.name), + ] + ); + break; + case NetworkContainerType.FILE_SHARE: + flexContainer.addItems( + [ + this.createInformationRow(constants.TYPE, constants.FILE_SHARE), + this.createInformationRow(constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel.databaseBackup.subscription.name), + this.createInformationRow(constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel.databaseBackup.storageAccount.name), + this.createInformationRow(constants.FILE_SHARE, this.migrationStateModel.databaseBackup.fileShare.name), + ] + ); + break; + case NetworkContainerType.BLOB_CONTAINER: + flexContainer.addItems( + [ + this.createInformationRow(constants.TYPE, constants.BLOB_CONTAINER), + this.createInformationRow(constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel.databaseBackup.blobContainer.subscription.name), + this.createInformationRow(constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel.databaseBackup.storageAccount.name), + this.createInformationRow(constants.BLOB_CONTAINER, this.migrationStateModel.databaseBackup.blobContainer.name), + ] + ); + } + return flexContainer; + } +} diff --git a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts index 36a4b323ce..3e895319bf 100644 --- a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts @@ -4,19 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { azureResource } from 'azureResource'; -import { getAvailableManagedInstanceProducts, getSubscriptions, SqlManagedInstance, Subscription } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../models/strings'; +import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; export class TempTargetSelectionPage extends MigrationWizardPage { private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent; private _managedInstanceDropdown!: azdata.DropDownComponent; - private _subscriptionDropdownValues: azdata.CategoryValue[] = []; - private _subscriptionMap: Map = new Map(); - constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.TARGET_SELECTION_PAGE_TITLE), migrationStateModel); @@ -24,17 +20,37 @@ export class TempTargetSelectionPage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView): Promise { - const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text().withProps({ - value: constants.SUBSCRIPTION - }).component(); - this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().component(); + const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text() + .withProps({ + value: constants.SUBSCRIPTION + }).component(); + + this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown() + .withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); this._managedInstanceSubscriptionDropdown.onValueChanged((e) => { - this.populateManagedInstanceDropdown(); + if (e.selected) { + this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index); + this.migrationStateModel._targetManagedInstances = undefined!; + this.populateManagedInstanceDropdown(); + } }); + const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({ value: constants.MANAGED_INSTANCE }).component(); - this._managedInstanceDropdown = view.modelBuilder.dropDown().component(); + + this._managedInstanceDropdown = view.modelBuilder.dropDown() + .withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + this._managedInstanceDropdown.onValueChanged((e) => { + if (e.selected) { + this.migrationStateModel.migrationControllers = undefined!; + this.migrationStateModel._targetManagedInstance = this.migrationStateModel.getManagedInstance(e.index); + } + }); const targetContainer = view.modelBuilder.flexContainer().withItems( [ @@ -61,6 +77,8 @@ export class TempTargetSelectionPage extends MigrationWizardPage { this.populateSubscriptionDropdown(); } public async onPageLeave(): Promise { + console.log(this.migrationStateModel._targetSubscription); + console.log(this.migrationStateModel._targetManagedInstance); } protected async handleStateChange(e: StateChangeEvent): Promise { } @@ -68,71 +86,26 @@ export class TempTargetSelectionPage extends MigrationWizardPage { private async populateSubscriptionDropdown(): Promise { this._managedInstanceSubscriptionDropdown.loading = true; this._managedInstanceDropdown.loading = true; - let subscriptions: azureResource.AzureResourceSubscription[] = []; + try { - subscriptions = await getSubscriptions(this.migrationStateModel.azureAccount); - subscriptions.forEach((subscription) => { - this._subscriptionMap.set(subscription.id, subscription); - this._subscriptionDropdownValues.push({ - name: subscription.id, - displayName: subscription.name + ' - ' + subscription.id, - }); - }); - - if (!this._subscriptionDropdownValues || this._subscriptionDropdownValues.length === 0) { - this._subscriptionDropdownValues = [ - { - displayName: constants.NO_SUBSCRIPTIONS_FOUND, - name: '' - } - ]; - } - - this._managedInstanceSubscriptionDropdown.values = this._subscriptionDropdownValues; - } catch (error) { - this.setEmptyDropdownPlaceHolder(this._managedInstanceSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND); + this._managedInstanceSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues(); + this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(0); + } catch (e) { + this.migrationStateModel._targetManagedInstances = undefined!; + } finally { + this.populateManagedInstanceDropdown(); + this._managedInstanceSubscriptionDropdown.loading = false; this._managedInstanceDropdown.loading = false; } - this.populateManagedInstanceDropdown(); - this._managedInstanceSubscriptionDropdown.loading = false; } private async populateManagedInstanceDropdown(): Promise { this._managedInstanceDropdown.loading = true; - let mis: SqlManagedInstance[] = []; - let miValues: azdata.CategoryValue[] = []; try { - const subscriptionId = (this._managedInstanceSubscriptionDropdown.value).name; - - mis = await getAvailableManagedInstanceProducts(this.migrationStateModel.azureAccount, this._subscriptionMap.get(subscriptionId)!); - mis.forEach((mi) => { - miValues.push({ - name: mi.name, - displayName: mi.name - }); - }); - - if (!miValues || miValues.length === 0) { - miValues = [ - { - displayName: constants.NO_MANAGED_INSTANCE_FOUND, - name: '' - } - ]; - } - - this._managedInstanceDropdown.values = miValues; - } catch (error) { - this.setEmptyDropdownPlaceHolder(this._managedInstanceDropdown, constants.NO_MANAGED_INSTANCE_FOUND); + this._managedInstanceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription); + this.migrationStateModel._targetManagedInstance = this.migrationStateModel.getManagedInstance(0); + } finally { + this._managedInstanceDropdown.loading = false; } - - this._managedInstanceDropdown.loading = false; - } - - private setEmptyDropdownPlaceHolder(dropDown: azdata.DropDownComponent, placeholder: string): void { - dropDown.values = [{ - displayName: placeholder, - name: '' - }]; } } diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 46e8d96dd5..acaadc1abc 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -15,7 +15,9 @@ import { DatabaseBackupPage } from './databaseBackupPage'; import { AccountsSelectionPage } from './accountsSelectionPage'; import { IntergrationRuntimePage } from './integrationRuntimePage'; import { TempTargetSelectionPage } from './tempTargetSelectionPage'; +import { SummaryPage } from './summaryPage'; +export const WIZARD_INPUT_COMPONENT_WIDTH = '400px'; export class WizardController { constructor(private readonly extensionContext: vscode.ExtensionContext) { @@ -34,7 +36,6 @@ export class WizardController { const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide'); wizard.generateScriptButton.enabled = false; wizard.generateScriptButton.hidden = true; - // Disabling unused pages const sourceConfigurationPage = new SourceConfigurationPage(wizard, stateModel); const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel); // const subscriptionSelectionPage = new SubscriptionSelectionPage(wizard, stateModel); @@ -42,6 +43,7 @@ export class WizardController { const tempTargetSelectionPage = new TempTargetSelectionPage(wizard, stateModel); const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel); const integrationRuntimePage = new IntergrationRuntimePage(wizard, stateModel); + const summaryPage = new SummaryPage(wizard, stateModel); const pages: MigrationWizardPage[] = [ // subscriptionSelectionPage, @@ -50,7 +52,8 @@ export class WizardController { sourceConfigurationPage, skuRecommendationPage, databaseBackupPage, - integrationRuntimePage + integrationRuntimePage, + summaryPage ]; wizard.pages = pages.map(p => p.getwizardPage()); @@ -79,5 +82,9 @@ export class WizardController { await Promise.all(wizardSetupPromises); await pages[0].onPageEnter(); + + wizard.doneButton.onClick(async (e) => { + await stateModel.startMigration(); + }); } }