diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index 1204b755d6..a04d3ede4f 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -84,6 +84,25 @@ declare module 'azureResource' { export interface FileShare extends AzureResource { } - export interface MigrationController extends AzureResource { } + export interface MigrationControllerProperties { + name: string; + subscriptionId: string; + resourceGroup: string; + location: string; + provisioningState: string; + integrationRunTimeState?: string; + isProvisioned?: boolean; + } + + export interface MigrationController { + properties: MigrationControllerProperties; + location: string; + id: string; + name: string; + error: { + code: string, + message: string + } + } } } diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts index 36cce59241..d30b5726f9 100644 --- a/extensions/azurecore/src/azureResource/utils.ts +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -7,7 +7,7 @@ import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { TokenCredentials } from '@azure/ms-rest-js'; import axios, { AxiosRequestConfig } from 'axios'; import * as azdata from 'azdata'; -import { HttpGetRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, GetMigrationControllersResult } from 'azurecore'; +import { HttpRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, GetMigrationControllerResult, CreateMigrationControllerResult, GetMigrationControllerAuthKeysResult } from 'azurecore'; import { azureResource } from 'azureResource'; import { EOL } from 'os'; import * as nls from 'vscode-nls'; @@ -288,11 +288,18 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?: return result; } +enum HttpRequestType { + GET, + POST, + PUT, + DELETE +} + /** - * makes a GET request to Azure REST apis. Currently, it only supports GET ARM queries. + * Make a HTTP request to Azure REST apis. */ -export async function makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string): Promise { - const result: HttpGetRequestResult = { response: {}, errors: [] }; +export async function makeHttpRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string, requestType: HttpRequestType, requestBody?: any): Promise { + const result: HttpRequestResult = { response: {}, errors: [] }; if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) { const error = new Error(invalidAzureAccount); @@ -340,7 +347,22 @@ export async function makeHttpGetRequest(account: azdata.Account, subscription: validateStatus: () => true // Never throw }; - const response = await axios.get(url, config); + let response; + + switch (requestType) { + case HttpRequestType.GET: + response = await axios.get(url, config); + break; + case HttpRequestType.POST: + response = await axios.post(url, requestBody, config); + break; + case HttpRequestType.PUT: + response = await axios.put(url, requestBody, config); + break; + case HttpRequestType.DELETE: + response = await axios.delete(url, config); + break; + } if (response.status !== 200) { let errorMessage: string[] = []; @@ -362,12 +384,8 @@ export async function makeHttpGetRequest(account: azdata.Account, subscription: } export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://management.azure.com` + - `/subscriptions/${subscription.id}` + - `/resourceGroups/${storageAccount.resourceGroup}` + - `/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}` + - `/blobServices/default/containers?api-version=2019-06-01`; - const response = await makeHttpGetRequest(account, subscription, ignoreErrors, apiEndpoint); + const apiEndpoint = `https://management.azure.com/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/blobServices/default/containers?api-version=2019-06-01`; + const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.GET); return { blobContainers: response?.response?.data?.value ?? [], errors: response.errors ? response.errors : [] @@ -375,26 +393,42 @@ export async function getBlobContainers(account: azdata.Account, subscription: a } export async function getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://management.azure.com` + - `/subscriptions/${subscription.id}` + - `/resourceGroups/${storageAccount.resourceGroup}` + - `/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}` + - `/fileServices/default/shares?api-version=2019-06-01`; - const response = await makeHttpGetRequest(account, subscription, ignoreErrors, apiEndpoint); + const apiEndpoint = `https://management.azure.com/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/fileServices/default/shares?api-version=2019-06-01`; + const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.GET); return { fileShares: response?.response?.data?.value ?? [], errors: response.errors ? response.errors : [] }; } -export async function getMigrationControllers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://${regionName}.management.azure.com` + - `/subscriptions/${subscription.id}` + - `/resourceGroups/${resourceGroupName}` + - `/providers/Microsoft.DataMigration/Controllers/default/shares?api-version=2020-09-01-preview`; - const response = await makeHttpGetRequest(account, subscription, ignoreErrors, apiEndpoint); +export async function getMigrationControllers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors: boolean): Promise { + const apiEndpoint = `https://${regionName}.management.azure.com/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`; + const response = await makeHttpRequest(account, subscription, false, apiEndpoint, HttpRequestType.GET); return { - controllers: response?.response?.data?.value ?? [], + controller: response?.response?.data ?? undefined, + errors: response.errors ? response.errors : [] + }; +} + +export async function createMigrationController(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors: boolean): Promise { + const apiEndpoint = `https://${regionName}.management.azure.com/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`; + + const requestBody = { + 'location': regionName + }; + const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.PUT, requestBody); + return { + controller: response?.response?.data ?? undefined, + errors: response.errors ? response.errors : [] + }; +} + +export async function getMigrationControllerAuthKeys(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors: boolean): Promise { + const apiEndpoint = `https://${regionName}.management.azure.com/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}/ListAuthKeys?api-version=2020-09-01-preview`; + const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.POST); + return { + keyName1: response?.response?.data?.keyName1 ?? '', + keyName2: response?.response?.data?.keyName2 ?? '', errors: response.errors ? response.errors : [] }; } diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index 48479e3c1b..39a54c1efa 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -75,7 +75,9 @@ declare module 'azurecore' { getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise; getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise; - getMigrationControllers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, ignoreErrors?: boolean): Promise; + getMigrationController(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors?: boolean): Promise; + createMigrationController(account:azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors?:boolean): Promise; + getMigrationControllerAuthKeys(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors?: boolean): Promise; /** * Converts a region value (@see AzureRegion) into the localized Display Name @@ -95,8 +97,10 @@ declare module 'azurecore' { export type GetStorageAccountResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; export type GetBlobContainersResult = { blobContainers: azureResource.BlobContainer[], errors: Error[] }; export type GetFileSharesResult = { fileShares: azureResource.FileShare[], errors: Error[] }; - export type GetMigrationControllersResult = { controllers: azureResource.MigrationController[], errors: Error[] }; + export type GetMigrationControllerResult = { controller: azureResource.MigrationController | undefined, errors: Error[] }; + export type CreateMigrationControllerResult = { controller: azureResource.MigrationController | undefined, errors: Error[] }; + export type GetMigrationControllerAuthKeysResult = { keyName1: string, keyName2: string, errors: Error[] }; export type ResourceQueryResult = { resources: T[], errors: Error[] }; - export type HttpGetRequestResult = { response: any, errors: Error[] }; + export type HttpRequestResult = { response: any, errors: Error[] }; } diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index 12e8cd980a..aed8018e6f 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -196,12 +196,29 @@ export async function activate(context: vscode.ExtensionContext): Promise { return azureResourceUtils.getFileShares(account, subscription, storageAccount, ignoreErrors); }, - getMigrationControllers(account: azdata.Account, + getMigrationController(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, - ignoreErrors: boolean): Promise { - return azureResourceUtils.getMigrationControllers(account, subscription, resourceGroupName, regionName, ignoreErrors); + controllerName: string, + ignoreErrors: boolean): Promise { + return azureResourceUtils.getMigrationControllers(account, subscription, resourceGroupName, regionName, controllerName, ignoreErrors); + }, + createMigrationController(account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + resourceGroupName: string, + regionName: string, + controllerName: string, + ignoreErrors: boolean): Promise { + return azureResourceUtils.createMigrationController(account, subscription, resourceGroupName, regionName, controllerName, ignoreErrors); + }, + getMigrationControllerAuthKeys(account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + resourceGroupName: string, + regionName: string, + controllerName: string, + ignoreErrors: boolean): Promise { + return azureResourceUtils.getMigrationControllerAuthKeys(account, subscription, resourceGroupName, regionName, controllerName, ignoreErrors); }, getRegionDisplayName: utils.getRegionDisplayName, runGraphQuery(account: azdata.Account, diff --git a/extensions/machine-learning/src/test/stubs.ts b/extensions/machine-learning/src/test/stubs.ts index 07f2914d3e..0927b9c461 100644 --- a/extensions/machine-learning/src/test/stubs.ts +++ b/extensions/machine-learning/src/test/stubs.ts @@ -8,7 +8,13 @@ import * as azurecore from 'azurecore'; import { azureResource } from 'azureResource'; export class AzurecoreApiStub implements azurecore.IExtension { - getMigrationControllers(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _resourceGroupName: string, _regionName: string, _ignoreErrors?: boolean): Promise { + getMigrationControllerAuthKeys(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _resourceGroupName: string, _regionName: string, _controllerName: string, _ignoreErrors?: boolean): Promise { + throw new Error('Method not implemented.'); + } + createMigrationController(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _resourceGroupName: string, _regionName: string, _controllerName: string, _ignoreErrors?: boolean): Promise { + throw new Error('Method not implemented.'); + } + getMigrationController(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _resourceGroupName: string, _regionName: string, _controllerName: string, _ignoreErrors?: boolean): Promise { throw new Error('Method not implemented.'); } getFileShares(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise { diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 9a0f87c0f6..2beed209cf 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -82,12 +82,34 @@ export async function getBlobContainers(account: azdata.Account, subscription: S return blobContainers!; } -export async function getMigrationControllers(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string): Promise { +export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { const api = await getAzureCoreAPI(); - let result = await api.getMigrationControllers(account, subscription, resourceGroupName, regionName, true); - let controllers = result.controllers; - sortResourceArrayByName(controllers); - return controllers!; + let result = await api.getMigrationController(account, subscription, resourceGroupName, regionName, controllerName, true); + return result.controller!; +} + +export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { + const api = await getAzureCoreAPI(); + let result = await api.createMigrationController(account, subscription, resourceGroupName, regionName, controllerName, true); + return result.controller!; +} + +export async function getMigrationControllerAuthKeys(accounts: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { + const api = await getAzureCoreAPI(); + let result = await api.getMigrationControllerAuthKeys(accounts, subscription, resourceGroupName, regionName, controllerName, true); + return result; +} + +/** + * For now only east us euap is supported. Actual API calls will be added in the public release. + */ +export function getMigrationControllerRegions(): azdata.CategoryValue[] { + return [ + { + displayName: 'East US EUAP', + name: 'eastus2euap' + } + ]; } type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.MigrationController | azureResource.AzureResourceSubscription; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 19cc2a58d3..6158b86f08 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -7,6 +7,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; import { SKURecommendations } from './externalContract'; +import { azureResource } from 'azureResource'; export enum State { INIT, @@ -86,6 +87,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined; private _azureAccount!: azdata.Account; private _databaseBackup!: DatabaseBackupModel; + private _migrationController!: azureResource.MigrationController | undefined; constructor( private readonly _extensionContext: vscode.ExtensionContext, @@ -156,6 +158,14 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._stateChangeEventEmitter.event; } + public set migrationController(controller: azureResource.MigrationController | undefined) { + this._migrationController = controller; + } + + public get migrationController(): azureResource.MigrationController | undefined { + return this._migrationController; + } + dispose() { this._stateChangeEventEmitter.dispose(); } diff --git a/extensions/sql-migration/src/models/strings.ts b/extensions/sql-migration/src/models/strings.ts index 067f6f1bf9..24fab0d461 100644 --- a/extensions/sql-migration/src/models/strings.ts +++ b/extensions/sql-migration/src/models/strings.ts @@ -86,3 +86,56 @@ export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare export const INVALID_BLOBCONTAINER_ERROR = localize('sql.migration.invalid.blobContainer.error', "Please select a valid blob container to proceed."); export const INVALID_NETWORK_SHARE_LOCATION = localize('sql.migration.invalid.network.share.location', "Invalid network share location format. Example: {0}", '\\\\Servername.domainname.com\\Backupfolder'); export const INVALID_USER_ACCOUNT = localize('sql.migration.invalid.user.account', "Invalid user account format. Example: {0}", 'Domain\\username'); + + +// 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 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"); + +// 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_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_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 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 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."); +export const INVALID_REGION_ERROR = localize('sql.migration.invalid.region.error', "Please select a valid region to proceed."); +export const INVALID_CONTROLLER_NAME_ERROR = localize('sql.migration.invalid.controller.name.error', "Please enter a valid name for the migration controller."); +export const CONTROLLER_NOT_FOUND = localize('sql.migration.controller.not.found', "No Migration Controllers found. Please create a new one."); +export const CONTROLLER_NOT_SETUP_ERROR = localize('sql.migration.controller.not.setup', "Please add a migration controller to proceed."); + +// common strings +export const LEARN_MORE = localize('sql.migration.learn.more', "Learn more"); +export const SUBSCRIPTION = localize('sql.migration.subscription', "Subscription"); +export const RESOURCE_GROUP = localize('sql.migration.resourceGroups', "Resource group"); +export const REGION = localize('sql.migration.region', "Region"); +export const NAME = localize('sql.migration.name', "Name"); +export const LOCATION = localize('sql.migration.location', "Location"); +export const NEW = localize('sql.migration.new', "New"); +export const FEATURE_NOT_AVAILABLE = localize('sql.migration.feature.not.available', "This feature is not available yet."); +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"); diff --git a/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts b/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts new file mode 100644 index 0000000000..9620563d2a --- /dev/null +++ b/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts @@ -0,0 +1,492 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { createMigrationController, getMigrationControllerRegions, getMigrationController, getResourceGroups, getSubscriptions, Subscription, getMigrationControllerAuthKeys } 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'; + +export class CreateMigrationControllerDialog { + + private migrationControllerSubscriptionDropdown!: azdata.DropDownComponent; + private migrationControllerResourceGroupDropdown!: azdata.DropDownComponent; + private migrationControllerRegionDropdown!: azdata.DropDownComponent; + private migrationControllerNameText!: azdata.InputBoxComponent; + private _formSubmitButton!: azdata.ButtonComponent; + + private _statusLoadingComponent!: azdata.LoadingComponent; + private migrationControllerAuthKeyTable!: azdata.DeclarativeTableComponent; + private _connectionStatus!: azdata.TextComponent; + private _copyKey1Button!: azdata.ButtonComponent; + private _copyKey2Button!: 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'); + } + + initialize() { + let tab = azdata.window.createTab(''); + this._dialogObject.registerCloseValidator(async () => { + return true; + }); + tab.registerContent((view: azdata.ModelView) => { + this._view = view; + + this._formSubmitButton = view.modelBuilder.button().withProps({ + label: constants.SUBMIT, + width: '80px' + }).component(); + + this._formSubmitButton.onDidClick(async (e) => { + this._statusLoadingComponent.loading = true; + this._formSubmitButton.enabled = false; + + const subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + const resourceGroup = (this.migrationControllerResourceGroupDropdown.value as azdata.CategoryValue).name; + const region = (this.migrationControllerRegionDropdown.value as azdata.CategoryValue).name; + const controllerName = this.migrationControllerNameText.value; + + const formValidationErrors = this.validateCreateControllerForm(subscription, resourceGroup, region, controllerName); + + if (formValidationErrors.length > 0) { + this.setDialogMessage(formValidationErrors); + this._statusLoadingComponent.loading = false; + this._formSubmitButton.enabled = true; + return; + } + + try { + const createdController = await createMigrationController(this.migrationStateModel.azureAccount, subscription, resourceGroup, region, controllerName!); + if (createdController.error) { + this.setDialogMessage(`${createdController.error.code} : ${createdController.error.message}`); + this._statusLoadingComponent.loading = false; + this._formSubmitButton.enabled = true; + return; + } + this._dialogObject.message = { + text: '' + }; + this.migrationStateModel.migrationController = createdController; + await this.refreshAuthTable(); + await this.refreshStatus(); + this._setupContainer.display = 'inline'; + this._statusLoadingComponent.loading = false; + } catch (e) { + console.log(e); + this._statusLoadingComponent.loading = false; + this._formSubmitButton.enabled = true; + return; + } + }); + + this._statusLoadingComponent = view.modelBuilder.loadingComponent().withProps({ + loadingText: constants.CONTROLLER_DIALOG_CONTROLLER_CONTAINER_LOADING_HELP, + loading: false + }).component(); + + const creationStatusContainer = this.createControllerStatus(); + + const formBuilder = view.modelBuilder.formContainer().withFormItems( + [ + { + component: this.migrationControllerDropdownsContainer() + }, + { + component: this._formSubmitButton + }, + { + component: this._statusLoadingComponent + }, + { + component: creationStatusContainer + } + ], + { + horizontal: false + } + ); + + const form = formBuilder.withLayout({ width: '100%' }).component(); + + return view.initializeModel(form).then(() => { + this.populateSubscriptions(); + }); + }); + + this._dialogObject.content = [tab]; + this._dialogObject.okButton.enabled = false; + azdata.window.openDialog(this._dialogObject); + this._dialogObject.cancelButton.onClick((e) => { + this.migrationStateModel.migrationController = undefined; + }); + this._dialogObject.okButton.onClick((e) => { + this.irPage.populateMigrationController(); + }); + } + + private migrationControllerDropdownsContainer(): azdata.FlexContainer { + const dialogDescription = this._view.modelBuilder.text().withProps({ + value: constants.IR_PAGE_DESCRIPTION, + links: [ + { + text: constants.LEARN_MORE, + url: 'https://www.microsoft.com' // TODO: add a proper link to the docs. + } + ] + }).component(); + + const formHeading = this._view.modelBuilder.text().withProps({ + value: constants.CONTROLLER_DIALOG_CREATE_CONTROLLER_FORM_HEADING + }).component(); + + const subscriptionDropdownLabel = this._view.modelBuilder.text().withProps({ + value: constants.SUBSCRIPTION + }).component(); + + this.migrationControllerSubscriptionDropdown = this._view.modelBuilder.dropDown().withProps({ + required: true + }).component(); + + this.migrationControllerSubscriptionDropdown.onValueChanged((e) => { + if (this.migrationControllerSubscriptionDropdown.value) { + this.populateResourceGroups(); + } + }); + + const resourceGroupDropdownLabel = this._view.modelBuilder.text().withProps({ + value: constants.RESOURCE_GROUP + }).component(); + + this.migrationControllerResourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({ + required: true + }).component(); + + const controllerNameLabel = this._view.modelBuilder.text().withProps({ + value: constants.NAME + }).component(); + + this.migrationControllerNameText = this._view.modelBuilder.inputBox().component(); + + const regionsDropdownLabel = this._view.modelBuilder.text().withProps({ + value: constants.REGION + }).component(); + + this.migrationControllerRegionDropdown = this._view.modelBuilder.dropDown().withProps({ + required: true, + values: getMigrationControllerRegions() + }).component(); + + const flexContainer = this._view.modelBuilder.flexContainer().withItems([ + dialogDescription, + formHeading, + subscriptionDropdownLabel, + this.migrationControllerSubscriptionDropdown, + resourceGroupDropdownLabel, + this.migrationControllerResourceGroupDropdown, + controllerNameLabel, + this.migrationControllerNameText, + regionsDropdownLabel, + this.migrationControllerRegionDropdown + ]).withLayout({ + flexFlow: 'column' + }).component(); + return flexContainer; + } + + private validateCreateControllerForm(subscription: azureResource.AzureResourceSubscription, resourceGroup: string | undefined, region: string | undefined, controllerName: string | undefined): string { + const errors: string[] = []; + if (!subscription) { + errors.push(constants.INVALID_SUBSCRIPTION_ERROR); + } + if (!resourceGroup) { + errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); + } + if (!region) { + errors.push(constants.INVALID_REGION_ERROR); + } + if (!controllerName || controllerName.length === 0) { + errors.push(constants.INVALID_CONTROLLER_NAME_ERROR); + } + return errors.join(os.EOL); + } + + 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.loading = false; + this.populateResourceGroups(); + } + + private async populateResourceGroups(): Promise { + this.migrationControllerResourceGroupDropdown.loading = true; + let subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + const resourceGroups = await getResourceGroups(this.migrationStateModel.azureAccount, subscription); + let resourceGroupDropdownValues: azdata.CategoryValue[] = []; + if (resourceGroups && resourceGroups.length > 0) { + resourceGroups.forEach((resourceGroup) => { + resourceGroupDropdownValues.push({ + name: resourceGroup.name, + displayName: resourceGroup.name + }); + }); + } else { + resourceGroupDropdownValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + this.migrationControllerResourceGroupDropdown.values = resourceGroupDropdownValues; + this.migrationControllerResourceGroupDropdown.loading = false; + } + + 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, + CSSStyles: { + 'font-weight': 'bold' + } + }).component(); + + const expressSetupLink = this._view.modelBuilder.hyperlink().withProps({ + label: constants.CONTROLLER_OPTION1_SETUP_LINK_TEXT, + url: '' + }).component(); + + expressSetupLink.onDidClick((e) => { + vscode.window.showInformationMessage(constants.FEATURE_NOT_AVAILABLE); + }); + + const manualSetupTitle = this._view.modelBuilder.text().withProps({ + value: constants.CONTROLLER_OPTION2_HEADING, + CSSStyles: { + 'font-weight': 'bold' + } + }).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, + }).component(); + + const refreshLoadingIndicator = this._view.modelBuilder.loadingComponent().withProps({ + loading: false + }).component(); + + refreshButton.onDidClick(async (e) => { + refreshLoadingIndicator.loading = true; + try { + await this.refreshStatus(); + } catch (e) { + console.log(e); + } + refreshLoadingIndicator.loading = false; + }); + + const connectionStatusContainer = this._view.modelBuilder.flexContainer().withItems( + [ + this._connectionStatus, + refreshButton, + refreshLoadingIndicator + ] + ).component(); + + + this.migrationControllerAuthKeyTable = this._view.modelBuilder.declarativeTable().withProps({ + columns: [ + { + displayName: constants.NAME, + valueType: azdata.DeclarativeDataType.string, + width: '100px', + isReadOnly: true, + }, + { + displayName: constants.AUTH_KEY_COLUMN_HEADER, + valueType: azdata.DeclarativeDataType.string, + width: '300px', + isReadOnly: true, + }, + { + displayName: '', + valueType: azdata.DeclarativeDataType.component, + width: '100px', + isReadOnly: true, + } + ], + CSSStyles: { + 'margin-top': '25px' + } + }).component(); + + const refreshKeyButton = this._view.modelBuilder.button().withProps({ + label: constants.REFRESH_KEYS, + CSSStyles: { + 'margin-top': '10px' + }, + width: '100px' + }).component(); + + refreshKeyButton.onDidClick(async (e) => { + this.refreshAuthTable(); + + }); + + this._setupContainer = this._view.modelBuilder.flexContainer().withItems( + [ + informationTextBox, + expressSetupTitle, + expressSetupLink, + manualSetupTitle, + manualSetupButton, + manualSetupSecondDescription, + refreshKeyButton, + this.migrationControllerAuthKeyTable, + connectionStatusTitle, + connectionStatusContainer + ] + ).withLayout({ + flexFlow: 'column' + }).component(); + + this._setupContainer.display = 'none'; + return this._setupContainer; + } + + private async refreshStatus(): Promise { + const subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + 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); + if (controllerStatus) { + const state = controllerStatus.properties.integrationRunTimeState; + + if (state === 'Online') { + this._connectionStatus.value = constants.CONTRLLER_READY(this.migrationStateModel.migrationController!.name, os.hostname()); + this._dialogObject.okButton.enabled = true; + } else { + this._connectionStatus.value = constants.CONTRLLER_NOT_READY(this.migrationStateModel.migrationController!.name); + this._dialogObject.okButton.enabled = false; + } + } + + } + private async refreshAuthTable(): Promise { + const subscription = this._subscriptionMap.get((this.migrationControllerSubscriptionDropdown.value as azdata.CategoryValue).name)!; + 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 + }).component(); + + this._copyKey1Button.onDidClick((e) => { + vscode.env.clipboard.writeText(this.migrationControllerAuthKeyTable.dataValues![0][1].value); + vscode.window.showInformationMessage(constants.CONTROLLER_KEY_COPIED_HELP); + }); + + this._copyKey2Button = this._view.modelBuilder.button().withProps({ + label: constants.COPY_KEY + }).component(); + + this._copyKey2Button.onDidClick((e) => { + vscode.env.clipboard.writeText(this.migrationControllerAuthKeyTable.dataValues![1][1].value); + vscode.window.showInformationMessage(constants.CONTROLLER_KEY_COPIED_HELP); + }); + + this.migrationControllerAuthKeyTable.updateProperties({ + dataValues: [ + [ + { + value: constants.CONTROLELR_KEY1_LABEL + }, + { + value: keys.keyName1 + }, + { + value: this._copyKey1Button + } + ], + [ + { + value: constants.CONTROLELR_KEY2_LABEL + }, + { + value: keys.keyName2 + }, + { + value: this._copyKey2Button + } + ] + ] + }); + + } + + private setDialogMessage(message: string, level: azdata.window.MessageLevel = azdata.window.MessageLevel.Error): void { + this._dialogObject.message = { + text: message, + level: level + }; + } +} diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index f3b7e7f861..00ad163931 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -38,8 +38,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _subscriptionMap: Map = new Map(); private _storageAccountMap: Map = new Map(); - private _errors: string[] = []; - constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel); this.wizardPage.description = constants.DATABASE_BACKUP_PAGE_DESCRIPTION; @@ -139,16 +137,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { }).component(); this._fileShareSubscriptionDropdown = view.modelBuilder.dropDown().withProps({ required: true, - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) { - if ((c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { - this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR); - } - } - return true; }).component(); this._fileShareSubscriptionDropdown.onValueChanged(async (value) => { if (this._fileShareSubscriptionDropdown.value) { @@ -165,16 +153,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._fileShareStorageAccountDropdown = view.modelBuilder.dropDown() .withProps({ required: true - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) { - if ((c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { - this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR); - } - } - return true; }).component(); this._fileShareStorageAccountDropdown.onValueChanged(async (value) => { if (this._fileShareStorageAccountDropdown.value) { @@ -191,16 +169,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._fileShareFileShareDropdown = view.modelBuilder.dropDown() .withProps({ required: true - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) { - if ((c.value).displayName === constants.NO_FILESHARES_FOUND) { - this.addErrorMessage(constants.INVALID_FILESHARE_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_FILESHARE_ERROR); - } - } - return true; }).component(); this._fileShareFileShareDropdown.onValueChanged((value) => { if (this._fileShareFileShareDropdown.value) { @@ -235,17 +203,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerSubscriptionDropdown = view.modelBuilder.dropDown() .withProps({ required: true - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { - if ( - (c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { - this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR); - } - } - return true; }).component(); this._blobContainerSubscriptionDropdown.onValueChanged(async (value) => { if (this._blobContainerSubscriptionDropdown.value) { @@ -262,16 +219,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerStorageAccountDropdown = view.modelBuilder.dropDown() .withProps({ required: true - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { - if ((c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { - this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR); - } - } - return true; }).component(); this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => { if (this._blobContainerStorageAccountDropdown.value) { @@ -287,16 +234,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerBlobDropdown = view.modelBuilder.dropDown() .withProps({ required: true - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { - if ((c.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { - this.addErrorMessage(constants.INVALID_BLOBCONTAINER_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_BLOBCONTAINER_ERROR); - } - } - return true; }).component(); this._blobContainerBlobDropdown.onValueChanged((value) => { if (this._blobContainerBlobDropdown.value) { @@ -339,9 +276,11 @@ export class DatabaseBackupPage extends MigrationWizardPage { validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION }) .withValidation((component) => { - if (component.value) { - if (!/^(\\)(\\[\w\.-_]+){2,}(\\?)$/.test(component.value)) { - return false; + if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { + if (component.value) { + if (!/^(\\)(\\[\w\.-_]+){2,}(\\?)$/.test(component.value)) { + return false; + } } } return true; @@ -362,9 +301,11 @@ export class DatabaseBackupPage extends MigrationWizardPage { validationErrorMessage: constants.INVALID_USER_ACCOUNT }) .withValidation((component) => { - if (component.value) { - if (!/^[a-zA-Z][a-zA-Z0-9\-\.]{0,61}[a-zA-Z]\\\w[\w\.\- ]*$/.test(component.value)) { - return false; + 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)) { + return false; + } } } return true; @@ -401,16 +342,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._networkShareContainerSubscriptionDropdown = view.modelBuilder.dropDown() .withProps({ required: true - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { - if ((c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { - this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR); - } - } - return true; }).component(); this._networkShareContainerSubscriptionDropdown.onValueChanged(async (value) => { if (this._networkShareContainerSubscriptionDropdown.value) { @@ -427,16 +358,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._networkShareContainerStorageAccountDropdown = view.modelBuilder.dropDown() .withProps({ required: true - }).withValidation((c) => { - if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { - if ((c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { - this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR); - return false; - } else { - this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR); - } - } - return true; }).component(); this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => { if (this._networkShareContainerStorageAccountDropdown.value) { @@ -533,6 +454,55 @@ export class DatabaseBackupPage extends MigrationWizardPage { public async onPageEnter(): Promise { await this.getSubscriptionValues(); + this.wizard.registerNavigationValidator((pageChangeInfo) => { + if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { + return true; + } + + const errors: string[] = []; + + switch (this.migrationStateModel.databaseBackup.networkContainerType) { + case NetworkContainerType.NETWORK_SHARE: + if ((this._networkShareContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { + errors.push(constants.INVALID_SUBSCRIPTION_ERROR); + } + if ((this._networkShareContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { + errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); + } + break; + case NetworkContainerType.BLOB_CONTAINER: + if ((this._blobContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { + errors.push(constants.INVALID_SUBSCRIPTION_ERROR); + } + if ((this._blobContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { + errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); + } + if ((this._blobContainerBlobDropdown.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { + errors.push(constants.INVALID_BLOBCONTAINER_ERROR); + } + break; + case NetworkContainerType.FILE_SHARE: + if ((this._fileShareSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { + errors.push(constants.INVALID_SUBSCRIPTION_ERROR); + } + if ((this._fileShareStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { + errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); + } + if ((this._fileShareFileShareDropdown.value).displayName === constants.NO_FILESHARES_FOUND) { + errors.push(constants.INVALID_FILESHARE_ERROR); + } + break; + } + + this.wizard.message = { + text: errors.join(EOL), + level: azdata.window.MessageLevel.Error + }; + if (errors.length > 0) { + return false; + } + return true; + }); } public async onPageLeave(): Promise { @@ -721,22 +691,4 @@ export class DatabaseBackupPage extends MigrationWizardPage { name: '' }]; } - - private addErrorMessage(message: string) { - if (!this._errors.includes(message)) { - this._errors.push(message); - } - this.wizard.message = { - text: this._errors.join(EOL), - level: azdata.window.MessageLevel.Error - }; - } - - private removeErrorMessage(message: string) { - this._errors = this._errors.filter(e => e !== message); - this.wizard.message = { - text: this._errors.join(EOL), - level: azdata.window.MessageLevel.Error - }; - } } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts new file mode 100644 index 0000000000..8ddc0fb71e --- /dev/null +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { MigrationWizardPage } from '../models/migrationWizardPage'; +import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; +import { CreateMigrationControllerDialog } from './createMigrationControllerDialog'; +import * as constants from '../models/strings'; +import * as os from 'os'; + +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 _view!: azdata.ModelView; + + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { + super(wizard, azdata.window.createWizardPage(constants.IR_PAGE_TITLE), migrationStateModel); + } + + protected async registerContent(view: azdata.ModelView): Promise { + this._view = view; + + const createNewController = view.modelBuilder.button().withProps({ + label: constants.NEW, + width: '100px', + }).component(); + + createNewController.onDidClick((e) => { + this.createMigrationContainer.display = 'inline'; + }); + + const setupButtonGroup = 'setupOptions'; + + 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' + }).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' + }).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() + .withFormItems( + [ + { + component: this.migrationControllerDropdownsContainer() + }, + { + component: createNewController + }, + { + component: this.createMigrationContainer + }, + { + component: this._connectionStatus + } + + ] + ); + await view.initializeModel(form.component()); + } + + public async onPageEnter(): Promise { + this.populateMigrationController(); + this.wizard.registerNavigationValidator((pageChangeInfo) => { + if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { + return true; + } + + const errors: string[] = []; + if (((this.migrationControllerDropdown.value).displayName === constants.CONTROLLER_NOT_FOUND)) { + errors.push(constants.CONTROLLER_NOT_SETUP_ERROR); + } + + this.wizard.message = { + text: errors.join(os.EOL), + level: azdata.window.MessageLevel.Error + }; + + if (errors.length > 0) { + return false; + } + + return true; + }); + } + + public async onPageLeave(): Promise { + } + + protected async handleStateChange(e: StateChangeEvent): Promise { + } + + private migrationControllerDropdownsContainer(): azdata.FlexContainer { + const descriptionText = this._view.modelBuilder.text().withProps({ + value: constants.IR_PAGE_DESCRIPTION, + links: [ + { + url: 'https://www.microsoft.com', // TODO: Add proper link + text: constants.LEARN_MORE + }, + ] + }).component(); + + const migrationControllerDropdownLabel = this._view.modelBuilder.text().withProps({ + value: constants.SELECT_A_MIGRATION_CONTROLLER + }).component(); + + this.migrationControllerDropdown = this._view.modelBuilder.dropDown().withProps({ + required: true, + }).component(); + + const flexContainer = this._view.modelBuilder.flexContainer().withItems([ + descriptionText, + migrationControllerDropdownLabel, + this.migrationControllerDropdown + ]).withLayout({ + flexFlow: 'column' + }).component(); + return flexContainer; + } + + public async populateMigrationController(controllerStatus?: string): Promise { + let migrationContollerValues: azdata.CategoryValue[] = []; + if (this.migrationStateModel.migrationController) { + migrationContollerValues = [ + { + displayName: this.migrationStateModel.migrationController.name, + name: this.migrationStateModel.migrationController.name + } + ]; + + this._connectionStatus.value = constants.CONTRLLER_READY(this.migrationStateModel.migrationController!.name, os.hostname()); + } + else { + migrationContollerValues = [ + { + displayName: constants.CONTROLLER_NOT_FOUND, + name: '' + } + ]; + this._connectionStatus.value = ''; + } + this.migrationControllerDropdown.values = migrationContollerValues; + this.migrationControllerDropdown.loading = false; + } + +} + + diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index e2fab6213d..5a49ecb874 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -13,6 +13,7 @@ import { SKURecommendationPage } from './skuRecommendationPage'; // import { SubscriptionSelectionPage } from './subscriptionSelectionPage'; import { DatabaseBackupPage } from './databaseBackupPage'; import { AccountsSelectionPage } from './accountsSelectionPage'; +import { IntergrationRuntimePage } from './integrationRuntimePage'; export class WizardController { constructor(private readonly extensionContext: vscode.ExtensionContext) { @@ -38,12 +39,16 @@ export class WizardController { // const subscriptionSelectionPage = new SubscriptionSelectionPage(wizard, stateModel); const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel); const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel); + const integrationRuntimePage = new IntergrationRuntimePage(wizard, stateModel); + const pages: MigrationWizardPage[] = [ // subscriptionSelectionPage, azureAccountsPage, sourceConfigurationPage, skuRecommendationPage, - databaseBackupPage]; + databaseBackupPage, + integrationRuntimePage + ]; wizard.pages = pages.map(p => p.getwizardPage());