Files
azuredatastudio/extensions/sql-migration/src/wizard/databaseSelectorPage.ts
Raymond Truong 6de9c5e1ae [SQL Migration] Add support for assessing XEvent session files (#22210)
* Template

* Refactor

* Update strings

* Clean up

* Add clear button

* Clean up

* Fix typo and use aka.ms link

* Refactor to use GroupContainer

* Remove dialog and clean up common strings

* Fix previous/forward behavior

* Make group container default to collapsed

* Clean up

* Slightly reword string

* Add https to aka.ms link
2023-05-02 10:45:13 -04:00

371 lines
12 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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, promptUserForFolder } from '../api/utils';
import * as styles from '../constants/styles';
import { IconPathHelper } from '../constants/iconPathHelper';
import { getDatabasesList, excludeDatabases, SourceDatabaseInfo, getSourceConnectionProfile } from '../api/sqlUtils';
export class DatabaseSelectorPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _databaseSelectorTable!: azdata.TableComponent;
private _xEventsGroup!: azdata.GroupContainer;
private _xEventsFolderPickerInput!: azdata.InputBoxComponent;
private _xEventsFilesFolderPath!: string;
private _dbNames!: string[];
private _dbCount!: azdata.TextComponent;
private _databaseTableValues!: any[];
private _disposables: vscode.Disposable[] = [];
private readonly TABLE_WIDTH = 650;
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.DATABASE_FOR_ASSESSMENT_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) => {
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
if (this.selectedDbs().length === 0) {
this.wizard.message = {
text: constants.SELECT_DATABASE_TO_CONTINUE,
level: azdata.window.MessageLevel.Error
};
return false;
}
return true;
});
this._xEventsFilesFolderPath = this.migrationStateModel._xEventsFilesFolderPath;
}
public async onPageLeave(): Promise<void> {
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
const assessedDatabases = this.migrationStateModel._assessedDatabaseList ?? [];
const selectedDatabases = this.migrationStateModel._databasesForAssessment;
// run assessment if
// * no prior assessment
// * the prior assessment had an error or
// * the assessed databases list is different from the selected databases list
// * the XEvents path has changed
this.migrationStateModel._runAssessments = !this.migrationStateModel._assessmentResults
|| !!this.migrationStateModel._assessmentResults?.assessmentError
|| assessedDatabases.length === 0
|| assessedDatabases.length !== selectedDatabases.length
|| assessedDatabases.some(db => selectedDatabases.indexOf(db) < 0)
|| this.migrationStateModel._xEventsFilesFolderPath.toLowerCase() !== this._xEventsFilesFolderPath.toLowerCase();
this.migrationStateModel._xEventsFilesFolderPath = this._xEventsFilesFolderPath;
}
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)));
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?: string[]): Promise<void> {
const selectedRows: number[] = [];
const selectedDatabases = selectedList || this.selectedDbs();
let tableRows = this._databaseTableValues;
if (this._databaseTableValues && value?.length > 0) {
tableRows = this._databaseTableValues
.filter(row => {
const searchText = value?.toLowerCase();
return row[2]?.toLowerCase()?.indexOf(searchText) > -1 // database name
|| row[3]?.toLowerCase()?.indexOf(searchText) > -1 // state
|| row[4]?.toLowerCase()?.indexOf(searchText) > -1; // size
});
}
for (let row = 0; row < tableRows.length; row++) {
const database: string = tableRows[row][2];
if (selectedDatabases.includes(database)) {
selectedRows.push(row);
}
}
await this._databaseSelectorTable.updateProperty('data', tableRows);
this._databaseSelectorTable.selectedRows = selectedRows;
await this.updateValuesOnSelection();
}
public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
await this._loadDatabaseList(this.migrationStateModel, this.migrationStateModel._assessedDatabaseList);
const text = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_FOR_ASSESSMENT_DESCRIPTION,
CSSStyles: {
...styles.BODY_CSS
}
}).component();
this._dbCount = this._view.modelBuilder.text().withProps({
value: constants.DATABASES_SELECTED(
this.selectedDbs().length,
this._databaseTableValues.length),
CSSStyles: {
...styles.BODY_CSS,
'margin-top': '8px'
},
ariaLive: 'polite'
}).component();
const cssClass = 'no-borders';
this._databaseSelectorTable = this._view.modelBuilder.table()
.withProps({
data: [],
width: this.TABLE_WIDTH,
height: '100%',
CSSStyles: { 'margin-bottom': '12px' },
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [
<azdata.CheckboxColumn>{
value: '',
width: 10,
type: azdata.ColumnType.checkBox,
action: azdata.ActionOnCellCheckboxCheck.selectRow,
resizable: false,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
value: 'databaseicon',
name: '',
width: 10,
type: azdata.ColumnType.icon,
headerCssClass: cssClass,
cssClass: cssClass,
resizable: false,
},
{
name: constants.DATABASE,
value: 'database',
type: azdata.ColumnType.text,
width: 360,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.STATUS,
value: 'status',
type: azdata.ColumnType.text,
width: 80,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.SIZE,
value: 'size',
type: azdata.ColumnType.text,
width: 80,
cssClass: cssClass,
headerCssClass: cssClass,
},
]
}).component();
this._disposables.push(
this._databaseSelectorTable.onRowSelected(
async (e) => await this.updateValuesOnSelection()));
// load unfiltered table list and pre-select list of databases saved in state
await this._filterTableList('', this.migrationStateModel._databasesForAssessment);
const xEventsDescription = this._view.modelBuilder.text()
.withProps({
value: constants.XEVENTS_ASSESSMENT_DESCRIPTION,
width: this.TABLE_WIDTH,
CSSStyles: { ...styles.BODY_CSS },
links: [{ text: constants.XEVENTS_ASSESSMENT_HELPLINK, url: 'https://aka.ms/sql-migration-xe-assess' }]
}).component();
const xEventsInstructions = this._view.modelBuilder.text()
.withProps({
value: constants.XEVENTS_ASSESSMENT_OPEN_FOLDER,
width: this.TABLE_WIDTH,
CSSStyles: { ...styles.LABEL_CSS },
}).component();
this._xEventsFolderPickerInput = this._view.modelBuilder.inputBox()
.withProps({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 460,
ariaLabel: constants.XEVENTS_ASSESSMENT_OPEN_FOLDER
}).component();
this._disposables.push(
this._xEventsFolderPickerInput.onTextChanged(async (value) => {
if (value) {
this._xEventsFilesFolderPath = value.trim();
}
}));
const xEventsFolderPickerButton = this._view.modelBuilder.button()
.withProps({
label: constants.OPEN,
width: 80,
}).component();
this._disposables.push(
xEventsFolderPickerButton.onDidClick(
async () => this._xEventsFolderPickerInput.value = await promptUserForFolder()));
const xEventsFolderPickerClearButton = this._view.modelBuilder.button()
.withProps({
label: constants.CLEAR,
width: 80,
}).component();
this._disposables.push(
xEventsFolderPickerClearButton.onDidClick(
async () => {
this._xEventsFolderPickerInput.value = '';
this._xEventsFilesFolderPath = '';
}));
const xEventsFolderPickerContainer = this._view.modelBuilder.flexContainer()
.withProps({
CSSStyles: { 'flex-direction': 'row', 'align-items': 'left' }
}).withItems([
this._xEventsFolderPickerInput,
xEventsFolderPickerButton,
xEventsFolderPickerClearButton
]).component();
this._xEventsGroup = this._view.modelBuilder.groupContainer()
.withLayout({
header: constants.XEVENTS_ASSESSMENT_TITLE,
collapsible: true,
collapsed: true
}).withItems([
xEventsDescription,
xEventsInstructions,
xEventsFolderPickerContainer
]).component();
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
height: '65%',
}).withProps({
CSSStyles: {
'margin': '0px 28px 0px 28px'
}
}).component();
flex.addItem(text, { flex: '0 0 auto' });
flex.addItem(this.createSearchComponent(), { flex: '0 0 auto' });
flex.addItem(this._dbCount, { flex: '0 0 auto' });
flex.addItem(this._databaseSelectorTable);
flex.addItem(this._xEventsGroup, { flex: '0 0 auto' });
return flex;
}
private async _loadDatabaseList(stateMachine: MigrationStateModel, selectedDatabases: string[]): Promise<void> {
const allDatabases = (<azdata.DatabaseInfo[]>await getDatabasesList(await getSourceConnectionProfile()));
const databaseList = allDatabases
.filter(database => !excludeDatabases.includes(database.options.name))
|| [];
databaseList.sort((a, b) => a.options.name.localeCompare(b.options.name));
this._dbNames = [];
stateMachine._databaseInfosForMigration = [];
this._databaseTableValues = databaseList.map(database => {
const databaseName = database.options.name;
this._dbNames.push(databaseName);
stateMachine._databaseInfosForMigration.push(this.getSourceDatabaseInfo(database));
return [
selectedDatabases?.indexOf(databaseName) > -1,
<azdata.IconColumnCellValue>{
icon: IconPathHelper.sqlDatabaseLogo,
title: databaseName,
},
databaseName,
database.options.state,
database.options.sizeInMB,
];
}) || [];
}
public selectedDbs(): string[] {
const rows = this._databaseSelectorTable?.data || [];
const databases = this._databaseSelectorTable?.selectedRows || [];
return databases
.filter(row => row < rows.length)
.map(row => rows[row][2])
|| [];
}
private async updateValuesOnSelection() {
const selectedDatabases = this.selectedDbs() || [];
await this._dbCount.updateProperties({
'value': constants.DATABASES_SELECTED(
selectedDatabases.length,
this._databaseSelectorTable.data?.length || 0)
});
this.migrationStateModel._databasesForAssessment = selectedDatabases;
}
private getSourceDatabaseInfo(database: azdata.DatabaseInfo): SourceDatabaseInfo {
return {
databaseName: database.options.name,
databaseCollation: database.options.collation,
databaseSizeInMB: database.options.sizeInMB,
databaseState: database.options.state
};
}
}