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

@@ -807,7 +807,9 @@ export function sortResourceArrayByName(resourceArray: SortableAzureResources[])
export function getMigrationTargetId(migration: DatabaseMigration): string {
// `${targetServerId}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=${DMSV2_API_VERSION}`
const paths = migration.id.split('/providers/Microsoft.DataMigration/', 1);
return paths[0];
return paths?.length > 0
? paths[0]
: '';
}
export function getMigrationTargetName(migration: DatabaseMigration): string {

View File

@@ -667,21 +667,24 @@ export const SELECT_RESOURCE_GROUP_PROMPT = localize('sql.migration.blob.resourc
export const SELECT_STORAGE_ACCOUNT = localize('sql.migration.blob.storageAccount.select', "Select a storage account value first.");
export const SELECT_BLOB_CONTAINER = localize('sql.migration.blob.container.select', "Select a blob container value first.");
export const MISSING_TABLE_NAME_COLUMN = localize('sql.migration.missing.table.name.column', "Table name");
export function SELECT_DATABASE_TABLES_TITLE(targetDatabaseName: string): string {
return localize('sql.migration.table.select.label', "Select tables for {0}", targetDatabaseName);
}
export const TABLE_SELECTION_EDIT = localize('sql.migration.table.selection.edit', "Edit");
export function TABLE_SELECTION_COUNT(selectedCount: number, rowCount: number): string {
return localize('sql.migration.table.selection.count', "{0} of {1}", selectedCount, rowCount);
return localize('sql.migration.table.selection.count', "{0} of {1}", formatNumber(selectedCount), formatNumber(rowCount));
}
export function TABLE_SELECTED_COUNT(selectedCount: number, rowCount: number): string {
return localize('sql.migration.table.selected.count', "{0} of {1} tables selected", selectedCount, rowCount);
return localize('sql.migration.table.selected.count', "{0} of {1} tables selected", formatNumber(selectedCount), formatNumber(rowCount));
}
export function MISSING_TARGET_TABLES_COUNT(tables: number): string {
return localize('sql.migration.table.missing.count', "Missing target tables excluded from list: {0}", tables);
return localize('sql.migration.table.missing.count', "Tables missing on target: {0}", formatNumber(tables));
}
export const DATABASE_MISSING_TABLES = localize('sql.migration.database.missing.tables', "0 tables found.");
export const SELECT_TABLES_FOR_MIGRATION = localize('sql.migration.select.migration.tables', "Select tables for migration");
export const DATABASE_MISSING_TABLES = localize('sql.migration.database.missing.tables', "0 tables found on source database.");
export const DATABASE_LOADING_TABLES = localize('sql.migration.database.loading.tables', "Loading tables list...");
export const TABLE_SELECTION_FILTER = localize('sql.migration.table.selection.filter', "Filter tables");
export const TABLE_SELECTION_UPDATE_BUTTON = localize('sql.migration.table.selection.update.button', "Update");
@@ -932,7 +935,9 @@ export const AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS = localize('sql.migration.a
export const SHIR = localize('sql.migration.shir', "Self-hosted integration runtime node");
export const DATABASE_TO_BE_MIGRATED = localize('sql.migration.database.to.be.migrated', "Database to be migrated");
export function COUNT_DATABASES(count: number): string {
return (count === 1) ? localize('sql.migration.count.database.single', "{0} database", count) : localize('sql.migration.count.database.multiple', "{0} databases", count);
return (count === 1)
? localize('sql.migration.count.database.single', "{0} database", count)
: localize('sql.migration.count.database.multiple', "{0} databases", formatNumber(count));
}
export function TOTAL_TABLES_SELECTED(selected: number, total: number): string {
return localize('total.tables.selected.of.total', "{0} of {1}", formatNumber(selected), formatNumber(total));

View File

@@ -401,7 +401,11 @@ export class CreateSqlMigrationServiceDialog {
constants.RESOURCE_GROUP_NOT_FOUND);
const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.name.toLowerCase() === this._resourceGroupPreset.toLowerCase());
this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0];
this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue)
? selectedResourceGroupValue
: this.migrationServiceResourceGroupDropdown.values?.length > 0
? this.migrationServiceResourceGroupDropdown.values[0]
: '';
} finally {
this.migrationServiceResourceGroupDropdown.loading = false;
}

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) {

View File

@@ -97,12 +97,15 @@ export function sendSqlMigrationActionEvent(telemetryView: TelemetryViews, telem
}
export function getTelemetryProps(migrationStateModel: MigrationStateModel): TelemetryEventProperties {
const tenantId = migrationStateModel._azureAccount?.properties?.tenants?.length > 0
? migrationStateModel._azureAccount?.properties?.tenants[0]?.id
: '';
return {
'sessionId': migrationStateModel._sessionId,
'subscriptionId': migrationStateModel._targetSubscription?.id,
'resourceGroup': migrationStateModel._resourceGroup?.name,
'targetType': migrationStateModel._targetType,
'tenantId': migrationStateModel._azureAccount?.properties?.tenants[0]?.id,
'tenantId': tenantId,
};
}

View File

@@ -897,14 +897,16 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._sqlSourceUsernameInput.value = username;
this._sqlSourcePassword.value = (await getSourceConnectionCredentials()).password;
this._windowsUserAccountText.value =
this.migrationStateModel._databaseBackup.networkShares[0]?.windowsUser
?? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser
?? '';
this._passwordText.value =
this.migrationStateModel._databaseBackup.networkShares[0]?.password
?? this.migrationStateModel.savedInfo?.networkShares[0]?.password
?? '';
const networkShares = this.migrationStateModel._databaseBackup?.networkShares?.length > 0
? this.migrationStateModel._databaseBackup?.networkShares
: this.migrationStateModel.savedInfo?.networkShares ?? [];
const networkShare = networkShares?.length > 0
? networkShares[0]
: undefined;
this._windowsUserAccountText.value = networkShare?.windowsUser ?? '';
this._passwordText.value = networkShare?.password ?? '';
this._networkShareTargetDatabaseNames = [];
this._networkShareLocations = [];
@@ -1379,13 +1381,15 @@ export class DatabaseBackupPage extends MigrationWizardPage {
break;
case NetworkContainerType.NETWORK_SHARE:
// All network share migrations use the same storage account
const storageAccount = this.migrationStateModel._databaseBackup.networkShares[0]?.storageAccount;
const storageKey = (await getStorageAccountAccessKeys(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
storageAccount)).keyName1;
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].storageKey = storageKey;
if (this.migrationStateModel._databaseBackup.networkShares?.length > 0) {
const storageAccount = this.migrationStateModel._databaseBackup.networkShares[0]?.storageAccount;
const storageKey = (await getStorageAccountAccessKeys(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
storageAccount)).keyName1;
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].storageKey = storageKey;
}
}
break;
}

View File

@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationMode, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { MigrationMode, MigrationStateModel, NetworkContainerType, NetworkShare, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { createHeadingTextComponent, createInformationRow, createLabelTextComponent } from './wizardController';
import { getResourceGroupFromId } from '../api/azure';
@@ -185,7 +185,10 @@ export class SummaryPage extends MigrationWizardPage {
.withLayout({ flexFlow: 'column' })
.component();
const networkShare = this.migrationStateModel._databaseBackup.networkShares[0];
const networkShare = this.migrationStateModel._databaseBackup.networkShares?.length > 0
? this.migrationStateModel._databaseBackup.networkShares[0]
: <NetworkShare>{};
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.NETWORK_SHARE:
flexContainer.addItems([