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

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