From ef02e2bfceb1c0115446620324944988dbfb2d42 Mon Sep 17 00:00:00 2001 From: brian-harris <61598682+brian-harris@users.noreply.github.com> Date: Wed, 29 Mar 2023 07:48:30 -0700 Subject: [PATCH] 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 --- extensions/sql-migration/src/api/azure.ts | 41 +- extensions/sql-migration/src/api/utils.ts | 18 +- .../sql-migration/src/constants/helper.ts | 88 ++- .../sql-migration/src/constants/strings.ts | 70 ++- .../src/dashboard/dashboardTab.ts | 13 +- .../migrationDetailsBlobContainerTab.ts | 208 ------- .../dashboard/migrationDetailsFileShareTab.ts | 392 ------------ .../src/dashboard/migrationDetailsTab.ts | 573 ++++++++++++++++++ .../src/dashboard/migrationDetailsTabBase.ts | 80 ++- .../src/dashboard/migrationDetailsTableTab.ts | 366 ++++++----- .../src/dashboard/migrationsListTab.ts | 3 +- .../src/dashboard/migrationsTab.ts | 33 +- .../sql-migration/src/dashboard/tabBase.ts | 2 +- .../assessmentResultsDialog.ts | 3 +- .../assessmentResults/sqlDatabasesTree.ts | 4 +- .../migrationCutover/confirmCutoverDialog.ts | 8 +- .../migrationCutoverDialogModel.ts | 43 +- .../skuRecommendationResultsDialog.ts | 3 +- .../validationResults/validateIrDialog.ts | 3 +- .../sql-migration/src/models/stateMachine.ts | 8 +- .../src/wizard/databaseBackupPage.ts | 3 +- .../loginMigrationTargetSelectionPage.ts | 3 +- .../src/wizard/skuRecommendationPage.ts | 3 +- .../sql-migration/src/wizard/summaryPage.ts | 3 +- .../src/wizard/targetSelectionPage.ts | 5 +- 25 files changed, 1068 insertions(+), 908 deletions(-) delete mode 100644 extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts delete mode 100644 extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts create mode 100644 extensions/sql-migration/src/dashboard/migrationDetailsTab.ts diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index e306440132..aacaef56c5 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -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; diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index c5682b5c68..0b488df038 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -37,6 +37,12 @@ export const MenuCommands = { SendFeedback: 'sqlmigration.sendfeedback', }; +export enum MigrationTargetType { + SQLVM = 'AzureSqlVirtualMachine', + SQLMI = 'AzureSqlManagedInstance', + SQLDB = 'AzureSqlDatabase' +} + export function deepClone(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; } diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts index 311108ce84..7af3714c49 100644 --- a/extensions/sql-migration/src/constants/helper.ts +++ b/extensions/sql-migration/src/constants/helper.ts @@ -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; } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 05c79df924..719a411920 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -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 = { [ParallelCopyTypeCodes.DynamicRange]: localize('sql.migration.parallel.copy.type.dynamic', 'Dynamic range'), }; +export const BackupTypeLookup: LookupTable = { + [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 = { + [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 = { + [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)); diff --git a/extensions/sql-migration/src/dashboard/dashboardTab.ts b/extensions/sql-migration/src/dashboard/dashboardTab.ts index 4cf22aa78a..975aeeed84 100644 --- a/extensions/sql-migration/src/dashboard/dashboardTab.ts +++ b/extensions/sql-migration/src/dashboard/dashboardTab.ts @@ -765,6 +765,13 @@ export class DashboardTab extends TabBase { }) .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 { )); await this.updateServiceContext(this._serviceContextButton); - this.disposables.push( - this._serviceContextButton.onDidClick(async () => { - const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent); - await dialog.initialize(); - })); - return this._serviceContextButton; } diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts deleted file mode 100644 index b019a7d92a..0000000000 --- a/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts +++ /dev/null @@ -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 { - 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, - statusBar: DashboardStatusBar, - ): Promise { - - this.view = view; - this.context = context; - this.openMigrationsListFcn = openMigrationsListFcn; - this.statusBar = statusBar; - - await this.initialize(this.view); - - return this; - } - - public async refresh(): Promise { - 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 { - try { - const formItems: azdata.FormComponent[] = [ - { 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 { - 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; - } -} diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts deleted file mode 100644 index 67b1f1d8f4..0000000000 --- a/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts +++ /dev/null @@ -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 { - 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, - statusBar: DashboardStatusBar): Promise { - - this.view = view; - this.context = context; - this.openMigrationsListFcn = openMigrationsListFcn; - this.statusBar = statusBar; - - await this.initialize(this.view); - - return this; - } - - public async refresh(): Promise { - 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 { - 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[] = [ - { 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 { - 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; - } -} diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTab.ts new file mode 100644 index 0000000000..7488619b25 --- /dev/null +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTab.ts @@ -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 { + 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, + statusBar: DashboardStatusBar): Promise { + + 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 { + 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 { + 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 { + 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 { + // 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(); + } +} diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts index b8ca673275..575e50b435 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts @@ -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 = { - [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 extends TabBase { migration: DatabaseMigration): Promise { 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 extends TabBase { 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 extends TabBase { 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 extends TabBase { 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; + } + } } diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts index 53467ae1d0..f357afeda2 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts @@ -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 { 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 { + public async refresh(initialize?: boolean): Promise { if (this.isRefreshing || - this.refreshLoader === undefined) { + this.refreshLoader === undefined || + this.model?.migration === undefined) { return; } @@ -88,7 +88,9 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase { - 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 = {}; - 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 = {}): Promise { - if (this._progressTable.data.length > 0) { - await this._progressTable.updateProperty('data', []); - } - - // Sort table data - this._sortTableMigrations( - this._progressDetail, - (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, - { - 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 { 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[] = [ - { 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 { + // 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 { + 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 = {}; + 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 = {}): Promise { + if (this._progressTable.data.length > 0) { + await this._progressTable.updateProperty('data', []); + } + + // Sort table data + this._sortTableMigrations( + this._progressDetail, + (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, + { + 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 { - 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; - } } diff --git a/extensions/sql-migration/src/dashboard/migrationsListTab.ts b/extensions/sql-migration/src/dashboard/migrationsListTab.ts index bd1628d60a..5f6da55b9e 100644 --- a/extensions/sql-migration/src/dashboard/migrationsListTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationsListTab.ts @@ -166,7 +166,8 @@ export class MigrationsListTab extends TabBase { description: loc.MIGRATION_SERVICE_DESCRIPTION, buttonType: azdata.ButtonType.Informational, width: 230, - }).component(); + }) + .component(); this.disposables.push( this._serviceContextButton.onDidClick( diff --git a/extensions/sql-migration/src/dashboard/migrationsTab.ts b/extensions/sql-migration/src/dashboard/migrationsTab.ts index 19e96e0651..059ce3a656 100644 --- a/extensions/sql-migration/src/dashboard/migrationsTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationsTab.ts @@ -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 { private _tab!: azdata.DivContainer; private _migrationsListTab!: MigrationsListTab; + private _migrationDetailsViewTab!: MigrationDetailsTabBase; private _migrationDetailsTab!: MigrationDetailsTabBase; - private _migrationDetailsFileShareTab!: MigrationDetailsTabBase; - private _migrationDetailsBlobTab!: MigrationDetailsTabBase; private _migrationDetailsTableTab!: MigrationDetailsTabBase; private _selectedTabId: string | undefined = undefined; private _migrationDetailsEvent!: vscode.EventEmitter; @@ -61,7 +59,7 @@ export class MigrationsTab extends TabBase { case MigrationsListTabId: return this._migrationsListTab.refresh(); default: - return this._migrationDetailsTab.refresh(); + return this._migrationDetailsViewTab.refresh(); } } @@ -93,26 +91,19 @@ export class MigrationsTab extends TabBase { } }; - 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 { public async openMigrationDetails(migration: DatabaseMigration): Promise { 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; } diff --git a/extensions/sql-migration/src/dashboard/tabBase.ts b/extensions/sql-migration/src/dashboard/tabBase.ts index 739e547886..b0082179bf 100644 --- a/extensions/sql-migration/src/dashboard/tabBase.ts +++ b/extensions/sql-migration/src/dashboard/tabBase.ts @@ -50,7 +50,7 @@ export abstract class TabBase implements azdata.Tab, vscode.Disposable { protected abstract initialize(view: azdata.ModelView): Promise; - public abstract refresh(): Promise; + public abstract refresh(initialize?: boolean): Promise; dispose() { this.disposables.forEach( diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index 1de7254e08..b5ca43bfc3 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -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'; diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index 714f8c70bc..c152e1441b 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -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'; diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts index 56c0355777..e78872a9e5 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts @@ -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 ((targetInstance)?.sku?.tier === ServiceTier.BusinessCritical) { diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts index 2cd9b64005..3f5b094930 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts @@ -14,23 +14,26 @@ export class MigrationCutoverDialogModel { public CancelMigrationError?: Error; constructor( - public serviceConstext: MigrationServiceContext, + public serviceContext: MigrationServiceContext, public migration: DatabaseMigration) { } public async fetchStatus(): Promise { - 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 { @@ -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, }, diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts index db16e1c456..6448ae55f3 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts @@ -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'); diff --git a/extensions/sql-migration/src/dialog/validationResults/validateIrDialog.ts b/extensions/sql-migration/src/dialog/validationResults/validateIrDialog.ts index 126400f71d..ee6d561d89 100644 --- a/extensions/sql-migration/src/dialog/validationResults/validateIrDialog.ts +++ b/extensions/sql-migration/src/dialog/validationResults/validateIrDialog.ts @@ -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'; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 8c8422bfae..87fc69f90c 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -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' diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 3292ba50ef..b4c5e259b4 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -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'; diff --git a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts index 4cca537162..f733b49584 100644 --- a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts @@ -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'; diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 93b96f6294..5985c39130 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -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'; diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 54adc0e3db..99800bc2bd 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -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; diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index 6717fba15b..7167379e76 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -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(