mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 09:35:37 -05:00
Add new wizard for login migrations experience (#21317)
This commit is contained in:
439
extensions/sql-migration/src/wizard/loginMigrationStatusPage.ts
Normal file
439
extensions/sql-migration/src/wizard/loginMigrationStatusPage.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
412
extensions/sql-migration/src/wizard/loginSelectorPage.ts
Normal file
412
extensions/sql-migration/src/wizard/loginSelectorPage.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user