Improvements in blob storage support for SQL Migration. (#15693)

* changing the cutover icon on migration cutover page.

* Fixing monitoring table and pending log backups

* converting file upload times in utc to local time zones

* adding autorefresh to dashboard, migration status and cutover dialogs.

* Supporting blob container e2e

* vbump extension

* Fixing some PR comments

* Fixed broken blob container dropdown onChange event

* Localizing display string in refresh dialog
Fixing some localized strings

* Fixing var declaration

* making a class readonly for 250px width

* removing refresh interval dialog and replacing it with hardcoded values.

* Fixing summary page IR information.

* surfacing test connection error

* Clearing intervals on view closed to remove auto refresh.
This commit is contained in:
Aasim Khan
2021-06-17 22:19:42 -07:00
committed by GitHub
parent 35832e83da
commit 488ccea731
16 changed files with 458 additions and 228 deletions

View File

@@ -1,3 +1,3 @@
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1.7L10.2 7L1 12.3V1.7ZM0 0V14L12.3 7L0 0Z" fill="#0078D4"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 0V0.5C16 1.44792 15.9167 2.33333 15.75 3.15625C15.5833 3.97917 15.3255 4.75781 14.9766 5.49219C14.6276 6.22656 14.1953 6.92448 13.6797 7.58594C13.1641 8.2474 12.5547 8.89583 11.8516 9.53125L10.3594 14H8V12.2266C7.28125 12.6484 6.55469 13.0547 5.82032 13.4453L2.55468 10.1797C2.94531 9.44531 3.35156 8.71875 3.77343 8H2.00001V5.64062L6.46876 4.15625C7.09896 3.45312 7.74479 2.84115 8.40625 2.32031C9.06771 1.79948 9.76822 1.36719 10.5078 1.02344C11.2474 0.679688 12.026 0.424479 12.8437 0.257812C13.6615 0.0911458 14.5469 0.00520833 15.5 0H16ZM4.35938 7C4.51042 6.76042 4.66146 6.52344 4.81251 6.28906C4.96355 6.05469 5.125 5.82292 5.29688 5.59375L3.00001 6.35938V7H4.35938ZM6.00782 12.2031C6.27344 12.0521 6.53907 11.9036 6.80469 11.7578C7.07032 11.612 7.33594 11.4609 7.60157 11.3047L4.69532 8.39844C4.54428 8.66406 4.39584 8.92969 4.25001 9.19531C4.10417 9.46094 3.95312 9.72917 3.79688 10L6.00782 12.2031ZM10.4062 10.7031C10.1771 10.8698 9.9453 11.0286 9.71093 11.1797C9.47656 11.3307 9.23958 11.4844 9 11.6406V13H9.64062L10.4062 10.7031ZM11.8359 8.14844C12.3516 7.63281 12.7995 7.10938 13.1797 6.57812C13.5599 6.04688 13.8828 5.48958 14.1484 4.90625C14.4141 4.32292 14.612 3.71094 14.7422 3.07031C14.8724 2.42969 14.9557 1.74219 14.9922 1.00781C14.263 1.03385 13.5781 1.11458 12.9375 1.25C12.2969 1.38542 11.6849 1.58594 11.1016 1.85156C10.5182 2.11719 9.95831 2.4375 9.42188 2.8125C8.88542 3.1875 8.35678 3.63542 7.83594 4.15625C7.32032 4.66146 6.84896 5.19271 6.42188 5.75C5.99479 6.30729 5.59376 6.89583 5.21876 7.51562L8.48438 10.7812C9.09896 10.4062 9.68489 10.0052 10.2422 9.57812C10.7995 9.15104 11.3307 8.67448 11.8359 8.14844ZM10 8C9.72396 8 9.46614 7.94792 9.22656 7.84375C8.98697 7.73958 8.77343 7.59635 8.58593 7.41406C8.39843 7.23177 8.25521 7.02083 8.15625 6.78125C8.05729 6.54167 8.00521 6.28125 8 6C8 5.72396 8.05208 5.46615 8.15625 5.22656C8.26042 4.98698 8.40364 4.77344 8.58593 4.58594C8.76822 4.39844 8.97917 4.25521 9.21875 4.15625C9.45833 4.05729 9.71875 4.00521 10 4C10.276 4 10.5338 4.05208 10.7734 4.15625C11.013 4.26042 11.2266 4.40365 11.4141 4.58594C11.6016 4.76823 11.7448 4.97917 11.8437 5.21875C11.9427 5.45833 11.9948 5.71875 12 6C12 6.27604 11.9479 6.53385 11.8437 6.77344C11.7396 7.01302 11.5963 7.22656 11.4141 7.41406C11.2318 7.60156 11.0208 7.74479 10.7812 7.84375C10.5417 7.94271 10.2812 7.99479 10 8ZM10 5C9.8594 5 9.72917 5.02604 9.60938 5.07812C9.48958 5.13021 9.38542 5.20052 9.29688 5.28906C9.20833 5.3776 9.13542 5.48438 9.07812 5.60938C9.02083 5.73438 8.99479 5.86458 9 6C9 6.14062 9.02604 6.27083 9.07812 6.39062C9.13021 6.51042 9.20052 6.61458 9.28906 6.70312C9.3776 6.79167 9.48438 6.86458 9.60938 6.92188C9.73438 6.97917 9.8646 7.00521 10 7C10.1406 7 10.2708 6.97396 10.3906 6.92188C10.5104 6.86979 10.6146 6.79948 10.7031 6.71094C10.7917 6.6224 10.8646 6.51562 10.9219 6.39062C10.9792 6.26562 11.0052 6.13542 11 6C11 5.85938 10.974 5.72917 10.9219 5.60938C10.8698 5.48958 10.7995 5.38542 10.7109 5.29688C10.6224 5.20833 10.5156 5.13542 10.3906 5.07812C10.2656 5.02083 10.1354 4.99479 10 5ZM2.00001 12C2.27605 12 2.53385 12.0521 2.77343 12.1562C3.01302 12.2604 3.22656 12.4036 3.41406 12.5859C3.60156 12.7682 3.7448 12.9792 3.84376 13.2188C3.9427 13.4583 3.9948 13.7188 4.00001 14C4.00001 14.276 3.94792 14.5339 3.84376 14.7734C3.73958 15.013 3.59635 15.2266 3.41406 15.4141C3.23177 15.6016 3.02083 15.7448 2.78126 15.8438C2.54167 15.9427 2.28126 15.9948 2.00001 16H0V14C0 13.724 0.052084 13.4661 0.15625 13.2266C0.260417 12.987 0.403646 12.7734 0.585938 12.5859C0.768229 12.3984 0.979165 12.2552 1.21876 12.1562C1.45834 12.0573 1.71876 12.0052 2.00001 12ZM2.00001 15C2.14062 15 2.27083 14.974 2.39062 14.9219C2.51042 14.8698 2.61458 14.7995 2.70312 14.7109C2.79167 14.6224 2.86458 14.5156 2.92188 14.3906C2.97917 14.2656 3.0052 14.1354 3.00001 14C3.00001 13.8594 2.97395 13.7292 2.92188 13.6094C2.8698 13.4896 2.79947 13.3854 2.71093 13.2969C2.62239 13.2083 2.51562 13.1354 2.39062 13.0781C2.26562 13.0208 2.13542 12.9948 2.00001 13C1.85938 13 1.72916 13.026 1.60938 13.0781C1.48959 13.1302 1.38541 13.2005 1.29688 13.2891C1.20834 13.3776 1.13541 13.4844 1.07812 13.6094C1.02084 13.7344 0.994795 13.8646 1.00001 14V15H2.00001Z" fill="#0078D4"/>
</svg>

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -2,7 +2,7 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.1.2",
"version": "0.1.3",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",

View File

@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azurecore from 'azurecore';
import { azureResource } from 'azureResource';
import * as constants from '../constants/strings';
async function getAzureCoreAPI(): Promise<azurecore.IExtension> {
const api = (await vscode.extensions.getExtension(azurecore.extension.name)?.activate()) as azurecore.IExtension;
@@ -159,6 +160,22 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
const asyncUrl = response.response.headers['azure-asyncoperation'];
const maxRetry = 5;
let i = 0;
for (i = 0; i < maxRetry; i++) {
const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true);
const creationStatus = asyncResponse.response.data.status;
if (creationStatus === 'Succeeded') {
break;
} else if (creationStatus === 'Failed') {
throw new Error(asyncResponse.errors.toString());
}
await new Promise(resolve => setTimeout(resolve, 3000)); //adding 3 sec delay before getting creation status
}
if (i === maxRetry) {
throw new Error(constants.DMS_PROVISIONING_FAILED);
}
return response.response.data;
}
@@ -426,6 +443,8 @@ export interface MigrationStatusDetails {
fileUploadBlockingErrors: string[];
currentRestoringFileName: string;
lastRestoredFilename: string;
pendingLogBackupsCount: number;
invalidFiles: string[];
}
export interface SqlConnectionInfo {
@@ -462,6 +481,8 @@ export interface BackupSetInfo {
isBackupRestored: boolean;
backupSize: number;
compressedBackupSize: number;
hasBackupChecksums: boolean;
familyCount: number;
}
export interface SourceLocation {
@@ -477,6 +498,12 @@ export interface TargetLocation {
export interface BackupFileInfo {
fileName: string;
status: 'Arrived' | 'Uploading' | 'Uploaded' | 'Restoring' | 'Restored' | 'Cancelled' | 'Ignored';
totalSize: number;
dataRead: number;
dataWritten: number;
copyThroughput: number;
copyDuration: number;
familySequenceNumber: number;
}
export interface DatabaseMigrationFileShare {

View File

@@ -122,6 +122,25 @@ export function filterMigrations(databaseMigrations: MigrationContext[], statusF
return filteredMigration;
}
export function convertByteSizeToReadableUnit(size: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
for (let i = 1; i < units.length; i++) {
const higherUnit = size / 1024;
if (higherUnit < 0.1) {
return `${size.toFixed(2)} ${units[i - 1]}`;
}
size = higherUnit;
}
return size.toString();
}
export function convertIsoTimeToLocalTime(isoTime: string): Date {
let isoDate = new Date(isoTime);
return new Date(isoDate.getTime() + (isoDate.getTimezoneOffset() * 60000));
}
export type SupportedAutoRefreshIntervals = -1 | 15000 | 30000 | 60000 | 180000 | 300000;
export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void {
if (index >= 0 && dropDown.values && index <= dropDown.values.length - 1) {
const value = dropDown.values[index];

View File

@@ -5,6 +5,7 @@
import { AzureAccount } from 'azurecore';
import * as nls from 'vscode-nls';
import { SupportedAutoRefreshIntervals } from '../api/utils';
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
const localize = nls.loadMessageBundle();
@@ -182,7 +183,7 @@ export const SERVICE_CONTAINER_DESCRIPTION2 = localize('sql.migration.service.co
export const SERVICE_STEP1 = localize('sql.migration.ir.setup.step1', "Step 1: {0}");
export const SERVICE_STEP1_LINK = localize('sql.migration.option', "Download and install integration runtime");
export const SERVICE_STEP2 = localize('sql.migration.ir.setup.step2', "Step 2: Use this key to register your integration runtime");
export const SERVICE_STEP3 = localize('sql.migration.ir.setup.step3', "Step 3: Check connection between Azure Database Migration Service and Integration Runtime");
export const SERVICE_STEP3 = localize('sql.migration.ir.setup.step3', "Step 3: Click on 'Test connection' button to check the connection between Azure Database Migration Service and Integration Runtime");
export const SERVICE_CONNECTION_STATUS = localize('sql.migration.connection.status', "Connection Status");
export const SERVICE_KEY1_LABEL = localize('sql.migration.key1.label', "Key 1");
export const SERVICE_KEY2_LABEL = localize('sql.migration.key2.label', "Key 2");
@@ -209,7 +210,9 @@ export const MANAGED_INSTANCE = localize('sql.migration.managed.instance', "Azur
export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instance found");
export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachine.found', "No virtual machine found");
export const TARGET_SELECTION_PAGE_TITLE = localize('sql.migration.target.page.title', "Choose the target Azure SQL");
export const TEST_CONNECTION = localize('sql.migration.test.connection', "Test connection");
export const DATA_MIGRATION_SERVICE_CREATED_SUCCESSFULLY = localize('sql.migration.database.migration.service.created.successfully', "Database migration service has been created successfully");
export const DMS_PROVISIONING_FAILED = localize('sql.migration.dms.provision.failed', "Database migration service has failed to provision. Please try again after some time.");
// common strings
export const LEARN_MORE = localize('sql.migration.learn.more', "Learn more");
export const SUBSCRIPTION = localize('sql.migration.subscription', "Subscription");
@@ -230,6 +233,9 @@ export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User Ac
export const VIEW_ALL = localize('sql.migration.view.all', "View All");
export const TARGET = localize('sql.migration.target', "Target");
export const AZURE_SQL = localize('sql.migration.azure.sql', "Azure SQL");
export const CLOSE = localize('sql.migration.close', "Close");
export const DATA_UPLOADED = localize('sql.migraiton.data.uploaded.size', "Data Uploaded/Size");
export const COPY_THROUGHPUT = localize('sql.migration.copy.throughput', "Copy Throughput (MBPS)");
//Summary Page
export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', "Summary");
@@ -331,15 +337,16 @@ export const YES = localize('sql.migration.yes', "Yes");
export const NO = localize('sql.migration.no', "No");
//Migration confirm cutover dialog
export const COMPLETING_CUTOVER_WARNING = localize('sql.migration.completing.cutover.warning', "Completing cutover without restoring all the backup(s) may result in loss of data.");
export const BUSINESS_CRITICAL_INFO = localize('sql.migration.bc.info', "Managed Instance migration cutover for Business Critical service tier can take significantly longer than General Purpose as three secondary replicas have to be seeded for Always On High Availability group. This operation duration depends on the size of data. Seeding speed in 90% of cases is 220 GB/hour or higher.");
export const CUTOVER_HELP_MAIN = localize('sql.migration.cutover.help.main', "When you are ready to do the migration cutover, perform the following steps to complete the database migration. Please note that the database is ready for cutover only after a full backup has been restored on the target Azure SQL Database Managed Instance.");
export const CUTOVER_HELP_STEP1 = localize('sql.migration.cutover.step.1', "1. Stop all the incoming transactions coming to the source database.");
export const CUTOVER_HELP_STEP2 = localize('sql.migration.cutover.step.2', "2. Take the final transaction log backup and provide backup file in the SMB network share.");
export const CUTOVER_HELP_STEP3 = localize('sql.migration.cutover.step.3', "3. Make sure all the pending log backups are restored on the target. At that point, “Pending log backups” counter shows zero and then perform the cutover. Performing cutover operation without applying all the transaction log backup files may result in loss of data.");
export const CUTOVER_HELP_STEP2 = localize('sql.migration.cutover.step.2', "2. Take final transaction log backup and provide it in the network share location.");
export const CUTOVER_HELP_STEP3 = localize('sql.migration.cutover.step.3', "3. Make sure all the log backups are restored on target database. The \"Log backups(s) pending restore\" should be zero.");
export function PENDING_BACKUPS(count: number): string {
return localize('sql.migartion.cutover.pending.backup', "Pending log backups: {0}", count);
}
export const CONFIRM_CUTOVER_CHECKBOX = localize('sql.migration.confirm.checkbox.message', "Confirm all pending log backups are restored");
export const CONFIRM_CUTOVER_CHECKBOX = localize('sql.migration.confirm.checkbox.message', "I confirm there are no additional log backup(s) to provide and want to complete cutover.");
export function CUTOVER_IN_PROGRESS(dbName: string): string {
return localize('sql.migration.cutover.in.progress', "Cutover in progress for database '{0}'", dbName);
}
@@ -444,3 +451,28 @@ export function WARNINGS_COUNT(totalCount: number): string {
export const AUTHENTICATION_TYPE = localize('sql.migration.authentication.type', "Authentication Type");
export const SQL_LOGIN = localize('sql.migration.sql.login', "SQL Login");
export const WINDOWS_AUTHENTICATION = localize('sql.migration.windows.auth', "Windows Authentication");
//AutoRefresh
export function AUTO_REFRESH_BUTTON_TEXT(interval: SupportedAutoRefreshIntervals): string {
switch (interval) {
case -1:
return localize('sql.migration.auto.refresh.off', 'Auto Refresh: Off');
case 15000:
return localize('sql.migration.auto.refresh.15.seconds', 'Auto refresh: 15 seconds');
case 30000:
return localize('sql.migration.auto.refresh.30.seconds', 'Auto refresh: 30 seconds');
case 60000:
return localize('sql.migration.auto.refresh.1.min', 'Auto refresh: 1 minute');
case 180000:
return localize('sql.migration.auto.refresh.3.min', 'Auto refresh: 3 minutes');
case 300000:
return localize('sql.migration.auto.refresh.5.min', 'Auto refresh: 5 minutes');
}
}
export const SELECT_THE_REFRESH_INTERVAL = localize('sql.migration.select.the.refresh.interval', "Select the refresh interval");
export const OFF = localize('sql.migration.off', "Off");
export const EVERY_30_SECOND = localize('sql.migration.every.30.second', "Every 30 seconds");
export const EVERY_1_MINUTE = localize('sql.migration.every.1.minute', "Every 1 minute");
export const EVERY_3_MINUTES = localize('sql.migration.every.3.minutes', "Every 3 minutes");
export const EVERY_5_MINUTES = localize('sql.migration.every.5.minutes', "Every 5 minutes");

View File

@@ -10,7 +10,7 @@ import * as loc from '../constants/strings';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { filterMigrations } from '../api/utils';
import { filterMigrations, SupportedAutoRefreshIntervals } from '../api/utils';
interface IActionMetadata {
title?: string,
@@ -21,6 +21,7 @@ interface IActionMetadata {
}
const maxWidth = 800;
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
interface StatusCard {
container: azdata.DivContainer;
@@ -46,10 +47,9 @@ export class DashboardWidget {
private _migrationStatusMap: Map<string, MigrationContext[]> = new Map();
private _viewAllMigrationsButton!: azdata.ButtonComponent;
private _autoRefreshHandle!: NodeJS.Timeout;
constructor() {
vscode.commands.registerCommand('sqlmigration.refreshMigrationTiles', () => {
this.refreshMigrations();
});
}
private async getCurrentMigrations(): Promise<MigrationContext[]> {
@@ -95,7 +95,9 @@ export class DashboardWidget {
}
});
await view.initializeModel(container);
this._view.onClosed((e) => {
clearInterval(this._autoRefreshHandle);
});
this.refreshMigrations();
});
}
@@ -107,11 +109,19 @@ export class DashboardWidget {
}).component();
const titleComponent = view.modelBuilder.text().withProps({
value: loc.DASHBOARD_TITLE,
width: '750px',
CSSStyles: {
'font-size': '36px',
'margin-bottom': '5px',
}
}).component();
this.setAutoRefresh(refreshFrequency);
const container = view.modelBuilder.flexContainer().withItems([
titleComponent,
]).component();
const descComponent = view.modelBuilder.text().withProps({
value: loc.DASHBOARD_DESCRIPTION,
CSSStyles: {
@@ -119,7 +129,7 @@ export class DashboardWidget {
'margin-top': '10px',
}
}).component();
header.addItems([titleComponent, descComponent], {
header.addItems([container, descComponent], {
CSSStyles: {
'width': `${maxWidth}px`,
'padding-left': '20px'
@@ -231,6 +241,14 @@ export class DashboardWidget {
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
}
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
let classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(function () { classVariable.refreshMigrations(); }, interval);
}
}
private async refreshMigrations(): Promise<void> {
this._viewAllMigrationsButton.enabled = false;
this._migrationStatusCardLoadingContainer.loading = true;

View File

@@ -58,7 +58,7 @@ export class AssessmentResultsDialog {
public async openDialog(dialogName?: string) {
if (!this._isOpen) {
this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(this.title, this.title, '90%');
this.dialog = azdata.window.createModelViewDialog(this.title, this.title, 'wide');
this.dialog.okButton.label = AssessmentResultsDialog.OkButtonText;
this.dialog.okButton.onClick(async () => await this.execute());

View File

@@ -5,17 +5,18 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { createSqlMigrationService, getSqlMigrationService, getResourceGroups, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../../api/azure';
import { MigrationStateModel } from '../../models/stateMachine';
import { createSqlMigrationService, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../../api/azure';
import { MigrationStateModel, NetworkContainerType } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import * as os from 'os';
import { azureResource } from 'azureResource';
import { IntergrationRuntimePage } from '../../wizard/integrationRuntimePage';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { selectDropDownIndex } from '../../api/utils';
import * as EventEmitter from 'events';
export class CreateSqlMigrationServiceDialog {
private _model!: MigrationStateModel;
private migrationServiceSubscription!: azdata.TextComponent;
private migrationServiceResourceGroupDropdown!: azdata.DropDownComponent;
private migrationServiceLocation!: azdata.InputBoxComponent;
@@ -23,6 +24,7 @@ export class CreateSqlMigrationServiceDialog {
private _formSubmitButton!: azdata.ButtonComponent;
private _statusLoadingComponent!: azdata.LoadingComponent;
private _refreshLoadingComponent!: azdata.LoadingComponent;
private migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent;
private _connectionStatus!: azdata.InfoBoxComponent;
private _copyKey1Button!: azdata.ButtonComponent;
@@ -30,18 +32,24 @@ export class CreateSqlMigrationServiceDialog {
private _refreshKey1Button!: azdata.ButtonComponent;
private _refreshKey2Button!: azdata.ButtonComponent;
private _setupContainer!: azdata.FlexContainer;
private _resourceGroupPreset!: string;
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
private createdMigrationService!: SqlMigrationService;
private createdMigrationServiceNodeNames!: string[];
private _createdMigrationService!: SqlMigrationService;
private _selectedResourceGroup!: string;
private _testConnectionButton!: azdata.window.Button;
constructor(private migrationStateModel: MigrationStateModel, private irPage: IntergrationRuntimePage) {
private _doneButtonEvent: EventEmitter = new EventEmitter();
private _isBlobContainerUsed: boolean = false;
private irNodes: string[] = [];
public async createNewDms(migrationStateModel: MigrationStateModel, resourceGroupPreset: string): Promise<CreateSqlMigrationServiceDialogResult> {
this._model = migrationStateModel;
this._resourceGroupPreset = resourceGroupPreset;
this._dialogObject = azdata.window.createModelViewDialog(constants.CREATE_MIGRATION_SERVICE_TITLE, 'MigrationServiceDialog', 'medium');
}
initialize() {
let tab = azdata.window.createTab('');
this._dialogObject.registerCloseValidator(async () => {
return true;
@@ -63,9 +71,9 @@ export class CreateSqlMigrationServiceDialog {
this.setFormEnabledState(false);
const subscription = this.migrationStateModel._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
const location = this.migrationStateModel._targetServerInstance.location;
const subscription = this._model._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).displayName;
const location = this._model._targetServerInstance.location;
const serviceName = this.migrationServiceNameText.value;
const formValidationErrors = this.validateCreateServiceForm(subscription, resourceGroup, location, serviceName);
@@ -78,9 +86,10 @@ export class CreateSqlMigrationServiceDialog {
}
try {
this.createdMigrationService = await createSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, serviceName!);
if (this.createdMigrationService.error) {
this.setDialogMessage(`${this.createdMigrationService.error.code} : ${this.createdMigrationService.error.message}`);
this._selectedResourceGroup = resourceGroup;
this._createdMigrationService = await createSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, serviceName!);
if (this._createdMigrationService.error) {
this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`);
this._statusLoadingComponent.loading = false;
this.setFormEnabledState(true);
return;
@@ -88,10 +97,22 @@ export class CreateSqlMigrationServiceDialog {
this._dialogObject.message = {
text: ''
};
await this.refreshAuthTable();
await this.refreshStatus();
this._setupContainer.display = 'inline';
this._statusLoadingComponent.loading = false;
if (this._isBlobContainerUsed) {
this._dialogObject.okButton.enabled = true;
this._statusLoadingComponent.loading = false;
this._setupContainer.display = 'none';
this._dialogObject.message = {
text: constants.DATA_MIGRATION_SERVICE_CREATED_SUCCESSFULLY,
level: azdata.window.MessageLevel.Information
};
} else {
await this.refreshStatus();
await this.refreshAuthTable();
this._setupContainer.display = 'inline';
this._testConnectionButton.hidden = false;
this._statusLoadingComponent.loading = false;
}
} catch (e) {
console.log(e);
this.setDialogMessage(e.message);
@@ -135,13 +156,45 @@ export class CreateSqlMigrationServiceDialog {
});
});
this._testConnectionButton = azdata.window.createButton(constants.TEST_CONNECTION);
this._testConnectionButton.hidden = true;
this._testConnectionButton.onClick(async (e) => {
this._refreshLoadingComponent.loading = true;
this._connectionStatus.updateCssStyles({
'display': 'none'
});
try {
await this.refreshStatus();
} catch (e) {
vscode.window.showErrorMessage(e);
}
this._connectionStatus.updateCssStyles({
'display': 'inline'
});
this._refreshLoadingComponent.loading = false;
});
this._dialogObject.customButtons = [this._testConnectionButton];
this._dialogObject.content = [tab];
this._dialogObject.okButton.enabled = false;
azdata.window.openDialog(this._dialogObject);
this._dialogObject.cancelButton.onClick((e) => {
});
this._dialogObject.okButton.onClick((e) => {
this.irPage.populateMigrationService(this.createdMigrationService, this.createdMigrationServiceNodeNames, (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name);
this._doneButtonEvent.emit('done', this._createdMigrationService, this._selectedResourceGroup);
});
this._isBlobContainerUsed = this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER;
return new Promise((resolve) => {
this._doneButtonEvent.once('done', (createdDms: SqlMigrationService, selectedResourceGroup: string) => {
azdata.window.closeDialog(this._dialogObject);
resolve(
{
service: createdDms,
resourceGroup: selectedResourceGroup
});
});
});
}
@@ -201,7 +254,7 @@ export class CreateSqlMigrationServiceDialog {
this.migrationServiceLocation = this._view.modelBuilder.inputBox().withProps({
required: true,
enabled: false,
value: await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)
value: await this._model.getLocationDisplayName(this._model._targetServerInstance.location)
}).component();
const targetlabel = this._view.modelBuilder.text().withProps({
@@ -254,33 +307,19 @@ export class CreateSqlMigrationServiceDialog {
private async populateSubscriptions(): Promise<void> {
this.migrationServiceResourceGroupDropdown.loading = true;
this.migrationServiceSubscription.value = this.migrationStateModel._targetSubscription.name;
this.migrationServiceSubscription.value = this._model._targetSubscription.name;
await this.populateResourceGroups();
}
private async populateResourceGroups(): Promise<void> {
this.migrationServiceResourceGroupDropdown.loading = true;
let subscription = this.migrationStateModel._targetSubscription;
const resourceGroups = await getResourceGroups(this.migrationStateModel._azureAccount, subscription);
let resourceGroupDropdownValues: azdata.CategoryValue[] = [];
if (resourceGroups && resourceGroups.length > 0) {
resourceGroups.forEach((resourceGroup) => {
resourceGroupDropdownValues.push({
name: resourceGroup.name,
displayName: resourceGroup.name
});
});
} else {
resourceGroupDropdownValues = [
{
displayName: constants.RESOURCE_GROUP_NOT_FOUND,
name: ''
}
];
try {
this.migrationServiceResourceGroupDropdown.values = await this._model.getAzureResourceGroupDropdownValues(this._model._targetSubscription);
const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.displayName.toLowerCase() === this._resourceGroupPreset.toLowerCase());
this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0];
} finally {
this.migrationServiceResourceGroupDropdown.loading = false;
}
this.migrationServiceResourceGroupDropdown.values = resourceGroupDropdownValues;
selectDropDownIndex(this.migrationServiceResourceGroupDropdown, 0);
this.migrationServiceResourceGroupDropdown.loading = false;
}
private createServiceStatus(): azdata.FlexContainer {
@@ -327,9 +366,8 @@ export class CreateSqlMigrationServiceDialog {
}
}).component();
const irSetupStep3Text = this._view.modelBuilder.hyperlink().withProps({
label: constants.SERVICE_STEP3,
url: '',
const irSetupStep3Text = this._view.modelBuilder.text().withProps({
value: constants.SERVICE_STEP3,
CSSStyles: {
'margin-top': '10px',
'margin-bottom': '10px',
@@ -337,23 +375,6 @@ export class CreateSqlMigrationServiceDialog {
}
}).component();
irSetupStep3Text.onDidClick(async (e) => {
refreshLoadingIndicator.loading = true;
this._connectionStatus.updateCssStyles({
'display': 'none'
});
try {
await this.refreshStatus();
} catch (e) {
console.log(e);
}
this._connectionStatus.updateCssStyles({
'display': 'inline'
});
refreshLoadingIndicator.loading = false;
});
this._connectionStatus = this._view.modelBuilder.infoBox().withProps({
text: '',
style: 'error',
@@ -366,7 +387,7 @@ export class CreateSqlMigrationServiceDialog {
'width': '350px'
};
const refreshLoadingIndicator = this._view.modelBuilder.loadingComponent().withProps({
this._refreshLoadingComponent = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
CSSStyles: {
'font-size': '13px'
@@ -428,7 +449,7 @@ export class CreateSqlMigrationServiceDialog {
this.migrationServiceAuthKeyTable,
irSetupStep3Text,
this._connectionStatus,
refreshLoadingIndicator
this._refreshLoadingComponent
], {
CSSStyles: {
'margin-bottom': '5px'
@@ -439,16 +460,28 @@ export class CreateSqlMigrationServiceDialog {
}).component();
this._setupContainer.display = 'none';
this._testConnectionButton.hidden = true;
return this._setupContainer;
}
private async refreshStatus(): Promise<void> {
const subscription = this.migrationStateModel._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
const location = this.migrationStateModel._targetServerInstance.location;
const migrationServiceStatus = await getSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name);
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name);
this.createdMigrationServiceNodeNames = migrationServiceMonitoringStatus.nodes.map((node) => {
const subscription = this._model._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).displayName;
const location = this._model._targetServerInstance.location;
const maxRetries = 5;
let migrationServiceStatus!: SqlMigrationService;
for (let i = 0; i < maxRetries; i++) {
try {
migrationServiceStatus = await getSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService.name);
break;
} catch (e) {
console.log(e);
}
await new Promise(r => setTimeout(r, 5000));
}
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name);
this.irNodes = migrationServiceMonitoringStatus.nodes.map((node) => {
return node.nodeName;
});
if (migrationServiceStatus) {
@@ -456,7 +489,7 @@ export class CreateSqlMigrationServiceDialog {
if (state === 'Online') {
this._connectionStatus.updateProperties(<azdata.InfoBoxComponentProperties>{
text: constants.SERVICE_READY(this.createdMigrationService!.name, this.createdMigrationServiceNodeNames.join(', ')),
text: constants.SERVICE_READY(this._createdMigrationService!.name, this.irNodes.join(', ')),
style: 'success',
CSSStyles: {
'font-size': '13px'
@@ -464,9 +497,9 @@ export class CreateSqlMigrationServiceDialog {
});
this._dialogObject.okButton.enabled = true;
} else {
this._connectionStatus.text = constants.SERVICE_NOT_READY(this.createdMigrationService!.name);
this._connectionStatus.text = constants.SERVICE_NOT_READY(this._createdMigrationService!.name);
this._connectionStatus.updateProperties(<azdata.InfoBoxComponentProperties>{
text: constants.SERVICE_NOT_READY(this.createdMigrationService!.name),
text: constants.SERVICE_NOT_READY(this._createdMigrationService!.name),
style: 'warning',
CSSStyles: {
'font-size': '13px'
@@ -478,10 +511,10 @@ export class CreateSqlMigrationServiceDialog {
}
private async refreshAuthTable(): Promise<void> {
const subscription = this.migrationStateModel._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
const location = this.migrationStateModel._targetServerInstance.location;
const keys = await getSqlMigrationServiceAuthKeys(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name);
const subscription = this._model._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).displayName;
const location = this._model._targetServerInstance.location;
const keys = await getSqlMigrationServiceAuthKeys(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name);
this._copyKey1Button = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.copy
@@ -557,3 +590,8 @@ export class CreateSqlMigrationServiceDialog {
this.migrationServiceNameText.enabled = enable;
}
}
export interface CreateSqlMigrationServiceDialogResult {
service: SqlMigrationService,
resourceGroup: string
}

View File

@@ -41,21 +41,6 @@ export class ConfirmCutoverDialog {
const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component();
let infoDisplay = 'none';
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
infoDisplay = 'inline';
}
const businessCriticalinfoBox = this._view.modelBuilder.infoBox().withProps({
text: constants.BUSINESS_CRITICAL_INFO,
style: 'information',
CSSStyles: {
'font-size': '13px',
'display': infoDisplay
}
}).component();
const helpMainText = this._view.modelBuilder.text().withProps({
value: constants.CUTOVER_HELP_MAIN,
CSSStyles: {
@@ -73,7 +58,7 @@ export class ConfirmCutoverDialog {
}).component();
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length ?? 0;
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount ?? 0;
const pendingText = this._view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '13px',
@@ -93,6 +78,29 @@ export class ConfirmCutoverDialog {
this._dialogObject.okButton.enabled = e;
});
const cutoverWarning = this._view.modelBuilder.infoBox().withProps({
text: constants.COMPLETING_CUTOVER_WARNING,
style: 'warning',
CSSStyles: {
'font-size': '13px',
}
}).component();
let infoDisplay = 'none';
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
infoDisplay = 'inline';
}
const businessCriticalinfoBox = this._view.modelBuilder.infoBox().withProps({
text: constants.BUSINESS_CRITICAL_INFO,
style: 'information',
CSSStyles: {
'font-size': '13px',
'display': infoDisplay
}
}).component();
const container = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
@@ -100,11 +108,12 @@ export class ConfirmCutoverDialog {
completeCutoverText,
sourceDatabaseText,
separator,
businessCriticalinfoBox,
helpMainText,
helpStepsText,
pendingText,
confirmCheckbox
confirmCheckbox,
cutoverWarning,
businessCriticalinfoBox
]).component();

View File

@@ -8,11 +8,13 @@ import { IconPathHelper } from '../../constants/iconPathHelper';
import { MigrationContext } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialogModel, MigrationStatus } from './migrationCutoverDialogModel';
import * as loc from '../../constants/strings';
import { getSqlServerName } from '../../api/utils';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, SupportedAutoRefreshIntervals } from '../../api/utils';
import { EOL } from 'os';
import * as vscode from 'vscode';
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
const refreshFrequency: SupportedAutoRefreshIntervals = 30000;
export class MigrationCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -41,10 +43,13 @@ export class MigrationCutoverDialog {
private _fileCount!: azdata.TextComponent;
private fileTable!: azdata.TableComponent;
private _autoRefreshHandle!: any;
readonly _infoFieldWidth: string = '250px';
constructor(migration: MigrationContext) {
this._model = new MigrationCutoverDialogModel(migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 1000);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
}
async initialize(): Promise<void> {
@@ -65,17 +70,17 @@ export class MigrationCutoverDialog {
flexServer.addItem(sourceDatabase.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexServer.addItem(sourceDetails.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexServer.addItem(sourceVersion.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
@@ -93,17 +98,17 @@ export class MigrationCutoverDialog {
flexTarget.addItem(targetDatabase.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexTarget.addItem(targetServer.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexTarget.addItem(targetVersion.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
@@ -122,17 +127,17 @@ export class MigrationCutoverDialog {
flexStatus.addItem(migrationStatus.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexStatus.addItem(fullBackupFileOn.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexStatus.addItem(backupLocation.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
@@ -150,30 +155,28 @@ export class MigrationCutoverDialog {
}).component();
flexFile.addItem(lastSSN.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexFile.addItem(lastAppliedBackup.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
flexFile.addItem(lastAppliedBackupOn.flexContainer, {
CSSStyles: {
'width': '200px'
'width': this._infoFieldWidth
}
});
const flexInfo = view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'width': '800px',
}
width: 1000
}).component();
flexInfo.addItem(flexServer, {
flex: '0',
CSSStyles: {
'flex': '0',
'width': '200px'
'width': this._infoFieldWidth
}
});
@@ -181,7 +184,7 @@ export class MigrationCutoverDialog {
flex: '0',
CSSStyles: {
'flex': '0',
'width': '200px'
'width': this._infoFieldWidth
}
});
@@ -189,7 +192,7 @@ export class MigrationCutoverDialog {
flex: '0',
CSSStyles: {
'flex': '0',
'width': '200px'
'width': this._infoFieldWidth
}
});
@@ -197,7 +200,7 @@ export class MigrationCutoverDialog {
flex: '0',
CSSStyles: {
'flex': '0',
'width': '200px'
'width': this._infoFieldWidth
}
});
@@ -213,7 +216,7 @@ export class MigrationCutoverDialog {
columns: [
{
value: loc.ACTIVE_BACKUP_FILES,
width: 280,
width: 230,
type: azdata.ColumnType.text,
},
{
@@ -226,23 +229,36 @@ export class MigrationCutoverDialog {
width: 60,
type: azdata.ColumnType.text
},
{
value: loc.DATA_UPLOADED,
width: 120,
type: azdata.ColumnType.text
},
{
value: loc.COPY_THROUGHPUT,
width: 150,
type: azdata.ColumnType.text
},
{
value: loc.BACKUP_START_TIME,
width: 130,
type: azdata.ColumnType.text
}, {
},
{
value: loc.FIRST_LSN,
width: 120,
type: azdata.ColumnType.text
}, {
},
{
value: loc.LAST_LSN,
width: 120,
type: azdata.ColumnType.text
}
],
data: [],
width: '800px',
width: '1100px',
height: '300px',
fontSize: '12px'
}).component();
const formBuilder = view.modelBuilder.formContainer().withFormItems(
@@ -251,13 +267,13 @@ export class MigrationCutoverDialog {
component: this.migrationContainerHeader()
},
{
component: this._view.modelBuilder.separator().withProps({ width: '800px' }).component()
component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component()
},
{
component: flexInfo
},
{
component: this._view.modelBuilder.separator().withProps({ width: '800px' }).component()
component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component()
},
{
component: this._fileCount
@@ -271,11 +287,21 @@ export class MigrationCutoverDialog {
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
this._view.onClosed(e => {
clearInterval(this._autoRefreshHandle);
});
return view.initializeModel(form).then((value) => {
this.refreshStatus();
});
});
this._dialogObject.content = [tab];
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
this._dialogObject.okButton.onClick(e => {
clearInterval(this._autoRefreshHandle);
});
azdata.window.openDialog(this._dialogObject);
}
@@ -295,6 +321,7 @@ export class MigrationCutoverDialog {
'font-weight': 'bold',
'margin': '0px'
},
width: 950,
value: this._model._migration.migrationContext.properties.sourceDatabaseName
}).component();
@@ -303,6 +330,7 @@ export class MigrationCutoverDialog {
'font-size': '10px',
'margin': '5px 0px'
},
width: 950,
value: loc.DATABASE
}).component();
@@ -311,31 +339,43 @@ export class MigrationCutoverDialog {
databaseSubTitle
]).withLayout({
'flexFlow': 'column'
}).withProps({
width: 950
}).component();
this.setAutoRefresh(refreshFrequency);
const titleLogoContainer = this._view.modelBuilder.flexContainer().component();
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
titleLogoContainer.addItem(sqlDatbaseLogo, {
flex: '0'
});
titleLogoContainer.addItem(titleContainer, {
flex: '0',
CSSStyles: {
'margin-left': '5px'
'margin-left': '5px',
'width': '930px'
}
});
const headerActions = this._view.modelBuilder.flexContainer().withLayout({
}).withProps({
width: 1000
}).component();
this._cutoverButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cutover,
iconHeight: '14px',
iconWidth: '12px',
iconHeight: '16px',
iconWidth: '16px',
label: loc.COMPLETE_CUTOVER,
height: '20px',
width: '130px',
enabled: false
width: '150px',
enabled: false,
CSSStyles: {
'font-size': '13px'
}
}).component();
this._cutoverButton.onDidClick(async (e) => {
@@ -355,7 +395,10 @@ export class MigrationCutoverDialog {
iconWidth: '16px',
label: loc.CANCEL_MIGRATION,
height: '20px',
width: '120px'
width: '150px',
CSSStyles: {
'font-size': '13px'
}
}).component();
this._cancelButton.onDidClick((e) => {
@@ -378,7 +421,10 @@ export class MigrationCutoverDialog {
iconWidth: '16px',
label: 'Refresh',
height: '20px',
width: '65px'
width: '100px',
CSSStyles: {
'font-size': '13px'
}
}).component();
this._refreshButton.onDidClick((e) => {
@@ -395,7 +441,10 @@ export class MigrationCutoverDialog {
iconWidth: '16px',
label: loc.COPY_MIGRATION_DETAILS,
height: '20px',
width: '150px'
width: '200px',
CSSStyles: {
'font-size': '13px'
}
}).component();
this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
@@ -435,6 +484,10 @@ export class MigrationCutoverDialog {
titleLogoContainer
]).withLayout({
flexFlow: 'column'
}).withProps({
CSSStyles: {
width: 1000
}
}).component();
header.addItem(headerActions, {
@@ -446,6 +499,14 @@ export class MigrationCutoverDialog {
return header;
}
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
const classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(function () { classVariable.refreshStatus(); }, interval);
}
}
private async refreshStatus(): Promise<void> {
try {
@@ -478,7 +539,6 @@ export class MigrationCutoverDialog {
const migrationStatusTextValue = this._model.migrationStatus.properties.migrationStatus ? this._model.migrationStatus.properties.migrationStatus : this._model.migrationStatus.properties.provisioningState;
let fullBackupFileName: string;
let lastAppliedSSN: string;
let lastAppliedBackupFileTakenOn: string;
@@ -486,19 +546,20 @@ export class MigrationCutoverDialog {
const tableData: ActiveBackupFileSchema[] = [];
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => {
tableData.push(
{
fileName: activeBackupSet.listOfBackupFiles[0].fileName,
type: activeBackupSet.backupType,
status: activeBackupSet.listOfBackupFiles[0].status,
dataUploaded: `${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].dataWritten)}/ ${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].totalSize)}`,
copyThroughput: (activeBackupSet.listOfBackupFiles[0].copyThroughput / 1024).toFixed(2),
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
}
);
if (activeBackupSet.listOfBackupFiles[0].fileName.substr(activeBackupSet.listOfBackupFiles[0].fileName.lastIndexOf('.') + 1) === 'bak') {
fullBackupFileName = activeBackupSet.listOfBackupFiles[0].fileName;
}
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
@@ -514,7 +575,7 @@ export class MigrationCutoverDialog {
this._targetVersion.value = targetServerVersion;
this._migrationStatus.value = migrationStatusTextValue ?? '---';
this._fullBackupFile.value = fullBackupFileName! ?? '-';
this._fullBackupFile.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
let backupLocation;
const isBlobMigration = this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined;
@@ -532,7 +593,7 @@ export class MigrationCutoverDialog {
this._lastAppliedLSN.value = lastAppliedSSN! ?? '-';
this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? new Date(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
@@ -544,7 +605,9 @@ export class MigrationCutoverDialog {
row.fileName,
row.type,
row.status,
new Date(row.backupStartTime).toLocaleString(),
row.dataUploaded,
row.copyThroughput,
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
row.firstLSN,
row.lastLSN
];
@@ -578,7 +641,8 @@ export class MigrationCutoverDialog {
value: label,
CSSStyles: {
'font-weight': 'bold',
'margin-bottom': '0'
'margin-bottom': '0',
'font-size': '12px'
}
}).component();
flexContainer.addItem(labelComponent);
@@ -590,7 +654,8 @@ export class MigrationCutoverDialog {
'margin-bottom': '0',
'width': '100%',
'overflow': 'hidden',
'text-overflow': 'ellipses'
'text-overflow': 'ellipses',
'font-size': '12px'
}
}).component();
flexContainer.addItem(textComponent);
@@ -610,6 +675,8 @@ interface ActiveBackupFileSchema {
fileName: string,
type: string,
status: string,
dataUploaded: string,
copyThroughput: string,
backupStartTime: string,
firstLSN: string,
lastLSN: string

View File

@@ -10,9 +10,11 @@ import { MigrationContext, MigrationLocalStorage } from '../../models/migrationL
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import * as loc from '../../constants/strings';
import { convertTimeDifferenceToDuration, filterMigrations } from '../../api/utils';
import { convertTimeDifferenceToDuration, filterMigrations, SupportedAutoRefreshIntervals } from '../../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
export class MigrationStatusDialog {
private _model: MigrationStatusDialogModel;
private _dialogObject!: azdata.window.Dialog;
@@ -22,6 +24,7 @@ export class MigrationStatusDialog {
private _statusDropdown!: azdata.DropDownComponent;
private _statusTable!: azdata.DeclarativeTableComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _autoRefreshHandle!: NodeJS.Timeout;
constructor(migrations: MigrationContext[], private _filter: AdsMigrationStatus) {
this._model = new MigrationStatusDialogModel(migrations);
@@ -65,9 +68,17 @@ export class MigrationStatusDialog {
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
this._view.onClosed(e => {
clearInterval(this._autoRefreshHandle);
});
return view.initializeModel(form);
});
this._dialogObject.content = [tab];
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
this._dialogObject.okButton.onClick(e => {
clearInterval(this._autoRefreshHandle);
});
azdata.window.openDialog(this._dialogObject);
}
@@ -97,9 +108,10 @@ export class MigrationStatusDialog {
});
const flexContainer = this._view.modelBuilder.flexContainer().withProps({
width: 900,
CSSStyles: {
'justify-content': 'left'
}
},
}).component();
flexContainer.addItem(this._searchBox, {
@@ -124,8 +136,25 @@ export class MigrationStatusDialog {
'margin-left': '20px'
}
});
this.setAutoRefresh(refreshFrequency);
const container = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
container.addItem(flexContainer, {
flex: '0 0 auto',
CSSStyles: {
'width': '980px'
}
});
return container;
}
return flexContainer;
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
let classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(function () { classVariable.refreshTable(); }, interval);
}
}
private populateMigrationTable(): void {
@@ -277,7 +306,7 @@ export class MigrationStatusDialog {
{
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
valueType: azdata.DeclarativeDataType.component,
width: '160px',
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles

View File

@@ -7,7 +7,6 @@ import { azureResource } from 'azureResource';
import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails, SqlVMServer } from '../api/azure';
import * as azdata from 'azdata';
export class MigrationLocalStorage {
private static context: vscode.ExtensionContext;
private static mementoToken: string = 'sqlmigration.databaseMigrations';

View File

@@ -122,6 +122,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _targetDatabaseNames!: string[];
public _serverDatabases!: string[];
public _sqlMigrationServiceResourceGroup!: string;
public _sqlMigrationService!: SqlMigrationService;
public _sqlMigrationServices!: SqlMigrationService[];
public _nodeNames!: string[];

View File

@@ -684,6 +684,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).component();
targetDatabaseInput.onTextChanged((value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
this.validateFields();
});
this._networkShareTargetDatabaseNames.push(targetDatabaseInput);
@@ -749,7 +750,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
fireOnTextChange: true,
}).component();
blobContainerDropdown.onValueChanged(value => {
const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value);
const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value);
if (selectedIndex > -1 && value !== constants.NO_BLOBCONTAINERS_FOUND) {
this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(selectedIndex);
}
@@ -830,23 +831,24 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
});
const duplicates: Map<string, number[]> = new Map();
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer.id;
if (duplicates.has(blobContainerId)) {
duplicates.get(blobContainerId)?.push(i);
} else {
duplicates.set(blobContainerId, [i]);
if (errors.length > 0) {
const duplicates: Map<string, number[]> = new Map();
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer?.id;
if (duplicates.has(blobContainerId)) {
duplicates.get(blobContainerId)?.push(i);
} else {
duplicates.set(blobContainerId, [i]);
}
}
duplicates.forEach((d) => {
if (d.length > 1) {
const dupString = `${d.map(index => this.migrationStateModel._migrationDbs[index]).join(', ')}`;
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
}
});
}
duplicates.forEach((d) => {
if (d.length > 1) {
const dupString = `${d.map(index => this.migrationStateModel._migrationDbs[index]).join(', ')}`;
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
}
});
break;
}

View File

@@ -6,11 +6,11 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog';
import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import { getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlManagedInstance, SqlMigrationService } from '../api/azure';
import { getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlManagedInstance } from '../api/azure';
import { IconPathHelper } from '../constants/iconPathHelper';
import { findDropDownItemIndex } from '../api/utils';
@@ -24,6 +24,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
private _resourceGroupDropdown!: azdata.DropDownComponent;
private _dmsDropdown!: azdata.DropDownComponent;
private _dmsInfoContainer!: azdata.FlexContainer;
private _dmsStatusInfoBox!: azdata.InfoBoxComponent;
private _authKeyTable!: azdata.DeclarativeTableComponent;
private _refreshButton!: azdata.ButtonComponent;
@@ -34,8 +35,6 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
private _refresh1!: azdata.ButtonComponent;
private _refresh2!: azdata.ButtonComponent;
private _firstEnter: boolean = true;
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.IR_PAGE_TITLE), migrationStateModel);
}
@@ -51,13 +50,20 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
}
}).component();
createNewMigrationService.onDidClick((e) => {
const dialog = new CreateSqlMigrationServiceDialog(this.migrationStateModel, this);
dialog.initialize();
createNewMigrationService.onDidClick(async (e) => {
const dialog = new CreateSqlMigrationServiceDialog();
const createdDmsResult = await dialog.createNewDms(this.migrationStateModel, (<azdata.CategoryValue>this._resourceGroupDropdown.value).displayName);
this.migrationStateModel._sqlMigrationServiceResourceGroup = createdDmsResult.resourceGroup;
this.migrationStateModel._sqlMigrationService = createdDmsResult.service;
await this.loadResourceGroupDropdown();
await this.populateDms(createdDmsResult.resourceGroup);
});
this._statusLoadingComponent = view.modelBuilder.loadingComponent().withItem(this.createDMSDetailsContainer()).component();
this._dmsInfoContainer = this._view.modelBuilder.flexContainer().withItems([
this._statusLoadingComponent
]).component();
const dmsPortalInfo = this._view.modelBuilder.infoBox().withProps({
text: constants.DMS_PORTAL_INFO,
style: 'information',
@@ -80,7 +86,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
component: dmsPortalInfo
},
{
component: this._statusLoadingComponent
component: this._dmsInfoContainer
}
]
@@ -89,10 +95,11 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
}
public async onPageEnter(): Promise<void> {
if (this._firstEnter) {
this.populateMigrationService();
this._firstEnter = false;
}
this._subscription.value = this.migrationStateModel._targetSubscription.name;
this._location.value = await getLocationDisplayName(this.migrationStateModel._targetServerInstance.location);
this.loadResourceGroupDropdown();
this._dmsInfoContainer.display = (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none';
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
this.wizard.message = {
@@ -108,7 +115,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
};
return false;
}
if (state !== 'Online') {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && state !== 'Online') {
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.SERVICE_OFFLINE_ERROR
@@ -202,6 +209,9 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this._dmsDropdown.onValueChanged(async (value) => {
if (value && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
this._dmsInfoContainer.display = 'inline';
}
this.wizard.message = {
text: ''
};
@@ -210,6 +220,8 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this.migrationStateModel._sqlMigrationService = this.migrationStateModel.getMigrationService(selectedIndex);
await this.loadMigrationServiceStatus();
}
} else {
this._dmsInfoContainer.display = 'none';
}
});
@@ -254,8 +266,11 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this._refreshButton.onDidClick(async (e) => {
this._connectionStatusLoader.loading = true;
await this.loadStatus();
this._connectionStatusLoader.loading = false;
try {
await this.loadStatus();
} finally {
this._connectionStatusLoader.loading = false;
}
});
const connectionLabelContainer = this._view.modelBuilder.flexContainer().withProps({
@@ -387,53 +402,24 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
return container;
}
public async populateMigrationService(sqlMigrationService?: SqlMigrationService, serviceNodes?: string[], resourceGroupName?: string): Promise<void> {
public async loadResourceGroupDropdown(): Promise<void> {
this._resourceGroupDropdown.loading = true;
this._dmsDropdown.loading = true;
if (sqlMigrationService && serviceNodes) {
this.migrationStateModel._sqlMigrationService = sqlMigrationService;
this.migrationStateModel._nodeNames = serviceNodes;
}
try {
this._subscription.value = this.migrationStateModel._targetSubscription.name;
this._location.value = await getLocationDisplayName(this.migrationStateModel._targetServerInstance.location);
this._resourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription);
let index = 0;
if (resourceGroupName) {
index = findDropDownItemIndex(this._resourceGroupDropdown, resourceGroupName);
}
if ((<azdata.CategoryValue>this._resourceGroupDropdown.value)?.displayName.toLowerCase() === (<azdata.CategoryValue>this._resourceGroupDropdown.values[index])?.displayName.toLowerCase()) {
await this.populateDms((<azdata.CategoryValue>this._resourceGroupDropdown.value)?.displayName);
} else {
this._resourceGroupDropdown.value = this._resourceGroupDropdown.values[index];
}
} catch (error) {
console.log(error);
const resourceGroupDropdownValue = this._resourceGroupDropdown.values.find(v => v.displayName === this.migrationStateModel._sqlMigrationServiceResourceGroup);
this._resourceGroupDropdown.value = (resourceGroupDropdownValue) ? resourceGroupDropdownValue : this._resourceGroupDropdown.values[0];
} finally {
this._resourceGroupDropdown.loading = false;
}
}
public async populateDms(resourceGroupName: string): Promise<void> {
if (!resourceGroupName) {
return;
}
this._dmsDropdown.loading = true;
try {
this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, <SqlManagedInstance>this.migrationStateModel._targetServerInstance, resourceGroupName);
let index = -1;
if (this.migrationStateModel._sqlMigrationService) {
index = findDropDownItemIndex(this._dmsDropdown, this.migrationStateModel._sqlMigrationService.name);
}
if (index > -1) {
this._dmsDropdown.value = this._dmsDropdown.values[index];
} else {
this._dmsDropdown.value = this._dmsDropdown.values[0];
}
} catch (e) {
console.log(e);
const selectedSqlMigrationService = this._dmsDropdown.values.find(v => v.displayName.toLowerCase() === this.migrationStateModel._sqlMigrationService?.name.toLowerCase());
this._dmsDropdown.value = (selectedSqlMigrationService) ? selectedSqlMigrationService : this._dmsDropdown.values[0];
} finally {
this._dmsDropdown.loading = false;
}

View File

@@ -61,10 +61,13 @@ export class SummaryPage extends MigrationWizardPage {
createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._sqlMigrationService.location),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._sqlMigrationService.properties.resourceGroup),
createInformationRow(this._view, constants.IR_PAGE_TITLE, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._sqlMigrationService.name),
createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames[0]),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._sqlMigrationService.name)
]
);
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._nodeNames.length > 0) {
this._flexContainer.addItem(createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames.join(', ')));
}
}
public async onPageLeave(): Promise<void> {