mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-05 09:35:39 -05:00
Add SQL DB offline migration wizard experience (#20403)
* sql db wizard with target selection * add database table selection * add sqldb to service and IR page * Code complete * navigation bug fixes * fix target db selection * improve sqldb error and status reporting * fix error count bug * remove table status inference * address review feedback * update resource strings and content * fix migraton status string, use localized value * fix ux navigation issues * fix back/fwd w/o changes from changing data
This commit is contained in:
@@ -6,20 +6,16 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../constants/strings';
|
||||
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils';
|
||||
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
|
||||
import { getMigrationTargetType, getMigrationMode, canRetryMigration, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime, MenuCommands } from '../api/utils';
|
||||
import { getMigrationTargetType, getMigrationMode, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
|
||||
import { DatabaseMigration, getResourceName } from '../api/azure';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { AdsMigrationStatus, EmptySettingValue, ServiceContextChangeEvent, TabBase } from './tabBase';
|
||||
import { MigrationMode } from '../models/stateMachine';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
export const MigrationsListTabId = 'MigrationsListTab';
|
||||
|
||||
@@ -58,12 +54,14 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
|
||||
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
|
||||
statusBar: DashboardStatusBar,
|
||||
): Promise<MigrationsListTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this._openMigrationDetails = openMigrationDetails;
|
||||
this.serviceContextChangedEvent = serviceContextChangedEvent;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize();
|
||||
@@ -71,29 +69,28 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
|
||||
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
|
||||
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
|
||||
.find(value => value.name === filter.toString());
|
||||
|
||||
this._statusDropdown.value = statusFilter;
|
||||
await this._statusDropdown.updateProperties({ 'value': statusFilter });
|
||||
}
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
if (this.isRefreshing ||
|
||||
this._refreshLoader === undefined) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._refresh.enabled = false;
|
||||
this._refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
this.isRefreshing = true;
|
||||
this._refreshLoader.loading = true;
|
||||
|
||||
await this.statusBar.clearError();
|
||||
|
||||
await this._statusTable.updateProperty('data', []);
|
||||
this._migrations = await getCurrentMigrations();
|
||||
await this._populateMigrationTable();
|
||||
@@ -105,26 +102,22 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
|
||||
} finally {
|
||||
this._refreshLoader.loading = false;
|
||||
this._refresh.enabled = true;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(): Promise<void> {
|
||||
this._registerCommands();
|
||||
|
||||
this._createStatusTable();
|
||||
this.content = this.view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[
|
||||
this._createToolbar(),
|
||||
await this._createSearchAndSortContainer(),
|
||||
this._createStatusTable()
|
||||
this._statusTable,
|
||||
],
|
||||
{ CSSStyles: { 'width': '100%' } }
|
||||
).withLayout({
|
||||
width: '100%',
|
||||
flexFlow: 'column',
|
||||
}).withProps({ CSSStyles: { 'padding': '0px' } })
|
||||
).withLayout({ width: '100%', flexFlow: 'column' })
|
||||
.withProps({ CSSStyles: { 'padding': '0px' } })
|
||||
.component();
|
||||
}
|
||||
|
||||
@@ -144,20 +137,16 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
this._refreshLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withItem(this._refresh)
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '6px'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
CSSStyles: { 'height': '8px', 'margin-top': '6px' }
|
||||
}).component();
|
||||
|
||||
toolbar.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this._refresh },
|
||||
<azdata.ToolbarComponent>{ component: this._refreshLoader },
|
||||
]);
|
||||
|
||||
@@ -178,16 +167,25 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
width: 230,
|
||||
}).component();
|
||||
|
||||
const onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
this.disposables.push(
|
||||
this._serviceContextButton.onDidClick(
|
||||
async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
|
||||
const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
this.disposables.push(
|
||||
this.serviceContextChangedEvent.event(
|
||||
async (e) => {
|
||||
if (e.connectionId === connectionProfile.connectionId) {
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
));
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
this._searchBox = this.view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
stopEnterPropagation: true,
|
||||
@@ -212,7 +210,9 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
.withProps({
|
||||
ariaLabel: loc.MIGRATION_STATUS_FILTER,
|
||||
values: this._statusDropdownValues,
|
||||
width: '150px'
|
||||
width: '150px',
|
||||
fireOnTextChange: true,
|
||||
value: this._statusDropdownValues[0],
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._statusDropdown.onValueChanged(
|
||||
@@ -311,173 +311,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
return container;
|
||||
}
|
||||
|
||||
private _registerCommands(): void {
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.Cutover,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(
|
||||
migration => migration.id === migrationId);
|
||||
|
||||
if (canRetryMigration(migration)) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
|
||||
await dialog.initialize();
|
||||
if (cutoverDialogModel.CutoverError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
|
||||
}
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
e.message);
|
||||
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewDatabase,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(m => m.id === migrationId);
|
||||
await this._openMigrationDetails(migration!);
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewTarget,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url));
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewService,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const dialog = new SqlMigrationServiceDetailsDialog(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await dialog.initialize();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CopyMigration,
|
||||
async (migrationId: string) => {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
|
||||
try {
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
|
||||
}
|
||||
|
||||
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
|
||||
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CancelMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canCancelMigration(migration)) {
|
||||
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
await cutoverDialogModel.cancelMigration();
|
||||
|
||||
if (cutoverDialogModel.CancelMigrationError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.RetryMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canRetryMigration(migration)) {
|
||||
let retryMigrationDialog = new RetryMigrationDialog(
|
||||
this.context,
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!,
|
||||
async () => await this.onDialogClosed());
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
|
||||
const sortDir = ascending ? -1 : 1;
|
||||
switch (columnName) {
|
||||
@@ -575,6 +408,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
|
||||
this._columnSortCheckbox.checked === true);
|
||||
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
const data: any[] = this._filteredMigrations.map((migration, index) => {
|
||||
return [
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
@@ -597,7 +431,11 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
getMigrationTime(migration.properties.endedOn), // finishTime
|
||||
<azdata.ContextMenuColumnCellValue>{
|
||||
title: '',
|
||||
context: migration.id,
|
||||
context: {
|
||||
connectionId: connectionProfile.connectionId,
|
||||
migrationId: migration.id,
|
||||
migrationOperationId: migration.properties.migrationOperationId,
|
||||
},
|
||||
commands: this._getMenuCommands(migration), // context menu
|
||||
},
|
||||
];
|
||||
@@ -632,7 +470,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
value: 'sourceDatabase',
|
||||
width: 190,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
showText: true,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
@@ -717,25 +554,26 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
]
|
||||
}).component();
|
||||
|
||||
this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
const migration = this._filteredMigrations[rowState.row];
|
||||
switch (buttonState?.column) {
|
||||
case 2:
|
||||
const status = getMigrationStatus(migration);
|
||||
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
|
||||
const errors = this.getMigrationErrors(migration!);
|
||||
this.disposables.push(
|
||||
this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
const migration = this._filteredMigrations[rowState.row];
|
||||
switch (buttonState?.column) {
|
||||
case 2:
|
||||
const status = getMigrationStatus(migration);
|
||||
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
|
||||
const errors = this.getMigrationErrors(migration!);
|
||||
|
||||
this.showDialogMessage(
|
||||
loc.DATABASE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errors);
|
||||
break;
|
||||
case 0:
|
||||
await this._openMigrationDetails(migration);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
this.showDialogMessage(
|
||||
loc.DATABASE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errors);
|
||||
break;
|
||||
case 0:
|
||||
await this._openMigrationDetails(migration);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
return this._statusTable;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user