SQL-Migration: add new migration monitoring data to migration details (#22460)

* add new migration details

* move migraiton target type enum to utils

* address review feedback, refectore, text update

* fix variable name

* limit and filter migrations list to mi/vm/db
This commit is contained in:
brian-harris
2023-03-29 07:48:30 -07:00
committed by GitHub
parent afafee844c
commit ef02e2bfce
25 changed files with 1068 additions and 908 deletions

View File

@@ -911,6 +911,7 @@ export enum AzureResourceKind {
SQLDB = 'SqlDb',
SQLMI = 'SqlMi',
SQLVM = 'SqlVm',
ORACLETOSQLDB = "OracleToSqlDb",
}
export interface ValidateIrSqlDatabaseMigrationRequest {
@@ -1008,6 +1009,7 @@ export interface DatabaseMigration {
export interface DatabaseMigrationProperties {
scope: string;
kind: string;
provisioningState: 'Succeeded' | 'Failed' | 'Creating';
provisioningError: string;
migrationStatus: 'Canceled' | 'Canceling' | 'Completing' | 'Creating' | 'Failed' | 'InProgress' | 'ReadyForCutover' | 'Restoring' | 'Retriable' | 'Succeeded' | 'UploadingFullBackup' | 'UploadingLogBackup';
@@ -1043,6 +1045,22 @@ export interface MigrationStatusDetails {
invalidFiles: string[];
listOfCopyProgressDetails: CopyProgressDetail[];
sqlDataCopyErrors: string[];
// new fields
pendingDiffBackupsCount: number;
restorePercentCompleted: number;
currentRestoredSize: number;
currentRestorePlanSize: number;
lastUploadedFileName: string;
lastUploadedFileTime: string;
lastRestoredFileTime: string;
miRestoreState: "None" | "Initializing" | "NotStarted" | "SearchingBackups" | "Restoring" | "RestorePaused" | "RestoreCompleted" | "Waiting" | "CompletingMigration" | "Cancelled" | "Failed" | "Completed" | "Blocked";
detectedFiles: number;
queuedFiles: number;
skippedFiles: number;
restoringFiles: number;
restoredFiles: number;
unrestorableFiles: number;
}
export interface MigrationStatusWarnings {
@@ -1053,7 +1071,7 @@ export interface MigrationStatusWarnings {
export interface CopyProgressDetail {
tableName: string;
status: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
status: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled';
parallelCopyType: string;
usedParallelCopies: number;
dataRead: number;
@@ -1061,7 +1079,7 @@ export interface CopyProgressDetail {
rowsRead: number;
rowsCopied: number;
copyStart: string;
copyThroughput: number,
copyThroughput: number;
copyDuration: number;
errors: string[];
}
@@ -1083,22 +1101,30 @@ export interface ErrorInfo {
export interface BackupSetInfo {
backupSetId: string;
firstLSN: string;
lastLSN: string;
backupType: string;
firstLSN: string; // SHIR scenario only
lastLSN: string; // SHIR scenario only
backupType: "Unknown" | "Database" | "TransactionLog" | "File" | "DifferentialDatabase" | "DifferentialFile" | "Partial" | "DifferentialPartial";
listOfBackupFiles: BackupFileInfo[];
backupStartDate: string;
backupFinishDate: string;
backupStartDate: string; // SHIR scenario only
backupFinishDate: string; // SHIR scenario only
isBackupRestored: boolean;
backupSize: number;
compressedBackupSize: number;
hasBackupChecksums: boolean;
familyCount: number;
// new fields
restoreStartDate: string;
restoreFinishDate: string;
restoreStatus: "None" | "Skipped" | "Queued" | "Restoring" | "Restored";
backupSizeMB: number;
numberOfStripes: number;
}
export interface SourceLocation {
fileShare?: DatabaseMigrationFileShare;
azureBlob?: DatabaseMigrationAzureBlob;
testConnectivity?: boolean;
fileStorageType: 'FileShare' | 'AzureBlob' | 'None';
}
@@ -1109,6 +1135,7 @@ export interface TargetLocation {
export interface BackupFileInfo {
fileName: string;
// fields below are only returned by SHIR scenarios
status: 'Arrived' | 'Uploading' | 'Uploaded' | 'Restoring' | 'Restored' | 'Canceled' | 'Ignored';
totalSize: number;
dataRead: number;

View File

@@ -37,6 +37,12 @@ export const MenuCommands = {
SendFeedback: 'sqlmigration.sendfeedback',
};
export enum MigrationTargetType {
SQLVM = 'AzureSqlVirtualMachine',
SQLMI = 'AzureSqlManagedInstance',
SQLDB = 'AzureSqlDatabase'
}
export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') {
return obj;
@@ -152,7 +158,16 @@ export function getMigrationDuration(startDate: string, endDate: string): string
}
export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, columnTextFilter?: string): azure.DatabaseMigration[] {
let filteredMigration: azure.DatabaseMigration[] = databaseMigrations || [];
const supportedKind: string[] = [
azure.AzureResourceKind.SQLDB,
azure.AzureResourceKind.SQLMI,
azure.AzureResourceKind.SQLVM,
];
let filteredMigration: azure.DatabaseMigration[] =
databaseMigrations.filter(m => supportedKind.includes(m.properties?.kind)) ||
[];
if (columnTextFilter) {
const filter = columnTextFilter.toLowerCase();
filteredMigration = filteredMigration.filter(
@@ -196,6 +211,7 @@ export function filterMigrations(databaseMigrations: azure.DatabaseMigration[],
return filteredMigration.filter(
value => getMigrationStatus(value) === constants.MigrationState.Completing);
}
return filteredMigration;
}

View File

@@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { DatabaseMigration } from '../api/azure';
import { DefaultSettingValue } from '../api/utils';
import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine';
import { AzureResourceKind, DatabaseMigration } from '../api/azure';
import { DefaultSettingValue, MigrationTargetType } from '../api/utils';
import { FileStorageType, MigrationMode } from '../models/stateMachine';
import * as loc from './strings';
export enum SQLTargetAssetType {
@@ -15,6 +15,48 @@ export enum SQLTargetAssetType {
SQLDB = 'Microsoft.Sql/servers',
}
export const FileStorageTypeCodes = {
FileShare: "FileShare",
AzureBlob: "AzureBlob",
None: "None",
};
export const BackupTypeCodes = {
// Type of backup. The values match the output of a RESTORE HEADERONLY query.
Unknown: "Unknown",
Database: "Database",
TransactionLog: "TransactionLog",
File: "File",
DifferentialDatabase: "DifferentialDatabase",
DifferentialFile: "DifferentialFile",
Partial: "Partial",
DifferentialPartial: "DifferentialPartial",
};
export const InternalManagedDatabaseRestoreDetailsBackupSetStatusCodes = {
None: "None",
Skipped: "Skipped",
Queued: "Queued",
Restoring: "Restoring",
Restored: "Restored",
};
export const InternalManagedDatabaseRestoreDetailsStatusCodes = {
None: "None", // Something went wrong most likely.
Initializing: "Initializing", // Restore is initializing.
NotStarted: "NotStarted", // Restore not started
SearchingBackups: "SearchingBackups", // Searching for backups
Restoring: "Restoring", // Restore is in progress
RestorePaused: "RestorePaused", // Restore is paused
RestoreCompleted: "RestoreCompleted", // Restore completed for all found log, but there may have more logs coming
Waiting: "Waiting", // Waiting for new files to be uploaded or for Complete restore signal.
CompletingMigration: "CompletingMigration", // Completing migration
Cancelled: "Cancelled", // Restore cancelled.
Failed: "Failed", // Restore failed.
Completed: "Completed", // Database is restored and recovery is complete.
Blocked: "Blocked", // Restore is temporarily blocked: "", awaiting for user to mitigate the issue.
};
export const ParallelCopyTypeCodes = {
None: 'None',
DynamicRange: 'DynamicRange',
@@ -78,7 +120,9 @@ export function formatTime(miliseconds: number): string {
if (miliseconds > 0) {
// hh:mm:ss
const matches = (new Date(miliseconds))?.toUTCString()?.match(/(\d\d:\d\d:\d\d)/) || [];
return matches?.length > 0 ? matches[0] : '';
return matches?.length > 0
? matches[0] ?? ''
: '';
}
return '';
}
@@ -118,12 +162,12 @@ export function getMigrationTargetType(migration: DatabaseMigration | undefined)
}
export function getMigrationTargetTypeEnum(migration: DatabaseMigration | undefined): MigrationTargetType | undefined {
switch (migration?.type) {
case SQLTargetAssetType.SQLMI:
switch (migration?.properties?.kind) {
case AzureResourceKind.SQLMI:
return MigrationTargetType.SQLMI;
case SQLTargetAssetType.SQLVM:
case AzureResourceKind.SQLVM:
return MigrationTargetType.SQLVM;
case SQLTargetAssetType.SQLDB:
case AzureResourceKind.SQLDB:
return MigrationTargetType.SQLDB;
default:
return undefined;
@@ -150,6 +194,24 @@ export function isBlobMigration(migration: DatabaseMigration | undefined): boole
return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob;
}
export function isShirMigration(migration?: DatabaseMigration): boolean {
return isLogicalMigration(migration)
|| isFileShareMigration(migration);
}
export function isLogicalMigration(migration?: DatabaseMigration): boolean {
return migration?.properties?.kind === AzureResourceKind.ORACLETOSQLDB
|| migration?.properties?.kind === AzureResourceKind.SQLDB;
}
export function isFileShareMigration(migration?: DatabaseMigration): boolean {
return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageTypeCodes.FileShare;
}
export function isTargetType(migration?: DatabaseMigration, kind?: string): boolean {
return migration?.properties?.kind === kind;
}
export function getMigrationStatus(migration: DatabaseMigration | undefined): string | undefined {
return migration?.properties.migrationStatus
?? migration?.properties.provisioningState;
@@ -167,6 +229,16 @@ export function hasMigrationOperationId(migration: DatabaseMigration | undefined
&& migationOperationId.length > 0;
}
export function getMigrationBackupLocation(migration: DatabaseMigration): string | undefined {
return migration?.properties?.backupConfiguration?.sourceLocation?.fileShare?.path
?? migration?.properties?.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName
?? migration?.properties?.migrationStatusDetails?.blobContainerName;
}
export function getMigrationFullBackupFiles(migration: DatabaseMigration): string | undefined {
return migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles?.map(file => file.fileName).join(',');
}
export function hasRestoreBlockingReason(migration: DatabaseMigration | undefined): boolean {
return (migration?.properties.migrationStatusWarnings?.restoreBlockingReason ?? '').length > 0;
}

View File

@@ -7,7 +7,7 @@ import { AzureAccount } from 'azurecore';
import * as nls from 'vscode-nls';
import { EOL } from 'os';
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
import { formatNumber, ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
import { BackupTypeCodes, formatNumber, InternalManagedDatabaseRestoreDetailsBackupSetStatusCodes, InternalManagedDatabaseRestoreDetailsStatusCodes, ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
import { ValidationError } from '../api/azure';
const localize = nls.loadMessageBundle();
@@ -982,10 +982,10 @@ export const TARGET_SERVER = localize('sql.migration.target.server', "Target ser
export const TARGET_VERSION = localize('sql.migration.target.version', "Target version");
export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status");
export const MIGRATION_STATUS_FILTER = localize('sql.migration.migration.status.filter', "Migration status filter");
export const FULL_BACKUP_FILES = localize('sql.migration.full.backup.files', "Full backup files");
export const FULL_BACKUP_FILES = localize('sql.migration.full.backup.files', "Full backup file(s)");
export const LAST_APPLIED_LSN = localize('sql.migration.last.applied.lsn', "Last applied LSN");
export const LAST_APPLIED_BACKUP_FILES = localize('sql.migration.last.applied.backup.files', "Last applied backup files");
export const LAST_APPLIED_BACKUP_FILES_TAKEN_ON = localize('sql.migration.last.applied.files.taken.on', "Last applied backup files taken on");
export const LAST_APPLIED_BACKUP_FILES = localize('sql.migration.last.applied.backup.files', "Last applied backup file(s)");
export const LAST_APPLIED_BACKUP_FILES_TAKEN_ON = localize('sql.migration.last.applied.files.taken.on', "Last applied backup taken on");
export const CURRENTLY_RESTORING_FILE = localize('sql.migration.currently.restoring.file', "Currently restoring file");
export const ALL_BACKUPS_RESTORED = localize('sql.migration.all.backups.restored', "All backups restored");
export const ACTIVE_BACKUP_FILES = localize('sql.migration.active.backup.files', "Active backup files");
@@ -993,6 +993,29 @@ export const MIGRATION_STATUS_REFRESH_ERROR = localize('sql.migration.cutover.st
export const MIGRATION_CANCELLATION_ERROR = localize('sql.migration.cancel.error', 'An error occurred while canceling the migration.');
export const MIGRATION_DELETE_ERROR = localize('sql.migration.delete.error', 'An error occurred while deleting the migration.');
export const FIELD_LABEL_LAST_UPLOADED_FILE = localize('sql.migration.field.label.last.uploaded.file', 'Last uploaded file');
export const FIELD_LABEL_LAST_UPLOADED_FILE_TIME = localize('sql.migration.field.label.last.uloaded.file.time', 'Last uploaded file time');
export const FIELD_LABEL_PENDING_DIFF_BACKUPS = localize('sql.migration.field.label.pending.differential.backups', 'Pending differential backups');
export const FIELD_LABEL_DETECTED_FILES = localize('sql.migration.field.label.deteected.files', 'Detected files');
export const FIELD_LABEL_QUEUED_FILES = localize('sql.migration.field.label.queued.files', 'Queued files');
export const FIELD_LABEL_SKIPPED_FILES = localize('sql.migration.field.label.skipped.files', 'Skipped files');
export const FIELD_LABEL_UNRESTORABLE_FILES = localize('sql.migration.field.label.unrestorable.files', 'Unrestorable files');
export const FIELD_LABEL_LAST_RESTORED_FILE_TIME = localize('sql.migration.field.label.last.restored.file.time', 'Last restored file time');
export const FIELD_LABEL_RESTORED_FILES = localize('sql.migration.field.label.restored.files', 'Restored files');
export const FIELD_LABEL_RESTORING_FILES = localize('sql.migration.field.label.restoring.files', 'Restoring files');
export const FIELD_LABEL_RESTORED_SIZE = localize('sql.migration.field.label.restored.size', 'Restored size (MB)');
export const FIELD_LABEL_RESTORE_PLAN_SIZE = localize('sql.migration.field.label.restore.plan.size', 'Restore plan size (MB)');
export const FIELD_LABEL_RESTORE_PERCENT_COMPLETED = localize('sql.migration.field.label.restore.percent.completed', 'Restore percent completed');
export const FIELD_LABEL_MI_RESTORE_STATE = localize('sql.migration.field.label.mi.restore.state', 'Managed instance restore state');
export const BACKUP_FILE_COLUMN_FILE_NAME = localize('sql.migration.backup.file.name', 'File name');
export const BACKUP_FILE_COLUMN_FILE_STATUS = localize('sql.migration.backup.file.status', 'File status');
export const BACKUP_FILE_COLUMN_RESTORE_STATUS = localize('sql.migration.backup.file.restore.status', 'Restore status');
export const BACKUP_FILE_COLUMN_BACKUP_SIZE_MB = localize('sql.migration.backup.file.backup.size', 'Backup size (MB)');
export const BACKUP_FILE_COLUMN_NUMBER_OF_STRIPES = localize('sql.migration.backup.file.number.of.stripes', 'Number of stripes');
export const BACKUP_FILE_COLUMN_RESTORE_START_DATE = localize('sql.migration.backup.file.restore.start.date', 'Restore start date');
export const BACKUP_FILE_COLUMN_RESTORE_FINISH_DATE = localize('sql.migration.backup.file.restore.finish.date', 'Restore finish date');
export const STATUS = localize('sql.migration.status', "Status");
export const BACKUP_START_TIME = localize('sql.migration.backup.start.time', "Backup start time");
export const FIRST_LSN = localize('sql.migration.first.lsn', "First LSN");
@@ -1149,6 +1172,41 @@ export const ParallelCopyType: LookupTable<string | undefined> = {
[ParallelCopyTypeCodes.DynamicRange]: localize('sql.migration.parallel.copy.type.dynamic', 'Dynamic range'),
};
export const BackupTypeLookup: LookupTable<string | undefined> = {
[BackupTypeCodes.Unknown]: localize('sql.migration.restore.backuptype.unknown', 'Unknown'),
[BackupTypeCodes.Database]: localize('sql.migration.restore.backuptype.database', 'Database'),
[BackupTypeCodes.TransactionLog]: localize('sql.migration.restore.backuptype.transactionlog', 'Transaction log'),
[BackupTypeCodes.File]: localize('sql.migration.restore.backuptype.file', 'File'),
[BackupTypeCodes.DifferentialDatabase]: localize('sql.migration.restore.backuptype.differentialdatabase', 'Differential database'),
[BackupTypeCodes.DifferentialFile]: localize('sql.migration.restore.backuptype.differentialfile', 'Differential file'),
[BackupTypeCodes.Partial]: localize('sql.migration.restore.backuptype.partial', 'Partial'),
[BackupTypeCodes.DifferentialPartial]: localize('sql.migration.restore.backuptype.differentialpartial', 'Differential partial'),
};
export const BackupSetRestoreStatusLookup: LookupTable<string | undefined> = {
[InternalManagedDatabaseRestoreDetailsBackupSetStatusCodes.None]: localize('sql.migration.restore.backupset.status.none', 'None'),
[InternalManagedDatabaseRestoreDetailsBackupSetStatusCodes.Queued]: localize('sql.migration.restore.backupset.status.queued', 'Queued'),
[InternalManagedDatabaseRestoreDetailsBackupSetStatusCodes.Restored]: localize('sql.migration.restore.backupset.status.restored', 'Restored'),
[InternalManagedDatabaseRestoreDetailsBackupSetStatusCodes.Restoring]: localize('sql.migration.restore.backupset.status.restoring', 'Restoring'),
[InternalManagedDatabaseRestoreDetailsBackupSetStatusCodes.Skipped]: localize('sql.migration.restore.backupset.status.skipped', 'Skipped'),
};
export const InternalManagedDatabaseRestoreDetailsStatusLookup: LookupTable<string | undefined> = {
[InternalManagedDatabaseRestoreDetailsStatusCodes.None]: localize('sql.migration.restore.status.none', 'None'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.Initializing]: localize('sql.migration.restore.status.initializing', 'Initializing'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.NotStarted]: localize('sql.migration.restore.status.not.started', 'Not started'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.SearchingBackups]: localize('sql.migration.restore.status.searching.backups', 'Searching backups'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.Restoring]: localize('sql.migration.restore.status.Restoring', 'Restoring'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.RestorePaused]: localize('sql.migration.restore.status.restore.paused', 'Restore paused'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.RestoreCompleted]: localize('sql.migration.restore.status.restore.completed', 'Restore completed'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.Waiting]: localize('sql.migration.restore.status.waiting', 'Waiting'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.CompletingMigration]: localize('sql.migration.restore.status.completing.migration', 'Completing migration'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.Cancelled]: localize('sql.migration.restore.status.cancelled', 'Cancelled'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.Failed]: localize('sql.migration.restore.status.failed', 'Failed'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.Completed]: localize('sql.migration.restore.status.completed', 'Completed'),
[InternalManagedDatabaseRestoreDetailsStatusCodes.Blocked]: localize('sql.migration.restore.status.blocked', 'Blocked'),
};
export function STATUS_WARNING_COUNT(status: string, count: number): string | undefined {
if (status === MigrationState.InProgress ||
status === MigrationState.ReadyForCutover ||
@@ -1199,8 +1257,8 @@ export const sizeFormatter = new Intl.NumberFormat(
maximumFractionDigits: 2,
});
export function formatSizeMb(sizeMb: number): string {
if (isNaN(sizeMb) || sizeMb < 0) {
export function formatSizeMb(sizeMb: number | undefined): string {
if (sizeMb === undefined || isNaN(sizeMb) || sizeMb < 0) {
return '';
} else if (sizeMb < 1024) {
return localize('sql.migration.size.mb', "{0} MB", sizeFormatter.format(sizeMb));

View File

@@ -765,6 +765,13 @@ export class DashboardTab extends TabBase<DashboardTab> {
})
.component();
this.disposables.push(
this._serviceContextButton.onDidClick(
async () => {
const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
await dialog.initialize();
}));
this.disposables.push(
this.serviceContextChangedEvent.event(
async (e) => {
@@ -776,12 +783,6 @@ export class DashboardTab extends TabBase<DashboardTab> {
));
await this.updateServiceContext(this._serviceContextButton);
this.disposables.push(
this._serviceContextButton.onDidClick(async () => {
const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
await dialog.initialize();
}));
return this._serviceContextButton;
}

View File

@@ -1,208 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as loc from '../constants/strings';
import { getSqlServerName, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemetry';
import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { getResourceName } from '../api/azure';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { EmptySettingValue } from './tabBase';
import { DashboardStatusBar } from './DashboardStatusBar';
import { getSourceConnectionServerInfo } from '../api/sqlUtils';
const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab';
export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<MigrationDetailsBlobContainerTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
constructor() {
super();
this.id = MigrationDetailsBlobContainerTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
openMigrationsListFcn: (refresh?: boolean) => Promise<void>,
statusBar: DashboardStatusBar,
): Promise<MigrationDetailsBlobContainerTab> {
this.view = view;
this.context = context;
this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
if (this.isRefreshing ||
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return;
}
this.isRefreshing = true;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
try {
await this.model.fetchStatus();
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
}
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
await this.showMigrationErrors(migration);
const sqlServerName = migration.properties.sourceServerName;
const sourceDatabaseName = migration.properties.sourceDatabaseName;
const sqlServerInfo = await getSourceConnectionServerInfo();
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration.name;
const targetServerName = getResourceName(migration.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
const blobContainerName
= migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName
?? migration.properties.migrationStatusDetails?.blobContainerName;
const backupLocation = storageAccountResourceId && blobContainerName
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
: blobContainerName;
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.deleteButton.enabled = canDeleteMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
this.refreshLoader.loading = false;
this.isRefreshing = false;
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
];
this.content = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withLayout({ width: '100%', padding: '0 0 0 15px' })
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
.component();
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected override async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: { 'flex': '0', 'width': infoFieldWidth }
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
}

View File

@@ -1,392 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import * as loc from '../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemetry';
import * as styles from '../constants/styles';
import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { getResourceName } from '../api/azure';
import { EmptySettingValue } from './tabBase';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { DashboardStatusBar } from './DashboardStatusBar';
import { getSourceConnectionServerInfo } from '../api/sqlUtils';
const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab';
interface ActiveBackupFileSchema {
fileName: string,
type: string,
status: string,
dataUploaded: string,
copyThroughput: string,
backupStartTime: string,
firstLSN: string,
lastLSN: string
}
export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<MigrationDetailsFileShareTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _fullBackupFileOnInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastLSNInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.TableComponent;
private _emptyTableFill!: azdata.FlexContainer;
constructor() {
super();
this.id = MigrationDetailsFileShareTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
openMigrationsListFcn: (refresh?: boolean) => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
this.view = view;
this.context = context;
this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
if (this.isRefreshing ||
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return;
}
try {
this.isRefreshing = true;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
await this._fileTable.updateProperty('data', []);
await this.model.fetchStatus();
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
await this.showMigrationErrors(migration);
const sqlServerName = migration.properties.sourceServerName;
const sourceDatabaseName = migration.properties.sourceDatabaseName;
const sqlServerInfo = await getSourceConnectionServerInfo();
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration.name;
const targetServerName = getResourceName(migration.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
let lastAppliedSSN: string;
let lastAppliedBackupFileTakenOn: string;
const tableData: ActiveBackupFileSchema[] = [];
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
(activeBackupSet) => {
tableData.push(
...activeBackupSet.listOfBackupFiles.map(f => {
return {
fileName: f.fileName,
type: activeBackupSet.backupType,
status: f.status,
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
};
})
);
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
const backupLocation = fileShare?.path! ?? EmptySettingValue;
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
if (tableData.length === 0) {
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
this._fileTable.height = '50px';
await this._fileTable.updateProperty('data', []);
} else {
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
this._fileTable.height = '300px';
// Sorting files in descending order of backupStartTime
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
}
const data = tableData.map(row => [
row.fileName,
row.type,
row.status,
row.dataUploaded,
row.copyThroughput,
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
row.firstLSN,
row.lastLSN
]) || [];
await this._fileTable.updateProperty('data', data);
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.deleteButton.enabled = canDeleteMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
} finally {
this.refreshLoader.loading = false;
this.isRefreshing = false;
}
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
this._fileCount = this.view.modelBuilder.text()
.withProps({
width: '500px',
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._fileTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
CSSStyles: { 'padding-left': '0px', 'max-width': '1020px' },
data: [],
height: '300px',
columns: [
{
value: 'files',
name: loc.ACTIVE_BACKUP_FILES,
type: azdata.ColumnType.text,
width: 230,
},
{
value: 'type',
name: loc.TYPE,
width: 90,
type: azdata.ColumnType.text,
},
{
value: 'status',
name: loc.STATUS,
width: 60,
type: azdata.ColumnType.text,
},
{
value: 'uploaded',
name: loc.DATA_UPLOADED,
width: 120,
type: azdata.ColumnType.text,
},
{
value: 'throughput',
name: loc.COPY_THROUGHPUT,
width: 150,
type: azdata.ColumnType.text,
},
{
value: 'starttime',
name: loc.BACKUP_START_TIME,
width: 130,
type: azdata.ColumnType.text,
},
{
value: 'firstlsn',
name: loc.FIRST_LSN,
width: 120,
type: azdata.ColumnType.text,
},
{
value: 'lastlsn',
name: loc.LAST_LSN,
width: 120,
type: azdata.ColumnType.text,
}
],
}).component();
const emptyTableImage = this.view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.emptyTable,
iconHeight: '100px',
iconWidth: '100px',
height: '100px',
width: '100px',
CSSStyles: { 'text-align': 'center' }
}).component();
const emptyTableText = this.view.modelBuilder.text()
.withProps({
value: loc.EMPTY_TABLE_TEXT,
CSSStyles: {
...styles.NOTE_CSS,
'margin-top': '8px',
'text-align': 'center',
'width': '300px'
}
}).component();
this._emptyTableFill = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
alignItems: 'center'
}).withItems([
emptyTableImage,
emptyTableText,
]).withProps({
width: '100%',
display: 'none'
}).component();
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
{ component: this._fileCount },
{ component: this._fileTable },
{ component: this._emptyTableFill }
];
const formContainer = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withLayout({ width: '100%', padding: '0 0 0 15px' })
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
.component();
this.content = formContainer;
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', false);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', false);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', false);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: {
'flex': '0',
'width': infoFieldWidth
}
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
}

View File

@@ -0,0 +1,573 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import * as loc from '../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemetry';
import * as styles from '../constants/styles';
import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, isShirMigration } from '../constants/helper';
import { AzureResourceKind, DatabaseMigration, getResourceName } from '../api/azure';
import * as utils from '../api/utils';
import * as helper from '../constants/helper';
import { EmptySettingValue } from './tabBase';
import { InfoFieldSchema, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { DashboardStatusBar } from './DashboardStatusBar';
const MigrationDetailsTabId = 'MigrationDetailsTab';
const enum BackupFileColumnIndex {
backupFileName = 0,
backupType = 1,
backupStatus = 2,
restoreStatus = 3,
backupSizeMB = 4,
numberOfStripes = 5,
dataUploadRate = 6,
throughput = 7,
backupStartTime = 8,
restoreStartDate = 9,
restoreFinishDate = 10,
firstLsn = 11,
lastLsn = 12,
}
interface ActiveBackupFileSchema {
backupFileName: string,
backupType: string,
// SHIR provided
backupStatus: string,
dataUploaded: string, // SHIR provided
dataUploadRate: string, // SHIR provided
backupStartTime: string, // SHIR provided
firstLSN: string, // SHIR provided
lastLSN: string, // SHIR provided
// SQLMI provided
restoreStartDate: string, // SQLMI provided
restoreFinishDate: string, // SQLMI provided
restoreStatus: string, // SQLMI provided
backupSizeMB: string, // SQLMI provided
numberOfStripes: number, // SQLMI provided
}
export class MigrationDetailsTab extends MigrationDetailsTabBase<MigrationDetailsTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _fullBackupFileOnInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastUploadedFileNameField!: InfoFieldSchema;
private _lastUploadedFileTimeField!: InfoFieldSchema;
private _pendingDiffBackupsCountField!: InfoFieldSchema;
private _detectedFilesField!: InfoFieldSchema;
private _queuedFilesField!: InfoFieldSchema;
private _skippedFilesField!: InfoFieldSchema;
private _unrestorableFilesField!: InfoFieldSchema;
private _lastLSNInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
private _lastRestoredFileTimeInfoField!: InfoFieldSchema;
private _restoredFilesInfoField!: InfoFieldSchema;
private _restoringFilesInfoField!: InfoFieldSchema;
private _currentRestoredSizeInfoField!: InfoFieldSchema;
private _currentRestorePlanSizeInfoField!: InfoFieldSchema;
private _restorePercentCompletedInfoField!: InfoFieldSchema;
private _miRestoreStateInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.TableComponent;
private _emptyTableFill!: azdata.FlexContainer;
constructor() {
super();
this.id = MigrationDetailsTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
openMigrationsListFcn: (refresh?: boolean) => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsTab> {
this.view = view;
this.context = context;
this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(initialize?: boolean): Promise<void> {
if (this.isRefreshing ||
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return;
}
try {
this.isRefreshing = true;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
if (initialize) {
await this._clearControlsValue();
await utils.updateControlDisplay(this._fileTable, false);
await this._fileTable.updateProperty('columns', this._getTableColumns(this.model?.migration));
await this._showControls(this.model?.migration);
}
await this._fileTable.updateProperty('data', []);
await this.model.fetchStatus();
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
await this.showMigrationErrors(migration);
let lastAppliedLSN: string = '';
let lastAppliedBackupFileTakenOn: string = '';
const tableData: ActiveBackupFileSchema[] = [];
migration?.properties?.migrationStatusDetails?.activeBackupSets?.forEach(
(activeBackupSet) => {
tableData.push(
...activeBackupSet?.listOfBackupFiles?.map(f => {
return {
backupFileName: f.fileName,
backupType: loc.BackupTypeLookup[activeBackupSet.backupType] ?? activeBackupSet.backupType,
backupStatus: loc.BackupSetRestoreStatusLookup[f.status] ?? f.status, // SHIR provided
restoreStatus: loc.InternalManagedDatabaseRestoreDetailsStatusLookup[activeBackupSet.restoreStatus] ?? activeBackupSet.restoreStatus,
backupSizeMB: loc.formatSizeMb(activeBackupSet.backupSizeMB),
numberOfStripes: activeBackupSet.numberOfStripes,
dataUploadRate: `${convertByteSizeToReadableUnit(f.dataWritten ?? 0)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
// SHIR provided
dataUploaded: f.copyThroughput
? (f.copyThroughput / 1024).toFixed(2)
: EmptySettingValue, // SHIR provided
backupStartTime: helper.formatDateTimeString(activeBackupSet.backupStartDate), // SHIR provided
restoreStartDate: helper.formatDateTimeString(activeBackupSet.restoreStartDate),
restoreFinishDate: helper.formatDateTimeString(activeBackupSet.restoreFinishDate),
firstLSN: activeBackupSet.firstLSN, // SHIR provided
lastLSN: activeBackupSet.lastLSN, // SHIR provided
};
})
);
if (activeBackupSet.listOfBackupFiles?.length > 0 &&
activeBackupSet.listOfBackupFiles[0].fileName === migration?.properties?.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedLSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
this.databaseLabel.value = migration?.properties?.sourceDatabaseName;
// Left side
this._updateInfoFieldValue(this._sourceDatabaseInfoField, migration?.properties?.sourceDatabaseName);
this._updateInfoFieldValue(this._sourceDetailsInfoField, migration?.properties?.sourceServerName);
this._updateInfoFieldValue(this._migrationStatusInfoField, getMigrationStatusString(migration));
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._updateInfoFieldValue(this._fullBackupFileOnInfoField, helper.getMigrationFullBackupFiles(migration) ?? EmptySettingValue);
this._updateInfoFieldValue(this._backupLocationInfoField, helper.getMigrationBackupLocation(migration) ?? EmptySettingValue);
// SQL MI supplied
const details = migration?.properties?.migrationStatusDetails;
this._updateInfoFieldValue(this._lastUploadedFileNameField, details?.lastUploadedFileName ?? EmptySettingValue);
this._updateInfoFieldValue(
this._lastUploadedFileTimeField,
details?.lastUploadedFileTime
? helper.formatDateTimeString(lastAppliedBackupFileTakenOn)
: EmptySettingValue);
this._updateInfoFieldValue(this._pendingDiffBackupsCountField, details?.pendingDiffBackupsCount?.toLocaleString() ?? EmptySettingValue);
this._updateInfoFieldValue(this._detectedFilesField, details?.detectedFiles?.toLocaleString() ?? EmptySettingValue);
this._updateInfoFieldValue(this._queuedFilesField, details?.queuedFiles?.toLocaleString() ?? EmptySettingValue);
this._updateInfoFieldValue(this._skippedFilesField, details?.skippedFiles?.toLocaleString() ?? EmptySettingValue);
this._updateInfoFieldValue(this._unrestorableFilesField, details?.unrestorableFiles?.toLocaleString() ?? EmptySettingValue);
// Right side
this._updateInfoFieldValue(this._targetDatabaseInfoField, migration.name ?? EmptySettingValue);
this._updateInfoFieldValue(this._targetServerInfoField, getResourceName(migration?.properties?.scope) ?? EmptySettingValue);
this._updateInfoFieldValue(this._targetVersionInfoField, MigrationTargetTypeName[getMigrationTargetTypeEnum(migration) ?? ''] ?? EmptySettingValue);
this._updateInfoFieldValue(
this._lastLSNInfoField,
lastAppliedLSN?.length > 0
? lastAppliedLSN
: EmptySettingValue);
this._updateInfoFieldValue(this._lastAppliedBackupInfoField, migration?.properties?.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue);
// FileShare
this._updateInfoFieldValue(
this._lastAppliedBackupTakenOnInfoField,
lastAppliedBackupFileTakenOn?.length > 0
? helper.formatDateTimeString(lastAppliedBackupFileTakenOn)
: EmptySettingValue);
// AzureBlob
this._updateInfoFieldValue(this._currentRestoringFileInfoField, this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue);
// SQL MI supplied
const lastRestoredFileTime = migration?.properties?.migrationStatusDetails?.lastRestoredFileTime ?? '';
this._updateInfoFieldValue(
this._lastRestoredFileTimeInfoField,
lastRestoredFileTime.length > 0
? convertIsoTimeToLocalTime(lastRestoredFileTime).toLocaleString()
: EmptySettingValue);
this._updateInfoFieldValue(this._restoredFilesInfoField, migration?.properties?.migrationStatusDetails?.restoredFiles?.toLocaleString() ?? EmptySettingValue);
this._updateInfoFieldValue(this._restoringFilesInfoField, migration?.properties?.migrationStatusDetails?.restoringFiles?.toLocaleString() ?? EmptySettingValue);
this._updateInfoFieldValue(
this._currentRestoredSizeInfoField,
migration?.properties?.migrationStatusDetails?.currentRestoredSize
? loc.formatSizeMb(migration?.properties?.migrationStatusDetails?.currentRestoredSize)
: EmptySettingValue);
this._updateInfoFieldValue(
this._currentRestorePlanSizeInfoField,
migration?.properties?.migrationStatusDetails?.currentRestorePlanSize
? loc.formatSizeMb(migration?.properties?.migrationStatusDetails?.currentRestorePlanSize)
: EmptySettingValue);
this._updateInfoFieldValue(this._restorePercentCompletedInfoField, migration?.properties?.migrationStatusDetails?.restorePercentCompleted?.toLocaleString() ?? EmptySettingValue);
this._updateInfoFieldValue(
this._miRestoreStateInfoField,
loc.InternalManagedDatabaseRestoreDetailsStatusLookup[migration?.properties?.migrationStatusDetails?.miRestoreState ?? '']
?? migration?.properties?.migrationStatusDetails?.miRestoreState
?? EmptySettingValue);
const isBlobMigration = helper.isBlobMigration(migration);
const isSqlVmTarget = helper.isTargetType(migration, AzureResourceKind.SQLVM);
if (!isBlobMigration && !isSqlVmTarget) {
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
}
if (tableData.length === 0) {
if (!isBlobMigration && !isSqlVmTarget) {
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
}
this._fileTable.height = '50px';
await this._fileTable.updateProperty('data', []);
} else {
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
this._fileTable.height = '340px';
// Sorting files in descending order of backupStartTime
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
}
const data = tableData.map(row => [
row.backupFileName, // 0
row.backupType, // 1
row.backupStatus, // 2
row.restoreStatus, // 3
row.backupSizeMB, // 4
row.numberOfStripes, // 5
row.dataUploadRate, // 6
row.dataUploaded, // 7
row.backupStartTime, // 8
row.restoreStartDate, // 9
row.restoreFinishDate, // 10
row.firstLSN, // 11
row.lastLSN, // 12
]) || [];
const filteredData = this._getTableData(migration, data);
await this._fileTable.updateProperty('data', filteredData);
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.deleteButton.enabled = canDeleteMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
} finally {
this.refreshLoader.loading = false;
this.isRefreshing = false;
}
}
private _clearControlsValue(): void {
this._updateInfoFieldValue(this._sourceDatabaseInfoField, '');
this._updateInfoFieldValue(this._sourceDetailsInfoField, '');
this._updateInfoFieldValue(this._targetDatabaseInfoField, '');
this._updateInfoFieldValue(this._targetServerInfoField, '');
this._updateInfoFieldValue(this._targetVersionInfoField, '');
this._updateInfoFieldValue(this._migrationStatusInfoField, '');
this._updateInfoFieldValue(this._fullBackupFileOnInfoField, '');
this._updateInfoFieldValue(this._backupLocationInfoField, '');
this._updateInfoFieldValue(this._lastUploadedFileNameField, '');
this._updateInfoFieldValue(this._lastUploadedFileTimeField, '');
this._updateInfoFieldValue(this._pendingDiffBackupsCountField, '');
this._updateInfoFieldValue(this._detectedFilesField, '');
this._updateInfoFieldValue(this._queuedFilesField, '');
this._updateInfoFieldValue(this._skippedFilesField, '');
this._updateInfoFieldValue(this._unrestorableFilesField, '');
this._updateInfoFieldValue(this._lastLSNInfoField, '');
this._updateInfoFieldValue(this._lastAppliedBackupInfoField, '');
this._updateInfoFieldValue(this._currentRestoringFileInfoField, '');
this._updateInfoFieldValue(this._lastAppliedBackupTakenOnInfoField, '');
this._updateInfoFieldValue(this._lastRestoredFileTimeInfoField, '');
this._updateInfoFieldValue(this._restoredFilesInfoField, '');
this._updateInfoFieldValue(this._restoringFilesInfoField, '');
this._updateInfoFieldValue(this._currentRestoredSizeInfoField, '');
this._updateInfoFieldValue(this._currentRestorePlanSizeInfoField, '');
this._updateInfoFieldValue(this._restorePercentCompletedInfoField, '');
this._updateInfoFieldValue(this._miRestoreStateInfoField, '');
}
private async _showControls(migration: DatabaseMigration): Promise<void> {
const isSHIR = helper.isShirMigration(migration);
const isSqlMiTarget = helper.isTargetType(migration, AzureResourceKind.SQLMI);
const isSqlVmTarget = helper.isTargetType(migration, AzureResourceKind.SQLVM);
const isBlobMigration = helper.isBlobMigration(migration);
await utils.updateControlDisplay(this._fullBackupFileOnInfoField.flexContainer, isSHIR);
await utils.updateControlDisplay(this._lastUploadedFileNameField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._lastUploadedFileTimeField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._pendingDiffBackupsCountField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._detectedFilesField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._queuedFilesField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._skippedFilesField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._unrestorableFilesField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._lastLSNInfoField.flexContainer, isSHIR);
await utils.updateControlDisplay(this._currentRestoringFileInfoField.flexContainer, isBlobMigration);
await utils.updateControlDisplay(this._lastAppliedBackupTakenOnInfoField.flexContainer, isSHIR);
await utils.updateControlDisplay(this._lastRestoredFileTimeInfoField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._restoredFilesInfoField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._restoringFilesInfoField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._currentRestoredSizeInfoField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._currentRestorePlanSizeInfoField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._restorePercentCompletedInfoField.flexContainer, isSqlMiTarget);
await utils.updateControlDisplay(this._miRestoreStateInfoField.flexContainer, isSqlMiTarget);
const showGrid = !(isBlobMigration && isSqlVmTarget);
await utils.updateControlDisplay(this._emptyTableFill, showGrid, 'flex');
await utils.updateControlDisplay(this._fileCount, showGrid);
await utils.updateControlDisplay(this._fileTable, showGrid);
}
private _addItemIfTrue(array: any[], value: any, add: boolean): void {
if (add) {
array.push(value);
}
}
private _getTableColumns(migration?: DatabaseMigration): azdata.TableColumn[] {
const columns: azdata.TableColumn[] = [];
const isSHIR = isShirMigration(migration);
const isSqlMiTarget = helper.isTargetType(migration, AzureResourceKind.SQLMI);
this._addItemIfTrue(columns, { value: 'backupFileName', name: loc.BACKUP_FILE_COLUMN_FILE_NAME, tooltip: loc.BACKUP_FILE_COLUMN_FILE_NAME, type: azdata.ColumnType.text, }, true);
this._addItemIfTrue(columns, { value: 'backupType', name: loc.TYPE, tooltip: loc.TYPE, type: azdata.ColumnType.text, }, true);
this._addItemIfTrue(columns, { value: 'backupStatus', name: loc.BACKUP_FILE_COLUMN_FILE_STATUS, tooltip: loc.BACKUP_FILE_COLUMN_FILE_STATUS, type: azdata.ColumnType.text }, isSHIR);
this._addItemIfTrue(columns, { value: 'restoreStatus', name: loc.BACKUP_FILE_COLUMN_RESTORE_STATUS, tooltip: loc.BACKUP_FILE_COLUMN_RESTORE_STATUS, type: azdata.ColumnType.text, }, isSqlMiTarget);
this._addItemIfTrue(columns, { value: 'backupSizeMB', name: loc.BACKUP_FILE_COLUMN_BACKUP_SIZE_MB, tooltip: loc.BACKUP_FILE_COLUMN_BACKUP_SIZE_MB, type: azdata.ColumnType.text, }, isSqlMiTarget);
this._addItemIfTrue(columns, { value: 'numberOfStripes', name: loc.BACKUP_FILE_COLUMN_NUMBER_OF_STRIPES, tooltip: loc.BACKUP_FILE_COLUMN_NUMBER_OF_STRIPES, type: azdata.ColumnType.text, }, isSqlMiTarget);
this._addItemIfTrue(columns, { value: 'dataUploadRate', name: loc.DATA_UPLOADED, tooltip: loc.DATA_UPLOADED, type: azdata.ColumnType.text, }, isSHIR);
this._addItemIfTrue(columns, { value: 'throughput', name: loc.COPY_THROUGHPUT, tooltip: loc.COPY_THROUGHPUT, type: azdata.ColumnType.text, }, isSHIR);
this._addItemIfTrue(columns, { value: 'backupStartTime', name: loc.BACKUP_START_TIME, tooltip: loc.BACKUP_START_TIME, type: azdata.ColumnType.text, }, isSHIR);
this._addItemIfTrue(columns, { value: 'restoreStartDate', name: loc.BACKUP_FILE_COLUMN_RESTORE_START_DATE, tooltip: loc.BACKUP_FILE_COLUMN_RESTORE_START_DATE, type: azdata.ColumnType.text, }, isSqlMiTarget);
this._addItemIfTrue(columns, { value: 'restoreFinishDate', name: loc.BACKUP_FILE_COLUMN_RESTORE_FINISH_DATE, tooltip: loc.BACKUP_FILE_COLUMN_RESTORE_FINISH_DATE, type: azdata.ColumnType.text, }, isSqlMiTarget);
this._addItemIfTrue(columns, { value: 'firstLsn', name: loc.FIRST_LSN, tooltip: loc.FIRST_LSN, type: azdata.ColumnType.text, }, isSHIR);
this._addItemIfTrue(columns, { value: 'lastLsn', name: loc.LAST_LSN, tooltip: loc.LAST_LSN, type: azdata.ColumnType.text, }, isSHIR);
return columns;
}
private _getTableData(migration?: DatabaseMigration, data?: (string | number)[][]): any[] {
const isSHIR = isShirMigration(migration);
const isSqlMiTarget = helper.isTargetType(migration, AzureResourceKind.SQLMI);
return data?.map(row => {
const rec: any[] = [];
this._addItemIfTrue(rec, row[BackupFileColumnIndex.backupFileName], true);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.backupType], true);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.backupStatus], isSHIR);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.restoreStatus], isSqlMiTarget);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.backupSizeMB], isSqlMiTarget);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.numberOfStripes], isSqlMiTarget);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.dataUploadRate], isSHIR);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.throughput], isSHIR);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.backupStartTime], isSHIR);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.restoreStartDate], isSqlMiTarget);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.restoreFinishDate], isSqlMiTarget);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.firstLsn], isSHIR);
this._addItemIfTrue(rec, row[BackupFileColumnIndex.lastLsn], isSHIR);
return rec;
}) || [];
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
this._fileCount = this.view.modelBuilder.text()
.withProps({
width: '500px',
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._fileTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
CSSStyles: { 'padding': '0px' },
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
display: 'inline-flex',
data: [],
height: '340px',
columns: [],
}).component();
const emptyTableImage = this.view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.emptyTable,
iconHeight: '100px',
iconWidth: '100px',
height: '100px',
width: '100px',
CSSStyles: { 'text-align': 'center' }
}).component();
const emptyTableText = this.view.modelBuilder.text()
.withProps({
value: loc.EMPTY_TABLE_TEXT,
CSSStyles: {
...styles.NOTE_CSS,
'margin-top': '8px',
'text-align': 'center',
'width': '300px'
}
}).component();
this._emptyTableFill = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
alignItems: 'center'
}).withItems([
emptyTableImage,
emptyTableText,
]).withProps({
width: '100%',
display: 'none'
}).component();
const container = this.view.modelBuilder.flexContainer()
.withItems([
this.createMigrationToolbarContainer(),
await this.migrationInfoGrid(),
this.view.modelBuilder.separator()
.withProps({
width: '100%',
CSSStyles: { 'padding': '0' }
})
.component(),
this._fileCount,
this._fileTable,
this._emptyTableFill,
], { CSSStyles: { 'padding': '0 15px 0 15px' } })
.withLayout({ flexFlow: 'column', })
.withProps({ width: '100%' })
.component();
this.content = container;
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected override async migrationInfoGrid(): Promise<azdata.FlexContainer> {
// left side
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', false);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
// SQL MI provided
this._lastUploadedFileNameField = await this.createInfoField(loc.FIELD_LABEL_LAST_UPLOADED_FILE, '');
this._lastUploadedFileTimeField = await this.createInfoField(loc.FIELD_LABEL_LAST_UPLOADED_FILE_TIME, '');
this._pendingDiffBackupsCountField = await this.createInfoField(loc.FIELD_LABEL_PENDING_DIFF_BACKUPS, '');
this._detectedFilesField = await this.createInfoField(loc.FIELD_LABEL_DETECTED_FILES, '');
this._queuedFilesField = await this.createInfoField(loc.FIELD_LABEL_QUEUED_FILES, '');
this._skippedFilesField = await this.createInfoField(loc.FIELD_LABEL_SKIPPED_FILES, '');
this._unrestorableFilesField = await this.createInfoField(loc.FIELD_LABEL_UNRESTORABLE_FILES, '');
// right side
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', false);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', false);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
this._lastRestoredFileTimeInfoField = await this.createInfoField(loc.FIELD_LABEL_LAST_RESTORED_FILE_TIME, '', false);
this._restoredFilesInfoField = await this.createInfoField(loc.FIELD_LABEL_RESTORED_FILES, '', false);
this._restoringFilesInfoField = await this.createInfoField(loc.FIELD_LABEL_RESTORING_FILES, '', false);
this._currentRestoredSizeInfoField = await this.createInfoField(loc.FIELD_LABEL_RESTORED_SIZE, '', false);
this._currentRestorePlanSizeInfoField = await this.createInfoField(loc.FIELD_LABEL_RESTORE_PLAN_SIZE, '', false);
this._restorePercentCompletedInfoField = await this.createInfoField(loc.FIELD_LABEL_RESTORE_PERCENT_COMPLETED, '', false);
this._miRestoreStateInfoField = await this.createInfoField(loc.FIELD_LABEL_MI_RESTORE_STATE, '', false);
const leftSide = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withProps({ width: '50%' })
.withItems([
this._sourceDatabaseInfoField.flexContainer,
this._sourceDetailsInfoField.flexContainer,
this._migrationStatusInfoField.flexContainer,
this._fullBackupFileOnInfoField.flexContainer,
this._backupLocationInfoField.flexContainer,
this._lastUploadedFileNameField.flexContainer,
this._lastUploadedFileTimeField.flexContainer,
this._pendingDiffBackupsCountField.flexContainer,
this._detectedFilesField.flexContainer,
this._queuedFilesField.flexContainer,
this._skippedFilesField.flexContainer,
this._unrestorableFilesField.flexContainer,
])
.component();
const rightSide = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withProps({ width: '50%' })
.withItems([
this._targetDatabaseInfoField.flexContainer,
this._targetServerInfoField.flexContainer,
this._targetVersionInfoField.flexContainer,
this._lastLSNInfoField.flexContainer,
this._lastAppliedBackupInfoField.flexContainer,
this._lastAppliedBackupTakenOnInfoField.flexContainer,
this._currentRestoringFileInfoField.flexContainer,
this._lastRestoredFileTimeInfoField.flexContainer,
this._restoredFilesInfoField.flexContainer,
this._restoringFilesInfoField.flexContainer,
this._currentRestoredSizeInfoField.flexContainer,
this._currentRestorePlanSizeInfoField.flexContainer,
this._restorePercentCompletedInfoField.flexContainer,
this._miRestoreStateInfoField.flexContainer,
])
.component();
return this.view.modelBuilder.flexContainer()
.withItems([leftSide, rightSide], { flex: '0 0 auto' })
.withLayout({ flexFlow: 'row', flexWrap: 'wrap' })
.component();
}
}

View File

@@ -8,17 +8,15 @@ import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import { MigrationServiceContext } from '../models/migrationLocalStorage';
import * as loc from '../constants/strings';
import * as styles from '../constants/styles';
import { DatabaseMigration, deleteMigration } from '../api/azure';
import { TabBase } from './tabBase';
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { MigrationTargetType } from '../models/stateMachine';
import { DashboardStatusBar } from './DashboardStatusBar';
import { canDeleteMigration } from '../constants/helper';
import { logError, TelemetryViews } from '../telemetry';
import { MenuCommands } from '../api/utils';
import { MenuCommands, MigrationTargetType } from '../api/utils';
export const infoFieldLgWidth: string = '330px';
export const infoFieldWidth: string = '250px';
@@ -26,9 +24,9 @@ export const infoFieldWidth: string = '250px';
const statusImageSize: number = 14;
export const MigrationTargetTypeName: loc.LookupTable<string> = {
[MigrationTargetType.SQLMI]: loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
[MigrationTargetType.SQLVM]: loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE,
[MigrationTargetType.SQLDB]: loc.AZURE_SQL_DATABASE,
[MigrationTargetType.SQLMI]: loc.SQL_MANAGED_INSTANCE,
[MigrationTargetType.SQLVM]: loc.SQL_VIRTUAL_MACHINE,
[MigrationTargetType.SQLDB]: loc.SQL_DATABASE,
};
export interface InfoFieldSchema {
@@ -70,7 +68,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
migration: DatabaseMigration): Promise<void> {
this.serviceContext = serviceContext;
this.model = new MigrationCutoverDialogModel(serviceContext, migration);
await this.refresh();
await this.refresh(true);
}
protected createBreadcrumbContainer(): azdata.FlexContainer {
@@ -404,37 +402,55 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
icon?: azdata.ImageComponent
}> {
const flexContainer = this.view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'column',
'padding-right': '12px'
}
}).component();
.withProps({ CSSStyles: { 'flex-direction': 'row', } })
.component();
const labelComponent = this.view.modelBuilder.text()
.withProps({
value: label,
title: label,
width: '170px',
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin-bottom': '0',
'font-size': '13px',
'line-height': '18px',
'margin': '2px 0 2px 0',
'float': 'left',
'overflow': 'hidden',
'text-overflow': 'ellipsis',
'display': 'inline-block',
'white-space': 'nowrap',
}
}).component();
flexContainer.addItem(labelComponent);
flexContainer.addItem(labelComponent, { flex: '0 0 auto' });
const separatorComponent = this.view.modelBuilder.text()
.withProps({
value: ':',
title: ':',
width: '15px',
CSSStyles: {
'font-size': '13px',
'line-height': '18px',
'margin': '2px 0 2px 0',
'float': 'left',
}
}).component();
flexContainer.addItem(separatorComponent, { flex: '0 0 auto' });
const textComponent = this.view.modelBuilder.text()
.withProps({
value: value,
title: value,
description: value,
width: '100%',
width: '300px',
CSSStyles: {
'font-size': '13px',
'line-height': '18px',
'margin': '4px 0 12px',
'margin': '2px 15px 2px 0',
'overflow': 'hidden',
'text-overflow': 'ellipsis',
'max-width': '230px',
'display': 'inline-block',
'float': 'left',
'white-space': 'nowrap',
}
}).component();
@@ -449,25 +465,24 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
width: statusImageSize,
title: value,
CSSStyles: {
'margin': '7px 3px 0 0',
'margin': '4px 4px 0 0',
'padding': '0'
}
}).component();
const iconTextComponent = this.view.modelBuilder.flexContainer()
.withItems([
iconComponent,
textComponent
]).withProps({
.withItems([iconComponent, textComponent])
.withProps({
CSSStyles: {
'margin': '0',
'padding': '0'
'padding': '0',
'height': '18px',
},
display: 'inline-flex'
}).component();
flexContainer.addItem(iconTextComponent);
flexContainer.addItem(iconTextComponent, { flex: '0 0 auto' });
} else {
flexContainer.addItem(textComponent);
flexContainer.addItem(textComponent, { flex: '0 0 auto' });
}
return {
@@ -505,4 +520,13 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
private _getMigrationDetails(): string {
return JSON.stringify(this.model.migration, undefined, 2);
}
protected _updateInfoFieldValue(info: InfoFieldSchema, value: string) {
info.text.value = value;
info.text.title = value;
info.text.description = value;
if (info.icon) {
info.icon.title = value;
}
}
}

View File

@@ -6,15 +6,15 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as loc from '../constants/strings';
import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils';
import { getMigrationStatusImage, getPipelineStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemetry';
import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
import { CopyProgressDetail, getResourceName } from '../api/azure';
import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { InfoFieldSchema, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { IconPathHelper } from '../constants/iconPathHelper';
import { EOL } from 'os';
import { DashboardStatusBar } from './DashboardStatusBar';
import { getSourceConnectionServerInfo } from '../api/sqlUtils';
import { EmptySettingValue } from './tabBase';
const MigrationDetailsTableTabId = 'MigrationDetailsTableTab';
@@ -43,12 +43,12 @@ enum SummaryCardIndex {
export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationDetailsTableTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _serverObjectsInfoField!: InfoFieldSchema;
private _tableFilterInputBox!: azdata.InputBoxComponent;
private _columnSortDropdown!: azdata.DropDownComponent;
private _columnSortCheckbox!: azdata.CheckBoxComponent;
@@ -76,10 +76,10 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
return this;
}
@debounce(500)
public async refresh(): Promise<void> {
public async refresh(initialize?: boolean): Promise<void> {
if (this.isRefreshing ||
this.refreshLoader === undefined) {
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return;
}
@@ -88,7 +88,9 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
this.isRefreshing = true;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
if (initialize) {
await this._clearControlsValue();
}
await this.model.fetchStatus();
await this._loadData();
} catch (e) {
@@ -102,118 +104,16 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
}
}
private async _loadData(): Promise<void> {
const migration = this.model?.migration;
await this.showMigrationErrors(this.model?.migration);
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
const sqlServerName = migration?.properties.sourceServerName;
const sourceDatabaseName = migration?.properties.sourceDatabaseName;
const sqlServerInfo = await getSourceConnectionServerInfo();
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration?.name;
const targetServerName = getResourceName(migration?.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
const hashSet: loc.LookupTable<number> = {};
await this._populateTableData(hashSet);
const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0;
const cancelledCount =
(hashSet[PipelineStatusCodes.Canceled] ?? 0) +
(hashSet[PipelineStatusCodes.Cancelled] ?? 0);
const failedCount = hashSet[PipelineStatusCodes.Failed] ?? 0;
const inProgressCount =
(hashSet[PipelineStatusCodes.Queued] ?? 0) +
(hashSet[PipelineStatusCodes.CopyFinished] ?? 0) +
(hashSet[PipelineStatusCodes.Copying] ?? 0) +
(hashSet[PipelineStatusCodes.PreparingForCopy] ?? 0) +
(hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) +
(hashSet[PipelineStatusCodes.InProgress] ?? 0);
const totalCount = this._progressDetail.length;
this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount);
this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount);
this._updateSummaryComponent(SummaryCardIndex.SuccessfulTables, successCount);
this._updateSummaryComponent(SummaryCardIndex.FailedTables, failedCount);
this._updateSummaryComponent(SummaryCardIndex.CanceledTables, cancelledCount);
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._serverObjectsInfoField.text.value = totalCount.toLocaleString();
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.deleteButton.enabled = canDeleteMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
}
private async _populateTableData(hashSet: loc.LookupTable<number> = {}): Promise<void> {
if (this._progressTable.data.length > 0) {
await this._progressTable.updateProperty('data', []);
}
// Sort table data
this._sortTableMigrations(
this._progressDetail,
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true);
const data = this._progressDetail.map((d) => {
hashSet[d.status] = (hashSet[d.status] ?? 0) + 1;
return [
d.tableName,
<azdata.HyperlinkColumnCellValue>{
icon: getPipelineStatusImage(d.status),
title: loc.PipelineRunStatus[d.status] ?? d.status?.toUpperCase(),
},
formatSizeBytes(d.dataRead),
formatSizeBytes(d.dataWritten),
formatNumber(d.rowsRead),
formatNumber(d.rowsCopied),
formatSizeKb(d.copyThroughput),
formatTime((d.copyDuration ?? 0) * 1000),
loc.ParallelCopyType[d.parallelCopyType] ?? d.parallelCopyType,
d.usedParallelCopies,
formatDateTimeString(d.copyStart),
];
}) ?? [];
// Filter tableData
const filteredData = this._filterTables(data, this._tableFilterInputBox.value);
await this._progressTable.updateProperty('data', filteredData);
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
this._progressTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
CSSStyles: {
'padding-left': '0px',
'max-width': '1111px'
},
CSSStyles: { 'padding-left': '0px' },
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
display: 'inline-flex',
data: [],
height: '300px',
height: '340px',
columns: [
{
value: TableColumns.tableName,
@@ -286,18 +186,23 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
],
}).component();
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
{ component: await this._createStatusBar() },
{ component: await this._createTableFilter() },
{ component: this._progressTable },
];
const container = this.view.modelBuilder.flexContainer()
.withItems([
this.createMigrationToolbarContainer(),
await this.migrationInfoGrid(),
this.view.modelBuilder.separator()
.withProps({
width: '100%',
CSSStyles: { 'padding': '0' }
})
.component(),
await this._createStatusBar(),
await this._createTableFilter(),
this._progressTable,
], { CSSStyles: { 'padding': '15px 15px 0 15px' } })
.withLayout({ flexFlow: 'column', })
.withProps({ width: '100%' })
.component();
this.disposables.push(
this._progressTable.onCellAction!(
@@ -319,19 +224,159 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
}
}));
const formContainer = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withProps({ width: '100%', CSSStyles: { margin: '0 0 0 5px', padding: '0 15px 0 15px' } })
.component();
this.content = formContainer;
this.content = container;
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected override async migrationInfoGrid(): Promise<azdata.FlexContainer> {
// left side
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
// right side
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
this._serverObjectsInfoField = await this.createInfoField(loc.SERVER_OBJECTS_FIELD_LABEL, '');
const leftSide = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withProps({ width: '50%' })
.withItems([
this._sourceDatabaseInfoField.flexContainer,
this._sourceDetailsInfoField.flexContainer,
this._migrationStatusInfoField.flexContainer,
])
.component();
const rightSide = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withProps({ width: '50%' })
.withItems([
this._targetDatabaseInfoField.flexContainer,
this._targetServerInfoField.flexContainer,
this._targetVersionInfoField.flexContainer,
this._serverObjectsInfoField.flexContainer,
])
.component();
return this.view.modelBuilder.flexContainer()
.withItems([leftSide, rightSide], { flex: '0 0 auto' })
.withLayout({ flexFlow: 'row', flexWrap: 'wrap' })
.component();
}
private async _loadData(): Promise<void> {
const migration = this.model?.migration;
await this.showMigrationErrors(this.model?.migration);
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
const sqlServerName = migration?.properties.sourceServerName;
const sourceDatabaseName = migration?.properties.sourceDatabaseName;
const targetDatabaseName = migration?.name;
const targetServerName = getResourceName(migration?.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
const hashSet: loc.LookupTable<number> = {};
await this._populateTableData(hashSet);
const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0;
const cancelledCount =
(hashSet[PipelineStatusCodes.Canceled] ?? 0) +
(hashSet[PipelineStatusCodes.Cancelled] ?? 0);
const failedCount = hashSet[PipelineStatusCodes.Failed] ?? 0;
const inProgressCount =
(hashSet[PipelineStatusCodes.Queued] ?? 0) +
(hashSet[PipelineStatusCodes.CopyFinished] ?? 0) +
(hashSet[PipelineStatusCodes.Copying] ?? 0) +
(hashSet[PipelineStatusCodes.PreparingForCopy] ?? 0) +
(hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) +
(hashSet[PipelineStatusCodes.InProgress] ?? 0);
const totalCount = this._progressDetail.length;
this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount);
this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount);
this._updateSummaryComponent(SummaryCardIndex.SuccessfulTables, successCount);
this._updateSummaryComponent(SummaryCardIndex.FailedTables, failedCount);
this._updateSummaryComponent(SummaryCardIndex.CanceledTables, cancelledCount);
this.databaseLabel.value = sourceDatabaseName;
// Left side
this._updateInfoFieldValue(this._sourceDatabaseInfoField, sourceDatabaseName ?? EmptySettingValue);
this._updateInfoFieldValue(this._sourceDetailsInfoField, sqlServerName ?? EmptySettingValue);
this._updateInfoFieldValue(this._migrationStatusInfoField, getMigrationStatusString(migration) ?? EmptySettingValue);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
// Right side
this._updateInfoFieldValue(this._targetDatabaseInfoField, targetDatabaseName ?? EmptySettingValue);
this._updateInfoFieldValue(this._targetServerInfoField, targetServerName ?? EmptySettingValue);
this._updateInfoFieldValue(this._targetVersionInfoField, targetServerVersion ?? EmptySettingValue);
this._updateInfoFieldValue(this._serverObjectsInfoField, totalCount.toLocaleString() ?? EmptySettingValue);
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.deleteButton.enabled = canDeleteMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
}
private async _populateTableData(hashSet: loc.LookupTable<number> = {}): Promise<void> {
if (this._progressTable.data.length > 0) {
await this._progressTable.updateProperty('data', []);
}
// Sort table data
this._sortTableMigrations(
this._progressDetail,
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true);
const data = this._progressDetail.map((d) => {
hashSet[d.status] = (hashSet[d.status] ?? 0) + 1;
return [
d.tableName,
<azdata.HyperlinkColumnCellValue>{
icon: getPipelineStatusImage(d.status),
title: loc.PipelineRunStatus[d.status] ?? d.status?.toUpperCase(),
},
formatSizeBytes(d.dataRead),
formatSizeBytes(d.dataWritten),
formatNumber(d.rowsRead),
formatNumber(d.rowsCopied),
formatSizeKb(d.copyThroughput),
formatTime((d.copyDuration ?? 0) * 1000),
loc.ParallelCopyType[d.parallelCopyType] ?? d.parallelCopyType,
d.usedParallelCopies,
formatDateTimeString(d.copyStart),
];
}) ?? [];
// Filter tableData
const filteredData = this._filterTables(data, this._tableFilterInputBox.value);
await this._progressTable.updateProperty('data', filteredData);
}
private _clearControlsValue(): void {
this._updateInfoFieldValue(this._sourceDatabaseInfoField, '');
this._updateInfoFieldValue(this._sourceDetailsInfoField, '');
this._updateInfoFieldValue(this._migrationStatusInfoField, '');
this._updateInfoFieldValue(this._targetDatabaseInfoField, '');
this._updateInfoFieldValue(this._targetServerInfoField, '');
this._updateInfoFieldValue(this._targetVersionInfoField, '');
this._updateInfoFieldValue(this._serverObjectsInfoField, '');
}
private _sortTableMigrations(data: CopyProgressDetail[], columnName: string, ascending: boolean): void {
const sortDir = ascending ? -1 : 1;
switch (columnName) {
@@ -515,55 +560,4 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
.withLayout({ flexFlow: 'column' })
.component();
}
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldLgWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._serverObjectsInfoField = await this.createInfoField(loc.SERVER_OBJECTS_FIELD_LABEL, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._serverObjectsInfoField, flexStatus);
const flexInfoProps = {
flex: '0',
CSSStyles: { 'flex': '0', 'width': infoFieldLgWidth }
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
return flexInfo;
}
}

View File

@@ -166,7 +166,8 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
description: loc.MIGRATION_SERVICE_DESCRIPTION,
buttonType: azdata.ButtonType.Informational,
width: 230,
}).component();
})
.component();
this.disposables.push(
this._serviceContextButton.onDidClick(

View File

@@ -12,8 +12,7 @@ import { DatabaseMigration, getMigrationDetails } from '../api/azure';
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
import { FileStorageType } from '../models/stateMachine';
import { MigrationDetailsTabBase } from './migrationDetailsTabBase';
import { MigrationDetailsFileShareTab } from './migrationDetailsFileShareTab';
import { MigrationDetailsBlobContainerTab } from './migrationDetailsBlobContainerTab';
import { MigrationDetailsTab } from './migrationDetailsTab';
import { MigrationDetailsTableTab } from './migrationDetailsTableTab';
import { DashboardStatusBar } from './DashboardStatusBar';
import { getSourceConnectionId } from '../api/sqlUtils';
@@ -23,9 +22,8 @@ export const MigrationsTabId = 'MigrationsTab';
export class MigrationsTab extends TabBase<MigrationsTab> {
private _tab!: azdata.DivContainer;
private _migrationsListTab!: MigrationsListTab;
private _migrationDetailsViewTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsFileShareTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>;
private _selectedTabId: string | undefined = undefined;
private _migrationDetailsEvent!: vscode.EventEmitter<MigrationDetailsEvent>;
@@ -61,7 +59,7 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
case MigrationsListTabId:
return this._migrationsListTab.refresh();
default:
return this._migrationDetailsTab.refresh();
return this._migrationDetailsViewTab.refresh();
}
}
@@ -93,26 +91,19 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
}
};
this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create(
this._migrationDetailsTab = await new MigrationDetailsTab().create(
this.context,
this.view,
openMigrationsListTab,
this.statusBar);
this.disposables.push(this._migrationDetailsBlobTab);
this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create(
this.context,
this.view,
openMigrationsListTab,
this.statusBar);
this.disposables.push(this._migrationDetailsFileShareTab);
this.disposables.push(this._migrationDetailsTab);
this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create(
this.context,
this.view,
openMigrationsListTab,
this.statusBar);
this.disposables.push(this._migrationDetailsFileShareTab);
this.disposables.push(this._migrationDetailsTableTab);
this.disposables.push(
this._migrationDetailsEvent.event(async e => {
@@ -135,22 +126,20 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
public async openMigrationDetails(migration: DatabaseMigration): Promise<void> {
switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) {
case FileStorageType.AzureBlob:
this._migrationDetailsTab = this._migrationDetailsBlobTab;
break;
case FileStorageType.FileShare:
this._migrationDetailsTab = this._migrationDetailsFileShareTab;
this._migrationDetailsViewTab = this._migrationDetailsTab;
break;
case FileStorageType.None:
this._migrationDetailsTab = this._migrationDetailsTableTab;
this._migrationDetailsViewTab = this._migrationDetailsTableTab;
break;
}
await this._migrationDetailsTab.setMigrationContext(
await this._migrationDetailsViewTab.setMigrationContext(
await MigrationLocalStorage.getMigrationServiceContext(),
migration);
const promise = this._migrationDetailsTab.refresh();
await this._openTab(this._migrationDetailsTab);
const promise = this._migrationDetailsViewTab.refresh();
await this._openTab(this._migrationDetailsViewTab);
await promise;
}

View File

@@ -50,7 +50,7 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
protected abstract initialize(view: azdata.ModelView): Promise<void>;
public abstract refresh(): Promise<void>;
public abstract refresh(initialize?: boolean): Promise<void>;
dispose() {
this.disposables.forEach(

View File

@@ -5,11 +5,12 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
import { MigrationStateModel } from '../../models/stateMachine';
import { SqlDatabaseTree } from './sqlDatabasesTree';
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
import * as constants from '../../constants/strings';
import * as utils from '../../api/utils';
import { MigrationTargetType } from '../../api/utils';
import * as fs from 'fs';
import path = require('path');
import { SqlMigrationImpactedObjectInfo } from '../../service/contracts';

View File

@@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
import { MigrationStateModel } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import { debounce } from '../../api/utils';
import { debounce, MigrationTargetType } from '../../api/utils';
import { IconPath, IconPathHelper } from '../../constants/iconPathHelper';
import * as styles from '../../constants/styles';
import { EOL } from 'os';

View File

@@ -9,10 +9,10 @@ import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as constants from '../../constants/strings';
import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
import { convertByteSizeToReadableUnit, get12HourTime, MigrationTargetType } from '../../api/utils';
import * as styles from '../../constants/styles';
import { getMigrationTargetTypeEnum, isBlobMigration } from '../../constants/helper';
import { MigrationTargetType, ServiceTier } from '../../models/stateMachine';
import { ServiceTier } from '../../models/stateMachine';
export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -79,8 +79,8 @@ export class ConfirmCutoverDialog {
let infoDisplay = 'none';
if (getMigrationTargetTypeEnum(this.migrationCutoverModel.migration) === MigrationTargetType.SQLMI) {
const targetInstance = await getMigrationTargetInstance(
this.migrationCutoverModel.serviceConstext.azureAccount!,
this.migrationCutoverModel.serviceConstext.subscription!,
this.migrationCutoverModel.serviceContext.azureAccount!,
this.migrationCutoverModel.serviceContext.subscription!,
this.migrationCutoverModel.migration);
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === ServiceTier.BusinessCritical) {

View File

@@ -14,23 +14,26 @@ export class MigrationCutoverDialogModel {
public CancelMigrationError?: Error;
constructor(
public serviceConstext: MigrationServiceContext,
public serviceContext: MigrationServiceContext,
public migration: DatabaseMigration) { }
public async fetchStatus(): Promise<void> {
const migrationStatus = await getMigrationDetails(
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.migration.id,
this.migration.properties?.migrationOperationId);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.MigrationStatus,
{ 'migrationStatus': migrationStatus.properties?.migrationStatus },
{});
this.migration = migrationStatus;
try {
const migrationStatus = await getMigrationDetails(
this.serviceContext.azureAccount!,
this.serviceContext.subscription!,
this.migration.id,
this.migration.properties?.migrationOperationId);
this.migration = migrationStatus;
} catch (error) {
logError(TelemetryViews.MigrationDetailsTab, 'fetchStatus', error);
} finally {
sendSqlMigrationActionEvent(
TelemetryViews.MigrationDetailsTab,
TelemetryAction.MigrationStatus,
{ 'migrationStatus': this.migration.properties?.migrationStatus },
{});
}
}
public async startCutover(): Promise<DatabaseMigration | undefined> {
@@ -38,14 +41,14 @@ export class MigrationCutoverDialogModel {
this.CutoverError = undefined;
if (this.migration) {
const cutover = await startMigrationCutover(
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.serviceContext.azureAccount!,
this.serviceContext.subscription!,
this.migration!);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CutoverMigration,
{
...this.getTelemetryProps(this.serviceConstext, this.migration),
...this.getTelemetryProps(this.serviceContext, this.migration),
'migrationEndTime': new Date().toString(),
},
{}
@@ -65,14 +68,14 @@ export class MigrationCutoverDialogModel {
if (this.migration) {
const cutoverStartTime = new Date().toString();
await stopMigration(
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.serviceContext.azureAccount!,
this.serviceContext.subscription!,
this.migration);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CancelMigration,
{
...this.getTelemetryProps(this.serviceConstext, this.migration),
...this.getTelemetryProps(this.serviceContext, this.migration),
'migrationMode': getMigrationMode(this.migration),
'cutoverStartTime': cutoverStartTime,
},

View File

@@ -5,11 +5,12 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
import { MigrationStateModel } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import * as contracts from '../../service/contracts';
import * as styles from '../../constants/styles';
import * as utils from '../../api/utils';
import { MigrationTargetType } from '../../api/utils';
import * as fs from 'fs';
import path = require('path');

View File

@@ -7,11 +7,12 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as constants from '../../constants/strings';
import { validateIrDatabaseMigrationSettings, validateIrSqlDatabaseMigrationSettings } from '../../api/azure';
import { MigrationStateModel, MigrationTargetType, NetworkShare, ValidateIrState, ValidationResult } from '../../models/stateMachine';
import { MigrationStateModel, NetworkShare, ValidateIrState, ValidationResult } from '../../models/stateMachine';
import { EOL } from 'os';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { getEncryptConnectionValue, getSourceConnectionProfile, getTrustServerCertificateValue } from '../../api/sqlUtils';
import { logError, TelemetryViews } from '../../telemetry';
import { MigrationTargetType } from '../../api/utils';
const DialogName = 'ValidateIrDialog';

View File

@@ -13,7 +13,7 @@ import * as constants from '../constants/strings';
import * as nls from 'vscode-nls';
import { v4 as uuidv4 } from 'uuid';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemetry';
import { hashString, deepClone, getBlobContainerNameWithFolder, Blob, getLastBackupFileNameWithoutFolder } from '../api/utils';
import { hashString, deepClone, getBlobContainerNameWithFolder, Blob, getLastBackupFileNameWithoutFolder, MigrationTargetType } from '../api/utils';
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
import { excludeDatabases, getEncryptConnectionValue, getSourceConnectionId, getSourceConnectionProfile, getSourceConnectionServerInfo, getSourceConnectionString, getSourceConnectionUri, getTrustServerCertificateValue, SourceDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
import { LoginMigrationModel } from './loginMigrationModel';
@@ -57,12 +57,6 @@ export enum ServiceTier {
BusinessCritical = 'BusinessCritical',
}
export enum MigrationTargetType {
SQLVM = 'AzureSqlVirtualMachine',
SQLMI = 'AzureSqlManagedInstance',
SQLDB = 'AzureSqlDatabase'
}
export enum MigrationSourceAuthenticationType {
Integrated = 'WindowsAuthentication',
Sql = 'SqlAuthentication'

View File

@@ -8,11 +8,12 @@ import * as vscode from 'vscode';
import { EOL } from 'os';
import { getStorageAccountAccessKeys, SqlVMServer } from '../api/azure';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, NetworkShare, StateChangeEvent, ValidateIrState, ValidationResult } from '../models/stateMachine';
import { MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, NetworkContainerType, NetworkShare, StateChangeEvent, ValidateIrState, ValidationResult } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { IconPathHelper } from '../constants/iconPathHelper';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import * as utils from '../api/utils';
import { MigrationTargetType } from '../api/utils';
import { logError, TelemetryViews } from '../telemetry';
import * as styles from '../constants/styles';
import { TableMigrationSelectionDialog } from '../dialog/tableMigrationSelection/tableMigrationSelectionDialog';

View File

@@ -7,11 +7,12 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { EOL } from 'os';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../models/stateMachine';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import * as styles from '../constants/styles';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import * as utils from '../api/utils';
import { MigrationTargetType } from '../api/utils';
import { azureResource } from 'azurecore';
import { AzureSqlDatabaseServer, getVMInstanceView, SqlVMServer } from '../api/azure';
import { collectSourceLogins, collectTargetLogins, getSourceConnectionId, getSourceConnectionProfile, isSourceConnectionSysAdmin, LoginTableInfo } from '../api/sqlUtils';

View File

@@ -6,9 +6,10 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as utils from '../api/utils';
import { MigrationTargetType } from '../api/utils';
import * as contracts from '../service/contracts';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, PerformanceDataSourceOptions, StateChangeEvent, AssessmentRuleId } from '../models/stateMachine';
import { MigrationStateModel, PerformanceDataSourceOptions, StateChangeEvent, AssessmentRuleId } from '../models/stateMachine';
import { AssessmentResultsDialog } from '../dialog/assessmentResults/assessmentResultsDialog';
import { SkuRecommendationResultsDialog } from '../dialog/skuRecommendationResults/skuRecommendationResultsDialog';
import { GetAzureRecommendationDialog } from '../dialog/skuRecommendationResults/getAzureRecommendationDialog';

View File

@@ -6,12 +6,13 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { MigrationMode, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { createHeadingTextComponent, createInformationRow, createLabelTextComponent } from './wizardController';
import { getResourceGroupFromId } from '../api/azure';
import { TargetDatabaseSummaryDialog } from '../dialog/targetDatabaseSummary/targetDatabaseSummaryDialog';
import * as styles from '../constants/styles';
import { MigrationTargetType } from '../api/utils';
export class SummaryPage extends MigrationWizardPage {
private _view!: azdata.ModelView;

View File

@@ -7,11 +7,12 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { EOL } from 'os';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../models/stateMachine';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import * as styles from '../constants/styles';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import * as utils from '../api/utils';
import { MigrationTargetType } from '../api/utils';
import { azureResource } from 'azurecore';
import { AzureSqlDatabaseServer, getVMInstanceView, SqlVMServer } from '../api/azure';
import { collectTargetDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
@@ -42,7 +43,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
private _targetPasswordInputBox!: azdata.InputBoxComponent;
private _testConectionButton!: azdata.ButtonComponent;
private _connectionResultsInfoBox!: azdata.InfoBoxComponent;
private _migrationTargetPlatform!: MigrationTargetType;
private _migrationTargetPlatform!: utils.MigrationTargetType;
private _serviceContext!: MigrationServiceContext;
constructor(