Add new wizard for login migrations experience (#21317)

This commit is contained in:
AkshayMata
2022-12-17 12:11:25 -05:00
committed by GitHub
parent ff4dc9fe86
commit e223b4e870
22 changed files with 2569 additions and 50 deletions

View File

@@ -0,0 +1,439 @@
/*---------------------------------------------------------------------------------------------
* 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 * as constants from '../constants/strings';
import { debounce, getPipelineStatusImage } from '../api/utils';
import * as styles from '../constants/styles';
import { IconPathHelper } from '../constants/iconPathHelper';
import { EOL } from 'os';
import { LoginMigrationStatusCodes } from '../constants/helper';
export class LoginMigrationStatusPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _migratingLoginsTable!: azdata.TableComponent;
private _loginCount!: azdata.TextComponent;
private _loginsTableValues!: any[];
private _disposables: vscode.Disposable[] = [];
private _progressLoaderContainer!: azdata.FlexContainer;
private _migrationProgress!: azdata.InfoBoxComponent;
private _progressLoader!: azdata.LoadingComponent;
private _progressContainer!: azdata.FlexContainer;
private _migrationProgressDetails!: azdata.TextComponent;
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.LOGIN_MIGRATIONS_STATUS_PAGE_TITLE), migrationStateModel);
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
height: '100%',
width: '100%'
}).component();
flex.addItem(await this.createRootContainer(view), { flex: '1 1 auto' });
this._disposables.push(this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await view.initializeModel(flex);
}
public async onPageEnter(): Promise<void> {
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
this.wizard.message = {
text: constants.LOGIN_MIGRATIONS_STATUS_PAGE_PREVIOUS_BUTTON_ERROR,
level: azdata.window.MessageLevel.Error
};
return false;
}
return true;
});
this.wizard.backButton.enabled = false;
this.wizard.backButton.hidden = true;
this.wizard.backButton.label = constants.LOGIN_MIGRATIONS_STATUS_PAGE_PREVIOUS_BUTTON_TITLE;
this.wizard.doneButton.enabled = false;
await this._loadMigratingLoginsList(this.migrationStateModel);
// load unfiltered table list
await this._filterTableList('');
var result = await this._runLoginMigrations();
if (!result) {
if (this.migrationStateModel._targetServerInstance) {
await this._migrationProgress.updateProperties({
'text': constants.LOGIN_MIGRATIONS_FAILED_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name),
'style': 'error',
});
} else {
await this._migrationProgress.updateProperties({
'text': constants.LOGIN_MIGRATIONS_FAILED,
'style': 'error',
});
}
this.wizard.message = {
text: constants.LOGIN_MIGRATIONS_FAILED,
level: azdata.window.MessageLevel.Error,
description: constants.LOGIN_MIGRATIONS_ERROR(this.migrationStateModel._loginMigrationsError.message),
};
this._progressLoader.loading = false;
}
await this._loadMigratingLoginsList(this.migrationStateModel);
await this._filterTableList('');
}
public async onPageLeave(): Promise<void> {
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
});
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private createMigrationProgressLoader(): azdata.FlexContainer {
this._progressLoader = this._view.modelBuilder.loadingComponent()
.withProps({
loadingText: constants.LOGIN_MIGRATION_IN_PROGRESS,
loadingCompletedText: constants.LOGIN_MIGRATIONS_COMPLETE,
loading: true,
CSSStyles: { 'margin-right': '20px' }
})
.component();
this._migrationProgress = this._view.modelBuilder.infoBox()
.withProps({
style: 'information',
text: constants.LOGIN_MIGRATION_IN_PROGRESS,
CSSStyles: {
...styles.PAGE_TITLE_CSS,
'margin-right': '20px',
'font-size': '13px',
'line-height': '126%'
}
}).component();
this._progressLoaderContainer = this._view.modelBuilder.flexContainer()
.withLayout({
height: '100%',
flexFlow: 'row',
alignItems: 'center'
}).component();
this._progressLoaderContainer.addItem(this._migrationProgress, { flex: '0 0 auto' });
this._progressLoaderContainer.addItem(this._progressLoader, { flex: '0 0 auto' });
return this._progressLoaderContainer;
}
private async createMigrationProgressDetails(): Promise<azdata.TextComponent> {
this._migrationProgressDetails = this._view.modelBuilder.text()
.withProps({
value: constants.STARTING_LOGIN_MIGRATION,
CSSStyles: {
...styles.BODY_CSS,
'width': '660px'
}
}).component();
return this._migrationProgressDetails;
}
private createSearchComponent(): azdata.DivContainer {
let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({
stopEnterPropagation: true,
placeHolder: constants.SEARCH,
width: 200
}).component();
this._disposables.push(
resourceSearchBox.onTextChanged(value => this._filterTableList(value)));
const searchContainer = this._view.modelBuilder.divContainer().withItems([resourceSearchBox]).withProps({
CSSStyles: {
'width': '200px',
'margin-top': '8px'
}
}).component();
return searchContainer;
}
@debounce(500)
private async _filterTableList(value: string): Promise<void> {
let tableRows = this._loginsTableValues;
if (this._loginsTableValues && value?.length > 0) {
tableRows = this._loginsTableValues
.filter(row => {
const searchText = value?.toLowerCase();
return row[0]?.toLowerCase()?.indexOf(searchText) > -1 // source login
|| row[1]?.toLowerCase()?.indexOf(searchText) > -1 // login type
|| row[2]?.toLowerCase()?.indexOf(searchText) > -1 // default database
|| row[3]?.title.toLowerCase()?.indexOf(searchText) > -1; // migration status
});
}
await this._migratingLoginsTable.updateProperty('data', tableRows);
await this.updateDisplayedLoginCount();
}
public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
await this._loadMigratingLoginsList(this.migrationStateModel);
this._progressContainer = this._view.modelBuilder.flexContainer()
.withLayout({ height: '100%', flexFlow: 'column' })
.withProps({ CSSStyles: { 'margin-bottom': '10px' } })
.component();
this._progressContainer.addItem(this.createMigrationProgressLoader(), { flex: '0 0 auto' });
this._progressContainer.addItem(await this.createMigrationProgressDetails(), { flex: '0 0 auto' });
this._loginCount = this._view.modelBuilder.text().withProps({
value: constants.NUMBER_LOGINS_MIGRATING(this._loginsTableValues.length, this._loginsTableValues.length),
CSSStyles: {
...styles.BODY_CSS,
'margin-top': '8px'
},
ariaLive: 'polite'
}).component();
const cssClass = 'no-borders';
this._migratingLoginsTable = this._view.modelBuilder.table()
.withProps({
data: [],
width: 650,
height: '100%',
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [
{
name: constants.SOURCE_LOGIN,
value: 'sourceLogin',
type: azdata.ColumnType.text,
width: 250,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.LOGIN_TYPE,
value: 'loginType',
type: azdata.ColumnType.text,
width: 90,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.DEFAULT_DATABASE,
value: 'defaultDatabase',
type: azdata.ColumnType.text,
width: 100,
cssClass: cssClass,
headerCssClass: cssClass,
},
<azdata.HyperlinkColumn>{
name: constants.LOGIN_MIGRATION_STATUS_COLUMN,
value: 'migrationStatus',
width: 120,
type: azdata.ColumnType.hyperlink,
icon: IconPathHelper.inProgressMigration,
showText: true,
cssClass: cssClass,
headerCssClass: cssClass,
},
]
}).component();
this._disposables.push(
this._migratingLoginsTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState;
switch (buttonState?.column) {
case 3:
const loginName = this._migratingLoginsTable!.data[rowState.row][0];
const status = this._migratingLoginsTable!.data[rowState.row][3].title;
const statusMessage = constants.LOGIN_MIGRATION_STATUS_LABEL(status);
var errors = [];
if (this.migrationStateModel._loginMigrationsResult?.exceptionMap) {
const exception_key = Object.keys(this.migrationStateModel._loginMigrationsResult.exceptionMap).find(key => key.toLocaleLowerCase() === loginName.toLocaleLowerCase());
if (exception_key) {
for (var exception of this.migrationStateModel._loginMigrationsResult.exceptionMap[exception_key]) {
if (Array.isArray(exception)) {
for (var inner_exception of exception) {
errors.push(inner_exception.Message);
}
} else {
errors.push(exception.Message);
}
}
}
}
const unique_errors = new Set(errors);
// TODO AKMA: Make errors prettier (spacing between errors is weird)
this.showDialogMessage(
constants.DATABASE_MIGRATION_STATUS_TITLE,
statusMessage,
[...unique_errors].join(EOL));
break;
}
}));
// load unfiltered table list
await this._filterTableList('');
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
height: '100%',
}).withProps({
CSSStyles: {
'margin': '0px 28px 0px 28px'
}
}).component();
flex.addItem(this._progressContainer, { flex: '0 0 auto' });
flex.addItem(this.createSearchComponent(), { flex: '0 0 auto' });
flex.addItem(this._loginCount, { flex: '0 0 auto' });
flex.addItem(this._migratingLoginsTable);
return flex;
}
private async _loadMigratingLoginsList(stateMachine: MigrationStateModel): Promise<void> {
const loginList = stateMachine._loginsForMigration || [];
loginList.sort((a, b) => a.loginName.localeCompare(b.loginName));
this._loginsTableValues = loginList.map(login => {
const loginName = login.loginName;
var status = LoginMigrationStatusCodes.InProgress;
var title = constants.LOGIN_MIGRATION_STATUS_IN_PROGRESS;
if (stateMachine._loginMigrationsError) {
status = LoginMigrationStatusCodes.Failed;
title = constants.LOGIN_MIGRATION_STATUS_FAILED;
} else if (stateMachine._loginMigrationsResult) {
status = LoginMigrationStatusCodes.Succeeded;
title = constants.LOGIN_MIGRATION_STATUS_SUCCEEDED;
var didLoginFail = Object.keys(stateMachine._loginMigrationsResult.exceptionMap).some(key => key.toLocaleLowerCase() === loginName.toLocaleLowerCase());
if (didLoginFail) {
status = LoginMigrationStatusCodes.Failed;
title = constants.LOGIN_MIGRATION_STATUS_FAILED;
}
}
return [
loginName,
login.loginType,
login.defaultDatabaseName,
<azdata.HyperlinkColumnCellValue>{
icon: getPipelineStatusImage(status),
title: title,
},
];
}) || [];
}
private _getTotalNumberOfLogins(): number {
return this._loginsTableValues?.length || 0;
}
private async updateDisplayedLoginCount() {
await this._loginCount.updateProperties({
'value': constants.NUMBER_LOGINS_MIGRATING(this._getNumberOfDisplayedLogins(), this._getTotalNumberOfLogins())
});
}
private _getNumberOfDisplayedLogins(): number {
return this._migratingLoginsTable?.data?.length || 0;
}
private async _runLoginMigrations(): Promise<Boolean> {
this._progressLoader.loading = true;
if (this.migrationStateModel._targetServerInstance) {
await this._migrationProgress.updateProperties({
'text': constants.LOGIN_MIGRATIONS_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name)
});
}
await this._migrationProgressDetails.updateProperties({
'value': constants.STARTING_LOGIN_MIGRATION
});
var result = await this.migrationStateModel.migrateLogins();
if (!result) {
await this._migrationProgressDetails.updateProperties({
'value': constants.STARTING_LOGIN_MIGRATION_FAILED
});
return false;
}
await this._migrationProgressDetails.updateProperties({
'value': constants.ESTABLISHING_USER_MAPPINGS
});
result = await this.migrationStateModel.establishUserMappings();
if (!result) {
await this._migrationProgressDetails.updateProperties({
'value': constants.ESTABLISHING_USER_MAPPINGS_FAILED
});
return false;
}
await this._migrationProgressDetails.updateProperties({
'value': constants.MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS
});
result = await this.migrationStateModel.migrateServerRolesAndSetPermissions();
if (!result) {
await this._migrationProgressDetails.updateProperties({
'value': constants.MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS_FAILED
});
return false;
}
await this._migrationProgressDetails.updateProperties({
'CSSStyles': { 'display': 'none' },
});
if (this.migrationStateModel._targetServerInstance) {
await this._migrationProgress.updateProperties({
'text': constants.LOGIN_MIGRATIONS_COMPLETED_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name),
'style': 'success',
});
} else {
await this._migrationProgress.updateProperties({
'text': constants.LOGIN_MIGRATIONS_COMPLETE,
'style': 'success',
});
}
this._progressLoader.loading = false;
this.wizard.doneButton.enabled = true;
return result;
}
}

View File

@@ -0,0 +1,974 @@
/*---------------------------------------------------------------------------------------------
* 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 { EOL } from 'os';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import * as styles from '../constants/styles';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import * as utils from '../api/utils';
import { azureResource } from 'azurecore';
import { AzureSqlDatabaseServer, SqlVMServer } from '../api/azure';
import { collectTargetDatabaseInfo, TargetDatabaseInfo, isSysAdmin } from '../api/sqlUtils';
export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _disposables: vscode.Disposable[] = [];
private _pageDescription!: azdata.TextComponent;
private _azureSqlTargetTypeDropdown!: azdata.DropDownComponent;
private _azureAccountsDropdown!: azdata.DropDownComponent;
private _accountTenantDropdown!: azdata.DropDownComponent;
private _accountTenantFlexContainer!: azdata.FlexContainer;
private _azureSubscriptionDropdown!: azdata.DropDownComponent;
private _azureLocationDropdown!: azdata.DropDownComponent;
private _azureResourceGroupLabel!: azdata.TextComponent;
private _azureResourceGroupDropdown!: azdata.DropDownComponent;
private _azureResourceDropdownLabel!: azdata.TextComponent;
private _azureResourceDropdown!: azdata.DropDownComponent;
private _resourceSelectionContainer!: azdata.FlexContainer;
private _resourceAuthenticationContainer!: azdata.FlexContainer;
private _targetUserNameInputBox!: azdata.InputBoxComponent;
private _targetPasswordInputBox!: azdata.InputBoxComponent;
private _testConectionButton!: azdata.ButtonComponent;
private _connectionResultsInfoBox!: azdata.InfoBoxComponent;
private _migrationTargetPlatform!: MigrationTargetType;
constructor(
wizard: azdata.window.Wizard,
migrationStateModel: MigrationStateModel) {
super(
wizard,
azdata.window.createWizardPage(constants.LOGIN_MIGRATIONS_AZURE_SQL_TARGET_PAGE_TITLE),
migrationStateModel);
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
const loginMigrationPreviewInfoBox = this._view.modelBuilder.infoBox()
.withProps({
style: 'information',
text: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_PREVIEW_WARNING,
CSSStyles: { ...styles.BODY_CSS }
}).component();
const loginMigrationInfoBox = this._view.modelBuilder.infoBox()
.withProps({
style: 'information',
text: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_DATA_MIGRATION_WARNING,
CSSStyles: { ...styles.BODY_CSS }
}).component();
const hasSysAdminPermissions: boolean = await isSysAdmin(this.migrationStateModel.sourceConnectionId);
const connectionProfile: azdata.connection.ConnectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
const permissionsInfoBox = this._view.modelBuilder.infoBox()
.withProps({
style: 'warning',
text: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_PERMISSIONS_WARNING(connectionProfile.userName, connectionProfile.serverName),
CSSStyles: { ...styles.BODY_CSS },
}).component();
if (hasSysAdminPermissions) {
await permissionsInfoBox.updateProperties({
'CSSStyles': { 'display': 'none' },
});
}
this._pageDescription = this._view.modelBuilder.text()
.withProps({
value: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_DESCRIPTION,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
const form = this._view.modelBuilder.formContainer()
.withFormItems([
{ component: loginMigrationPreviewInfoBox },
{ component: loginMigrationInfoBox },
{ component: permissionsInfoBox },
{ component: this._pageDescription },
{ component: this.createAzureSqlTargetTypeDropdown() },
{ component: this.createAzureAccountsDropdown() },
{ component: this.createAzureTenantContainer() },
{ component: this.createTargetDropdownContainer() }
]).withProps({
CSSStyles: { 'padding-top': '0' }
}).component();
this._disposables.push(
this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await this._view.initializeModel(form);
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.nextButton.enabled = false;
this.wizard.registerNavigationValidator((pageChangeInfo) => {
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
if (!this.migrationStateModel._targetServerInstance || !this.migrationStateModel._targetUserName || !this.migrationStateModel._targetPassword) {
this.wizard.message = {
text: constants.SELECT_DATABASE_TO_CONTINUE,
level: azdata.window.MessageLevel.Error
}; ``
return false;
}
return true;
});
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_MI_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
break;
case MigrationTargetType.SQLVM:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_VM_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
break;
case MigrationTargetType.SQLDB:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE;
this._updateConnectionButtonState();
break;
}
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
console.log(isSqlDbTarget);
if (this._targetUserNameInputBox) {
await this._targetUserNameInputBox.updateProperty('required', true);
}
if (this._targetPasswordInputBox) {
await this._targetPasswordInputBox.updateProperty('required', true);
}
await utils.updateControlDisplay(this._resourceAuthenticationContainer, true);
await this.populateAzureAccountsDropdown();
if (this._migrationTargetPlatform !== this.migrationStateModel._targetType) {
// if the user had previously selected values on this page, then went back to change the migration target platform
// and came back, forcibly reload the location/resource group/resource values since they will now be different
this._migrationTargetPlatform = this.migrationStateModel._targetType;
this._targetPasswordInputBox.value = '';
this.migrationStateModel._sqlMigrationServices = undefined!;
this.migrationStateModel._targetServerInstance = undefined!;
this.migrationStateModel._resourceGroup = undefined!;
this.migrationStateModel._location = undefined!;
await this.populateLocationDropdown();
}
if (this.migrationStateModel._didUpdateDatabasesForMigration) {
this._updateConnectionButtonState();
}
this.wizard.registerNavigationValidator((pageChangeInfo) => {
this.wizard.message = { text: '' };
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
const errors: string[] = [];
if (!this.migrationStateModel._azureAccount) {
errors.push(constants.INVALID_ACCOUNT_ERROR);
}
if (!this.migrationStateModel._targetSubscription ||
(<azdata.CategoryValue>this._azureSubscriptionDropdown.value)?.displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if (!this.migrationStateModel._location ||
(<azdata.CategoryValue>this._azureLocationDropdown.value)?.displayName === constants.NO_LOCATION_FOUND) {
errors.push(constants.INVALID_LOCATION_ERROR);
}
if (!this.migrationStateModel._resourceGroup ||
(<azdata.CategoryValue>this._azureResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
const resourceDropdownValue = (<azdata.CategoryValue>this._azureResourceDropdown.value)?.displayName;
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
const targetMi = this.migrationStateModel._targetServerInstance as azureResource.AzureSqlManagedInstance;
if (!targetMi || resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) {
errors.push(constants.INVALID_MANAGED_INSTANCE_ERROR);
}
if (targetMi.properties.state !== 'Ready') {
errors.push(constants.MI_NOT_READY_ERROR(targetMi.name, targetMi.properties.state));
}
break;
case MigrationTargetType.SQLVM:
const targetVm = this.migrationStateModel._targetServerInstance as SqlVMServer;
if (!targetVm || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) {
errors.push(constants.INVALID_VIRTUAL_MACHINE_ERROR);
}
break;
case MigrationTargetType.SQLDB:
const targetSqlDB = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer;
if (!targetSqlDB || resourceDropdownValue === constants.NO_SQL_DATABASE_FOUND) {
errors.push(constants.INVALID_SQL_DATABASE_ERROR);
}
// TODO: verify what state check is needed/possible?
if (targetSqlDB.properties.state !== 'Ready') {
errors.push(constants.SQLDB_NOT_READY_ERROR(targetSqlDB.name, targetSqlDB.properties.state));
}
// validate target sqldb username exists
const targetUsernameValue = this._targetUserNameInputBox.value ?? '';
if (targetUsernameValue.length < 1) {
errors.push(constants.MISSING_TARGET_USERNAME_ERROR);
}
// validate target sqldb password exists
const targetPasswordValue = this._targetPasswordInputBox.value ?? '';
if (targetPasswordValue.length < 1) {
errors.push(constants.MISSING_TARGET_PASSWORD_ERROR);
}
break;
}
if (errors.length > 0) {
this.wizard.message = {
text: errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
return false;
}
return true;
});
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator(async (pageChangeInfo) => true);
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private createAzureSqlTargetTypeDropdown(): azdata.FlexContainer {
const azureSqlTargetTypeLabel = this._view.modelBuilder.text().withProps({
value: constants.LOGIN_MIGRATIONS_TARGET_TYPE_SELECTION_TITLE,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS,
'margin-top': '-1em'
}
}).component();
this._azureSqlTargetTypeDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.LOGIN_MIGRATIONS_TARGET_TYPE_SELECTION_TITLE,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_AN_TARGET_TYPE,
CSSStyles: {
'margin-top': '-1em'
},
}).component();
this._azureSqlTargetTypeDropdown.values = [/* constants.LOGIN_MIGRATIONS_DB_TEXT, */ constants.LOGIN_MIGRATIONS_MI_TEXT, constants.LOGIN_MIGRATIONS_VM_TEXT];
this._disposables.push(this._azureSqlTargetTypeDropdown.onValueChanged(async (value) => {
switch (value) {
case constants.LOGIN_MIGRATIONS_DB_TEXT: {
this.migrationStateModel._targetType = MigrationTargetType.SQLDB;
break;
}
case constants.LOGIN_MIGRATIONS_MI_TEXT: {
this.migrationStateModel._targetType = MigrationTargetType.SQLMI;
break;
}
case constants.LOGIN_MIGRATIONS_VM_TEXT: {
this.migrationStateModel._targetType = MigrationTargetType.SQLVM;
break;
}
}
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_MI_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
break;
case MigrationTargetType.SQLVM:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_VM_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
break;
case MigrationTargetType.SQLDB:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE;
this._updateConnectionButtonState();
break;
}
await this.populateAzureAccountsDropdown();
await this.populateSubscriptionDropdown();
await this.populateLocationDropdown();
console.log(this.migrationStateModel._targetType);
}));
const flexContainer = this._view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column'
})
.withItems([
azureSqlTargetTypeLabel,
this._azureSqlTargetTypeDropdown
])
.component();
return flexContainer;
}
private createAzureAccountsDropdown(): azdata.FlexContainer {
const azureAccountLabel = this._view.modelBuilder.text()
.withProps({
value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' }
}).component();
this._azureAccountsDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_AN_ACCOUNT,
CSSStyles: { 'margin-top': '-1em' },
}).component();
this._disposables.push(
this._azureAccountsDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined') {
const selectedAccount = this.migrationStateModel._azureAccounts.find(account => account.displayInfo.displayName === value);
this.migrationStateModel._azureAccount = (selectedAccount)
? utils.deepClone(selectedAccount)!
: undefined!;
}
await this.populateTenantsDropdown();
}));
const linkAccountButton = this._view.modelBuilder.hyperlink()
.withProps({
label: constants.ACCOUNT_LINK_BUTTON_LABEL,
url: '',
CSSStyles: { ...styles.BODY_CSS }
})
.component();
this._disposables.push(
linkAccountButton.onDidClick(async (event) => {
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
await this.populateAzureAccountsDropdown();
this.wizard.message = { text: '' };
await this._azureAccountsDropdown.validate();
}));
return this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
azureAccountLabel,
this._azureAccountsDropdown,
linkAccountButton])
.component();
}
private createAzureTenantContainer(): azdata.FlexContainer {
const azureTenantDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.AZURE_TENANT,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
this._accountTenantDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.AZURE_TENANT,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_TENANT
}).component();
this._disposables.push(
this._accountTenantDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined') {
/**
* Replacing all the tenants in azure account with the tenant user has selected.
* All azure requests will only run on this tenant from now on
*/
const selectedTenant = this.migrationStateModel._accountTenants.find(tenant => tenant.displayName === value);
if (selectedTenant) {
this.migrationStateModel._azureTenant = utils.deepClone(selectedTenant)!;
this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel._azureTenant];
}
}
await this.populateSubscriptionDropdown();
}));
this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
azureTenantDropdownLabel,
this._accountTenantDropdown])
.withProps({ CSSStyles: { 'display': 'none' } })
.component();
return this._accountTenantFlexContainer;
}
private createTargetDropdownContainer(): azdata.FlexContainer {
const subscriptionDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SUBSCRIPTION,
description: constants.TARGET_SUBSCRIPTION_INFO,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS, }
}).component();
this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.SUBSCRIPTION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_SUBSCRIPTION,
CSSStyles: { 'margin-top': '-1em' },
}).component();
this._disposables.push(
this._azureSubscriptionDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined' && value !== constants.NO_SUBSCRIPTIONS_FOUND) {
const selectedSubscription = this.migrationStateModel._subscriptions.find(subscription => `${subscription.name} - ${subscription.id}` === value);
this.migrationStateModel._targetSubscription = (selectedSubscription)
? utils.deepClone(selectedSubscription)!
: undefined!;
this.migrationStateModel.refreshDatabaseBackupPage = true;
}
await this.populateLocationDropdown();
}));
const azureLocationLabel = this._view.modelBuilder.text()
.withProps({
value: constants.LOCATION,
description: constants.TARGET_LOCATION_INFO,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
this._azureLocationDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.LOCATION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_LOCATION,
CSSStyles: { 'margin-top': '-1em' },
}).component();
this._disposables.push(
this._azureLocationDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined' && value !== constants.NO_LOCATION_FOUND) {
const selectedLocation = this.migrationStateModel._locations.find(location => location.displayName === value);
this.migrationStateModel._location = (selectedLocation)
? utils.deepClone(selectedLocation)!
: undefined!;
}
this.migrationStateModel.refreshDatabaseBackupPage = true;
await this.populateResourceGroupDropdown();
}));
this._resourceSelectionContainer = this._createResourceDropdowns();
this._resourceAuthenticationContainer = this._createResourceAuthenticationContainer();
return this._view.modelBuilder.flexContainer()
.withItems([
subscriptionDropdownLabel,
this._azureSubscriptionDropdown,
azureLocationLabel,
this._azureLocationDropdown,
this._resourceSelectionContainer,
this._resourceAuthenticationContainer])
.withLayout({ flexFlow: 'column' })
.component();
}
private _createResourceAuthenticationContainer(): azdata.FlexContainer {
// target user name
const targetUserNameLabel = this._view.modelBuilder.text()
.withProps({
value: constants.TARGET_USERNAME_LAbEL,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' }
}).component();
this._targetUserNameInputBox = this._view.modelBuilder.inputBox()
.withProps({
width: '300px',
inputType: 'text',
placeHolder: constants.TARGET_USERNAME_PLACEHOLDER,
required: false,
CSSStyles: { 'margin-top': '-1em' },
}).component();
this._disposables.push(
this._targetUserNameInputBox.onTextChanged(
(value: string) => {
this.migrationStateModel._targetUserName = value ?? '';
this._updateConnectionButtonState();
}));
this._disposables.push(
this._targetUserNameInputBox.onValidityChanged(
valid => this._updateConnectionButtonState()));
// target password
const targetPasswordLabel = this._view.modelBuilder.text()
.withProps({
value: constants.TARGET_PASSWORD_LAbEL,
requiredIndicator: true,
title: '',
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' }
}).component();
this._targetPasswordInputBox = this._view.modelBuilder.inputBox()
.withProps({
width: '300px',
inputType: 'password',
placeHolder: constants.TARGET_PASSWORD_PLACEHOLDER,
required: false,
CSSStyles: { 'margin-top': '-1em' },
}).component();
this._disposables.push(
this._targetPasswordInputBox.onTextChanged(
(value: string) => {
this.migrationStateModel._targetPassword = value ?? '';
this._updateConnectionButtonState();
}));
this._disposables.push(
this._targetPasswordInputBox.onValidityChanged(
valid => this._updateConnectionButtonState()));
// test connection button
this._testConectionButton = this._view.modelBuilder.button()
.withProps({
enabled: false,
label: constants.TARGET_CONNECTION_LABEL,
width: '80px',
}).component();
this._connectionResultsInfoBox = this._view.modelBuilder.infoBox()
.withProps({
style: 'success',
text: '',
announceText: true,
CSSStyles: { 'display': 'none' },
})
.component();
const connectionButtonLoadingContainer = this._view.modelBuilder.loadingComponent()
.withItem(this._testConectionButton)
.withProps({ loading: false })
.component();
this._disposables.push(
this._testConectionButton.onDidClick(async (value) => {
this.wizard.message = { text: '' };
const targetDatabaseServer = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer;
const userName = this.migrationStateModel._targetUserName;
const password = this.migrationStateModel._targetPassword;
const targetDatabases: TargetDatabaseInfo[] = [];
if (targetDatabaseServer && userName && password) {
try {
connectionButtonLoadingContainer.loading = true;
await utils.updateControlDisplay(this._connectionResultsInfoBox, false);
this.wizard.nextButton.enabled = false;
targetDatabases.push(
...await collectTargetDatabaseInfo(
targetDatabaseServer,
userName,
password));
await this._showConnectionResults(targetDatabases);
} catch (error) {
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE,
description: constants.SQL_TARGET_CONNECTION_ERROR(error.message),
};
await this._showConnectionResults(
targetDatabases,
constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE);
}
finally {
connectionButtonLoadingContainer.loading = false;
}
}
}));
const connectionContainer = this._view.modelBuilder.flexContainer()
.withItems([
connectionButtonLoadingContainer,
this._connectionResultsInfoBox],
{ flex: '0 0 auto' })
.withLayout({
flexFlow: 'row',
alignContent: 'center',
alignItems: 'center',
})
.withProps({ CSSStyles: { 'margin': '15px 0 0 0' } })
.component();
return this._view.modelBuilder.flexContainer()
.withItems([
targetUserNameLabel,
this._targetUserNameInputBox,
targetPasswordLabel,
this._targetPasswordInputBox,
connectionContainer])
.withLayout({ flexFlow: 'column' })
.withProps({ CSSStyles: { 'margin': '15px 0 0 0' } })
.component();
}
private async _showConnectionResults(
databases: TargetDatabaseInfo[],
errorMessage?: string): Promise<void> {
const hasError = errorMessage !== undefined;
const hasDatabases = databases.length > 0;
this._connectionResultsInfoBox.style = hasError
? 'error'
: hasDatabases
? 'success'
: 'warning';
this._connectionResultsInfoBox.text = hasError
? constants.SQL_TARGET_CONNECTION_ERROR(errorMessage)
: constants.SQL_TARGET_CONNECTION_SUCCESS_LOGINS(databases.length.toLocaleString());
await utils.updateControlDisplay(this._connectionResultsInfoBox, true);
if (!hasError) {
this.wizard.nextButton.enabled = true;
}
}
private _createResourceDropdowns(): azdata.FlexContainer {
this._azureResourceGroupLabel = this._view.modelBuilder.text()
.withProps({
value: constants.RESOURCE_GROUP,
description: constants.TARGET_RESOURCE_GROUP_INFO,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.RESOURCE_GROUP,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_RESOURCE_GROUP,
CSSStyles: { 'margin-top': '-1em' },
}).component();
this._disposables.push(
this._azureResourceGroupDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) {
const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value);
this.migrationStateModel._resourceGroup = (selectedResourceGroup)
? utils.deepClone(selectedResourceGroup)!
: undefined!;
}
await this.populateResourceInstanceDropdown();
}));
this._azureResourceDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
description: constants.TARGET_RESOURCE_INFO,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
this._azureResourceDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_SERVICE_PLACEHOLDER,
CSSStyles: { 'margin-top': '-1em' },
loading: false,
}).component();
this._disposables.push(
this._azureResourceDropdown.onValueChanged(async (value) => {
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
if (value && value !== 'undefined' &&
value !== constants.NO_MANAGED_INSTANCE_FOUND &&
value !== constants.NO_SQL_DATABASE_SERVER_FOUND &&
value !== constants.NO_VIRTUAL_MACHINE_FOUND) {
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLVM:
const selectedVm = this.migrationStateModel._targetSqlVirtualMachines.find(vm => vm.name === value);
if (selectedVm) {
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer;
}
break;
case MigrationTargetType.SQLMI:
const selectedMi = this.migrationStateModel._targetManagedInstances
.find(mi => mi.name === value
|| constants.UNAVAILABLE_TARGET_PREFIX(mi.name) === value);
if (selectedMi) {
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance;
this.wizard.message = { text: '' };
if (this.migrationStateModel._targetServerInstance.properties.state !== 'Ready') {
this.wizard.message = {
text: constants.MI_NOT_READY_ERROR(
this.migrationStateModel._targetServerInstance.name,
this.migrationStateModel._targetServerInstance.properties.state),
level: azdata.window.MessageLevel.Error
};
}
}
break;
case MigrationTargetType.SQLDB:
const sqlDatabaseServer = this.migrationStateModel._targetSqlDatabaseServers.find(
sqldb => sqldb.name === value || constants.UNAVAILABLE_TARGET_PREFIX(sqldb.name) === value);
if (sqlDatabaseServer) {
this.migrationStateModel._targetServerInstance = utils.deepClone(sqlDatabaseServer)! as AzureSqlDatabaseServer;
this.wizard.message = { text: '' };
if (this.migrationStateModel._targetServerInstance.properties.state === 'Ready') {
this._targetUserNameInputBox.value = this.migrationStateModel._targetServerInstance.properties.administratorLogin;
} else {
this.wizard.message = {
text: constants.SQLDB_NOT_READY_ERROR(
this.migrationStateModel._targetServerInstance.name,
this.migrationStateModel._targetServerInstance.properties.state),
level: azdata.window.MessageLevel.Error
};
}
}
break;
}
} else {
this.migrationStateModel._targetServerInstance = undefined!;
if (isSqlDbTarget) {
this._targetUserNameInputBox.value = '';
}
}
this.migrationStateModel._sqlMigrationServices = undefined!;
if (isSqlDbTarget) {
this._targetPasswordInputBox.value = '';
this._updateConnectionButtonState();
}
}));
return this._view.modelBuilder.flexContainer()
.withItems([
this._azureResourceGroupLabel,
this._azureResourceGroupDropdown,
this._azureResourceDropdownLabel,
this._azureResourceDropdown])
.withLayout({ flexFlow: 'column' })
.component();
}
private _updateConnectionButtonState(): void {
const targetDatabaseServer = (this._azureResourceDropdown.value as azdata.CategoryValue)?.name ?? '';
const userName = this._targetUserNameInputBox.value ?? '';
const password = this._targetPasswordInputBox.value ?? '';
this._testConectionButton.enabled = targetDatabaseServer.length > 0
&& userName.length > 0
&& password.length > 0;
}
private async populateAzureAccountsDropdown(): Promise<void> {
try {
this._azureAccountsDropdown.loading = true;
this.migrationStateModel._azureAccounts = await utils.getAzureAccounts();
this._azureAccountsDropdown.values = await utils.getAzureAccountsDropdownValues(this.migrationStateModel._azureAccounts);
} finally {
this._azureAccountsDropdown.loading = false;
utils.selectDefaultDropdownValue(
this._azureAccountsDropdown,
this.migrationStateModel._azureAccount?.displayInfo?.userId,
false);
}
}
private async populateTenantsDropdown(): Promise<void> {
try {
this._accountTenantDropdown.loading = true;
if (this.migrationStateModel._azureAccount && this.migrationStateModel._azureAccount.isStale === false && this.migrationStateModel._azureAccount.properties.tenants.length > 0) {
this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount);
this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants);
}
utils.selectDefaultDropdownValue(
this._accountTenantDropdown,
this.migrationStateModel._azureTenant?.id,
true);
await this._accountTenantFlexContainer.updateCssStyles(this.migrationStateModel._azureAccount.properties.tenants.length > 1
? { 'display': 'inline' }
: { 'display': 'none' }
);
await this._azureAccountsDropdown.validate();
} finally {
this._accountTenantDropdown.loading = false;
}
}
private async populateSubscriptionDropdown(): Promise<void> {
try {
this._azureSubscriptionDropdown.loading = true;
this.migrationStateModel._subscriptions = await utils.getAzureSubscriptions(this.migrationStateModel._azureAccount);
this._azureSubscriptionDropdown.values = await utils.getAzureSubscriptionsDropdownValues(this.migrationStateModel._subscriptions);
} catch (e) {
console.log(e);
} finally {
this._azureSubscriptionDropdown.loading = false;
utils.selectDefaultDropdownValue(
this._azureSubscriptionDropdown,
this.migrationStateModel._targetSubscription?.id,
false);
}
}
public async populateLocationDropdown(): Promise<void> {
try {
this._azureLocationDropdown.loading = true;
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
this.migrationStateModel._targetManagedInstances = await utils.getManagedInstances(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription);
this.migrationStateModel._locations = await utils.getResourceLocations(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription,
this.migrationStateModel._targetManagedInstances);
break;
case MigrationTargetType.SQLVM:
this.migrationStateModel._targetSqlVirtualMachines = await utils.getVirtualMachines(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription);
this.migrationStateModel._locations = await utils.getResourceLocations(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription,
this.migrationStateModel._targetSqlVirtualMachines);
break;
case MigrationTargetType.SQLDB:
this.migrationStateModel._targetSqlDatabaseServers = await utils.getAzureSqlDatabaseServers(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription);
this.migrationStateModel._locations = await utils.getResourceLocations(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription,
this.migrationStateModel._targetSqlDatabaseServers);
break;
}
this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations);
} catch (e) {
console.log(e);
} finally {
this._azureLocationDropdown.loading = false;
utils.selectDefaultDropdownValue(
this._azureLocationDropdown,
this.migrationStateModel._location?.displayName,
true);
}
}
public async populateResourceGroupDropdown(): Promise<void> {
try {
this._azureResourceGroupDropdown.loading = true;
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
this.migrationStateModel._targetManagedInstances,
this.migrationStateModel._location);
break;
case MigrationTargetType.SQLVM:
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
this.migrationStateModel._targetSqlVirtualMachines,
this.migrationStateModel._location);
break;
case MigrationTargetType.SQLDB:
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
this.migrationStateModel._targetSqlDatabaseServers,
this.migrationStateModel._location);
break;
}
this._azureResourceGroupDropdown.values = utils.getResourceDropdownValues(
this.migrationStateModel._resourceGroups,
constants.RESOURCE_GROUP_NOT_FOUND);
} catch (e) {
console.log(e);
} finally {
this._azureResourceGroupDropdown.loading = false;
utils.selectDefaultDropdownValue(
this._azureResourceGroupDropdown,
this.migrationStateModel._resourceGroup?.id,
false);
}
}
private async populateResourceInstanceDropdown(): Promise<void> {
try {
this._azureResourceDropdown.loading = true;
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
this._azureResourceDropdown.values = await utils.getManagedInstancesDropdownValues(
this.migrationStateModel._targetManagedInstances,
this.migrationStateModel._location,
this.migrationStateModel._resourceGroup);
break;
case MigrationTargetType.SQLVM:
this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(
this.migrationStateModel._targetSqlVirtualMachines,
this.migrationStateModel._location,
this.migrationStateModel._resourceGroup?.name,
constants.NO_VIRTUAL_MACHINE_FOUND);
break;
case MigrationTargetType.SQLDB:
this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(
this.migrationStateModel._targetSqlDatabaseServers,
this.migrationStateModel._location,
this.migrationStateModel._resourceGroup?.name,
constants.NO_SQL_DATABASE_SERVER_FOUND);
break;
}
} finally {
this._azureResourceDropdown.loading = false;
utils.selectDefaultDropdownValue(
this._azureResourceDropdown,
this.migrationStateModel._targetServerInstance?.name,
true);
}
}
}

View File

@@ -0,0 +1,412 @@
/*---------------------------------------------------------------------------------------------
* 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 * as constants from '../constants/strings';
import { debounce, getLoginStatusImage, getLoginStatusMessage } from '../api/utils';
import * as styles from '../constants/styles';
import { collectSourceLogins, collectTargetLogins, LoginTableInfo } from '../api/sqlUtils';
import { AzureSqlDatabaseServer } from '../api/azure';
import { IconPathHelper } from '../constants/iconPathHelper';
import * as utils from '../api/utils';
export class LoginSelectorPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _loginSelectorTable!: azdata.TableComponent;
private _loginNames!: string[];
private _loginCount!: azdata.TextComponent;
private _loginTableValues!: any[];
private _disposables: vscode.Disposable[] = [];
private _isCurrentPage: boolean;
private _refreshResultsInfoBox!: azdata.InfoBoxComponent;
private _refreshButton!: azdata.ButtonComponent;
private _refreshLoading!: azdata.LoadingComponent;
private _filterTableValue!: string;
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.LOGIN_MIGRATIONS_SELECT_LOGINS_PAGE_TITLE), migrationStateModel);
this._isCurrentPage = false;
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
height: '100%',
width: '100%'
}).component();
flex.addItem(await this.createRootContainer(view), { flex: '1 1 auto' });
this._disposables.push(this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await view.initializeModel(flex);
}
public async onPageEnter(): Promise<void> {
this._isCurrentPage = true;
this.updateNextButton();
this.wizard.registerNavigationValidator((pageChangeInfo) => {
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
if (this.selectedLogins().length === 0) {
this.wizard.message = {
text: constants.SELECT_LOGIN_TO_CONTINUE,
level: azdata.window.MessageLevel.Error
};
return false;
}
return true;
});
await this._loadLoginList();
// load unfiltered table list and pre-select list of logins saved in state
await this._filterTableList('', this.migrationStateModel._loginsForMigration);
}
public async onPageLeave(): Promise<void> {
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
});
this._isCurrentPage = false;
this.resetNextButton();
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private createSearchComponent(): azdata.DivContainer {
let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({
stopEnterPropagation: true,
placeHolder: constants.SEARCH,
width: 200
}).component();
this._disposables.push(
resourceSearchBox.onTextChanged(value => this._filterTableList(value, this.migrationStateModel._loginsForMigration || [])));
const searchContainer = this._view.modelBuilder.divContainer().withItems([resourceSearchBox]).withProps({
CSSStyles: {
'width': '200px',
'margin-top': '8px'
}
}).component();
return searchContainer;
}
@debounce(500)
private async _filterTableList(value: string, selectedList?: LoginTableInfo[]): Promise<void> {
this._filterTableValue = value;
const selectedRows: number[] = [];
const selectedLogins = selectedList || this.selectedLogins();
let tableRows = this._loginTableValues ?? [];
if (this._loginTableValues && value?.length > 0) {
tableRows = this._loginTableValues
.filter(row => {
const searchText = value?.toLowerCase();
return row[1]?.toLowerCase()?.indexOf(searchText) > -1 // source login
|| row[2]?.toLowerCase()?.indexOf(searchText) > -1 // login type
|| row[3]?.toLowerCase()?.indexOf(searchText) > -1 // default database
|| row[4]?.toLowerCase()?.indexOf(searchText) > -1 // status
|| row[5]?.title?.toLowerCase()?.indexOf(searchText) > -1; // target status
});
}
for (let rowIdx = 0; rowIdx < tableRows.length; rowIdx++) {
const login: string = tableRows[rowIdx][1];
if (selectedLogins.some(selectedLogin => selectedLogin.loginName.toLowerCase() === login.toLowerCase())) {
selectedRows.push(rowIdx);
}
}
await this._loginSelectorTable.updateProperty('data', tableRows);
this._loginSelectorTable.selectedRows = selectedRows;
await this.updateValuesOnSelection();
}
public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
const windowsAuthInfoBox = this._view.modelBuilder.infoBox()
.withProps({
style: 'information',
text: constants.LOGIN_MIGRATIONS_SELECT_LOGINS_WINDOWS_AUTH_WARNING,
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._refreshButton = this._view.modelBuilder.button()
.withProps({
buttonType: azdata.ButtonType.Normal,
iconHeight: 16,
iconWidth: 16,
iconPath: IconPathHelper.refresh,
label: constants.DATABASE_TABLE_REFRESH_LABEL,
width: 70,
CSSStyles: { 'margin': '15px 0 0 0' },
})
.component();
this._disposables.push(
this._refreshButton.onDidClick(
async e => await this._loadLoginList()));
this._refreshLoading = this._view.modelBuilder.loadingComponent()
.withItem(this._refreshButton)
.withProps({
loading: false,
CSSStyles: { 'margin-right': '20px', 'margin-top': '15px' }
})
.component();
this._refreshResultsInfoBox = this._view.modelBuilder.infoBox()
.withProps({
style: 'success',
text: '',
announceText: true,
CSSStyles: { 'display': 'none', 'margin-left': '5px' },
})
.component();
const refreshContainer = this._view.modelBuilder.flexContainer()
.withItems([
this._refreshLoading,
this._refreshResultsInfoBox],
{ flex: '0 0 auto' })
.withLayout({
flexFlow: 'row',
alignContent: 'center',
alignItems: 'center',
})
.component();
await this._loadLoginList();
this._loginCount = this._view.modelBuilder.text().withProps({
value: constants.LOGINS_SELECTED(
this.selectedLogins().length,
this._loginTableValues.length),
CSSStyles: {
...styles.BODY_CSS,
'margin-top': '8px'
},
ariaLive: 'polite'
}).component();
const cssClass = 'no-borders';
this._loginSelectorTable = this._view.modelBuilder.table()
.withProps({
data: [],
width: 650,
height: '100%',
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [
<azdata.CheckboxColumn>{
value: '',
width: 10,
type: azdata.ColumnType.checkBox,
action: azdata.ActionOnCellCheckboxCheck.selectRow,
resizable: false,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.SOURCE_LOGIN,
value: 'sourceLogin',
type: azdata.ColumnType.text,
width: 250,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.LOGIN_TYPE,
value: 'loginType',
type: azdata.ColumnType.text,
width: 90,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.DEFAULT_DATABASE,
value: 'defaultDatabase',
type: azdata.ColumnType.text,
width: 130,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.LOGIN_STATUS_COLUMN,
value: 'status',
type: azdata.ColumnType.text,
width: 90,
cssClass: cssClass,
headerCssClass: cssClass,
},
<azdata.HyperlinkColumn>{
name: constants.LOGIN_TARGET_STATUS_COLUMN,
value: 'targetStatus',
width: 150,
type: azdata.ColumnType.hyperlink,
icon: IconPathHelper.inProgressMigration,
showText: true,
cssClass: cssClass,
headerCssClass: cssClass,
},
]
}).component();
this._disposables.push(this._loginSelectorTable.onRowSelected(async (e) => {
await this.updateValuesOnSelection();
}));
// load unfiltered table list and pre-select list of logins saved in state
await this._filterTableList('', this.migrationStateModel._loginsForMigration);
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
height: '100%',
}).withProps({
CSSStyles: {
'margin': '0px 28px 0px 28px'
}
}).component();
flex.addItem(windowsAuthInfoBox, { flex: '0 0 auto' });
flex.addItem(refreshContainer, { flex: '0 0 auto' });
flex.addItem(this.createSearchComponent(), { flex: '0 0 auto' });
flex.addItem(this._loginCount, { flex: '0 0 auto' });
flex.addItem(this._loginSelectorTable);
return flex;
}
private async _loadLoginList(): Promise<void> {
this._refreshLoading.loading = true;
this.wizard.nextButton.enabled = false;
await utils.updateControlDisplay(this._refreshResultsInfoBox, true);
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESHING_LOGIN_DATA;
this._refreshResultsInfoBox.style = 'information';
const stateMachine: MigrationStateModel = this.migrationStateModel;
const selectedLogins: LoginTableInfo[] = stateMachine._loginsForMigration || [];
const sourceLogins: LoginTableInfo[] = [];
const targetLogins: string[] = [];
// execute a query against the source to get the logins
try {
sourceLogins.push(...await collectSourceLogins(stateMachine.sourceConnectionId));
} catch (error) {
this._refreshLoading.loading = false;
this._refreshResultsInfoBox.style = 'error';
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESH_SOURCE_LOGIN_DATA_FAILED;
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('source'),
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
};
}
// execute a query against the target to get the logins
try {
if (this.isTargetInstanceSet()) {
targetLogins.push(...await collectTargetLogins(stateMachine._targetServerInstance as AzureSqlDatabaseServer, stateMachine._targetUserName, stateMachine._targetPassword));
}
else {
// TODO AKMA : Emit telemetry here saying target info is empty
}
} catch (error) {
this._refreshLoading.loading = false;
this._refreshResultsInfoBox.style = 'error';
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESH_TARGET_LOGIN_DATA_FAILED;
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('target'),
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
};
}
this._loginNames = [];
this._loginTableValues = sourceLogins.map(row => {
const loginName = row.loginName;
this._loginNames.push(loginName);
const isLoginOnTarget = targetLogins.some(targetLogin => targetLogin.toLowerCase() === loginName.toLowerCase());
return [
selectedLogins?.some(selectedLogin => selectedLogin.loginName.toLowerCase() === loginName.toLowerCase()),
loginName,
row.loginType,
row.defaultDatabaseName,
row.status,
<azdata.HyperlinkColumnCellValue>{
icon: getLoginStatusImage(isLoginOnTarget),
title: getLoginStatusMessage(isLoginOnTarget),
},
];
}) || [];
await this._filterTableList(this._filterTableValue);
this._refreshLoading.loading = false;
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESH_LOGIN_DATA_SUCCESSFUL(sourceLogins.length, targetLogins.length);
this._refreshResultsInfoBox.style = 'success';
this.updateNextButton();
}
public selectedLogins(): LoginTableInfo[] {
const rows = this._loginSelectorTable?.data || [];
const logins = this._loginSelectorTable?.selectedRows || [];
return logins
.filter(rowIdx => rowIdx < rows.length)
.map(rowIdx => {
return {
loginName: rows[rowIdx][1],
loginType: rows[rowIdx][2],
defaultDatabaseName: rows[rowIdx][3],
status: rows[rowIdx][4].title,
};
})
|| [];
}
private async updateValuesOnSelection() {
const selectedLogins = this.selectedLogins() || [];
await this._loginCount.updateProperties({
'value': constants.LOGINS_SELECTED(
selectedLogins.length,
this._loginSelectorTable.data?.length || 0)
});
this.migrationStateModel._loginsForMigration = selectedLogins;
this.migrationStateModel._aadDomainName = "";
this.updateNextButton();
}
private updateNextButton() {
// Only uppdate next label if we are currently on this page
if (this._isCurrentPage) {
this.wizard.nextButton.label = constants.LOGIN_MIGRATE_BUTTON_TEXT;
this.wizard.nextButton.enabled = this.migrationStateModel?._loginsForMigration?.length > 0;
}
}
private resetNextButton() {
this.wizard.nextButton.label = constants.NEXT_LABEL;
this.wizard.nextButton.enabled = true;
}
private isTargetInstanceSet() {
const stateMachine: MigrationStateModel = this.migrationStateModel;
return stateMachine._targetServerInstance && stateMachine._targetUserName && stateMachine._targetPassword;
}
}

View File

@@ -11,9 +11,12 @@ import { MigrationWizardPage } from '../models/migrationWizardPage';
import { SKURecommendationPage } from './skuRecommendationPage';
import { DatabaseBackupPage } from './databaseBackupPage';
import { TargetSelectionPage } from './targetSelectionPage';
import { LoginMigrationTargetSelectionPage } from './loginMigrationTargetSelectionPage';
import { IntergrationRuntimePage } from './integrationRuntimePage';
import { SummaryPage } from './summaryPage';
import { LoginMigrationStatusPage } from './loginMigrationStatusPage';
import { DatabaseSelectorPage } from './databaseSelectorPage';
import { LoginSelectorPage } from './loginSelectorPage';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
import * as styles from '../constants/styles';
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
@@ -38,6 +41,14 @@ export class WizardController {
}
}
public async openLoginWizard(connectionId: string): Promise<void> {
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
if (api) {
this.extensionContext.subscriptions.push(this._model);
await this.createLoginWizard(this._model);
}
}
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
const serverName = (await stateModel.getSourceConnectionProfile()).serverName;
this._wizardObject = azdata.window.createWizard(
@@ -176,6 +187,66 @@ export class WizardController {
}));
}
private async createLoginWizard(stateModel: MigrationStateModel): Promise<void> {
const serverName = (await stateModel.getSourceConnectionProfile()).serverName;
this._wizardObject = azdata.window.createWizard(
loc.LOGIN_WIZARD_TITLE(serverName),
'LoginMigrationWizard',
'wide');
this._wizardObject.generateScriptButton.enabled = false;
this._wizardObject.generateScriptButton.hidden = true;
const targetSelectionPage = new LoginMigrationTargetSelectionPage(this._wizardObject, stateModel);
const loginSelectorPage = new LoginSelectorPage(this._wizardObject, stateModel);
const migrationStatusPage = new LoginMigrationStatusPage(this._wizardObject, stateModel);
const pages: MigrationWizardPage[] = [
targetSelectionPage,
loginSelectorPage,
migrationStatusPage
];
this._wizardObject.pages = pages.map(p => p.getwizardPage());
const wizardSetupPromises: Thenable<void>[] = [];
wizardSetupPromises.push(...pages.map(p => p.registerWizardContent()));
wizardSetupPromises.push(this._wizardObject.open());
this._model.extensionContext.subscriptions.push(
this._wizardObject.onPageChanged(
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
const newPage = pageChangeInfo.newPage;
const lastPage = pageChangeInfo.lastPage;
this.sendPageButtonClickEvent(pageChangeInfo)
.catch(e => logError(
TelemetryViews.LoginMigrationWizardController,
'ErrorSendingPageButtonClick', e));
await pages[lastPage]?.onPageLeave(pageChangeInfo);
await pages[newPage]?.onPageEnter(pageChangeInfo);
}));
this._wizardObject.registerNavigationValidator(async validator => {
return true;
});
await Promise.all(wizardSetupPromises);
this._disposables.push(
this._wizardObject.cancelButton.onClick(e => {
// TODO AKMA: add dialog prompting confirmation of cancel if migration is in progress
sendSqlMigrationActionEvent(
TelemetryViews.LoginMigrationWizard,
TelemetryAction.PageButtonClick,
{
...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Cancel,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
},
{});
}));
}
private async updateServiceContext(
stateModel: MigrationStateModel,
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {