Adding Integration Runtime Page to Migration extension wizard (#14020)

* - Fixed GetMigrationController
- Added createMigrationController and getControllerAuthKeys API in azure core.
- Added typings for Migration Controller
- Fixed database backup page validation logic
- Added IR page with create controller

* Fixing all the comments from the PR

* Fixed typings
This commit is contained in:
Aasim Khan
2021-01-25 14:46:39 -08:00
committed by GitHub
parent ed26938dc8
commit 42a8680738
12 changed files with 968 additions and 145 deletions

View File

@@ -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<azureResource.MigrationController[]> {
export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise<azureResource.MigrationController> {
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<azureResource.MigrationController> {
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<azurecore.GetMigrationControllerAuthKeysResult> {
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;

View File

@@ -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();
}

View File

@@ -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");

View File

@@ -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<string, Subscription> = 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<void> {
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<void> {
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<void> {
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<void> {
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(<string>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(<string>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
};
}
}

View File

@@ -38,8 +38,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private _subscriptionMap: Map<string, Subscription> = new Map();
private _storageAccountMap: Map<string, StorageAccount> = 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 ((<azdata.CategoryValue>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 ((<azdata.CategoryValue>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 ((<azdata.CategoryValue>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 (
(<azdata.CategoryValue>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 ((<azdata.CategoryValue>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 ((<azdata.CategoryValue>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 ((<azdata.CategoryValue>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 ((<azdata.CategoryValue>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<void> {
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 ((<azdata.CategoryValue>this._networkShareContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>this._networkShareContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
break;
case NetworkContainerType.BLOB_CONTAINER:
if ((<azdata.CategoryValue>this._blobContainerSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>this._blobContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
if ((<azdata.CategoryValue>this._blobContainerBlobDropdown.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) {
errors.push(constants.INVALID_BLOBCONTAINER_ERROR);
}
break;
case NetworkContainerType.FILE_SHARE:
if ((<azdata.CategoryValue>this._fileShareSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>this._fileShareStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
if ((<azdata.CategoryValue>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<void> {
@@ -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
};
}
}

View File

@@ -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<void> {
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<void> {
this.populateMigrationController();
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
const errors: string[] = [];
if (((<azdata.CategoryValue>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<void> {
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
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<void> {
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;
}
}

View File

@@ -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());