SQL-Migration: improve SQL DB table selection ux to include missing tables (#22659)

* add missing target tables ux

* fix number formatting
This commit is contained in:
brian-harris
2023-04-07 16:00:12 -07:00
committed by GitHub
parent 0412ba194b
commit a60d6107b4
7 changed files with 213 additions and 85 deletions

View File

@@ -9,6 +9,8 @@ import * as constants from '../../constants/strings';
import { AzureSqlDatabaseServer } from '../../api/azure';
import { collectSourceDatabaseTableInfo, collectTargetDatabaseTableInfo, TableInfo } from '../../api/sqlUtils';
import { MigrationStateModel } from '../../models/stateMachine';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { Tab } from 'azdata';
import { updateControlDisplay } from '../../api/utils';
const DialogName = 'TableMigrationSelection';
@@ -16,10 +18,11 @@ const DialogName = 'TableMigrationSelection';
export class TableMigrationSelectionDialog {
private _dialog: azdata.window.Dialog | undefined;
private _headingText!: azdata.TextComponent;
private _missingTablesText!: azdata.TextComponent;
private _refreshButton!: azdata.ButtonComponent;
private _filterInputBox!: azdata.InputBoxComponent;
private _tableSelectionTable!: azdata.TableComponent;
private _tableLoader!: azdata.LoadingComponent;
private _missingTargetTablesTable!: azdata.TableComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _disposables: vscode.Disposable[] = [];
private _isOpen: boolean = false;
private _model: MigrationStateModel;
@@ -28,6 +31,9 @@ export class TableMigrationSelectionDialog {
private _targetTableMap!: Map<string, TableInfo>;
private _onSaveCallback: () => Promise<void>;
private _missingTableCount: number = 0;
private _selectableTablesTab!: Tab;
private _missingTablesTab!: Tab;
private _tabs!: azdata.TabbedPanelComponent;
constructor(
model: MigrationStateModel,
@@ -41,7 +47,12 @@ export class TableMigrationSelectionDialog {
private async _loadData(): Promise<void> {
try {
this._tableLoader.loading = true;
this._refreshLoader.loading = true;
this._updateRowSelection();
await updateControlDisplay(this._tableSelectionTable, false);
await updateControlDisplay(this._missingTargetTablesTable, false);
const targetDatabaseInfo = this._model._sourceTargetMapping.get(this._sourceDatabaseName);
if (targetDatabaseInfo) {
const sourceTableList: TableInfo[] = await collectSourceDatabaseTableInfo(
@@ -86,6 +97,7 @@ export class TableMigrationSelectionDialog {
this._tableSelectionMap.set(table.tableName, tableInfo);
});
}
this._dialog!.message = { text: '', level: azdata.window.MessageLevel.Information };
} catch (error) {
this._dialog!.message = {
text: constants.DATABASE_TABLE_CONNECTION_ERROR,
@@ -93,13 +105,16 @@ export class TableMigrationSelectionDialog {
level: azdata.window.MessageLevel.Error
};
} finally {
this._tableLoader.loading = false;
this._refreshLoader.loading = false;
await updateControlDisplay(this._tableSelectionTable, true, 'flex');
await updateControlDisplay(this._missingTargetTablesTable, true, 'flex');
await this._loadControls();
}
}
private async _loadControls(): Promise<void> {
const data: any[][] = [];
const missingData: any[][] = [];
const filterText = this._filterInputBox.value ?? '';
const selectedItems: number[] = [];
let tableRow = 0;
@@ -125,67 +140,45 @@ export class TableMigrationSelectionDialog {
}
tableRow++;
} else {
this._missingTableCount++;
missingData.push([sourceTable.tableName]);
}
this._missingTableCount += targetTable ? 0 : 1;
}
});
await this._tableSelectionTable.updateProperty('data', data);
this._tableSelectionTable.selectedRows = selectedItems;
await this._updateRowSelection();
await this._missingTargetTablesTable.updateProperty('data', missingData);
this._updateRowSelection();
if (this._missingTableCount > 0 && this._tabs.items.length === 1) {
this._tabs.updateTabs([this._selectableTablesTab, this._missingTablesTab]);
}
}
private async _initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
dialog.registerContent(async (view) => {
this._filterInputBox = view.modelBuilder.inputBox()
.withProps({
inputType: 'search',
placeHolder: constants.TABLE_SELECTION_FILTER,
width: 268,
}).component();
const tab = azdata.window.createTab('');
tab.registerContent(async (view) => {
this._disposables.push(
this._filterInputBox.onTextChanged(
async e => await this._loadControls()));
this._headingText = view.modelBuilder.text()
.withProps({ value: constants.DATABASE_LOADING_TABLES })
this._tabs = view.modelBuilder.tabbedPanel()
.withTabs([])
.component();
this._missingTablesText = view.modelBuilder.text()
.withProps({ display: 'none' })
.component();
await this._createSelectableTablesTab(view);
await this._createMissingTablesTab(view);
this._tableSelectionTable = await this._createSelectionTable(view);
this._tableLoader = view.modelBuilder.loadingComponent()
.withItem(this._tableSelectionTable)
.withProps({
loading: false,
loadingText: constants.DATABASE_TABLE_DATA_LOADING
}).component();
const flex = view.modelBuilder.flexContainer()
.withItems([
this._filterInputBox,
this._headingText,
this._missingTablesText,
this._tableLoader],
{ flex: '0 0 auto' })
.withProps({ CSSStyles: { 'margin': '0 0 0 15px' } })
.withLayout({
flexFlow: 'column',
height: '100%',
width: 565,
}).component();
this._tabs.updateTabs([this._selectableTablesTab]);
this._disposables.push(
view.onClosed(e =>
this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
await view.initializeModel(flex);
await view.initializeModel(this._tabs);
await this._loadData();
});
dialog.content = [tab];
}
public async openDialog(dialogTitle: string) {
@@ -194,7 +187,7 @@ export class TableMigrationSelectionDialog {
this._dialog = azdata.window.createModelViewDialog(
dialogTitle,
DialogName,
600);
600, undefined, undefined, false);
this._dialog.okButton.label = constants.TABLE_SELECTION_UPDATE_BUTTON;
this._dialog.okButton.position = 'left';
@@ -214,13 +207,105 @@ export class TableMigrationSelectionDialog {
}
}
private async _createSelectionTable(view: azdata.ModelView): Promise<azdata.TableComponent> {
private async _createSelectableTablesTab(view: azdata.ModelView): Promise<void> {
this._headingText = view.modelBuilder.text()
.withProps({ value: constants.DATABASE_LOADING_TABLES })
.component();
this._filterInputBox = view.modelBuilder.inputBox()
.withProps({
inputType: 'search',
placeHolder: constants.TABLE_SELECTION_FILTER,
width: 268,
}).component();
this._disposables.push(
this._filterInputBox.onTextChanged(
async e => await this._loadControls()));
this._refreshButton = 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': '5px 0 0 15px' },
})
.component();
this._disposables.push(
this._refreshButton.onDidClick(
async e => await this._loadData()));
this._refreshLoader = view.modelBuilder.loadingComponent()
.withItem(this._refreshButton)
.withProps({
loading: false,
CSSStyles: { 'height': '8px', 'margin': '5px 0 0 15px' }
})
.component();
const flexTopRow = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
flexWrap: 'wrap',
})
.component();
flexTopRow.addItem(this._filterInputBox, { flex: '0 0 auto' });
flexTopRow.addItem(this._refreshLoader, { flex: '0 0 auto' });
this._tableSelectionTable = this._createSelectionTable(view);
const flex = view.modelBuilder.flexContainer()
.withItems([
flexTopRow,
this._headingText,
this._tableSelectionTable],
{ flex: '0 0 auto' })
.withProps({ CSSStyles: { 'margin': '10px 0 0 15px' } })
.withLayout({
flexFlow: 'column',
height: '100%',
width: 550,
}).component();
this._selectableTablesTab = {
content: flex,
id: 'tableSelectionTab',
title: constants.SELECT_TABLES_FOR_MIGRATION,
};
}
private async _createMissingTablesTab(view: azdata.ModelView): Promise<void> {
this._missingTargetTablesTable = this._createMissingTablesTable(view);
const flex = view.modelBuilder.flexContainer()
.withItems(
[this._missingTargetTablesTable],
{ flex: '0 0 auto' })
.withProps({ CSSStyles: { 'margin': '10px 0 0 15px' } })
.withLayout({
flexFlow: 'column',
height: '100%',
width: 550,
}).component();
this._missingTablesTab = {
content: flex,
id: 'missingTablesTab',
title: constants.MISSING_TARGET_TABLES_COUNT(this._missingTableCount),
};
}
private _createSelectionTable(view: azdata.ModelView): azdata.TableComponent {
const cssClass = 'no-borders';
const table = view.modelBuilder.table()
.withProps({
data: [],
width: 565,
width: 550,
height: '600px',
display: 'flex',
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [
<azdata.CheckboxColumn>{
@@ -236,7 +321,7 @@ export class TableMigrationSelectionDialog {
name: constants.TABLE_SELECTION_TABLENAME_COLUMN,
value: 'tableName',
type: azdata.ColumnType.text,
width: 300,
width: 285,
cssClass: cssClass,
headerCssClass: cssClass,
},
@@ -275,23 +360,45 @@ export class TableMigrationSelectionDialog {
}
});
await this._updateRowSelection();
this._updateRowSelection();
}));
return table;
}
private async _updateRowSelection(): Promise<void> {
this._headingText.value = this._tableSelectionTable.data.length > 0
? constants.TABLE_SELECTED_COUNT(
this._tableSelectionTable.selectedRows?.length ?? 0,
this._tableSelectionTable.data.length)
: this._tableLoader.loading
? constants.DATABASE_LOADING_TABLES
private _createMissingTablesTable(view: azdata.ModelView): azdata.TableComponent {
const cssClass = 'no-borders';
const table = view.modelBuilder.table()
.withProps({
data: [],
width: 550,
height: '600px',
display: 'flex',
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [{
name: constants.MISSING_TABLE_NAME_COLUMN,
value: 'tableName',
type: azdata.ColumnType.text,
cssClass: cssClass,
headerCssClass: cssClass,
}],
})
.withValidation(() => true)
.component();
return table;
}
private _updateRowSelection(): void {
this._headingText.value = this._refreshLoader.loading
? constants.DATABASE_LOADING_TABLES
: this._tableSelectionTable.data?.length > 0
? constants.TABLE_SELECTED_COUNT(
this._tableSelectionTable.selectedRows?.length ?? 0,
this._tableSelectionTable.data?.length ?? 0)
: constants.DATABASE_MISSING_TABLES;
this._missingTablesText.value = constants.MISSING_TARGET_TABLES_COUNT(this._missingTableCount);
await updateControlDisplay(this._missingTablesText, this._missingTableCount > 0);
this._missingTablesTab.title = constants.MISSING_TARGET_TABLES_COUNT(this._missingTableCount);
}
private async _save(): Promise<void> {
@@ -303,7 +410,7 @@ export class TableMigrationSelectionDialog {
selectedRows.forEach(rowIndex => {
const tableRow = this._tableSelectionTable.data[rowIndex];
const tableName = tableRow.length > 1
? this._tableSelectionTable.data[rowIndex][1] as string
? tableRow[1] as string
: '';
const tableInfo = this._tableSelectionMap.get(tableName);
if (tableInfo) {