mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-17 01:25:36 -05:00
* 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
371 lines
12 KiB
TypeScript
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
|
|
};
|
|
}
|
|
}
|