mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-04 09:35:38 -05:00
Add new wizard for login migrations experience (#21317)
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user