mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-07 01:25:38 -05:00
[SQL-Migration] Login Migration Improvements (#21694)
This PR adds various login migration improvements: - Enabled windows login by prompting user for AAD domain name if a windows login is selected image - Adds new login details dialog which gives granular status on each step of the login migration for each login image - Checks if windows login migration is supported for selected target type, and only collections source windows logins accordingly - Perf optimization by source and target login in background of step 1 in order to significantly speed up loading of page 2
This commit is contained in:
@@ -11,8 +11,8 @@ 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';
|
||||
import { MultiStepStatusDialog } from '../dialog/generic/multiStepStatusDialog';
|
||||
|
||||
export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -268,32 +268,7 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
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));
|
||||
await this._showLoginDetailsDialog(loginName);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
@@ -402,14 +377,14 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS
|
||||
'value': constants.MIGRATING_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
|
||||
'value': constants.MIGRATING_SERVER_ROLES_AND_SET_PERMISSIONS_FAILED
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -436,4 +411,15 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
this.wizard.doneButton.enabled = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _showLoginDetailsDialog(loginName: string): Promise<void> {
|
||||
this.wizard.message = { text: '' };
|
||||
const dialog = new MultiStepStatusDialog(
|
||||
() => { });
|
||||
|
||||
const loginResults = this.migrationStateModel._loginMigrationModel.GetLoginMigrationResults(loginName);
|
||||
const isMigrationComplete = this.migrationStateModel._loginMigrationModel.isMigrationComplete;
|
||||
|
||||
await dialog.openDialog(constants.LOGIN_MIGRATIONS_LOGIN_STATUS_DETAILS_TITLE(loginName), loginResults, isMigrationComplete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ 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';
|
||||
import { collectSourceLogins, collectTargetLogins, isSysAdmin, LoginTableInfo } from '../api/sqlUtils';
|
||||
|
||||
export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -328,6 +328,14 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
await this.populateAzureAccountsDropdown();
|
||||
await this.populateSubscriptionDropdown();
|
||||
await this.populateLocationDropdown();
|
||||
|
||||
// Collect source login info here, as it will speed up loading the next page
|
||||
const sourceLogins: LoginTableInfo[] = [];
|
||||
sourceLogins.push(...await collectSourceLogins(
|
||||
this.migrationStateModel.sourceConnectionId,
|
||||
this.migrationStateModel.isWindowsAuthMigrationSupported));
|
||||
this.migrationStateModel._loginMigrationModel.collectedSourceLogins = true;
|
||||
this.migrationStateModel._loginMigrationModel.loginsOnSource = sourceLogins;
|
||||
console.log(this.migrationStateModel._targetType);
|
||||
}));
|
||||
|
||||
@@ -599,18 +607,22 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
const targetDatabaseServer = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer;
|
||||
const userName = this.migrationStateModel._targetUserName;
|
||||
const password = this.migrationStateModel._targetPassword;
|
||||
const targetDatabases: TargetDatabaseInfo[] = [];
|
||||
const loginsOnTarget: string[] = [];
|
||||
if (targetDatabaseServer && userName && password) {
|
||||
try {
|
||||
connectionButtonLoadingContainer.loading = true;
|
||||
await utils.updateControlDisplay(this._connectionResultsInfoBox, false);
|
||||
this.wizard.nextButton.enabled = false;
|
||||
targetDatabases.push(
|
||||
...await collectTargetDatabaseInfo(
|
||||
loginsOnTarget.push(
|
||||
...await collectTargetLogins(
|
||||
targetDatabaseServer,
|
||||
userName,
|
||||
password));
|
||||
await this._showConnectionResults(targetDatabases);
|
||||
password,
|
||||
this.migrationStateModel.isWindowsAuthMigrationSupported));
|
||||
this.migrationStateModel._loginMigrationModel.collectedTargetLogins = true;
|
||||
this.migrationStateModel._loginMigrationModel.loginsOnTarget = loginsOnTarget;
|
||||
|
||||
await this._showConnectionResults(loginsOnTarget);
|
||||
} catch (error) {
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
@@ -618,7 +630,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
description: constants.SQL_TARGET_CONNECTION_ERROR(error.message),
|
||||
};
|
||||
await this._showConnectionResults(
|
||||
targetDatabases,
|
||||
loginsOnTarget,
|
||||
constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE);
|
||||
}
|
||||
finally {
|
||||
@@ -653,19 +665,16 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
private async _showConnectionResults(
|
||||
databases: TargetDatabaseInfo[],
|
||||
logins: string[],
|
||||
errorMessage?: string): Promise<void> {
|
||||
|
||||
const hasError = errorMessage !== undefined;
|
||||
const hasDatabases = databases.length > 0;
|
||||
this._connectionResultsInfoBox.style = hasError
|
||||
? 'error'
|
||||
: hasDatabases
|
||||
? 'success'
|
||||
: 'warning';
|
||||
: 'success';
|
||||
this._connectionResultsInfoBox.text = hasError
|
||||
? constants.SQL_TARGET_CONNECTION_ERROR(errorMessage)
|
||||
: constants.SQL_TARGET_CONNECTION_SUCCESS_LOGINS(databases.length.toLocaleString());
|
||||
: constants.SQL_TARGET_CONNECTION_SUCCESS_LOGINS(logins.length.toLocaleString());
|
||||
await utils.updateControlDisplay(this._connectionResultsInfoBox, true);
|
||||
|
||||
if (!hasError) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { collectSourceLogins, collectTargetLogins, LoginTableInfo } from '../api
|
||||
import { AzureSqlDatabaseServer } from '../api/azure';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as utils from '../api/utils';
|
||||
import { LoginType } from '../models/loginMigrationModel';
|
||||
|
||||
export class LoginSelectorPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -24,9 +25,11 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _isCurrentPage: boolean;
|
||||
private _refreshResultsInfoBox!: azdata.InfoBoxComponent;
|
||||
private _windowsAuthInfoBox!: azdata.InfoBoxComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
private _refreshLoading!: azdata.LoadingComponent;
|
||||
private _filterTableValue!: string;
|
||||
private _aadDomainNameContainer!: azdata.FlexContainer;
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.LOGIN_MIGRATIONS_SELECT_LOGINS_PAGE_TITLE), migrationStateModel);
|
||||
@@ -59,9 +62,11 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
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,
|
||||
@@ -69,10 +74,23 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.selectedWindowsLogins() && !this.migrationStateModel._aadDomainName) {
|
||||
this.wizard.message = {
|
||||
text: constants.ENTER_AAD_DOMAIN_NAME,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await this._loadLoginList();
|
||||
// Dispaly windows auth info box if windows auth is not supported
|
||||
await utils.updateControlDisplay(this._windowsAuthInfoBox, !this.migrationStateModel.isWindowsAuthMigrationSupported);
|
||||
|
||||
// Refresh login list
|
||||
await this._loadLoginList(false);
|
||||
|
||||
// load unfiltered table list and pre-select list of logins saved in state
|
||||
await this._filterTableList('', this.migrationStateModel._loginsForMigration);
|
||||
@@ -110,6 +128,40 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
return searchContainer;
|
||||
}
|
||||
|
||||
private createAadDomainNameComponent(): azdata.FlexContainer {
|
||||
// target user name
|
||||
const aadDomainNameLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.LOGIN_MIGRATIONS_AAD_DOMAIN_NAME_INPUT_BOX_LABEL,
|
||||
requiredIndicator: false,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
|
||||
const aadDomainNameInputBox = this._view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
width: '300px',
|
||||
inputType: 'text',
|
||||
placeHolder: constants.LOGIN_MIGRATIONS_AAD_DOMAIN_NAME_INPUT_BOX_PLACEHOLDER,
|
||||
required: false,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
aadDomainNameInputBox.onTextChanged(
|
||||
(value: string) => {
|
||||
this.migrationStateModel._aadDomainName = value ?? '';
|
||||
}));
|
||||
|
||||
this._aadDomainNameContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
aadDomainNameLabel,
|
||||
aadDomainNameInputBox])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withProps({ CSSStyles: { 'margin': '10px 0px 0px 0px', 'display': 'none' } })
|
||||
.component();
|
||||
|
||||
return this._aadDomainNameContainer;
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private async _filterTableList(value: string, selectedList?: LoginTableInfo[]): Promise<void> {
|
||||
this._filterTableValue = value;
|
||||
@@ -143,13 +195,14 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
|
||||
public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
|
||||
const windowsAuthInfoBox = this._view.modelBuilder.infoBox()
|
||||
this._windowsAuthInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'information',
|
||||
text: constants.LOGIN_MIGRATIONS_SELECT_LOGINS_WINDOWS_AUTH_WARNING,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
CSSStyles: { ...styles.BODY_CSS, 'display': 'none', }
|
||||
}).component();
|
||||
|
||||
|
||||
this._refreshButton = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
@@ -158,7 +211,7 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
iconPath: IconPathHelper.refresh,
|
||||
label: constants.DATABASE_TABLE_REFRESH_LABEL,
|
||||
width: 70,
|
||||
CSSStyles: { 'margin': '15px 0 0 0' },
|
||||
CSSStyles: { 'margin': '15px 5px 0 0' },
|
||||
})
|
||||
.component();
|
||||
|
||||
@@ -281,32 +334,29 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
height: '100%',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0px 28px 0px 28px'
|
||||
'margin': '-20px 28px 0px 28px'
|
||||
}
|
||||
}).component();
|
||||
flex.addItem(windowsAuthInfoBox, { flex: '0 0 auto' });
|
||||
flex.addItem(this._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);
|
||||
flex.addItem(this.createAadDomainNameComponent(), { flex: '0 0 auto', CSSStyles: { 'margin-top': '8px' } });
|
||||
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';
|
||||
|
||||
private async _getSourceLogins() {
|
||||
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));
|
||||
sourceLogins.push(...await collectSourceLogins(
|
||||
stateMachine.sourceConnectionId,
|
||||
stateMachine.isWindowsAuthMigrationSupported));
|
||||
stateMachine._loginMigrationModel.collectedSourceLogins = true;
|
||||
stateMachine._loginMigrationModel.loginsOnSource = sourceLogins;
|
||||
} catch (error) {
|
||||
this._refreshLoading.loading = false;
|
||||
this._refreshResultsInfoBox.style = 'error';
|
||||
@@ -317,11 +367,22 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async _getTargetLogins() {
|
||||
const stateMachine: MigrationStateModel = this.migrationStateModel;
|
||||
const targetLogins: string[] = [];
|
||||
|
||||
// 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));
|
||||
targetLogins.push(...await collectTargetLogins(
|
||||
stateMachine._targetServerInstance as AzureSqlDatabaseServer,
|
||||
stateMachine._targetUserName,
|
||||
stateMachine._targetPassword,
|
||||
stateMachine.isWindowsAuthMigrationSupported));
|
||||
stateMachine._loginMigrationModel.collectedTargetLogins = true;
|
||||
stateMachine._loginMigrationModel.loginsOnTarget = targetLogins;
|
||||
}
|
||||
else {
|
||||
// TODO AKMA : Emit telemetry here saying target info is empty
|
||||
@@ -336,7 +397,41 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async _markRefreshDataStart() {
|
||||
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';
|
||||
}
|
||||
|
||||
private _markRefreshDataComplete(numSourceLogins: number, numTargetLogins: number) {
|
||||
this._refreshLoading.loading = false;
|
||||
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESH_LOGIN_DATA_SUCCESSFUL(numSourceLogins, numTargetLogins);
|
||||
this._refreshResultsInfoBox.style = 'success';
|
||||
this.updateNextButton();
|
||||
}
|
||||
|
||||
private async _loadLoginList(runQuery: boolean = true): Promise<void> {
|
||||
const stateMachine: MigrationStateModel = this.migrationStateModel;
|
||||
const selectedLogins: LoginTableInfo[] = stateMachine._loginsForMigration || [];
|
||||
|
||||
// Get source logins if caller asked us to or if we haven't collected in the past
|
||||
if (runQuery || !stateMachine._loginMigrationModel.collectedSourceLogins) {
|
||||
await this._markRefreshDataStart();
|
||||
await this._getSourceLogins();
|
||||
}
|
||||
|
||||
// Get target logins if caller asked us to or if we haven't collected in the past
|
||||
if (runQuery || !stateMachine._loginMigrationModel.collectedTargetLogins) {
|
||||
await this._markRefreshDataStart();
|
||||
await this._getTargetLogins();
|
||||
}
|
||||
|
||||
const sourceLogins: LoginTableInfo[] = stateMachine._loginMigrationModel.loginsOnSource;
|
||||
const targetLogins: string[] = stateMachine._loginMigrationModel.loginsOnTarget;
|
||||
this._loginNames = [];
|
||||
|
||||
this._loginTableValues = sourceLogins.map(row => {
|
||||
@@ -357,10 +452,7 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
}) || [];
|
||||
|
||||
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();
|
||||
this._markRefreshDataComplete(sourceLogins.length, targetLogins.length);
|
||||
}
|
||||
|
||||
public selectedLogins(): LoginTableInfo[] {
|
||||
@@ -379,6 +471,10 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
|| [];
|
||||
}
|
||||
|
||||
private selectedWindowsLogins(): boolean {
|
||||
return this.selectedLogins().some(logins => logins.loginType.toLocaleLowerCase() === LoginType.Windows_Login);
|
||||
}
|
||||
|
||||
private async updateValuesOnSelection() {
|
||||
const selectedLogins = this.selectedLogins() || [];
|
||||
await this._loginCount.updateProperties({
|
||||
@@ -387,8 +483,12 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
this._loginSelectorTable.data?.length || 0)
|
||||
});
|
||||
|
||||
// Display AAD Domain Name input box if windows logins selected, else disable
|
||||
const hasSelectedWindowsLogins = this.selectedWindowsLogins()
|
||||
await utils.updateControlDisplay(this._aadDomainNameContainer, hasSelectedWindowsLogins);
|
||||
await this._loginSelectorTable.updateProperty("height", hasSelectedWindowsLogins ? 600 : 650);
|
||||
|
||||
this.migrationStateModel._loginsForMigration = selectedLogins;
|
||||
this.migrationStateModel._aadDomainName = "";
|
||||
this.updateNextButton();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user