diff --git a/extensions/sql-migration/images/addNew.svg b/extensions/sql-migration/images/addNew.svg
new file mode 100644
index 0000000000..1ef77155d1
--- /dev/null
+++ b/extensions/sql-migration/images/addNew.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/sql-migration/images/allTables.svg b/extensions/sql-migration/images/allTables.svg
new file mode 100644
index 0000000000..db28a06bd9
--- /dev/null
+++ b/extensions/sql-migration/images/allTables.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/sql-migration/images/breadCrumb.svg b/extensions/sql-migration/images/breadCrumb.svg
new file mode 100644
index 0000000000..d8cad89ee2
--- /dev/null
+++ b/extensions/sql-migration/images/breadCrumb.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json
index 9b590503f7..07f42c6553 100644
--- a/extensions/sql-migration/package.json
+++ b/extensions/sql-migration/package.json
@@ -2,7 +2,7 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
- "version": "1.0.4",
+ "version": "1.0.5",
"publisher": "Microsoft",
"preview": false,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -147,32 +147,7 @@
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud && mssql:engineedition != 8",
"hideRefreshTask": true,
"container": {
- "grid-container": [
- {
- "name": "",
- "row": 0,
- "col": 0,
- "widget": {
- "tasks-widget": [
- "sqlmigration.start",
- "sqlmigration.newsupportrequest",
- "sqlmigration.sendfeedback"
- ]
- }
- },
- {
- "name": "",
- "row": 0,
- "col": 1,
- "rowspan": 2.5,
- "colspan": 3.5,
- "widget": {
- "modelview": {
- "id": "migration.dashboard"
- }
- }
- }
- ]
+ "modelview-container": null
}
}
]
diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts
index 27d9e6fa46..c519bf2568 100644
--- a/extensions/sql-migration/src/api/azure.ts
+++ b/extensions/sql-migration/src/api/azure.ts
@@ -526,6 +526,22 @@ export interface MigrationStatusDetails {
lastRestoredFilename: string;
pendingLogBackupsCount: number;
invalidFiles: string[];
+ listOfCopyProgressDetails: CopyProgressDetail[];
+}
+
+export interface CopyProgressDetail {
+ tableName: string;
+ status: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
+ parallelCopyType: string;
+ usedParallelCopies: number;
+ dataRead: number;
+ dataWritten: number;
+ rowsRead: number;
+ rowsCopied: number;
+ copyStart: string;
+ copyThroughput: number,
+ copyDuration: number;
+ errors: string[];
}
export interface SqlConnectionInfo {
diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts
index 362a53e389..b7cf8826b4 100644
--- a/extensions/sql-migration/src/api/utils.ts
+++ b/extensions/sql-migration/src/api/utils.ts
@@ -5,13 +5,16 @@
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
import { IconPathHelper } from '../constants/iconPathHelper';
-import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
import * as crypto from 'crypto';
import * as azure from './azure';
import { azureResource, Tenant } from 'azurecore';
import * as constants from '../constants/strings';
import { logError, TelemetryViews } from '../telemtery';
+import { AdsMigrationStatus } from '../dashboard/tabBase';
+import { getMigrationMode, getMigrationStatus, getMigrationTargetType, PipelineStatusCodes } from '../constants/helper';
+
+export const DefaultSettingValue = '---';
export function deepClone(obj: T): T {
if (!obj || typeof obj !== 'object') {
@@ -92,34 +95,68 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date):
}
}
-export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): azure.DatabaseMigration[] {
- let filteredMigration: azure.DatabaseMigration[] = [];
- if (statusFilter === AdsMigrationStatus.ALL) {
- filteredMigration = databaseMigrations;
- } else if (statusFilter === AdsMigrationStatus.ONGOING) {
- filteredMigration = databaseMigrations.filter(
- value => {
- const status = value.properties?.migrationStatus;
- return status === MigrationStatus.InProgress
- || status === MigrationStatus.Creating
- || value.properties?.provisioningState === MigrationStatus.Creating;
- });
- } else if (statusFilter === AdsMigrationStatus.SUCCEEDED) {
- filteredMigration = databaseMigrations.filter(
- value => value.properties?.migrationStatus === MigrationStatus.Succeeded);
- } else if (statusFilter === AdsMigrationStatus.FAILED) {
- filteredMigration = databaseMigrations.filter(
- value =>
- value.properties?.migrationStatus === MigrationStatus.Failed ||
- value.properties?.provisioningState === ProvisioningState.Failed);
- } else if (statusFilter === AdsMigrationStatus.COMPLETING) {
- filteredMigration = databaseMigrations.filter(
- value => value.properties?.migrationStatus === MigrationStatus.Completing);
+export function getMigrationTime(migrationTime: string): string {
+ return migrationTime
+ ? new Date(migrationTime).toLocaleString()
+ : DefaultSettingValue;
+}
+
+export function getMigrationDuration(startDate: string, endDate: string): string {
+ if (startDate) {
+ if (endDate) {
+ return convertTimeDifferenceToDuration(
+ new Date(startDate),
+ new Date(endDate));
+ } else {
+ return convertTimeDifferenceToDuration(
+ new Date(startDate),
+ new Date());
+ }
}
- if (databaseNameFilter) {
- const filter = databaseNameFilter.toLowerCase();
+
+ return DefaultSettingValue;
+}
+
+export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, columnTextFilter?: string): azure.DatabaseMigration[] {
+ let filteredMigration: azure.DatabaseMigration[] = databaseMigrations || [];
+ if (columnTextFilter) {
+ const filter = columnTextFilter.toLowerCase();
filteredMigration = filteredMigration.filter(
- migration => migration.name?.toLowerCase().includes(filter));
+ migration => migration.properties.sourceServerName?.toLowerCase().includes(filter)
+ || migration.properties.sourceDatabaseName?.toLowerCase().includes(filter)
+ || getMigrationStatus(migration)?.toLowerCase().includes(filter)
+ || getMigrationMode(migration)?.toLowerCase().includes(filter)
+ || getMigrationTargetType(migration)?.toLowerCase().includes(filter)
+ || azure.getResourceName(migration.properties.scope)?.toLowerCase().includes(filter)
+ || azure.getResourceName(migration.id)?.toLowerCase().includes(filter)
+ || getMigrationDuration(
+ migration.properties.startedOn,
+ migration.properties.endedOn)?.toLowerCase().includes(filter)
+ || getMigrationTime(migration.properties.startedOn)?.toLowerCase().includes(filter)
+ || getMigrationTime(migration.properties.endedOn)?.toLowerCase().includes(filter)
+ || getMigrationMode(migration)?.toLowerCase().includes(filter));
+ }
+
+ switch (statusFilter) {
+ case AdsMigrationStatus.ALL:
+ return filteredMigration;
+ case AdsMigrationStatus.ONGOING:
+ return filteredMigration.filter(
+ value => {
+ const status = getMigrationStatus(value);
+ return status === MigrationStatus.InProgress
+ || status === MigrationStatus.Retriable
+ || status === MigrationStatus.Creating;
+ });
+ case AdsMigrationStatus.SUCCEEDED:
+ return filteredMigration.filter(
+ value => getMigrationStatus(value) === MigrationStatus.Succeeded);
+ case AdsMigrationStatus.FAILED:
+ return filteredMigration.filter(
+ value => getMigrationStatus(value) === MigrationStatus.Failed);
+ case AdsMigrationStatus.COMPLETING:
+ return filteredMigration.filter(
+ value => getMigrationStatus(value) === MigrationStatus.Completing);
}
return filteredMigration;
}
@@ -208,12 +245,61 @@ export function decorate(decorator: (fn: Function, key: string) => Function): Fu
}
export function getSessionIdHeader(sessionId: string): { [key: string]: string } {
- return {
- 'SqlMigrationSessionId': sessionId
- };
+ return { 'SqlMigrationSessionId': sessionId };
}
-export function getMigrationStatusImage(status: string): IconPath {
+export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration): string {
+ const properties = migration.properties;
+ const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
+ let warningCount = 0;
+
+ if (properties.migrationFailureError?.message) {
+ warningCount++;
+ }
+ if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
+ const blockingErrors = properties.migrationStatusDetails?.fileUploadBlockingErrors.length ?? 0;
+ warningCount += blockingErrors;
+ }
+ if (properties.migrationStatusDetails?.restoreBlockingReason) {
+ warningCount++;
+ }
+
+ return constants.STATUS_VALUE(migrationStatus, warningCount)
+ + (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
+}
+
+export function getPipelineStatusImage(status: string | undefined): IconPath {
+ // status codes: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
+ switch (status) {
+ case PipelineStatusCodes.Copying: // Copying: 'Copying',
+ return IconPathHelper.copy;
+ case PipelineStatusCodes.CopyFinished: // CopyFinished: 'CopyFinished',
+ case PipelineStatusCodes.RebuildingIndexes: // RebuildingIndexes: 'RebuildingIndexes',
+ return IconPathHelper.inProgressMigration;
+ case PipelineStatusCodes.Canceled: // Canceled: 'Canceled',
+ return IconPathHelper.cancel;
+ case PipelineStatusCodes.PreparingForCopy: // PreparingForCopy: 'PreparingForCopy',
+ return IconPathHelper.notStartedMigration;
+ case PipelineStatusCodes.Failed: // Failed: 'Failed',
+ return IconPathHelper.error;
+ case PipelineStatusCodes.Succeeded: // Succeeded: 'Succeeded',
+ return IconPathHelper.completedMigration;
+
+ // legacy status codes: Queued: 'Queued', InProgress: 'InProgress',Cancelled: 'Cancelled',
+ case PipelineStatusCodes.Queued:
+ return IconPathHelper.notStartedMigration;
+ case PipelineStatusCodes.InProgress:
+ return IconPathHelper.inProgressMigration;
+ case PipelineStatusCodes.Cancelled:
+ return IconPathHelper.cancel;
+ // default:
+ default:
+ return IconPathHelper.error;
+ }
+}
+
+export function getMigrationStatusImage(migration: azure.DatabaseMigration): IconPath {
+ const status = getMigrationStatus(migration);
switch (status) {
case MigrationStatus.InProgress:
return IconPathHelper.inProgressMigration;
@@ -223,7 +309,10 @@ export function getMigrationStatusImage(status: string): IconPath {
return IconPathHelper.notStartedMigration;
case MigrationStatus.Completing:
return IconPathHelper.completingCutover;
+ case MigrationStatus.Retriable:
+ return IconPathHelper.retry;
case MigrationStatus.Canceling:
+ case MigrationStatus.Canceled:
return IconPathHelper.cancel;
case MigrationStatus.Failed:
default:
diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts
index 1c20d22848..90d032b9a7 100644
--- a/extensions/sql-migration/src/constants/helper.ts
+++ b/extensions/sql-migration/src/constants/helper.ts
@@ -15,8 +15,84 @@ export enum SQLTargetAssetType {
SQLDB = 'Microsoft.Sql/servers',
}
-export function getMigrationTargetType(migration: DatabaseMigration): string {
- const id = migration.id?.toLowerCase();
+export const ParallelCopyTypeCodes = {
+ None: 'None',
+ DynamicRange: 'DynamicRange',
+ PhysicalPartitionsOfTable: 'PhysicalPartitionsOfTable',
+};
+
+export const PipelineStatusCodes = {
+ // status codes: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
+ PreparingForCopy: 'PreparingForCopy',
+ Copying: 'Copying',
+ CopyFinished: 'CopyFinished',
+ RebuildingIndexes: 'RebuildingIndexes',
+ Succeeded: 'Succeeded',
+ Failed: 'Failed',
+ Canceled: 'Canceled',
+
+ // legacy status codes
+ Queued: 'Queued',
+ InProgress: 'InProgress',
+ Cancelled: 'Cancelled',
+};
+
+const _dateFormatter = new Intl.DateTimeFormat(
+ undefined, {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+});
+
+const _numberFormatter = new Intl.NumberFormat(
+ undefined, {
+ style: 'decimal',
+ useGrouping: true,
+ minimumIntegerDigits: 1,
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+});
+
+export function formatDateTimeString(dateTime: string): string {
+ return dateTime
+ ? _dateFormatter.format(new Date(dateTime))
+ : '';
+}
+
+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 '';
+}
+
+export function formatNumber(value: number): string {
+ return value >= 0
+ ? _numberFormatter.format(value)
+ : '';
+}
+
+export function formatCopyThroughPut(value: number): string {
+ return value >= 0
+ ? loc.sizeFormatter.format(value / 1024)
+ : '';
+}
+
+export function formatSizeBytes(sizeBytes: number): string {
+ return formatSizeKb(sizeBytes / 1024);
+}
+
+export function formatSizeKb(sizeKb: number): string {
+ return loc.formatSizeMb(sizeKb / 1024);
+}
+
+export function getMigrationTargetType(migration: DatabaseMigration | undefined): string {
+ const id = migration?.id?.toLowerCase() || '';
if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) {
return loc.SQL_MANAGED_INSTANCE;
}
@@ -29,8 +105,8 @@ export function getMigrationTargetType(migration: DatabaseMigration): string {
return '';
}
-export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined {
- switch (migration.type) {
+export function getMigrationTargetTypeEnum(migration: DatabaseMigration | undefined): MigrationTargetType | undefined {
+ switch (migration?.type) {
case SQLTargetAssetType.SQLMI:
return MigrationTargetType.SQLMI;
case SQLTargetAssetType.SQLVM:
@@ -42,37 +118,86 @@ export function getMigrationTargetTypeEnum(migration: DatabaseMigration): Migrat
}
}
-export function getMigrationMode(migration: DatabaseMigration): string {
+export function getMigrationMode(migration: DatabaseMigration | undefined): string {
return isOfflineMigation(migration)
? loc.OFFLINE
: loc.ONLINE;
}
-export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode {
+export function getMigrationModeEnum(migration: DatabaseMigration | undefined): MigrationMode {
return isOfflineMigation(migration)
? MigrationMode.OFFLINE
: MigrationMode.ONLINE;
}
-export function isOfflineMigation(migration: DatabaseMigration): boolean {
- return migration.properties.offlineConfiguration?.offline === true;
+export function isOfflineMigation(migration: DatabaseMigration | undefined): boolean {
+ return migration?.properties?.offlineConfiguration?.offline === true;
}
-export function isBlobMigration(migration: DatabaseMigration): boolean {
+export function isBlobMigration(migration: DatabaseMigration | undefined): boolean {
return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob;
}
-export function getMigrationStatus(migration: DatabaseMigration): string {
- return migration.properties.migrationStatus
- ?? migration.properties.provisioningState;
+export function getMigrationStatus(migration: DatabaseMigration | undefined): string | undefined {
+ return migration?.properties.migrationStatus
+ ?? migration?.properties.provisioningState;
}
+export function hasMigrationOperationId(migration: DatabaseMigration | undefined): boolean {
+ const migrationId = migration?.id ?? '';
+ const migationOperationId = migration?.properties?.migrationOperationId ?? '';
+ return migrationId.length > 0
+ && migationOperationId.length > 0;
+}
-export function canRetryMigration(status: string | undefined): boolean {
- return status === undefined ||
- status === MigrationStatus.Failed ||
- status === MigrationStatus.Succeeded ||
- status === MigrationStatus.Canceled;
+export function canCancelMigration(migration: DatabaseMigration | undefined): boolean {
+ const status = getMigrationStatus(migration);
+ return hasMigrationOperationId(migration)
+ && (status === MigrationStatus.InProgress ||
+ status === MigrationStatus.Retriable ||
+ status === MigrationStatus.Creating);
+}
+
+export function canDeleteMigration(migration: DatabaseMigration | undefined): boolean {
+ const status = getMigrationStatus(migration);
+ return status === MigrationStatus.Canceled
+ || status === MigrationStatus.Failed
+ || status === MigrationStatus.Retriable
+ || status === MigrationStatus.Succeeded;
+}
+
+export function canRetryMigration(migration: DatabaseMigration | undefined): boolean {
+ const status = getMigrationStatus(migration);
+ return status === MigrationStatus.Canceled
+ || status === MigrationStatus.Retriable
+ || status === MigrationStatus.Failed
+ || status === MigrationStatus.Succeeded;
+}
+
+export function canCutoverMigration(migration: DatabaseMigration | undefined): boolean {
+ const status = getMigrationStatus(migration);
+ return hasMigrationOperationId(migration)
+ && status === MigrationStatus.InProgress
+ && isOnlineMigration(migration)
+ && isFullBackupRestored(migration);
+}
+
+export function isActiveMigration(migration: DatabaseMigration | undefined): boolean {
+ const status = getMigrationStatus(migration);
+ return status === MigrationStatus.Completing
+ || status === MigrationStatus.Retriable
+ || status === MigrationStatus.Creating
+ || status === MigrationStatus.InProgress;
+}
+
+export function isFullBackupRestored(migration: DatabaseMigration | undefined): boolean {
+ const fileName = migration?.properties?.migrationStatusDetails?.lastRestoredFilename ?? '';
+ return migration?.properties?.migrationStatusDetails?.isFullBackupRestored
+ || fileName.length > 0;
+}
+
+export function isOnlineMigration(migration: DatabaseMigration | undefined): boolean {
+ return getMigrationModeEnum(migration) === MigrationMode.ONLINE;
}
export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] {
diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts
index 5b2d5d8b94..a5450d130b 100644
--- a/extensions/sql-migration/src/constants/iconPathHelper.ts
+++ b/extensions/sql-migration/src/constants/iconPathHelper.ts
@@ -45,6 +45,9 @@ export class IconPathHelper {
public static stop: IconPath;
public static view: IconPath;
public static sqlMigrationService: IconPath;
+ public static addNew: IconPath;
+ public static breadCrumb: IconPath;
+ public static allTables: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -183,5 +186,17 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/sqlMigrationService.svg'),
dark: context.asAbsolutePath('images/sqlMigrationService.svg'),
};
+ IconPathHelper.addNew = {
+ light: context.asAbsolutePath('images/addNew.svg'),
+ dark: context.asAbsolutePath('images/addNew.svg'),
+ };
+ IconPathHelper.breadCrumb = {
+ light: context.asAbsolutePath('images/breadCrumb.svg'),
+ dark: context.asAbsolutePath('images/breadCrumb.svg'),
+ };
+ IconPathHelper.allTables = {
+ light: context.asAbsolutePath('images/allTables.svg'),
+ dark: context.asAbsolutePath('images/allTables.svg'),
+ };
}
}
diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts
index fe45aeb2eb..ce85a09edb 100644
--- a/extensions/sql-migration/src/constants/strings.ts
+++ b/extensions/sql-migration/src/constants/strings.ts
@@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
import { EOL } from 'os';
import { MigrationStatus } from '../models/migrationLocalStorage';
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
+import { ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
const localize = nls.loadMessageBundle();
@@ -518,9 +519,9 @@ export const NOTEBOOK_SQL_MIGRATION_ASSESSMENT_TITLE = localize('sql.migration.s
export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Failed to open the migration notebook.");
// Dashboard
-export function DASHBOARD_REFRESH_MIGRATIONS(error: string): string {
- return localize('sql.migration.refresh.migrations.error', "An error occurred while refreshing the migrations list: '{0}'. Please check your linked Azure connection and click refresh to try again.", error);
-}
+export const DASHBOARD_REFRESH_MIGRATIONS_TITLE = localize('sql.migration.refresh.migrations.error.title', 'An error has occured while refreshing the migrations list.');
+export const DASHBOARD_REFRESH_MIGRATIONS_LABEL = localize('sql.migration.refresh.migrations.error.label', "An error occurred while refreshing the migrations list. Please check your linked Azure connection and click refresh to try again.");
+
export const DASHBOARD_TITLE = localize('sql.migration.dashboard.title', "Azure SQL Migration");
export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines.");
export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL");
@@ -547,6 +548,7 @@ export function MIGRATION_INPROGRESS_WARNING(count: number) {
export const FEEDBACK_ISSUE_TITLE = localize('sql.migration.feedback.issue.title', "Feedback on the migration experience");
//Migration cutover dialog
+export const BREADCRUMB_MIGRATIONS = localize('sql.migration.details.breadcrumb.migrations', 'Migrations');
export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
export const COMPLETE_CUTOVER = localize('sql.migration.complete.cutover', "Complete cutover");
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database name");
@@ -588,6 +590,16 @@ export const NA = localize('sql.migration.na', "N/A");
export const EMPTY_TABLE_TEXT = localize('sql.migration.empty.table.text', "No backup files");
export const EMPTY_TABLE_SUBTEXT = localize('sql.migration.empty.table.subtext', "If results were expected, verify the connection to the SQL Server instance.");
export const MIGRATION_CUTOVER_ERROR = localize('sql.migration.cutover.error', 'An error occurred while initiating cutover.');
+export const REFRESH_BUTTON_TEXT = localize('sql.migration.details.refresh', 'Refresh');
+export const SERVER_OBJECTS_FIELD_LABEL = localize('sql.migration.details.serverobjects.field.label', 'Server objects');
+export const SERVER_OBJECTS_LABEL = localize('sql.migration.details.serverobjects.label', 'Server objects');
+export const SERVER_OBJECTS_ALL_TABLES_LABEL = localize('sql.migration.details.serverobjects.all.tables.label', 'Total tables');
+export const SERVER_OBJECTS_IN_PROGRESS_TABLES_LABEL = localize('sql.migration.details.serverobjects.inprogress.tables.label', 'In progress');
+export const SERVER_OBJECTS_SUCCESSFUL_TABLES_LABEL = localize('sql.migration.details.serverobjects.successful.tables.label', 'Successful');
+export const SERVER_OBJECTS_FAILED_TABLES_LABEL = localize('sql.migration.details.serverobjects.failed.tables.label', 'Failed');
+export const SERVER_OBJECTS_CANCELLED_TABLES_LABEL = localize('sql.migration.details.serverobjects.cancelled.tables.label', 'Cancelled');
+export const FILTER_SERVER_OBJECTS_PLACEHOLDER = localize('sql.migration.details.serverobjects.filter.label', 'Filter table migration results');
+export const FILTER_SERVER_OBJECTS_ARIA_LABEL = localize('sql.migration.details.serverobjects.filter.aria.label', 'Filter table migration results using keywords');
//Migration confirm cutover dialog
export const COMPLETING_CUTOVER_WARNING = localize('sql.migration.completing.cutover.warning', "Completing cutover without restoring all the backups may result in a data loss.");
@@ -616,6 +628,7 @@ export const MIGRATION_CANNOT_CUTOVER = localize('sql.migration.cannot.cutover',
export const FILE_NAME = localize('sql.migration.file.name', "File name");
export const SIZE_COLUMN_HEADER = localize('sql.migration.size.column.header', "Size");
export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "No pending backups. Click refresh to check current status.");
+
//Migration status dialog
export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account");
export const ADD_ACCOUNT_MESSAGE = localize('sql.migration.status.add.account.MESSAGE', "Add your Azure account to view existing migrations and their status.");
@@ -625,11 +638,14 @@ export const STATUS_ONGOING = localize('sql.migration.status.dropdown.ongoing',
export const STATUS_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing");
export const STATUS_SUCCEEDED = localize('sql.migration.status.dropdown.succeeded', "Status: Succeeded");
export const STATUS_FAILED = localize('sql.migration.status.dropdown.failed', "Status: Failed");
-export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
+export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Filter migration results");
export const ONLINE = localize('sql.migration.online', "Online");
export const OFFLINE = localize('sql.migration.offline', "Offline");
export const DATABASE = localize('sql.migration.database', "Database");
-export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Status");
+export const SRC_DATABASE = localize('sql.migration.src.database', "Source database");
+export const SRC_SERVER = localize('sql.migration.src.server', "Source name");
+
+export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Migration status");
export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
export const DURATION = localize('sql.migration.duration', "Duration");
export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type");
@@ -637,7 +653,9 @@ export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance
export const SQL_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine");
export const SQL_DATABASE = localize('sql.migration.sql.database', "SQL Database");
export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target name");
-export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration mode");
+export const TARGET_SERVER_COLUMN = localize('sql.migration.target.azure.sql.instance.server.name', "Target name");
+export const TARGET_DATABASE_COLUMN = localize('sql.migration.target.azure.sql.instance.database.name', "Target database");
+export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Mode");
export const START_TIME = localize('sql.migration.start.time', "Start time");
export const FINISH_TIME = localize('sql.migration.finish.time', "Finish time");
@@ -648,20 +666,53 @@ export function STATUS_VALUE(status: string, count: number): string {
return localize('sql.migration.status.error.count.none', "{0}", StatusLookup[status] ?? status);
}
+export const MIGRATION_ERROR_DETAILS_TITLE = localize('sql.migration.error.details.title', "Migration error details");
+export const MIGRATION_ERROR_DETAILS_LABEL = localize('sql.migration.error.details.label', "Migration error(s))");
+export const OPEN_MIGRATION_DETAILS_ERROR = localize('sql.migration.open.migration.destails.error', "Error opening migration details dialog");
+export const OPEN_MIGRATION_TARGET_ERROR = localize('sql.migration.open.migration.target.error', "Error opening migration target");
+export const OPEN_MIGRATION_SERVICE_ERROR = localize('sql.migration.open.migration.service.error', "Error opening migration service dialog");
+export const LOAD_MIGRATION_LIST_ERROR = localize('sql.migration.load.migration.list.error', "Error loading migrations list");
+export const ERROR_DIALOG_CLEAR_BUTTON_LABEL = localize('sql.migration.error.dialog.clear.button.label', "Clear");
+
export interface LookupTable {
[key: string]: T;
}
+
export const StatusLookup: LookupTable = {
- ['InProgress']: localize('sql.migration.status.inprogress', 'In progress'),
- ['Succeeded']: localize('sql.migration.status.succeeded', 'Succeeded'),
- ['Creating']: localize('sql.migration.status.creating', 'Creating'),
- ['Completing']: localize('sql.migration.status.completing', 'Completing'),
- ['Canceling']: localize('sql.migration.status.canceling', 'Canceling'),
- ['Failed']: localize('sql.migration.status.failed', 'Failed'),
+ [MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'),
+ [MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'),
+ [MigrationStatus.Creating]: localize('sql.migration.status.creating', 'Creating'),
+ [MigrationStatus.Completing]: localize('sql.migration.status.completing', 'Completing'),
+ [MigrationStatus.Retriable]: localize('sql.migration.status.retriable', 'Retriable'),
+ [MigrationStatus.Canceling]: localize('sql.migration.status.canceling', 'Canceling'),
+ [MigrationStatus.Canceled]: localize('sql.migration.status.canceled', 'Canceled'),
+ [MigrationStatus.Failed]: localize('sql.migration.status.failed', 'Failed'),
default: undefined
};
+export const PipelineRunStatus: LookupTable = {
+ // status codes: ['PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled']
+ [PipelineStatusCodes.PreparingForCopy]: localize('sql.migration.copy.status.preparingforcopy', 'Preparing'),
+ [PipelineStatusCodes.Copying]: localize('sql.migration.copy.status.copying', 'Copying'),
+ [PipelineStatusCodes.CopyFinished]: localize('sql.migration.copy.status.copyfinished', 'Copy finished'),
+ [PipelineStatusCodes.RebuildingIndexes]: localize('sql.migration.copy.status.rebuildingindexes', 'Rebuilding indexes'),
+ [PipelineStatusCodes.Succeeded]: localize('sql.migration.copy.status.succeeded', 'Succeeded'),
+ [PipelineStatusCodes.Failed]: localize('sql.migration.copy.status.failed', 'Failed'),
+ [PipelineStatusCodes.Canceled]: localize('sql.migration.copy.status.canceled', 'Canceled'),
+
+ // legacy status codes ['Queued', 'InProgress', 'Cancelled']
+ [PipelineStatusCodes.Queued]: localize('sql.migration.copy.status.queued', 'Queued'),
+ [PipelineStatusCodes.InProgress]: localize('sql.migration.copy.status.inprogress', 'In progress'),
+ [PipelineStatusCodes.Cancelled]: localize('sql.migration.copy.status.cancelled', 'Cancelled'),
+};
+
+export const ParallelCopyType: LookupTable = {
+ [ParallelCopyTypeCodes.None]: localize('sql.migration.parallel.copy.type.none', 'None'),
+ [ParallelCopyTypeCodes.PhysicalPartitionsOfTable]: localize('sql.migration.parallel.copy.type.physical', 'Physical partitions'),
+ [ParallelCopyTypeCodes.DynamicRange]: localize('sql.migration.parallel.copy.type.dynamic', 'Dynamic range'),
+};
+
export function STATUS_WARNING_COUNT(status: string, count: number): string | undefined {
if (status === MigrationStatus.InProgress ||
status === MigrationStatus.Creating ||
@@ -699,6 +750,27 @@ export function SEC(sec: number): string {
return localize('sql.migration.sec', "{0} sec", sec);
}
+export const sizeFormatter = new Intl.NumberFormat(
+ undefined, {
+ style: 'decimal',
+ useGrouping: true,
+ minimumIntegerDigits: 1,
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+});
+
+export function formatSizeMb(sizeMb: number): string {
+ if (isNaN(sizeMb) || sizeMb < 0) {
+ return '';
+ } else if (sizeMb < 1024) {
+ return localize('sql.migration.size.mb', "{0} MB", sizeFormatter.format(sizeMb));
+ } else if (sizeMb < 1024 * 1024) {
+ return localize('sql.migration.size.gb', "{0} GB", sizeFormatter.format(sizeMb / 1024));
+ } else {
+ return localize('sql.migration.size.tb', "{0} TB", sizeFormatter.format(sizeMb / 1024 / 1024));
+ }
+}
+
// SQL Migration Service Details page.
export const SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE = localize('sql.migration.service.details.dialog.title', "Azure Database Migration Service");
export const SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL = localize('sql.migration.service.details.button.label', "Close");
@@ -761,6 +833,9 @@ export function WARNINGS_COUNT(totalCount: number): string {
export const AUTHENTICATION_TYPE = localize('sql.migration.authentication.type', "Authentication type");
export const REFRESH_BUTTON_LABEL = localize('sql.migration.status.refresh.label', 'Refresh');
+export const STATUS_LABEL = localize('sql.migration.status.status.label', 'Status');
+export const SORT_LABEL = localize('sql.migration.migration.list.sort.label', 'Sort');
+export const ASCENDING_LABEL = localize('sql.migration.migration.list.ascending.label', 'Ascending');
// Saved Assessment Dialog
export const NEXT_LABEL = localize('sql.migration.saved.assessment.next', "Next");
@@ -786,3 +861,46 @@ export function MIGRATION_SERVICE_SERVICE_PROMPT(serviceName: string): string {
return localize('sql.migration.service.prompt', '{0} (change)', serviceName);
}
export const MIGRATION_SERVICE_DESCRIPTION = localize('sql.migration.select.service.description', 'Azure Database Migration Service');
+
+// Desktop tabs
+export const DESKTOP_MIGRATION_BUTTON_LABEL = localize('sql.migration.tab.button.migration.label', 'New migration');
+export const DESKTOP_MIGRATION_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.migration.description', 'Migrate to Azure SQL');
+export const DESKTOP_SUPPORT_BUTTON_LABEL = localize('sql.migration.tab.button.support.label', 'New support request');
+export const DESKTOP_SUPPORT_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.support.description', 'New support request');
+export const DESKTOP_FEEDBACK_BUTTON_LABEL = localize('sql.migration.tab.button.feedback.label', 'Feedback');
+export const DESKTOP_FEEDBACK_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.feedback.description', 'Feedback');
+export const DESKTOP_DASHBOARD_TAB_TITLE = localize('sql.migration.tab.dashboard.title', 'Dashboard');
+export const DESKTOP_MIGRATIONS_TAB_TITLE = localize('sql.migration.tab.migrations.title', 'Migrations');
+
+// dashboard tab
+export const DASHBOARD_HELP_LINK_MIGRATE_USING_ADS = localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio');
+export const DASHBOARD_HELP_DESCRIPTION_MIGRATE_USING_ADS = localize('sql.migration.dashboard.help.description.migrateUsingADS', 'The Azure SQL Migration extension for Azure Data Studio provides capabilities to assess, get right-sized Azure recommendations and migrate SQL Server databases to Azure.');
+export const DASHBOARD_HELP_LINK_MI_TUTORIAL = localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)');
+export const DASHBOARD_HELP_DESCRIPTION_MI_TUTORIAL = localize('sql.migration.dashboard.help.description.mi', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises or Azure Virtual Machines) to Azure SQL Managed Instance with minimal downtime.');
+export const DASHBOARD_HELP_LINK_VM_TUTORIAL = localize('sql.migration.dashboard.help.link.vm', 'Tutorial: Migrate to SQL Server on Azure Virtual Machines (online)');
+export const DASHBOARD_HELP_DESCRIPTION_VMTUTORIAL = localize('sql.migration.dashboard.help.description.vm', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises) to SQL Server on Azure Virtual Machines with minimal downtime.');
+export const DASHBOARD_HELP_LINK_DMS_GUIDE = localize('sql.migration.dashboard.help.link.dmsGuide', 'Azure Database Migration Guides');
+export const DASHBOARD_HELP_DESCRIPTION_DMS_GUIDE = localize('sql.migration.dashboard.help.description.dmsGuide', 'A hub of migration articles that provides step-by-step guidance for migrating and modernizing your data assets in Azure.');
+
+// Error info
+export const DATABASE_MIGRATION_STATUS_TITLE = localize('sql.migration.error.title', 'Migration status details');
+export const TABLE_MIGRATION_STATUS_TITLE = localize('sql.migration.table.error.title', 'Table migration status details');
+
+export function DATABASE_MIGRATION_STATUS_LABEL(status?: string): string {
+ return localize('sql.migration.database.migration.status.label', 'Database migration status: {0}', status ?? '');
+}
+
+export function TABLE_MIGRATION_STATUS_LABEL(status?: string): string {
+ return localize('sql.migration.table.migration.status.label', 'Table migration status: {0}', status ?? '');
+}
+
+export const SQLDB_COL_TABLE_NAME = localize('sql.migration.sqldb.column.tablename', 'Table name');
+export const SQLDB_COL_DATA_READ = localize('sql.migration.sqldb.column.dataread', 'Data read');
+export const SQLDB_COL_DATA_WRITTEN = localize('sql.migration.sqldb.column.datawritten', 'Data written');
+export const SQLDB_COL_ROWS_READ = localize('sql.migration.sqldb.column.rowsread', 'Rows read');
+export const SQLDB_COL_ROWS_COPIED = localize('sql.migration.sqldb.column.rowscopied', 'Rows copied');
+export const SQLDB_COL_COPY_THROUGHPUT = localize('sql.migration.sqldb.column.copythroughput', 'Copy throughput');
+export const SQLDB_COL_COPY_DURATION = localize('sql.migration.sqldb.column.copyduration', 'Copy duration');
+export const SQLDB_COL_PARRALEL_COPY_TYPE = localize('sql.migration.sqldb.column.parallelcopytype', 'Parallel copy type');
+export const SQLDB_COL_USED_PARALLEL_COPIES = localize('sql.migration.sqldb.column.usedparallelcopies', 'Used parallel copies');
+export const SQLDB_COL_COPY_START = localize('sql.migration.sqldb.column.copystart', 'Copy start');
diff --git a/extensions/sql-migration/src/dashboard/dashboardTab.ts b/extensions/sql-migration/src/dashboard/dashboardTab.ts
new file mode 100644
index 0000000000..4d91decc68
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/dashboardTab.ts
@@ -0,0 +1,789 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { IconPath, IconPathHelper } from '../constants/iconPathHelper';
+import * as styles from '../constants/styles';
+import * as loc from '../constants/strings';
+import { filterMigrations } from '../api/utils';
+import { DatabaseMigration } from '../api/azure';
+import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
+import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
+import { logError, TelemetryViews } from '../telemtery';
+import { AdsMigrationStatus, MenuCommands, TabBase } from './tabBase';
+import { DashboardStatusBar } from './sqlServerDashboard';
+
+interface IActionMetadata {
+ title?: string,
+ description?: string,
+ link?: string,
+ iconPath?: azdata.ThemedIconPath,
+ command?: string;
+}
+
+interface StatusCard {
+ container: azdata.DivContainer;
+ count: azdata.TextComponent,
+ textContainer?: azdata.FlexContainer,
+ warningContainer?: azdata.FlexContainer,
+ warningText?: azdata.TextComponent,
+}
+
+export const DashboardTabId = 'DashboardTab';
+
+const maxWidth = 800;
+const BUTTON_CSS = {
+ 'font-size': '13px',
+ 'line-height': '18px',
+ 'margin': '4px 0',
+ 'text-align': 'left',
+};
+
+export class DashboardTab extends TabBase {
+ private _migrationStatusCardsContainer!: azdata.FlexContainer;
+ private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
+ private _inProgressMigrationButton!: StatusCard;
+ private _inProgressWarningMigrationButton!: StatusCard;
+ private _allMigrationButton!: StatusCard;
+ private _successfulMigrationButton!: StatusCard;
+ private _failedMigrationButton!: StatusCard;
+ private _completingMigrationButton!: StatusCard;
+ private _selectServiceText!: azdata.TextComponent;
+ private _serviceContextButton!: azdata.ButtonComponent;
+ private _refreshButton!: azdata.ButtonComponent;
+
+ constructor() {
+ super();
+ this.title = loc.DESKTOP_DASHBOARD_TAB_TITLE;
+ this.id = DashboardTabId;
+ this.icon = IconPathHelper.sqlMigrationLogo;
+ }
+
+ public onDialogClosed = async (): Promise =>
+ await this.updateServiceContext(this._serviceContextButton);
+
+ public async create(
+ view: azdata.ModelView,
+ openMigrationsFcn: (status: AdsMigrationStatus) => Promise,
+ statusBar: DashboardStatusBar): Promise {
+
+ this.view = view;
+ this.openMigrationFcn = openMigrationsFcn;
+ this.statusBar = statusBar;
+
+ await this.initialize(this.view);
+
+ return this;
+ }
+
+ public async refresh(): Promise {
+ if (this.isRefreshing) {
+ return;
+ }
+
+ this.isRefreshing = true;
+ this._migrationStatusCardLoadingContainer.loading = true;
+ let migrations: DatabaseMigration[] = [];
+ try {
+ await this.statusBar.clearError();
+ migrations = await getCurrentMigrations();
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
+ loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
+ e.message);
+ logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
+ }
+
+ const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
+ let warningCount = 0;
+ for (let i = 0; i < inProgressMigrations.length; i++) {
+ if (inProgressMigrations[i].properties.migrationFailureError?.message ||
+ inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
+ inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
+ warningCount += 1;
+ }
+ }
+ if (warningCount > 0) {
+ this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
+ this._inProgressMigrationButton.container.display = 'none';
+ this._inProgressWarningMigrationButton.container.display = '';
+ } else {
+ this._inProgressMigrationButton.container.display = '';
+ this._inProgressWarningMigrationButton.container.display = 'none';
+ }
+
+ this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
+ this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
+
+ this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
+ this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
+ this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
+ this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
+
+ await this._updateSummaryStatus();
+ this.isRefreshing = false;
+ this._migrationStatusCardLoadingContainer.loading = false;
+ }
+
+ protected async initialize(view: azdata.ModelView): Promise {
+ const container = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ width: '100%',
+ height: '100%'
+ }).component();
+
+ const toolbar = view.modelBuilder.toolbarContainer();
+ toolbar.addToolbarItems([
+ { component: this.createNewMigrationButton() },
+ { component: this.createNewSupportRequestButton() },
+ { component: this.createFeedbackButton() },
+ ]);
+
+ container.addItem(
+ toolbar.component(),
+ { CSSStyles: { 'flex': '0 0 auto' } });
+
+ const header = this._createHeader(view);
+ // Files need to have the vscode-file scheme to be loaded by ADS
+ const watermarkUri = vscode.Uri
+ .file(IconPathHelper.migrationDashboardHeaderBackground.light)
+ .with({ scheme: 'vscode-file' });
+
+ container.addItem(header, {
+ CSSStyles: {
+ 'background-image': `
+ url(${watermarkUri}),
+ linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)`,
+ 'background-repeat': 'no-repeat',
+ 'background-position': '91.06% 100%',
+ 'margin-bottom': '20px'
+ }
+ });
+
+ const tasksContainer = await this._createTasks(view);
+ header.addItem(tasksContainer, {
+ CSSStyles: {
+ 'width': `${maxWidth}px`,
+ 'margin': '24px'
+ }
+ });
+ container.addItem(
+ await this._createFooter(view),
+ { CSSStyles: { 'margin': '0 24px' } });
+
+ this.content = container;
+ }
+
+ private _createHeader(view: azdata.ModelView): azdata.FlexContainer {
+ const header = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ width: maxWidth,
+ }).component();
+ const titleComponent = view.modelBuilder.text()
+ .withProps({
+ value: loc.DASHBOARD_TITLE,
+ width: '750px',
+ CSSStyles: { ...styles.DASHBOARD_TITLE_CSS }
+ }).component();
+
+ const descriptionComponent = view.modelBuilder.text()
+ .withProps({
+ value: loc.DASHBOARD_DESCRIPTION,
+ CSSStyles: { ...styles.NOTE_CSS }
+ }).component();
+ header.addItems([titleComponent, descriptionComponent], {
+ CSSStyles: {
+ 'width': `${maxWidth}px`,
+ 'padding-left': '24px'
+ }
+ });
+ return header;
+ }
+
+ private async _createTasks(view: azdata.ModelView): Promise {
+ const tasksContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'row',
+ width: '100%',
+ }).component();
+
+ const migrateButtonMetadata: IActionMetadata = {
+ title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
+ description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
+ iconPath: IconPathHelper.sqlMigrationLogo,
+ command: MenuCommands.StartMigration
+ };
+
+ const preRequisiteListTitle = view.modelBuilder.text()
+ .withProps({
+ value: loc.PRE_REQ_TITLE,
+ CSSStyles: {
+ ...styles.BODY_CSS,
+ 'margin': '0px',
+ }
+ }).component();
+
+ const migrateButton = this._createTaskButton(view, migrateButtonMetadata);
+
+ const preRequisiteListElement = view.modelBuilder.text()
+ .withProps({
+ value: [
+ loc.PRE_REQ_1,
+ loc.PRE_REQ_2,
+ loc.PRE_REQ_3
+ ],
+ CSSStyles: {
+ ...styles.SMALL_NOTE_CSS,
+ 'padding-left': '12px',
+ 'margin': '-0.5em 0px',
+ }
+ }).component();
+
+ const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink()
+ .withProps({
+ label: loc.LEARN_MORE,
+ ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
+ url: 'https://aka.ms/azuresqlmigrationextension',
+ }).component();
+
+ const preReqContainer = view.modelBuilder.flexContainer()
+ .withItems([
+ preRequisiteListTitle,
+ preRequisiteListElement,
+ preRequisiteLearnMoreLink])
+ .withLayout({ flexFlow: 'column' })
+ .component();
+
+ tasksContainer.addItem(migrateButton, {});
+ tasksContainer.addItems(
+ [preReqContainer],
+ { CSSStyles: { 'margin-left': '20px' } });
+
+ return tasksContainer;
+ }
+
+ private _createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
+ const maxHeight: number = 84;
+ const maxWidth: number = 236;
+ const buttonContainer = view.modelBuilder.button().withProps({
+ buttonType: azdata.ButtonType.Informational,
+ description: taskMetaData.description,
+ height: maxHeight,
+ iconHeight: 32,
+ iconPath: taskMetaData.iconPath,
+ iconWidth: 32,
+ label: taskMetaData.title,
+ title: taskMetaData.title,
+ width: maxWidth,
+ CSSStyles: {
+ 'border': '1px solid',
+ 'display': 'flex',
+ 'flex-direction': 'column',
+ 'justify-content': 'flex-start',
+ 'border-radius': '4px',
+ 'transition': 'all .5s ease',
+ }
+ }).component();
+ this.disposables.push(
+ buttonContainer.onDidClick(async () => {
+ if (taskMetaData.command) {
+ await vscode.commands.executeCommand(taskMetaData.command);
+ }
+ }));
+ return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
+ }
+
+ private async _createFooter(view: azdata.ModelView): Promise {
+ const footerContainer = view.modelBuilder.flexContainer().withLayout({
+ flexFlow: 'row',
+ width: maxWidth,
+ justifyContent: 'flex-start'
+ }).component();
+ const statusContainer = await this._createMigrationStatusContainer(view);
+ const videoLinksContainer = this._createVideoLinks(view);
+ footerContainer.addItem(statusContainer);
+ footerContainer.addItem(
+ videoLinksContainer,
+ { CSSStyles: { 'padding-left': '8px', } });
+
+ return footerContainer;
+ }
+
+ private _createVideoLinks(view: azdata.ModelView): azdata.Component {
+ const linksContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ width: '440px',
+ height: '365px',
+ justifyContent: 'flex-start',
+ }).withProps({
+ CSSStyles: {
+ 'border': '1px solid rgba(0, 0, 0, 0.1)',
+ 'padding': '10px',
+ 'overflow': 'scroll',
+ }
+ }).component();
+
+ const titleComponent = view.modelBuilder.text()
+ .withProps({
+ value: loc.HELP_TITLE,
+ CSSStyles: { ...styles.SECTION_HEADER_CSS }
+ })
+ .component();
+
+ linksContainer.addItems(
+ [titleComponent],
+ { CSSStyles: { 'margin-bottom': '16px' } });
+
+ const links = [
+ {
+ title: loc.DASHBOARD_HELP_LINK_MIGRATE_USING_ADS,
+ description: loc.DASHBOARD_HELP_DESCRIPTION_MIGRATE_USING_ADS,
+ link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
+ },
+ {
+ title: loc.DASHBOARD_HELP_LINK_MI_TUTORIAL,
+ description: loc.DASHBOARD_HELP_DESCRIPTION_MI_TUTORIAL,
+ link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
+ },
+ {
+ title: loc.DASHBOARD_HELP_LINK_VM_TUTORIAL,
+ description: loc.DASHBOARD_HELP_DESCRIPTION_VMTUTORIAL,
+ link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
+ },
+ {
+ title: loc.DASHBOARD_HELP_LINK_DMS_GUIDE,
+ description: loc.DASHBOARD_HELP_DESCRIPTION_DMS_GUIDE,
+ link: 'https://docs.microsoft.com/data-migration/'
+ },
+ ];
+
+ linksContainer.addItems(links.map(l => this._createLink(view, l)), {});
+
+ const videoLinks: IActionMetadata[] = [];
+ const videosContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'row',
+ width: maxWidth,
+ }).component();
+ videosContainer.addItems(videoLinks.map(l => this._createVideoLink(view, l)), {});
+ linksContainer.addItem(videosContainer);
+
+ return linksContainer;
+ }
+
+ private _createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
+ const maxWidth = 400;
+ const labelsContainer = view.modelBuilder.flexContainer()
+ .withProps({
+ CSSStyles: {
+ 'flex-direction': 'column',
+ 'width': `${maxWidth}px`,
+ 'justify-content': 'flex-start',
+ 'margin-bottom': '12px'
+ }
+ }).component();
+ const linkContainer = view.modelBuilder.flexContainer()
+ .withProps({
+ CSSStyles: {
+ 'flex-direction': 'row',
+ 'width': `${maxWidth}px`,
+ 'justify-content': 'flex-start',
+ 'margin-bottom': '4px'
+ }
+
+ }).component();
+ const descriptionComponent = view.modelBuilder.text()
+ .withProps({
+ value: linkMetaData.description,
+ width: maxWidth,
+ CSSStyles: { ...styles.NOTE_CSS }
+ }).component();
+ const linkComponent = view.modelBuilder.hyperlink()
+ .withProps({
+ label: linkMetaData.title!,
+ url: linkMetaData.link!,
+ showLinkIcon: true,
+ CSSStyles: { ...styles.BODY_CSS }
+ }).component();
+ linkContainer.addItem(linkComponent);
+ labelsContainer.addItems([linkContainer, descriptionComponent]);
+ return labelsContainer;
+ }
+
+ private _createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
+ const maxWidth = 150;
+ const videosContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ width: maxWidth,
+ justifyContent: 'flex-start'
+ }).component();
+ const video1Container = view.modelBuilder.divContainer()
+ .withProps({
+ clickable: true,
+ width: maxWidth,
+ height: '100px'
+ }).component();
+ const descriptionComponent = view.modelBuilder.text()
+ .withProps({
+ value: linkMetaData.description,
+ width: maxWidth,
+ height: '50px',
+ CSSStyles: { ...styles.BODY_CSS }
+ }).component();
+ this.disposables.push(
+ video1Container.onDidClick(async () => {
+ if (linkMetaData.link) {
+ await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
+ }
+ }));
+ videosContainer.addItem(video1Container, {
+ CSSStyles: {
+ 'background-image': `url(${vscode.Uri.file(linkMetaData.iconPath?.light)})`,
+ 'background-repeat': 'no-repeat',
+ 'background-position': 'top',
+ 'width': `${maxWidth}px`,
+ 'height': '104px',
+ 'background-size': `${maxWidth}px 120px`
+ }
+ });
+ videosContainer.addItem(descriptionComponent);
+ return videosContainer;
+ }
+
+ private _createStatusCard(
+ view: azdata.ModelView,
+ cardIconPath: IconPath,
+ cardTitle: string,
+ hasSubtext: boolean = false
+ ): StatusCard {
+ const buttonWidth = '400px';
+ const buttonHeight = hasSubtext ? '70px' : '50px';
+ const statusCard = view.modelBuilder.flexContainer()
+ .withProps({
+ CSSStyles: {
+ 'width': buttonWidth,
+ 'height': buttonHeight,
+ 'align-items': 'center',
+ }
+ }).component();
+
+ const statusIcon = view.modelBuilder.image()
+ .withProps({
+ iconPath: cardIconPath!.light,
+ iconHeight: 24,
+ iconWidth: 24,
+ height: 32,
+ CSSStyles: { 'margin': '0 8px' }
+ }).component();
+
+ const textContainer = view.modelBuilder.flexContainer()
+ .withLayout({ flexFlow: 'column' })
+ .component();
+
+ const cardTitleText = view.modelBuilder.text()
+ .withProps({ value: cardTitle })
+ .withProps({
+ CSSStyles: {
+ ...styles.SECTION_HEADER_CSS,
+ 'width': '240px',
+ }
+ }).component();
+ textContainer.addItem(cardTitleText);
+
+ const cardCount = view.modelBuilder.text()
+ .withProps({
+ value: '0',
+ CSSStyles: {
+ ...styles.BIG_NUMBER_CSS,
+ 'margin': '0 0 0 8px',
+ 'text-align': 'center',
+ }
+ }).component();
+
+ let warningContainer;
+ let warningText;
+ if (hasSubtext) {
+ const warningIcon = view.modelBuilder.image()
+ .withProps({
+ iconPath: IconPathHelper.warning,
+ iconWidth: 12,
+ iconHeight: 12,
+ width: 12,
+ height: 18,
+ }).component();
+
+ const warningDescription = '';
+ warningText = view.modelBuilder.text()
+ .withProps({
+ value: warningDescription,
+ CSSStyles: {
+ ...styles.BODY_CSS,
+ 'padding-left': '8px',
+ }
+ }).component();
+
+ warningContainer = view.modelBuilder.flexContainer()
+ .withItems(
+ [warningIcon, warningText],
+ { flex: '0 0 auto' })
+ .withProps({ CSSStyles: { 'align-items': 'center' } })
+ .component();
+
+ textContainer.addItem(warningContainer);
+ }
+
+ statusCard.addItems([
+ statusIcon,
+ textContainer,
+ cardCount,
+ ]);
+
+ const compositeButton = view.modelBuilder.divContainer()
+ .withItems([statusCard])
+ .withProps({
+ ariaRole: 'button',
+ ariaLabel: loc.SHOW_STATUS,
+ clickable: true,
+ CSSStyles: {
+ 'height': buttonHeight,
+ 'margin-bottom': '16px',
+ 'border': '1px solid',
+ 'display': 'flex',
+ 'flex-direction': 'column',
+ 'justify-content': 'flex-start',
+ 'border-radius': '4px',
+ 'transition': 'all .5s ease',
+ }
+ }).component();
+ return {
+ container: compositeButton,
+ count: cardCount,
+ textContainer: textContainer,
+ warningContainer: warningContainer,
+ warningText: warningText
+ };
+ }
+
+ private async _createMigrationStatusContainer(view: azdata.ModelView): Promise {
+ const statusContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ width: '400px',
+ height: '365px',
+ justifyContent: 'flex-start',
+ })
+ .withProps({
+ CSSStyles: {
+ 'border': '1px solid rgba(0, 0, 0, 0.1)',
+ 'padding': '10px',
+ }
+ })
+ .component();
+
+ const statusContainerTitle = view.modelBuilder.text()
+ .withProps({
+ value: loc.DATABASE_MIGRATION_STATUS,
+ width: '100%',
+ CSSStyles: { ...styles.SECTION_HEADER_CSS }
+ }).component();
+
+ this._refreshButton = view.modelBuilder.button()
+ .withProps({
+ label: loc.REFRESH,
+ iconPath: IconPathHelper.refresh,
+ iconHeight: 16,
+ iconWidth: 16,
+ width: 70,
+ CSSStyles: { 'float': 'right' }
+ }).component();
+
+ const statusHeadingContainer = view.modelBuilder.flexContainer()
+ .withItems([
+ statusContainerTitle,
+ this._refreshButton,
+ ]).withLayout({
+ alignContent: 'center',
+ alignItems: 'center',
+ flexFlow: 'row',
+ }).component();
+
+ this.disposables.push(
+ this._refreshButton.onDidClick(async (e) => {
+ this._refreshButton.enabled = false;
+ await this.refresh();
+ this._refreshButton.enabled = true;
+ }));
+
+ const buttonContainer = view.modelBuilder.flexContainer()
+ .withProps({
+ CSSStyles: {
+ 'justify-content': 'left',
+ 'align-iems': 'center',
+ },
+ })
+ .component();
+
+ buttonContainer.addItem(
+ await this._createServiceSelector(view));
+
+ this._selectServiceText = view.modelBuilder.text()
+ .withProps({
+ value: loc.SELECT_SERVICE_MESSAGE,
+ CSSStyles: {
+ 'font-size': '12px',
+ 'margin': '10px',
+ 'font-weight': '350',
+ 'text-align': 'center',
+ 'display': 'none'
+ }
+ }).component();
+
+ const header = view.modelBuilder.flexContainer()
+ .withItems([statusHeadingContainer, buttonContainer])
+ .withLayout({ flexFlow: 'column', })
+ .component();
+
+ this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ height: '272px',
+ })
+ .withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
+ .component();
+
+ await this._updateSummaryStatus();
+
+ // in progress
+ this._inProgressMigrationButton = this._createStatusCard(
+ view,
+ IconPathHelper.inProgressMigration,
+ loc.MIGRATION_IN_PROGRESS);
+ this.disposables.push(
+ this._inProgressMigrationButton.container.onDidClick(
+ async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
+ this._migrationStatusCardsContainer.addItem(
+ this._inProgressMigrationButton.container,
+ { flex: '0 0 auto' });
+
+ // in progress warning
+ this._inProgressWarningMigrationButton = this._createStatusCard(
+ view,
+ IconPathHelper.inProgressMigration,
+ loc.MIGRATION_IN_PROGRESS,
+ true);
+ this.disposables.push(
+ this._inProgressWarningMigrationButton.container.onDidClick(
+ async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
+ this._migrationStatusCardsContainer.addItem(
+ this._inProgressWarningMigrationButton.container,
+ { flex: '0 0 auto' });
+
+ // successful
+ this._successfulMigrationButton = this._createStatusCard(
+ view,
+ IconPathHelper.completedMigration,
+ loc.MIGRATION_COMPLETED);
+ this.disposables.push(
+ this._successfulMigrationButton.container.onDidClick(
+ async (e) => await this.openMigrationFcn(AdsMigrationStatus.SUCCEEDED)));
+ this._migrationStatusCardsContainer.addItem(
+ this._successfulMigrationButton.container,
+ { flex: '0 0 auto' });
+
+ // completing
+ this._completingMigrationButton = this._createStatusCard(
+ view,
+ IconPathHelper.completingCutover,
+ loc.MIGRATION_CUTOVER_CARD);
+ this.disposables.push(
+ this._completingMigrationButton.container.onDidClick(
+ async (e) => await this.openMigrationFcn(AdsMigrationStatus.COMPLETING)));
+ this._migrationStatusCardsContainer.addItem(
+ this._completingMigrationButton.container,
+ { flex: '0 0 auto' });
+
+ // failed
+ this._failedMigrationButton = this._createStatusCard(
+ view,
+ IconPathHelper.error,
+ loc.MIGRATION_FAILED);
+ this.disposables.push(
+ this._failedMigrationButton.container.onDidClick(
+ async (e) => await this.openMigrationFcn(AdsMigrationStatus.FAILED)));
+ this._migrationStatusCardsContainer.addItem(
+ this._failedMigrationButton.container,
+ { flex: '0 0 auto' });
+
+ // all migrations
+ this._allMigrationButton = this._createStatusCard(
+ view,
+ IconPathHelper.view,
+ loc.VIEW_ALL);
+ this.disposables.push(
+ this._allMigrationButton.container.onDidClick(
+ async (e) => await this.openMigrationFcn(AdsMigrationStatus.ALL)));
+ this._migrationStatusCardsContainer.addItem(
+ this._allMigrationButton.container,
+ { flex: '0 0 auto' });
+
+ this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
+ .withItem(this._migrationStatusCardsContainer)
+ .component();
+ statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
+ statusContainer.addItem(this._selectServiceText, {});
+ statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
+ return statusContainer;
+ }
+
+ private async _createServiceSelector(view: azdata.ModelView): Promise {
+ const serviceContextLabel = await getSelectedServiceStatus();
+ this._serviceContextButton = view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.sqlMigrationService,
+ iconHeight: 22,
+ iconWidth: 22,
+ label: serviceContextLabel,
+ title: serviceContextLabel,
+ description: loc.MIGRATION_SERVICE_DESCRIPTION,
+ buttonType: azdata.ButtonType.Informational,
+ width: 375,
+ CSSStyles: { ...BUTTON_CSS },
+ })
+ .component();
+
+ this.disposables.push(
+ this._serviceContextButton.onDidClick(async () => {
+ const dialog = new SelectMigrationServiceDialog(async () => await this.onDialogClosed());
+ await dialog.initialize();
+ }));
+
+ return this._serviceContextButton;
+ }
+
+ private _updateStatusCard(
+ migrations: DatabaseMigration[],
+ card: StatusCard,
+ status: AdsMigrationStatus,
+ show?: boolean): void {
+ const list = filterMigrations(migrations, status);
+ const count = list?.length || 0;
+ card.container.display = count > 0 || show ? '' : 'none';
+ card.count.value = count.toString();
+ }
+
+ private async _updateSummaryStatus(): Promise {
+ const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
+ const isContextValid = isServiceContextValid(serviceContext);
+ await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
+ await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
+ this._refreshButton.enabled = isContextValid;
+ }
+}
diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts
new file mode 100644
index 0000000000..3508ec2753
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts
@@ -0,0 +1,205 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 '../telemtery';
+import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
+import { getResourceName } from '../api/azure';
+import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
+import { EmptySettingValue } from './tabBase';
+import { DashboardStatusBar } from './sqlServerDashboard';
+
+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,
+ onClosedCallback: () => Promise,
+ statusBar: DashboardStatusBar,
+ ): Promise {
+
+ this.view = view;
+ this.context = context;
+ this.onClosedCallback = onClosedCallback;
+ this.statusBar = statusBar;
+
+ await this.initialize(this.view);
+
+ return this;
+ }
+
+ public async refresh(): Promise {
+ if (this.isRefreshing || this.model?.migration === undefined) {
+ return;
+ }
+
+ this.isRefreshing = true;
+ this.refreshButton.enabled = false;
+ 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 azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
+ 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 = getMigrationStatus(migration) ?? EmptySettingValue;
+ 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.retryButton.enabled = canRetryMigration(migration);
+
+ this.isRefreshing = false;
+ this.refreshLoader.loading = false;
+ this.refreshButton.enabled = true;
+ }
+
+ 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
new file mode 100644
index 0000000000..4912fd027a
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts
@@ -0,0 +1,389 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 '../telemtery';
+import * as styles from '../constants/styles';
+import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
+import { getResourceName } from '../api/azure';
+import { EmptySettingValue } from './tabBase';
+import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
+import { DashboardStatusBar } from './sqlServerDashboard';
+
+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,
+ onClosedCallback: () => Promise,
+ statusBar: DashboardStatusBar): Promise {
+
+ this.view = view;
+ this.context = context;
+ this.onClosedCallback = onClosedCallback;
+ this.statusBar = statusBar;
+
+ await this.initialize(this.view);
+
+ return this;
+ }
+
+ public async refresh(): Promise {
+ if (this.isRefreshing || this.model?.migration === undefined) {
+ return;
+ }
+
+ this.isRefreshing = true;
+ this.refreshButton.enabled = false;
+ this.refreshLoader.loading = true;
+ await this.statusBar.clearError();
+ await this._fileTable.updateProperty('data', []);
+
+ 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 azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
+ 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 = getMigrationStatus(migration) ?? EmptySettingValue;
+ 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.retryButton.enabled = canRetryMigration(migration);
+ this.isRefreshing = false;
+ this.refreshLoader.loading = false;
+ this.refreshButton.enabled = true;
+ }
+
+ 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/migrationDetailsTabBase.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts
new file mode 100644
index 0000000000..e2bdd589e6
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts
@@ -0,0 +1,463 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { MigrationServiceContext } from '../models/migrationLocalStorage';
+import * as loc from '../constants/strings';
+import * as styles from '../constants/styles';
+import { DatabaseMigration } 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 './sqlServerDashboard';
+
+export const infoFieldLgWidth: string = '330px';
+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,
+};
+
+export interface InfoFieldSchema {
+ flexContainer: azdata.FlexContainer,
+ text: azdata.TextComponent,
+ icon?: azdata.ImageComponent,
+}
+
+export abstract class MigrationDetailsTabBase extends TabBase {
+ protected model!: MigrationCutoverDialogModel;
+ protected databaseLabel!: azdata.TextComponent;
+ protected serviceContext!: MigrationServiceContext;
+ protected onClosedCallback!: () => Promise;
+
+ protected cutoverButton!: azdata.ButtonComponent;
+ protected refreshButton!: azdata.ButtonComponent;
+ protected cancelButton!: azdata.ButtonComponent;
+ protected refreshLoader!: azdata.LoadingComponent;
+ protected copyDatabaseMigrationDetails!: azdata.ButtonComponent;
+ protected newSupportRequest!: azdata.ButtonComponent;
+ protected retryButton!: azdata.ButtonComponent;
+ protected summaryTextComponent: azdata.TextComponent[] = [];
+
+ public abstract create(context: vscode.ExtensionContext, view: azdata.ModelView, onClosedCallback: () => Promise, statusBar: DashboardStatusBar): Promise;
+
+ protected abstract migrationInfoGrid(): Promise;
+
+ constructor() {
+ super();
+ this.title = '';
+ }
+
+ public async setMigrationContext(
+ serviceContext: MigrationServiceContext,
+ migration: DatabaseMigration): Promise {
+ this.serviceContext = serviceContext;
+ this.model = new MigrationCutoverDialogModel(serviceContext, migration);
+ await this.refresh();
+ }
+
+ protected createBreadcrumbContainer(): azdata.FlexContainer {
+ const migrationsTabLink = this.view.modelBuilder.hyperlink()
+ .withProps({
+ label: loc.BREADCRUMB_MIGRATIONS,
+ url: '',
+ title: loc.BREADCRUMB_MIGRATIONS,
+ CSSStyles: {
+ 'padding': '5px 5px 5px 0',
+ 'font-size': '13px'
+ }
+ })
+ .component();
+ this.disposables.push(
+ migrationsTabLink.onDidClick(
+ async (e) => await this.onClosedCallback()));
+
+ const breadCrumbImage = this.view.modelBuilder.image()
+ .withProps({
+ iconPath: IconPathHelper.breadCrumb,
+ iconHeight: 8,
+ iconWidth: 8,
+ width: 8,
+ height: 8,
+ CSSStyles: { 'padding': '4px' }
+ }).component();
+
+ this.databaseLabel = this.view.modelBuilder.text()
+ .withProps({
+ textType: azdata.TextType.Normal,
+ value: '...',
+ CSSStyles: {
+ 'font-size': '16px',
+ 'font-weight': '600',
+ 'margin-block-start': '0',
+ 'margin-block-end': '0',
+ }
+ }).component();
+
+ return this.view.modelBuilder.flexContainer()
+ .withItems(
+ [migrationsTabLink, breadCrumbImage, this.databaseLabel],
+ { flex: '0 0 auto' })
+ .withLayout({
+ flexFlow: 'row',
+ alignItems: 'center',
+ alignContent: 'center',
+ })
+ .withProps({
+ height: 20,
+ CSSStyles: { 'padding': '0', 'margin-bottom': '5px' }
+ })
+ .component();
+ }
+
+ protected createMigrationToolbarContainer(): azdata.FlexContainer {
+ const toolbarContainer = this.view.modelBuilder.toolbarContainer();
+ const buttonHeight = 20;
+ this.cutoverButton = this.view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.cutover,
+ iconHeight: '16px',
+ iconWidth: '16px',
+ label: loc.COMPLETE_CUTOVER,
+ height: buttonHeight,
+ enabled: false,
+ CSSStyles: { 'display': 'none' }
+ }).component();
+
+ this.disposables.push(
+ this.cutoverButton.onDidClick(async (e) => {
+ await this.statusBar.clearError();
+ await this.refresh();
+ const dialog = new ConfirmCutoverDialog(this.model);
+ await dialog.initialize();
+
+ if (this.model.CutoverError) {
+ await this.statusBar.showError(
+ loc.MIGRATION_CUTOVER_ERROR,
+ loc.MIGRATION_CUTOVER_ERROR,
+ this.model.CutoverError.message);
+ }
+ }));
+
+ this.cancelButton = this.view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.cancel,
+ iconHeight: '16px',
+ iconWidth: '16px',
+ label: loc.CANCEL_MIGRATION,
+ height: buttonHeight,
+ enabled: false,
+ }).component();
+
+ this.disposables.push(
+ this.cancelButton.onDidClick((e) => {
+ void vscode.window.showInformationMessage(
+ loc.CANCEL_MIGRATION_CONFIRMATION,
+ { modal: true },
+ loc.YES,
+ loc.NO
+ ).then(async (v) => {
+ if (v === loc.YES) {
+ await this.statusBar.clearError();
+ await this.model.cancelMigration();
+ await this.refresh();
+ if (this.model.CancelMigrationError) {
+ {
+ await this.statusBar.showError(
+ loc.MIGRATION_CANCELLATION_ERROR,
+ loc.MIGRATION_CANCELLATION_ERROR,
+ this.model.CancelMigrationError.message);
+ }
+ }
+ }
+ });
+ }));
+
+
+ this.retryButton = this.view.modelBuilder.button()
+ .withProps({
+ label: loc.RETRY_MIGRATION,
+ iconPath: IconPathHelper.retry,
+ enabled: false,
+ iconHeight: '16px',
+ iconWidth: '16px',
+ height: buttonHeight,
+ }).component();
+
+ this.disposables.push(
+ this.retryButton.onDidClick(
+ async (e) => {
+ await this.refresh();
+ const retryMigrationDialog = new RetryMigrationDialog(
+ this.context,
+ this.serviceContext,
+ this.model.migration,
+ this.onClosedCallback);
+ await retryMigrationDialog.openDialog();
+ }
+ ));
+
+ this.copyDatabaseMigrationDetails = this.view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.copy,
+ iconHeight: '16px',
+ iconWidth: '16px',
+ label: loc.COPY_MIGRATION_DETAILS,
+ height: buttonHeight,
+ }).component();
+
+ this.disposables.push(
+ this.copyDatabaseMigrationDetails.onDidClick(async (e) => {
+ await this.refresh();
+ await vscode.env.clipboard.writeText(this._getMigrationDetails());
+
+ void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
+ }));
+
+ this.newSupportRequest = this.view.modelBuilder.button()
+ .withProps({
+ label: loc.NEW_SUPPORT_REQUEST,
+ iconPath: IconPathHelper.newSupportRequest,
+ iconHeight: '16px',
+ iconWidth: '16px',
+ height: buttonHeight,
+ }).component();
+
+ this.disposables.push(
+ this.newSupportRequest.onDidClick(async (e) => {
+ const serviceId = this.model.migration.properties.migrationService;
+ const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
+ await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
+ }));
+
+ this.refreshButton = this.view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.refresh,
+ iconHeight: '16px',
+ iconWidth: '16px',
+ label: loc.REFRESH_BUTTON_TEXT,
+ height: buttonHeight,
+ }).component();
+
+ this.disposables.push(
+ this.refreshButton.onDidClick(
+ async (e) => await this.refresh()));
+
+ this.refreshLoader = this.view.modelBuilder.loadingComponent()
+ .withProps({
+ loading: false,
+ CSSStyles: {
+ 'height': '8px',
+ 'margin-top': '4px'
+ }
+ }).component();
+
+ toolbarContainer.addToolbarItems([
+ { component: this.cutoverButton },
+ { component: this.cancelButton },
+ { component: this.retryButton },
+ { component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
+ { component: this.newSupportRequest, toolbarSeparatorAfter: true },
+ { component: this.refreshButton },
+ { component: this.refreshLoader },
+ ]);
+
+ return this.view.modelBuilder.flexContainer()
+ .withItems([
+ this.createBreadcrumbContainer(),
+ toolbarContainer.component(),
+ ])
+ .withLayout({ flexFlow: 'column', width: '100%' })
+ .component();
+ }
+
+ protected async createInfoCard(
+ label: string,
+ iconPath: azdata.IconPath
+ ): Promise {
+ const defaultValue = (0).toLocaleString();
+ const flexContainer = this.view.modelBuilder.flexContainer()
+ .withProps({
+ width: 168,
+ CSSStyles: {
+ 'flex-direction': 'column',
+ 'margin': '0 12px 0 0',
+ 'box-sizing': 'border-box',
+ 'border': '1px solid rgba(204, 204, 204, 0.5)',
+ 'box-shadow': '0px 2px 4px rgba(0, 0, 0, 0.1)',
+ 'border-radius': '2px',
+ }
+ }).component();
+
+ const labelComponent = this.view.modelBuilder.text()
+ .withProps({
+ value: label,
+ CSSStyles: {
+ 'font-size': '13px',
+ 'font-weight': '600',
+ 'margin': '5px',
+ }
+ }).component();
+ flexContainer.addItem(labelComponent);
+
+ const iconComponent = this.view.modelBuilder.image()
+ .withProps({
+ iconPath: iconPath,
+ iconHeight: 16,
+ iconWidth: 16,
+ height: 16,
+ width: 16,
+ CSSStyles: {
+ 'margin': '5px 5px 5px 5px',
+ 'padding': '0'
+ }
+ }).component();
+
+ const textComponent = this.view.modelBuilder.text()
+ .withProps({
+ value: defaultValue,
+ title: defaultValue,
+ CSSStyles: {
+ 'font-size': '20px',
+ 'font-weight': '600',
+ 'margin': '0 5px 0 5px'
+ }
+ }).component();
+
+ this.summaryTextComponent.push(textComponent);
+
+ const iconTextComponent = this.view.modelBuilder.flexContainer()
+ .withItems([iconComponent, textComponent])
+ .withLayout({ alignItems: 'center' })
+ .withProps({
+ CSSStyles: {
+ 'flex-direction': 'row',
+ 'margin': '0 0 0 5px',
+ 'padding': '0',
+ },
+ display: 'inline-flex'
+ }).component();
+
+ flexContainer.addItem(iconTextComponent);
+
+ return flexContainer;
+ }
+
+ protected async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
+ flexContainer: azdata.FlexContainer,
+ text: azdata.TextComponent,
+ icon?: azdata.ImageComponent
+ }> {
+ const flexContainer = this.view.modelBuilder.flexContainer()
+ .withProps({
+ CSSStyles: {
+ 'flex-direction': 'column',
+ 'padding-right': '12px'
+ }
+ }).component();
+
+ const labelComponent = this.view.modelBuilder.text()
+ .withProps({
+ value: label,
+ CSSStyles: {
+ ...styles.LIGHT_LABEL_CSS,
+ 'margin-bottom': '0',
+ }
+ }).component();
+ flexContainer.addItem(labelComponent);
+
+ const textComponent = this.view.modelBuilder.text()
+ .withProps({
+ value: value,
+ title: value,
+ description: value,
+ width: '100%',
+ CSSStyles: {
+ 'font-size': '13px',
+ 'line-height': '18px',
+ 'margin': '4px 0 12px',
+ 'overflow': 'hidden',
+ 'text-overflow': 'ellipsis',
+ 'max-width': '230px',
+ 'display': 'inline-block',
+ }
+ }).component();
+
+ let iconComponent;
+ if (iconPath) {
+ iconComponent = this.view.modelBuilder.image()
+ .withProps({
+ iconPath: (iconPath === ' ') ? undefined : iconPath,
+ iconHeight: statusImageSize,
+ iconWidth: statusImageSize,
+ height: statusImageSize,
+ width: statusImageSize,
+ title: value,
+ CSSStyles: {
+ 'margin': '7px 3px 0 0',
+ 'padding': '0'
+ }
+ }).component();
+
+ const iconTextComponent = this.view.modelBuilder.flexContainer()
+ .withItems([
+ iconComponent,
+ textComponent
+ ]).withProps({
+ CSSStyles: {
+ 'margin': '0',
+ 'padding': '0'
+ },
+ display: 'inline-flex'
+ }).component();
+ flexContainer.addItem(iconTextComponent);
+ } else {
+ flexContainer.addItem(textComponent);
+ }
+
+ return {
+ flexContainer: flexContainer,
+ text: textComponent,
+ icon: iconComponent
+ };
+ }
+
+ protected async showMigrationErrors(migration: DatabaseMigration): Promise {
+ const errorMessage = this.getMigrationErrors(migration);
+ if (errorMessage?.length > 0) {
+ await this.statusBar.showError(
+ loc.MIGRATION_ERROR_DETAILS_TITLE,
+ loc.MIGRATION_ERROR_DETAILS_LABEL,
+ errorMessage);
+ }
+ }
+
+ protected getMigrationCurrentlyRestoringFile(migration: DatabaseMigration): string | undefined {
+ const lastAppliedBackupFile = this.getMigrationLastAppliedBackupFile(migration);
+ const currentRestoringFile = migration?.properties?.migrationStatusDetails?.currentRestoringFilename;
+
+ return currentRestoringFile === lastAppliedBackupFile
+ && currentRestoringFile && currentRestoringFile.length > 0
+ ? loc.ALL_BACKUPS_RESTORED
+ : currentRestoringFile;
+ }
+
+ protected getMigrationLastAppliedBackupFile(migration: DatabaseMigration): string | undefined {
+ return migration?.properties?.migrationStatusDetails?.lastRestoredFilename
+ || migration?.properties?.offlineConfiguration?.lastBackupName;
+ }
+
+ private _getMigrationDetails(): string {
+ return JSON.stringify(this.model.migration, undefined, 2);
+ }
+}
diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts
new file mode 100644
index 0000000000..fab2b4395c
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts
@@ -0,0 +1,567 @@
+/*---------------------------------------------------------------------------------------------
+ * 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, getPipelineStatusImage, debounce } from '../api/utils';
+import { logError, TelemetryViews } from '../telemtery';
+import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
+import { CopyProgressDetail, getResourceName } from '../api/azure';
+import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
+import { EmptySettingValue } from './tabBase';
+import { IconPathHelper } from '../constants/iconPathHelper';
+import { DashboardStatusBar } from './sqlServerDashboard';
+import { EOL } from 'os';
+
+const MigrationDetailsTableTabId = 'MigrationDetailsTableTab';
+
+const TableColumns = {
+ tableName: 'tableName',
+ status: 'status',
+ dataRead: 'dataRead',
+ dataWritten: 'dataWritten',
+ rowsRead: 'rowsRead',
+ rowsCopied: 'rowsCopied',
+ copyThroughput: 'copyThroughput',
+ copyDuration: 'copyDuration',
+ parallelCopyType: 'parallelCopyType',
+ usedParallelCopies: 'usedParallelCopies',
+ copyStart: 'copyStart',
+};
+
+enum SummaryCardIndex {
+ TotalTables = 0,
+ InProgressTables = 1,
+ SuccessfulTables = 2,
+ FailedTables = 3,
+ CanceledTables = 4,
+}
+
+export class MigrationDetailsTableTab extends MigrationDetailsTabBase {
+ private _sourceDatabaseInfoField!: InfoFieldSchema;
+ private _sourceDetailsInfoField!: InfoFieldSchema;
+ private _sourceVersionInfoField!: 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;
+ private _progressTable!: azdata.TableComponent;
+ private _progressDetail: CopyProgressDetail[] = [];
+
+ constructor() {
+ super();
+ this.id = MigrationDetailsTableTabId;
+ }
+
+ public async create(
+ context: vscode.ExtensionContext,
+ view: azdata.ModelView,
+ onClosedCallback: () => Promise,
+ statusBar: DashboardStatusBar): Promise {
+
+ this.view = view;
+ this.context = context;
+ this.onClosedCallback = onClosedCallback;
+ this.statusBar = statusBar;
+
+ await this.initialize(this.view);
+
+ return this;
+ }
+
+ @debounce(500)
+ public async refresh(): Promise {
+ if (this.isRefreshing) {
+ return;
+ }
+
+ this.isRefreshing = true;
+ this.refreshButton.enabled = false;
+ this.refreshLoader.loading = true;
+ await this.statusBar.clearError();
+
+ try {
+ await this.model.fetchStatus();
+ await this._loadData();
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.MIGRATION_STATUS_REFRESH_ERROR,
+ loc.MIGRATION_STATUS_REFRESH_ERROR,
+ e.message);
+ }
+
+ this.isRefreshing = false;
+ this.refreshLoader.loading = false;
+ this.refreshButton.enabled = true;
+ }
+
+ 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 sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
+ 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 ?? ''];
+
+ const hashSet: loc.LookupTable = {};
+ this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
+ 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 = migration.properties.migrationStatusDetails?.listOfCopyProgressDetails.length ?? 0;
+
+ 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 = getMigrationStatus(migration) ?? EmptySettingValue;
+ this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
+ this._serverObjectsInfoField.text.value = totalCount.toLocaleString();
+
+ this.cutoverButton.enabled = canCutoverMigration(migration);
+ this.cancelButton.enabled = canCancelMigration(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'
+ },
+ data: [],
+ height: '300px',
+ columns: [
+ {
+ value: TableColumns.tableName,
+ name: loc.SQLDB_COL_TABLE_NAME,
+ type: azdata.ColumnType.text,
+ width: 170,
+ },
+ {
+ name: loc.STATUS,
+ value: TableColumns.status,
+ width: 106,
+ type: azdata.ColumnType.hyperlink,
+ icon: IconPathHelper.inProgressMigration,
+ showText: true,
+ },
+ {
+ value: TableColumns.dataRead,
+ name: loc.SQLDB_COL_DATA_READ,
+ width: 64,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.dataWritten,
+ name: loc.SQLDB_COL_DATA_WRITTEN,
+ width: 77,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.rowsRead,
+ name: loc.SQLDB_COL_ROWS_READ,
+ width: 68,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.rowsCopied,
+ name: loc.SQLDB_COL_ROWS_COPIED,
+ width: 77,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.copyThroughput,
+ name: loc.SQLDB_COL_COPY_THROUGHPUT,
+ width: 102,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.copyDuration,
+ name: loc.SQLDB_COL_COPY_DURATION,
+ width: 87,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.parallelCopyType,
+ name: loc.SQLDB_COL_PARRALEL_COPY_TYPE,
+ width: 104,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.usedParallelCopies,
+ name: loc.SQLDB_COL_USED_PARALLEL_COPIES,
+ width: 116,
+ type: azdata.ColumnType.text,
+ },
+ {
+ value: TableColumns.copyStart,
+ name: loc.SQLDB_COL_COPY_START,
+ width: 140,
+ type: azdata.ColumnType.text,
+ },
+ ],
+ }).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: await this._createStatusBar() },
+ { component: await this._createTableFilter() },
+ { component: this._progressTable },
+ ];
+
+ this.disposables.push(
+ this._progressTable.onCellAction!(
+ async (rowState: azdata.ICellActionEventArgs) => {
+ const buttonState = rowState;
+ if (buttonState?.column === 1) {
+ const tableName = this._progressTable!.data[rowState.row][0] || null;
+ const tableProgress = this.model.migration.properties.migrationStatusDetails?.listOfCopyProgressDetails?.find(
+ progress => progress.tableName === tableName);
+ const errors = tableProgress?.errors || [];
+ const tableStatus = loc.PipelineRunStatus[tableProgress?.status ?? ''] ?? tableProgress?.status;
+ const statusMessage = loc.TABLE_MIGRATION_STATUS_LABEL(tableStatus);
+ const errorMessage = errors.join(EOL);
+
+ this.showDialogMessage(
+ loc.TABLE_MIGRATION_STATUS_TITLE,
+ statusMessage,
+ errorMessage);
+ }
+ }));
+
+ const formContainer = this.view.modelBuilder.formContainer()
+ .withFormItems(
+ formItems,
+ { horizontal: false })
+ .withProps({ width: '100%', CSSStyles: { margin: '0 0 0 5px', padding: '0 15px 0 15px' } })
+ .component();
+
+ this.content = formContainer;
+ } catch (e) {
+ logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
+ }
+ }
+
+ private _sortTableMigrations(data: CopyProgressDetail[], columnName: string, ascending: boolean): void {
+ const sortDir = ascending ? -1 : 1;
+ switch (columnName) {
+ case TableColumns.tableName:
+ data.sort((t1, t2) => this.stringCompare(t1.tableName, t2.tableName, sortDir));
+ return;
+ case TableColumns.status:
+ data.sort((t1, t2) => this.stringCompare(t1.status, t2.status, sortDir));
+ return;
+ case TableColumns.dataRead:
+ data.sort((t1, t2) => this.numberCompare(t1.dataRead, t2.dataRead, sortDir));
+ return;
+ case TableColumns.dataWritten:
+ data.sort((t1, t2) => this.numberCompare(t1.dataWritten, t2.dataWritten, sortDir));
+ return;
+ case TableColumns.rowsRead:
+ data.sort((t1, t2) => this.numberCompare(t1.rowsRead, t2.rowsRead, sortDir));
+ return;
+ case TableColumns.rowsCopied:
+ data.sort((t1, t2) => this.numberCompare(t1.rowsCopied, t2.rowsCopied, sortDir));
+ return;
+ case TableColumns.copyThroughput:
+ data.sort((t1, t2) => this.numberCompare(t1.copyThroughput, t2.copyThroughput, sortDir));
+ return;
+ case TableColumns.copyDuration:
+ data.sort((t1, t2) => this.numberCompare(t1.copyDuration, t2.copyDuration, sortDir));
+ return;
+ case TableColumns.parallelCopyType:
+ data.sort((t1, t2) => this.stringCompare(t1.parallelCopyType, t2.parallelCopyType, sortDir));
+ return;
+ case TableColumns.usedParallelCopies:
+ data.sort((t1, t2) => this.numberCompare(t1.usedParallelCopies, t2.usedParallelCopies, sortDir));
+ return;
+ case TableColumns.copyStart:
+ data.sort((t1, t2) => this.dateCompare(t1.copyStart, t2.copyStart, sortDir));
+ return;
+ }
+ }
+
+ private _updateSummaryComponent(cardIndex: number, value: number): void {
+ const stringValue = value.toLocaleString();
+ const textComponent = this.summaryTextComponent[cardIndex];
+ textComponent.value = stringValue;
+ textComponent.title = stringValue;
+ }
+
+ private _filterTables(tables: any[], value: string | undefined): any[] {
+ const lcValue = value?.toLowerCase() ?? '';
+
+ return lcValue.length > 0
+ ? tables.filter((table: string[]) =>
+ table.some((col: string | { title: string }) => {
+ return typeof (col) === 'string'
+ ? col.toLowerCase().includes(lcValue)
+ : col.title?.toLowerCase().includes(lcValue);
+ }))
+ : tables;
+ }
+
+ private async _createTableFilter(): Promise {
+ this._tableFilterInputBox = this.view.modelBuilder.inputBox()
+ .withProps({
+ inputType: 'text',
+ maxLength: 100,
+ width: 268,
+ placeHolder: loc.FILTER_SERVER_OBJECTS_PLACEHOLDER,
+ ariaLabel: loc.FILTER_SERVER_OBJECTS_ARIA_LABEL,
+ })
+ .component();
+
+ this.disposables.push(
+ this._tableFilterInputBox.onTextChanged(
+ async (value) => await this._populateTableData()));
+
+ const sortLabel = this.view.modelBuilder.text()
+ .withProps({
+ value: loc.SORT_LABEL,
+ CSSStyles: {
+ 'font-size': '13px',
+ 'font-weight': '600',
+ 'margin': '3px 0 0 0',
+ },
+ }).component();
+
+ this._columnSortDropdown = this.view.modelBuilder.dropDown()
+ .withProps({
+ editable: false,
+ width: 150,
+ CSSStyles: { 'margin-left': '5px' },
+ value: { name: TableColumns.copyStart, displayName: loc.START_TIME },
+ values: [
+ { name: TableColumns.tableName, displayName: loc.SQLDB_COL_TABLE_NAME },
+ { name: TableColumns.status, displayName: loc.STATUS },
+ { name: TableColumns.dataRead, displayName: loc.SQLDB_COL_DATA_READ },
+ { name: TableColumns.dataWritten, displayName: loc.SQLDB_COL_DATA_WRITTEN },
+ { name: TableColumns.rowsRead, displayName: loc.SQLDB_COL_ROWS_READ },
+ { name: TableColumns.rowsCopied, displayName: loc.SQLDB_COL_ROWS_COPIED },
+ { name: TableColumns.copyThroughput, displayName: loc.SQLDB_COL_COPY_THROUGHPUT },
+ { name: TableColumns.copyDuration, displayName: loc.SQLDB_COL_COPY_DURATION },
+ { name: TableColumns.parallelCopyType, displayName: loc.SQLDB_COL_PARRALEL_COPY_TYPE },
+ { name: TableColumns.usedParallelCopies, displayName: loc.SQLDB_COL_USED_PARALLEL_COPIES },
+ { name: TableColumns.copyStart, displayName: loc.SQLDB_COL_COPY_START },
+ ],
+ })
+ .component();
+ this.disposables.push(
+ this._columnSortDropdown.onValueChanged(
+ async (value) => await this._populateTableData()));
+
+ this._columnSortCheckbox = this.view.modelBuilder.checkBox()
+ .withProps({
+ label: loc.ASCENDING_LABEL,
+ checked: false,
+ CSSStyles: { 'margin-left': '15px' },
+ })
+ .component();
+ this.disposables.push(
+ this._columnSortCheckbox.onChanged(
+ async (value) => await this._populateTableData()));
+
+ const columnSortContainer = this.view.modelBuilder.flexContainer()
+ .withItems([sortLabel, this._columnSortDropdown])
+ .withProps({
+ CSSStyles: {
+ 'justify-content': 'left',
+ 'align-items': 'center',
+ 'padding': '0px',
+ 'display': 'flex',
+ 'flex-direction': 'row',
+ },
+ }).component();
+ columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
+
+ const flexContainer = this.view.modelBuilder.flexContainer()
+ .withProps({
+ width: '100%',
+ CSSStyles: {
+ 'justify-content': 'left',
+ 'align-items': 'center',
+ 'padding': '0px',
+ 'display': 'flex',
+ 'flex-direction': 'row',
+ 'flex-flow': 'wrap',
+ },
+ }).component();
+ flexContainer.addItem(this._tableFilterInputBox, { flex: '0' });
+ flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
+
+ return flexContainer;
+ }
+
+ private async _createStatusBar(): Promise {
+ const serverObjectsLabel = this.view.modelBuilder.text()
+ .withProps({
+ value: loc.SERVER_OBJECTS_LABEL,
+ CSSStyles: {
+ 'font-weight': '600',
+ 'font-size': '14px',
+ 'margin': '0 0 5px 0',
+ },
+ })
+ .component();
+
+ const flexContainer = this.view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'row',
+ flexWrap: 'wrap',
+ })
+ .component();
+
+ flexContainer.addItems([
+ await this.createInfoCard(loc.SERVER_OBJECTS_ALL_TABLES_LABEL, IconPathHelper.allTables),
+ await this.createInfoCard(loc.SERVER_OBJECTS_IN_PROGRESS_TABLES_LABEL, IconPathHelper.inProgressMigration),
+ await this.createInfoCard(loc.SERVER_OBJECTS_SUCCESSFUL_TABLES_LABEL, IconPathHelper.completedMigration),
+ await this.createInfoCard(loc.SERVER_OBJECTS_FAILED_TABLES_LABEL, IconPathHelper.error),
+ await this.createInfoCard(loc.SERVER_OBJECTS_CANCELLED_TABLES_LABEL, IconPathHelper.cancel)
+ ], { flex: '0 0 auto', CSSStyles: { 'width': '168px' } });
+
+ return this.view.modelBuilder.flexContainer()
+ .withItems([serverObjectsLabel, flexContainer])
+ .withLayout({ flexFlow: 'column' })
+ .component();
+ }
+
+ protected async migrationInfoGrid(): Promise {
+ 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
new file mode 100644
index 0000000000..6c5093cf5e
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/migrationsListTab.ts
@@ -0,0 +1,771 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage } from '../models/migrationLocalStorage';
+import * as loc from '../constants/strings';
+import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils';
+import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
+import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
+import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
+import { getMigrationTargetType, getMigrationMode, canRetryMigration, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
+import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
+import { DatabaseMigration, getResourceName } from '../api/azure';
+import { logError, TelemetryViews } from '../telemtery';
+import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
+import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase';
+import { DashboardStatusBar } from './sqlServerDashboard';
+import { MigrationMode } from '../models/stateMachine';
+
+export const MigrationsListTabId = 'MigrationsListTab';
+
+const TableColumns = {
+ sourceDatabase: 'sourceDatabase',
+ sourceServer: 'sourceServer',
+ status: 'status',
+ mode: 'mode',
+ targetType: 'targetType',
+ targetDatabse: 'targetDatabase',
+ targetServer: 'TargetServer',
+ duration: 'duration',
+ startTime: 'startTime',
+ finishTime: 'finishTime',
+};
+
+export class MigrationsListTab extends TabBase {
+ private _searchBox!: azdata.InputBoxComponent;
+ private _refresh!: azdata.ButtonComponent;
+ private _serviceContextButton!: azdata.ButtonComponent;
+ private _statusDropdown!: azdata.DropDownComponent;
+ private _columnSortDropdown!: azdata.DropDownComponent;
+ private _columnSortCheckbox!: azdata.CheckBoxComponent;
+ private _statusTable!: azdata.TableComponent;
+ private _refreshLoader!: azdata.LoadingComponent;
+ private _filteredMigrations: DatabaseMigration[] = [];
+ private _openMigrationDetails!: (migration: DatabaseMigration) => Promise;
+ private _migrations: DatabaseMigration[] = [];
+
+ constructor() {
+ super();
+ this.id = MigrationsListTabId;
+ }
+
+ public async create(
+ context: vscode.ExtensionContext,
+ view: azdata.ModelView,
+ openMigrationDetails: (migration: DatabaseMigration) => Promise,
+ statusBar: DashboardStatusBar,
+ ): Promise {
+
+ this.view = view;
+ this.context = context;
+ this._openMigrationDetails = openMigrationDetails;
+ this.statusBar = statusBar;
+
+ await this.initialize();
+
+ return this;
+ }
+
+ public onDialogClosed = async (): Promise =>
+ await this.updateServiceContext(this._serviceContextButton);
+
+ public async setMigrationFilter(filter: AdsMigrationStatus): Promise {
+ if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
+ const statusFilter = (this._statusDropdown.values)
+ .find(value => value.name === filter.toString());
+
+ this._statusDropdown.value = statusFilter;
+ }
+ }
+
+ public async refresh(): Promise {
+ if (this.isRefreshing) {
+ return;
+ }
+
+ this.isRefreshing = true;
+ this._refresh.enabled = false;
+ this._refreshLoader.loading = true;
+ await this.statusBar.clearError();
+
+ try {
+ await this._statusTable.updateProperty('data', []);
+ this._migrations = await getCurrentMigrations();
+ await this._populateMigrationTable();
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
+ loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
+ e.message);
+ logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
+ } finally {
+ this._refreshLoader.loading = false;
+ this._refresh.enabled = true;
+ this.isRefreshing = false;
+ }
+ }
+
+ protected async initialize(): Promise {
+ this._registerCommands();
+
+ this.content = this.view.modelBuilder.flexContainer()
+ .withItems(
+ [
+ this._createToolbar(),
+ await this._createSearchAndSortContainer(),
+ this._createStatusTable()
+ ],
+ { CSSStyles: { 'width': '100%' } }
+ ).withLayout({
+ width: '100%',
+ flexFlow: 'column',
+ }).withProps({ CSSStyles: { 'padding': '0px' } })
+ .component();
+ }
+
+ private _createToolbar(): azdata.ToolbarContainer {
+ const toolbar = this.view.modelBuilder.toolbarContainer();
+
+ this._refresh = this.view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.refresh,
+ iconHeight: 24,
+ iconWidth: 24,
+ height: 24,
+ label: loc.REFRESH_BUTTON_LABEL,
+ }).component();
+ this.disposables.push(
+ this._refresh.onDidClick(
+ async (e) => await this.refresh()));
+
+ this._refreshLoader = this.view.modelBuilder.loadingComponent()
+ .withProps({
+ loading: false,
+ CSSStyles: {
+ 'height': '8px',
+ 'margin-top': '6px'
+ }
+ })
+ .component();
+
+ toolbar.addToolbarItems([
+ { component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
+ { component: this.createNewSupportRequestButton() },
+ { component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
+ { component: this._refresh },
+ { component: this._refreshLoader },
+ ]);
+
+ return toolbar.component();
+ }
+
+ private async _createSearchAndSortContainer(): Promise {
+ const serviceContextLabel = await getSelectedServiceStatus();
+ this._serviceContextButton = this.view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.sqlMigrationService,
+ iconHeight: 22,
+ iconWidth: 22,
+ label: serviceContextLabel,
+ title: serviceContextLabel,
+ description: loc.MIGRATION_SERVICE_DESCRIPTION,
+ buttonType: azdata.ButtonType.Informational,
+ width: 230,
+ }).component();
+
+ const onDialogClosed = async (): Promise =>
+ await this.updateServiceContext(this._serviceContextButton);
+
+ this.disposables.push(
+ this._serviceContextButton.onDidClick(
+ async () => {
+ const dialog = new SelectMigrationServiceDialog(onDialogClosed);
+ await dialog.initialize();
+ }));
+
+ this._searchBox = this.view.modelBuilder.inputBox()
+ .withProps({
+ stopEnterPropagation: true,
+ placeHolder: loc.SEARCH_FOR_MIGRATIONS,
+ width: '200px',
+ }).component();
+ this.disposables.push(
+ this._searchBox.onTextChanged(
+ async (value) => await this._populateMigrationTable()));
+
+ const searchLabel = this.view.modelBuilder.text()
+ .withProps({
+ value: loc.STATUS_LABEL,
+ CSSStyles: {
+ 'font-size': '13px',
+ 'font-weight': '600',
+ 'margin': '3px 0 0 0',
+ },
+ }).component();
+
+ this._statusDropdown = this.view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: loc.MIGRATION_STATUS_FILTER,
+ values: this._statusDropdownValues,
+ width: '150px'
+ }).component();
+ this.disposables.push(
+ this._statusDropdown.onValueChanged(
+ async (value) => await this._populateMigrationTable()));
+
+ const searchContainer = this.view.modelBuilder.flexContainer()
+ .withLayout({
+ alignContent: 'center',
+ alignItems: 'center',
+ }).withProps({ CSSStyles: { 'margin-left': '10px' } })
+ .component();
+ searchContainer.addItem(searchLabel, { flex: '0' });
+ searchContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
+
+ const sortLabel = this.view.modelBuilder.text()
+ .withProps({
+ value: loc.SORT_LABEL,
+ CSSStyles: {
+ 'font-size': '13px',
+ 'font-weight': '600',
+ 'margin': '3px 0 0 0',
+ },
+ }).component();
+
+ this._columnSortDropdown = this.view.modelBuilder.dropDown()
+ .withProps({
+ editable: false,
+ width: 120,
+ CSSStyles: { 'margin-left': '5px' },
+ value: { name: TableColumns.startTime, displayName: loc.START_TIME },
+ values: [
+ { name: TableColumns.sourceDatabase, displayName: loc.SRC_DATABASE },
+ { name: TableColumns.sourceServer, displayName: loc.SRC_SERVER },
+ { name: TableColumns.status, displayName: loc.STATUS_COLUMN },
+ { name: TableColumns.mode, displayName: loc.MIGRATION_MODE },
+ { name: TableColumns.targetType, displayName: loc.AZURE_SQL_TARGET },
+ { name: TableColumns.targetDatabse, displayName: loc.TARGET_DATABASE_COLUMN },
+ { name: TableColumns.targetServer, displayName: loc.TARGET_SERVER_COLUMN },
+ { name: TableColumns.duration, displayName: loc.DURATION },
+ { name: TableColumns.startTime, displayName: loc.START_TIME },
+ { name: TableColumns.finishTime, displayName: loc.FINISH_TIME },
+ ],
+ })
+ .component();
+ this.disposables.push(
+ this._columnSortDropdown.onValueChanged(
+ async (e) => await this._populateMigrationTable()));
+
+ this._columnSortCheckbox = this.view.modelBuilder.checkBox()
+ .withProps({
+ label: loc.ASCENDING_LABEL,
+ checked: false,
+ CSSStyles: { 'margin-left': '15px' },
+ })
+ .component();
+ this.disposables.push(
+ this._columnSortCheckbox.onChanged(
+ async (e) => await this._populateMigrationTable()));
+
+ const columnSortContainer = this.view.modelBuilder.flexContainer()
+ .withItems([sortLabel, this._columnSortDropdown])
+ .withProps({
+ CSSStyles: {
+ 'justify-content': 'left',
+ 'align-items': 'center',
+ 'padding': '0px',
+ 'display': 'flex',
+ 'flex-direction': 'row',
+ },
+ }).component();
+ columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
+
+ const flexContainer = this.view.modelBuilder.flexContainer()
+ .withProps({
+ width: '100%',
+ CSSStyles: {
+ 'justify-content': 'left',
+ 'align-items': 'center',
+ 'padding': '0px',
+ 'display': 'flex',
+ 'flex-direction': 'row',
+ 'flex-flow': 'wrap',
+ },
+ }).component();
+
+ flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
+ flexContainer.addItem(this._searchBox, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
+ flexContainer.addItem(searchContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
+ flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
+
+ const container = this.view.modelBuilder.flexContainer()
+ .withProps({ width: '100%' })
+ .component();
+
+ container.addItem(flexContainer);
+ return container;
+ }
+
+ private _registerCommands(): void {
+ this.disposables.push(vscode.commands.registerCommand(
+ MenuCommands.Cutover,
+ async (migrationId: string) => {
+ try {
+ await this.statusBar.clearError();
+ const migration = this._migrations.find(
+ migration => migration.id === migrationId);
+
+ if (canRetryMigration(migration)) {
+ const cutoverDialogModel = new MigrationCutoverDialogModel(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
+ await cutoverDialogModel.fetchStatus();
+ const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
+ await dialog.initialize();
+ if (cutoverDialogModel.CutoverError) {
+ void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
+ }
+ } else {
+ await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
+ }
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.MIGRATION_CUTOVER_ERROR,
+ loc.MIGRATION_CUTOVER_ERROR,
+ e.message);
+
+ logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
+ }
+ }));
+
+ this.disposables.push(vscode.commands.registerCommand(
+ MenuCommands.ViewDatabase,
+ async (migrationId: string) => {
+ try {
+ await this.statusBar.clearError();
+ const migration = this._migrations.find(m => m.id === migrationId);
+ await this._openMigrationDetails(migration!);
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.OPEN_MIGRATION_DETAILS_ERROR,
+ loc.OPEN_MIGRATION_DETAILS_ERROR,
+ e.message);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
+ }
+ }));
+
+ this.disposables.push(vscode.commands.registerCommand(
+ MenuCommands.ViewTarget,
+ async (migrationId: string) => {
+ try {
+ const migration = this._migrations.find(migration => migration.id === migrationId);
+ const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
+ await vscode.env.openExternal(vscode.Uri.parse(url));
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.OPEN_MIGRATION_TARGET_ERROR,
+ loc.OPEN_MIGRATION_TARGET_ERROR,
+ e.message);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
+ }
+ }));
+
+ this.disposables.push(vscode.commands.registerCommand(
+ MenuCommands.ViewService,
+ async (migrationId: string) => {
+ try {
+ await this.statusBar.clearError();
+ const migration = this._migrations.find(migration => migration.id === migrationId);
+ const dialog = new SqlMigrationServiceDetailsDialog(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
+ await dialog.initialize();
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.OPEN_MIGRATION_SERVICE_ERROR,
+ loc.OPEN_MIGRATION_SERVICE_ERROR,
+ e.message);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
+ }
+ }));
+
+ this.disposables.push(vscode.commands.registerCommand(
+ MenuCommands.CopyMigration,
+ async (migrationId: string) => {
+ await this.statusBar.clearError();
+ const migration = this._migrations.find(migration => migration.id === migrationId);
+ const cutoverDialogModel = new MigrationCutoverDialogModel(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
+
+ try {
+ await cutoverDialogModel.fetchStatus();
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.MIGRATION_STATUS_REFRESH_ERROR,
+ loc.MIGRATION_STATUS_REFRESH_ERROR,
+ e.message);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
+ }
+
+ await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
+ await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
+ }));
+
+ this.disposables.push(vscode.commands.registerCommand(
+ MenuCommands.CancelMigration,
+ async (migrationId: string) => {
+ try {
+ await this.statusBar.clearError();
+ const migration = this._migrations.find(migration => migration.id === migrationId);
+ if (canCancelMigration(migration)) {
+ void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
+ if (v === loc.YES) {
+ const cutoverDialogModel = new MigrationCutoverDialogModel(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
+ await cutoverDialogModel.fetchStatus();
+ await cutoverDialogModel.cancelMigration();
+
+ if (cutoverDialogModel.CancelMigrationError) {
+ void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
+ }
+ }
+ });
+ } else {
+ await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
+ }
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.MIGRATION_CANCELLATION_ERROR,
+ loc.MIGRATION_CANCELLATION_ERROR,
+ e.message);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
+ }
+ }));
+
+ this.disposables.push(vscode.commands.registerCommand(
+ MenuCommands.RetryMigration,
+ async (migrationId: string) => {
+ try {
+ await this.statusBar.clearError();
+ const migration = this._migrations.find(migration => migration.id === migrationId);
+ if (canRetryMigration(migration)) {
+ let retryMigrationDialog = new RetryMigrationDialog(
+ this.context,
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!,
+ async () => await this.onDialogClosed());
+ await retryMigrationDialog.openDialog();
+ }
+ else {
+ await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
+ }
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.MIGRATION_RETRY_ERROR,
+ loc.MIGRATION_RETRY_ERROR,
+ e.message);
+ logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
+ }
+ }));
+ }
+
+ private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
+ const sortDir = ascending ? -1 : 1;
+ switch (columnName) {
+ case TableColumns.sourceDatabase:
+ migrations.sort(
+ (m1, m2) => this.stringCompare(
+ m1.properties.sourceDatabaseName,
+ m2.properties.sourceDatabaseName,
+ sortDir));
+ return;
+ case TableColumns.sourceServer:
+ migrations.sort(
+ (m1, m2) => this.stringCompare(
+ m1.properties.sourceServerName,
+ m2.properties.sourceServerName,
+ sortDir));
+ return;
+ case TableColumns.status:
+ migrations.sort(
+ (m1, m2) => this.stringCompare(
+ getMigrationStatusWithErrors(m1),
+ getMigrationStatusWithErrors(m2),
+ sortDir));
+ return;
+ case TableColumns.mode:
+ migrations.sort(
+ (m1, m2) => this.stringCompare(
+ getMigrationMode(m1),
+ getMigrationMode(m2),
+ sortDir));
+ return;
+ case TableColumns.targetType:
+ migrations.sort(
+ (m1, m2) => this.stringCompare(
+ getMigrationTargetType(m1),
+ getMigrationTargetType(m2),
+ sortDir));
+ return;
+ case TableColumns.targetDatabse:
+ migrations.sort(
+ (m1, m2) => this.stringCompare(
+ getResourceName(m1.id),
+ getResourceName(m2.id),
+ sortDir));
+ return;
+ case TableColumns.targetServer:
+ migrations.sort(
+ (m1, m2) => this.stringCompare(
+ getResourceName(m1.properties.scope),
+ getResourceName(m2.properties.scope),
+ sortDir));
+ return;
+ case TableColumns.duration:
+ migrations.sort((m1, m2) => {
+ if (!m1.properties.startedOn) {
+ return sortDir;
+ } else if (!m2.properties.startedOn) {
+ return -sortDir;
+ }
+ const m1_startedOn = new Date(m1.properties.startedOn);
+ const m2_startedOn = new Date(m2.properties.startedOn);
+ const m1_endedOn = new Date(m1.properties.endedOn ?? Date.now());
+ const m2_endedOn = new Date(m2.properties.endedOn ?? Date.now());
+ const m1_duration = m1_endedOn.getTime() - m1_startedOn.getTime();
+ const m2_duration = m2_endedOn.getTime() - m2_startedOn.getTime();
+ return m1_duration > m2_duration ? -sortDir : sortDir;
+ });
+ return;
+ case TableColumns.startTime:
+ migrations.sort(
+ (m1, m2) => this.dateCompare(
+ m1.properties.startedOn,
+ m2.properties.startedOn,
+ sortDir));
+ return;
+ case TableColumns.finishTime:
+ migrations.sort(
+ (m1, m2) => this.dateCompare(
+ m1.properties.endedOn,
+ m2.properties.endedOn,
+ sortDir));
+ return;
+ }
+ }
+
+ private async _populateMigrationTable(): Promise {
+ try {
+ this._filteredMigrations = filterMigrations(
+ this._migrations,
+ (this._statusDropdown.value).name,
+ this._searchBox.value!);
+
+ this._sortMigrations(
+ this._filteredMigrations,
+ (this._columnSortDropdown.value).name,
+ this._columnSortCheckbox.checked === true);
+
+ const data: any[] = this._filteredMigrations.map((migration, index) => {
+ return [
+ {
+ icon: IconPathHelper.sqlDatabaseLogo,
+ title: migration.properties.sourceDatabaseName ?? EmptySettingValue,
+ }, // sourceDatabase
+ migration.properties.sourceServerName ?? EmptySettingValue, // sourceServer
+ {
+ icon: getMigrationStatusImage(migration),
+ title: getMigrationStatusWithErrors(migration),
+ }, // statue
+ getMigrationMode(migration), // mode
+ getMigrationTargetType(migration), // targetType
+ getResourceName(migration.id), // targetDatabase
+ getResourceName(migration.properties.scope), // targetServer
+ getMigrationDuration(
+ migration.properties.startedOn,
+ migration.properties.endedOn), // duration
+ getMigrationTime(migration.properties.startedOn), // startTime
+ getMigrationTime(migration.properties.endedOn), // finishTime
+ {
+ title: '',
+ context: migration.id,
+ commands: this._getMenuCommands(migration), // context menu
+ },
+ ];
+ });
+
+ await this._statusTable.updateProperty('data', data);
+ } catch (e) {
+ await this.statusBar.showError(
+ loc.LOAD_MIGRATION_LIST_ERROR,
+ loc.LOAD_MIGRATION_LIST_ERROR,
+ e.message);
+ logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
+ }
+ }
+
+ private _createStatusTable(): azdata.TableComponent {
+ const headerCssStyles = undefined;
+ const rowCssStyles = undefined;
+
+ this._statusTable = this.view.modelBuilder.table()
+ .withProps({
+ ariaLabel: loc.MIGRATION_STATUS,
+ CSSStyles: { 'margin-left': '10px' },
+ data: [],
+ forceFitColumns: azdata.ColumnSizingMode.AutoFit,
+ height: '500px',
+ columns: [
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.SRC_DATABASE,
+ value: 'sourceDatabase',
+ width: 190,
+ type: azdata.ColumnType.hyperlink,
+ showText: true,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.SRC_SERVER,
+ value: 'sourceServer',
+ width: 190,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.STATUS_COLUMN,
+ value: 'status',
+ width: 120,
+ type: azdata.ColumnType.hyperlink,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.MIGRATION_MODE,
+ value: 'mode',
+ width: 55,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.AZURE_SQL_TARGET,
+ value: 'targetType',
+ width: 120,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.TARGET_DATABASE_COLUMN,
+ value: 'targetDatabase',
+ width: 125,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.TARGET_SERVER_COLUMN,
+ value: 'targetServer',
+ width: 125,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.DURATION,
+ value: 'duration',
+ width: 55,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.START_TIME,
+ value: 'startTime',
+ width: 115,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.FINISH_TIME,
+ value: 'finishTime',
+ width: 115,
+ type: azdata.ColumnType.text,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: '',
+ value: 'contextMenu',
+ width: 25,
+ type: azdata.ColumnType.contextMenu,
+ }
+ ]
+ }).component();
+
+ this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
+ const buttonState = rowState;
+ const migration = this._filteredMigrations[rowState.row];
+ switch (buttonState?.column) {
+ case 2:
+ const status = getMigrationStatus(migration);
+ const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
+ const errors = this.getMigrationErrors(migration!);
+
+ this.showDialogMessage(
+ loc.DATABASE_MIGRATION_STATUS_TITLE,
+ statusMessage,
+ errors);
+ break;
+ case 0:
+ await this._openMigrationDetails(migration);
+ break;
+ }
+ }));
+
+ return this._statusTable;
+ }
+
+ private _getMenuCommands(migration: DatabaseMigration): string[] {
+ const menuCommands: string[] = [];
+
+ if (getMigrationModeEnum(migration) === MigrationMode.ONLINE &&
+ canCutoverMigration(migration)) {
+ menuCommands.push(MenuCommands.Cutover);
+ }
+
+ menuCommands.push(...[
+ MenuCommands.ViewDatabase,
+ MenuCommands.ViewTarget,
+ MenuCommands.ViewService,
+ MenuCommands.CopyMigration]);
+
+ if (canCancelMigration(migration)) {
+ menuCommands.push(MenuCommands.CancelMigration);
+ }
+
+ return menuCommands;
+ }
+
+ private _statusDropdownValues: azdata.CategoryValue[] = [
+ { displayName: loc.STATUS_ALL, name: AdsMigrationStatus.ALL },
+ { displayName: loc.STATUS_ONGOING, name: AdsMigrationStatus.ONGOING },
+ { displayName: loc.STATUS_COMPLETING, name: AdsMigrationStatus.COMPLETING },
+ { displayName: loc.STATUS_SUCCEEDED, name: AdsMigrationStatus.SUCCEEDED },
+ { displayName: loc.STATUS_FAILED, name: AdsMigrationStatus.FAILED }
+ ];
+}
diff --git a/extensions/sql-migration/src/dashboard/migrationsTab.ts b/extensions/sql-migration/src/dashboard/migrationsTab.ts
new file mode 100644
index 0000000000..b56ef3712c
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/migrationsTab.ts
@@ -0,0 +1,148 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { AdsMigrationStatus, TabBase } from './tabBase';
+import { MigrationsListTab, MigrationsListTabId } from './migrationsListTab';
+import { DatabaseMigration } 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 { MigrationDetailsTableTab } from './migrationDetailsTableTab';
+import { DashboardStatusBar } from './sqlServerDashboard';
+
+export const MigrationsTabId = 'MigrationsTab';
+
+export class MigrationsTab extends TabBase {
+ private _tab!: azdata.DivContainer;
+ private _migrationsListTab!: MigrationsListTab;
+ private _migrationDetailsTab!: MigrationDetailsTabBase;
+ private _migrationDetailsFileShareTab!: MigrationDetailsTabBase;
+ private _migrationDetailsBlobTab!: MigrationDetailsTabBase;
+ private _migrationDetailsTableTab!: MigrationDetailsTabBase;
+ private _selectedTabId: string | undefined = undefined;
+
+ constructor() {
+ super();
+ this.title = loc.DESKTOP_MIGRATIONS_TAB_TITLE;
+ this.id = MigrationsTabId;
+ }
+
+ public onDialogClosed = async (): Promise =>
+ await this._migrationsListTab.onDialogClosed();
+
+ public async create(
+ context: vscode.ExtensionContext,
+ view: azdata.ModelView,
+ statusBar: DashboardStatusBar): Promise {
+
+ this.context = context;
+ this.view = view;
+ this.statusBar = statusBar;
+
+ await this.initialize(view);
+ await this._openTab(this._migrationsListTab);
+
+ return this;
+ }
+
+ public async refresh(): Promise {
+ switch (this._selectedTabId) {
+ case undefined:
+ case MigrationsListTabId:
+ return await this._migrationsListTab?.refresh();
+ default:
+ return await this._migrationDetailsTab?.refresh();
+ }
+ }
+
+ protected async initialize(view: azdata.ModelView): Promise {
+ this._tab = this.view.modelBuilder.divContainer()
+ .withLayout({ height: '100%' })
+ .withProps({
+ CSSStyles: {
+ 'margin': '0px',
+ 'padding': '0px',
+ 'width': '100%'
+ }
+ })
+ .component();
+
+ this._migrationsListTab = await new MigrationsListTab().create(
+ this.context,
+ this.view,
+ async (migration) => await this._openMigrationDetails(migration),
+ this.statusBar);
+ this.disposables.push(this._migrationsListTab);
+
+ this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create(
+ this.context,
+ this.view,
+ async () => await this._openMigrationsListTab(),
+ this.statusBar);
+ this.disposables.push(this._migrationDetailsBlobTab);
+
+ this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create(
+ this.context,
+ this.view,
+ async () => await this._openMigrationsListTab(),
+ this.statusBar);
+ this.disposables.push(this._migrationDetailsFileShareTab);
+
+ this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create(
+ this.context,
+ this.view,
+ async () => await this._openMigrationsListTab(),
+ this.statusBar);
+ this.disposables.push(this._migrationDetailsFileShareTab);
+
+ this.content = this._tab;
+ }
+
+ public async setMigrationFilter(filter: AdsMigrationStatus): Promise {
+ await this._migrationsListTab?.setMigrationFilter(filter);
+ await this._openTab(this._migrationsListTab);
+ await this._migrationsListTab?.setMigrationFilter(filter);
+ }
+
+ private 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;
+ break;
+ case FileStorageType.None:
+ this._migrationDetailsTab = this._migrationDetailsTableTab;
+ break;
+ }
+
+ await this._migrationDetailsTab.setMigrationContext(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration);
+
+ await this._openTab(this._migrationDetailsTab);
+ }
+
+ private async _openMigrationsListTab(): Promise {
+ await this.statusBar.clearError();
+ await this._openTab(this._migrationsListTab);
+ }
+
+ private async _openTab(tab: azdata.Tab): Promise {
+ if (tab.id === this._selectedTabId) {
+ return;
+ }
+
+ this._tab.clearItems();
+ this._tab.addItem(tab.content);
+ this._selectedTabId = tab.id;
+ }
+}
diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
index 20507936f0..304a2e2a1d 100644
--- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
+++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
@@ -5,793 +5,193 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
-import { logError, TelemetryViews } from '../telemtery';
import * as loc from '../constants/strings';
-import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
-import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
-import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
-import { filterMigrations } from '../api/utils';
-import * as styles from '../constants/styles';
-import * as nls from 'vscode-nls';
-import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
-import { DatabaseMigration } from '../api/azure';
-import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
-const localize = nls.loadMessageBundle();
+import { DashboardTab } from './dashboardTab';
+import { MigrationsTab, MigrationsTabId } from './migrationsTab';
+import { AdsMigrationStatus } from './tabBase';
-interface IActionMetadata {
- title?: string,
- description?: string,
- link?: string,
- iconPath?: azdata.ThemedIconPath,
- command?: string;
+export interface DashboardStatusBar {
+ showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise;
+ clearError: () => Promise;
+ errorTitle: string;
+ errorLabel: string;
+ errorDescription: string;
}
-const maxWidth = 800;
-const BUTTON_CSS = {
- 'font-size': '13px',
- 'line-height': '18px',
- 'margin': '4px 0',
- 'text-align': 'left',
-};
-
-interface StatusCard {
- container: azdata.DivContainer;
- count: azdata.TextComponent,
- textContainer?: azdata.FlexContainer,
- warningContainer?: azdata.FlexContainer,
- warningText?: azdata.TextComponent,
-}
-
-export class DashboardWidget {
+export class DashboardWidget implements DashboardStatusBar {
private _context: vscode.ExtensionContext;
- private _migrationStatusCardsContainer!: azdata.FlexContainer;
- private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
private _view!: azdata.ModelView;
- private _inProgressMigrationButton!: StatusCard;
- private _inProgressWarningMigrationButton!: StatusCard;
- private _allMigrationButton!: StatusCard;
- private _successfulMigrationButton!: StatusCard;
- private _failedMigrationButton!: StatusCard;
- private _completingMigrationButton!: StatusCard;
- private _selectServiceText!: azdata.TextComponent;
- private _serviceContextButton!: azdata.ButtonComponent;
- private _refreshButton!: azdata.ButtonComponent;
-
+ private _tabs!: azdata.TabbedPanelComponent;
+ private _statusInfoBox!: azdata.InfoBoxComponent;
+ private _dashboardTab!: DashboardTab;
+ private _migrationsTab!: MigrationsTab;
private _disposables: vscode.Disposable[] = [];
- private isRefreshing: boolean = false;
-
- public onDialogClosed = async (): Promise => {
- const label = await getSelectedServiceStatus();
- this._serviceContextButton.label = label;
- this._serviceContextButton.title = label;
- await this.refreshMigrations();
- };
constructor(context: vscode.ExtensionContext) {
this._context = context;
}
+ public errorTitle: string = '';
+ public errorLabel: string = '';
+ public errorDescription: string = '';
+
+ public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise {
+ this.errorTitle = errorTitle;
+ this.errorLabel = errorLabel;
+ this.errorDescription = errorDescription;
+ this._statusInfoBox.style = 'error';
+ this._statusInfoBox.text = errorTitle;
+ await this._updateStatusDisplay(this._statusInfoBox, true);
+ }
+
+ public async clearError(): Promise {
+ await this._updateStatusDisplay(this._statusInfoBox, false);
+ this.errorTitle = '';
+ this.errorLabel = '';
+ this.errorDescription = '';
+ this._statusInfoBox.style = 'success';
+ this._statusInfoBox.text = '';
+ }
+
public register(): void {
- azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
+ azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => {
this._view = view;
-
- const container = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'column',
- width: '100%',
- height: '100%'
- }).component();
-
- const header = this.createHeader(view);
- // Files need to have the vscode-file scheme to be loaded by ADS
- const watermarkUri = vscode.Uri
- .file(IconPathHelper.migrationDashboardHeaderBackground.light)
- .with({ scheme: 'vscode-file' });
-
- container.addItem(header, {
- CSSStyles: {
- 'background-image': `
- url(${watermarkUri}),
- linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)
- `,
- 'background-repeat': 'no-repeat',
- 'background-position': '91.06% 100%',
- 'margin-bottom': '20px'
- }
- });
-
- const tasksContainer = await this.createTasks(view);
- header.addItem(tasksContainer, {
- CSSStyles: {
- 'width': `${maxWidth}px`,
- 'margin': '24px'
- }
- });
- container.addItem(await this.createFooter(view), {
- CSSStyles: {
- 'margin': '0 24px'
- }
- });
this._disposables.push(
this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
- await view.initializeModel(container);
- await this.refreshMigrations();
+ const openMigrationFcn = async (filter: AdsMigrationStatus): Promise => {
+ this._tabs.selectTab(MigrationsTabId);
+ await this._migrationsTab.setMigrationFilter(filter);
+ };
+
+ this._dashboardTab = await new DashboardTab().create(
+ view,
+ async (filter: AdsMigrationStatus) => await openMigrationFcn(filter),
+ this);
+ this._disposables.push(this._dashboardTab);
+
+ this._migrationsTab = await new MigrationsTab().create(
+ this._context,
+ view,
+ this);
+ this._disposables.push(this._migrationsTab);
+
+ this._tabs = view.modelBuilder.tabbedPanel()
+ .withTabs([this._dashboardTab, this._migrationsTab])
+ .withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal })
+ .withProps({
+ CSSStyles: {
+ 'margin': '0px',
+ 'padding': '0px',
+ 'width': '100%'
+ }
+ })
+ .component();
+
+ this._disposables.push(
+ this._tabs.onTabChanged(
+ async id => {
+ await this.clearError();
+ await this.onDialogClosed();
+ }));
+
+ this._statusInfoBox = view.modelBuilder.infoBox()
+ .withProps({
+ style: 'error',
+ text: '',
+ announceText: true,
+ isClickable: true,
+ display: 'none',
+ CSSStyles: { 'font-size': '14px' },
+ }).component();
+
+ this._disposables.push(
+ this._statusInfoBox.onDidClick(
+ async e => await this.openErrorDialog()));
+
+ const flexContainer = view.modelBuilder.flexContainer()
+ .withLayout({ flexFlow: 'column' })
+ .withItems([this._statusInfoBox, this._tabs])
+ .component();
+ await view.initializeModel(flexContainer);
+
+ await this.refresh();
});
}
- private createHeader(view: azdata.ModelView): azdata.FlexContainer {
- const header = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'column',
- width: maxWidth,
- }).component();
- const titleComponent = view.modelBuilder.text().withProps({
- value: loc.DASHBOARD_TITLE,
- width: '750px',
- CSSStyles: {
- ...styles.DASHBOARD_TITLE_CSS
- }
- }).component();
-
- const descriptionComponent = view.modelBuilder.text().withProps({
- value: loc.DASHBOARD_DESCRIPTION,
- CSSStyles: {
- ...styles.NOTE_CSS
- }
- }).component();
- header.addItems([titleComponent, descriptionComponent], {
- CSSStyles: {
- 'width': `${maxWidth}px`,
- 'padding-left': '24px'
- }
- });
- return header;
+ public async refresh(): Promise {
+ void this._migrationsTab.refresh();
+ await this._dashboardTab.refresh();
}
- private async createTasks(view: azdata.ModelView): Promise {
- const tasksContainer = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'row',
- width: '100%',
- }).component();
-
- const migrateButtonMetadata: IActionMetadata = {
- title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
- description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
- iconPath: IconPathHelper.sqlMigrationLogo,
- command: 'sqlmigration.start'
- };
-
- const preRequisiteListTitle = view.modelBuilder.text().withProps({
- value: loc.PRE_REQ_TITLE,
- CSSStyles: {
- ...styles.BODY_CSS,
- 'margin': '0px',
- }
- }).component();
-
- const migrateButton = this.createTaskButton(view, migrateButtonMetadata);
-
- const preRequisiteListElement = view.modelBuilder.text().withProps({
- value: [
- loc.PRE_REQ_1,
- loc.PRE_REQ_2,
- loc.PRE_REQ_3
- ],
- CSSStyles: {
- ...styles.SMALL_NOTE_CSS,
- 'padding-left': '12px',
- 'margin': '-0.5em 0px',
- }
- }).component();
-
- const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink().withProps({
- label: loc.LEARN_MORE,
- ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
- url: 'https://aka.ms/azuresqlmigrationextension',
- }).component();
-
- const preReqContainer = view.modelBuilder.flexContainer().withItems([
- preRequisiteListTitle,
- preRequisiteListElement,
- preRequisiteLearnMoreLink
- ]).withLayout({
- flexFlow: 'column'
- }).component();
-
- tasksContainer.addItem(migrateButton, {});
- tasksContainer.addItems([preReqContainer], {
- CSSStyles: {
- 'margin-left': '20px'
- }
- });
- return tasksContainer;
+ public async onDialogClosed(): Promise {
+ await this._dashboardTab.onDialogClosed();
+ await this._migrationsTab.onDialogClosed();
}
- private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
- const maxHeight: number = 84;
- const maxWidth: number = 236;
- const buttonContainer = view.modelBuilder.button().withProps({
- buttonType: azdata.ButtonType.Informational,
- description: taskMetaData.description,
- height: maxHeight,
- iconHeight: 32,
- iconPath: taskMetaData.iconPath,
- iconWidth: 32,
- label: taskMetaData.title,
- title: taskMetaData.title,
- width: maxWidth,
- CSSStyles: {
- 'border': '1px solid',
- 'display': 'flex',
- 'flex-direction': 'column',
- 'justify-content': 'flex-start',
- 'border-radius': '4px',
- 'transition': 'all .5s ease',
- }
- }).component();
- this._disposables.push(
- buttonContainer.onDidClick(async () => {
- if (taskMetaData.command) {
- await vscode.commands.executeCommand(taskMetaData.command);
- }
- }));
- return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
- }
+ private _errorDialogIsOpen: boolean = false;
- public async refreshMigrations(): Promise {
- if (this.isRefreshing) {
+ protected async openErrorDialog(): Promise {
+ if (this._errorDialogIsOpen) {
return;
}
- this.isRefreshing = true;
- this._migrationStatusCardLoadingContainer.loading = true;
- let migrations: DatabaseMigration[] = [];
try {
- migrations = await getCurrentMigrations();
- } catch (e) {
- logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
- void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
- }
+ const tab = azdata.window.createTab(this.errorTitle);
+ tab.registerContent(async (view) => {
+ const flex = view.modelBuilder.flexContainer()
+ .withItems([
+ view.modelBuilder.text()
+ .withProps({ value: this.errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
+ .component(),
+ view.modelBuilder.inputBox()
+ .withProps({
+ value: this.errorDescription,
+ readOnly: true,
+ multiline: true,
+ inputType: 'text',
+ rows: 20,
+ CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
+ })
+ .component()
+ ])
+ .withLayout({
+ flexFlow: 'column',
+ width: 420,
+ })
+ .withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
+ .component();
- const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
- let warningCount = 0;
- for (let i = 0; i < inProgressMigrations.length; i++) {
- if (inProgressMigrations[i].properties.migrationFailureError?.message ||
- inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
- inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
- warningCount += 1;
- }
- }
- if (warningCount > 0) {
- this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
- this._inProgressMigrationButton.container.display = 'none';
- this._inProgressWarningMigrationButton.container.display = '';
- } else {
- this._inProgressMigrationButton.container.display = '';
- this._inProgressWarningMigrationButton.container.display = 'none';
- }
+ await view.initializeModel(flex);
+ });
- this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
- this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
-
- this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
- this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
- this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
- this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
-
- await this._updateSummaryStatus();
- this.isRefreshing = false;
- this._migrationStatusCardLoadingContainer.loading = false;
- }
-
- private _updateStatusCard(
- migrations: DatabaseMigration[],
- card: StatusCard,
- status: AdsMigrationStatus,
- show?: boolean): void {
- const list = filterMigrations(migrations, status);
- const count = list?.length || 0;
- card.container.display = count > 0 || show ? '' : 'none';
- card.count.value = count.toString();
- }
- private createStatusCard(
- cardIconPath: IconPath,
- cardTitle: string,
- hasSubtext: boolean = false
- ): StatusCard {
- const buttonWidth = '400px';
- const buttonHeight = hasSubtext ? '70px' : '50px';
- const statusCard = this._view.modelBuilder.flexContainer()
- .withProps({
- CSSStyles: {
- 'width': buttonWidth,
- 'height': buttonHeight,
- 'align-items': 'center',
- }
- }).component();
-
- const statusIcon = this._view.modelBuilder.image()
- .withProps({
- iconPath: cardIconPath!.light,
- iconHeight: 24,
- iconWidth: 24,
- height: 32,
- CSSStyles: { 'margin': '0 8px' }
- }).component();
-
- const textContainer = this._view.modelBuilder.flexContainer()
- .withLayout({ flexFlow: 'column' })
- .component();
-
- const cardTitleText = this._view.modelBuilder.text()
- .withProps({ value: cardTitle })
- .withProps({
- CSSStyles: {
- ...styles.SECTION_HEADER_CSS,
- 'width': '240px',
- }
- }).component();
- textContainer.addItem(cardTitleText);
-
- const cardCount = this._view.modelBuilder.text().withProps({
- value: '0',
- CSSStyles: {
- ...styles.BIG_NUMBER_CSS,
- 'margin': '0 0 0 8px',
- 'text-align': 'center',
- }
- }).component();
-
- let warningContainer;
- let warningText;
- if (hasSubtext) {
- const warningIcon = this._view.modelBuilder.image()
- .withProps({
- iconPath: IconPathHelper.warning,
- iconWidth: 12,
- iconHeight: 12,
- width: 12,
- height: 18,
- }).component();
-
- const warningDescription = '';
- warningText = this._view.modelBuilder.text().withProps({ value: warningDescription })
- .withProps({
- CSSStyles: {
- ...styles.BODY_CSS,
- 'padding-left': '8px',
+ const dialog = azdata.window.createModelViewDialog(
+ this.errorTitle,
+ 'errorDialog',
+ 450,
+ 'flyout');
+ dialog.content = [tab];
+ dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
+ dialog.okButton.focused = true;
+ dialog.cancelButton.label = loc.CLOSE;
+ this._disposables.push(
+ dialog.onClosed(async e => {
+ if (e === 'ok') {
+ await this.clearError();
}
- }).component();
+ this._errorDialogIsOpen = false;
+ }));
- warningContainer = this._view.modelBuilder.flexContainer()
- .withItems(
- [warningIcon, warningText],
- { flex: '0 0 auto' })
- .withProps({
- CSSStyles: { 'align-items': 'center' }
- }).component();
-
- textContainer.addItem(warningContainer);
+ azdata.window.openDialog(dialog);
+ } catch (error) {
+ this._errorDialogIsOpen = false;
}
-
- statusCard.addItems([
- statusIcon,
- textContainer,
- cardCount,
- ]);
-
- const compositeButton = this._view.modelBuilder.divContainer()
- .withItems([statusCard])
- .withProps({
- ariaRole: 'button',
- ariaLabel: loc.SHOW_STATUS,
- clickable: true,
- CSSStyles: {
- 'height': buttonHeight,
- 'margin-bottom': '16px',
- 'border': '1px solid',
- 'display': 'flex',
- 'flex-direction': 'column',
- 'justify-content': 'flex-start',
- 'border-radius': '4px',
- 'transition': 'all .5s ease',
- }
- }).component();
- return {
- container: compositeButton,
- count: cardCount,
- textContainer: textContainer,
- warningContainer: warningContainer,
- warningText: warningText
- };
}
- private async createFooter(view: azdata.ModelView): Promise {
- const footerContainer = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'row',
- width: maxWidth,
- justifyContent: 'flex-start'
- }).component();
- const statusContainer = await this.createMigrationStatusContainer(view);
- const videoLinksContainer = this.createVideoLinks(view);
- footerContainer.addItem(statusContainer);
- footerContainer.addItem(videoLinksContainer, {
- CSSStyles: {
- 'padding-left': '8px',
- }
- });
-
- return footerContainer;
- }
-
- private async createMigrationStatusContainer(view: azdata.ModelView): Promise {
- const statusContainer = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'column',
- width: '400px',
- height: '385px',
- justifyContent: 'flex-start',
- }).withProps({
- CSSStyles: {
- 'border': '1px solid rgba(0, 0, 0, 0.1)',
- 'padding': '10px',
- }
- }).component();
-
- const statusContainerTitle = view.modelBuilder.text()
- .withProps({
- value: loc.DATABASE_MIGRATION_STATUS,
- width: '100%',
- CSSStyles: { ...styles.SECTION_HEADER_CSS }
- }).component();
-
- this._refreshButton = view.modelBuilder.button()
- .withProps({
- label: loc.REFRESH,
- iconPath: IconPathHelper.refresh,
- iconHeight: 16,
- iconWidth: 16,
- width: 70,
- CSSStyles: { 'float': 'right' }
- }).component();
-
- const statusHeadingContainer = view.modelBuilder.flexContainer()
- .withItems([
- statusContainerTitle,
- this._refreshButton,
- ]).withLayout({
- alignContent: 'center',
- alignItems: 'center',
- flexFlow: 'row',
- }).component();
-
- this._disposables.push(
- this._refreshButton.onDidClick(async (e) => {
- this._refreshButton.enabled = false;
- await this.refreshMigrations();
- this._refreshButton.enabled = true;
- }));
-
- const buttonContainer = view.modelBuilder.flexContainer()
- .withProps({
- CSSStyles: {
- 'justify-content': 'left',
- 'align-iems': 'center',
- },
- })
- .component();
-
- buttonContainer.addItem(
- await this.createServiceSelector(this._view));
-
- this._selectServiceText = view.modelBuilder.text()
- .withProps({
- value: loc.SELECT_SERVICE_MESSAGE,
- CSSStyles: {
- 'font-size': '12px',
- 'margin': '10px',
- 'font-weight': '350',
- 'text-align': 'center',
- 'display': 'none'
- }
- }).component();
-
- const header = view.modelBuilder.flexContainer()
- .withItems([statusHeadingContainer, buttonContainer])
- .withLayout({ flexFlow: 'column', })
- .component();
-
- this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
- .withLayout({
- flexFlow: 'column',
- height: '272px',
- })
- .withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
- .component();
-
- await this._updateSummaryStatus();
-
- // in progress
- this._inProgressMigrationButton = this.createStatusCard(
- IconPathHelper.inProgressMigration,
- loc.MIGRATION_IN_PROGRESS);
- this._disposables.push(
- this._inProgressMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(
- this._context,
- AdsMigrationStatus.ONGOING,
- this.onDialogClosed);
- await dialog.initialize();
- }));
-
- this._migrationStatusCardsContainer.addItem(
- this._inProgressMigrationButton.container,
- { flex: '0 0 auto' });
-
- // in progress warning
- this._inProgressWarningMigrationButton = this.createStatusCard(
- IconPathHelper.inProgressMigration,
- loc.MIGRATION_IN_PROGRESS,
- true);
- this._disposables.push(
- this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(
- this._context,
- AdsMigrationStatus.ONGOING,
- this.onDialogClosed);
- await dialog.initialize();
- }));
-
- this._migrationStatusCardsContainer.addItem(
- this._inProgressWarningMigrationButton.container,
- { flex: '0 0 auto' });
-
- // successful
- this._successfulMigrationButton = this.createStatusCard(
- IconPathHelper.completedMigration,
- loc.MIGRATION_COMPLETED);
- this._disposables.push(
- this._successfulMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(
- this._context,
- AdsMigrationStatus.SUCCEEDED,
- this.onDialogClosed);
- await dialog.initialize();
- }));
- this._migrationStatusCardsContainer.addItem(
- this._successfulMigrationButton.container,
- { flex: '0 0 auto' });
-
- // completing
- this._completingMigrationButton = this.createStatusCard(
- IconPathHelper.completingCutover,
- loc.MIGRATION_CUTOVER_CARD);
- this._disposables.push(
- this._completingMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(
- this._context,
- AdsMigrationStatus.COMPLETING,
- this.onDialogClosed);
- await dialog.initialize();
- }));
- this._migrationStatusCardsContainer.addItem(
- this._completingMigrationButton.container,
- { flex: '0 0 auto' });
-
- // failed
- this._failedMigrationButton = this.createStatusCard(
- IconPathHelper.error,
- loc.MIGRATION_FAILED);
- this._disposables.push(
- this._failedMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(
- this._context,
- AdsMigrationStatus.FAILED,
- this.onDialogClosed);
- await dialog.initialize();
- }));
- this._migrationStatusCardsContainer.addItem(
- this._failedMigrationButton.container,
- { flex: '0 0 auto' });
-
- // all migrations
- this._allMigrationButton = this.createStatusCard(
- IconPathHelper.view,
- loc.VIEW_ALL);
- this._disposables.push(
- this._allMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(
- this._context,
- AdsMigrationStatus.ALL,
- this.onDialogClosed);
- await dialog.initialize();
- }));
- this._migrationStatusCardsContainer.addItem(
- this._allMigrationButton.container,
- { flex: '0 0 auto' });
-
- this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
- .withItem(this._migrationStatusCardsContainer)
- .component();
- statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
- statusContainer.addItem(this._selectServiceText, {});
- statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
- return statusContainer;
- }
-
- private async _updateSummaryStatus(): Promise {
- const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
- const isContextValid = isServiceContextValid(serviceContext);
- await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
- await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
- this._refreshButton.enabled = isContextValid;
- }
-
- private async createServiceSelector(view: azdata.ModelView): Promise {
- const serviceContextLabel = await getSelectedServiceStatus();
- this._serviceContextButton = view.modelBuilder.button()
- .withProps({
- iconPath: IconPathHelper.sqlMigrationService,
- iconHeight: 22,
- iconWidth: 22,
- label: serviceContextLabel,
- title: serviceContextLabel,
- description: loc.MIGRATION_SERVICE_DESCRIPTION,
- buttonType: azdata.ButtonType.Informational,
- width: 375,
- CSSStyles: { ...BUTTON_CSS },
- })
- .component();
-
- this._disposables.push(
- this._serviceContextButton.onDidClick(async () => {
- const dialog = new SelectMigrationServiceDialog(this.onDialogClosed);
- await dialog.initialize();
- }));
-
- return this._serviceContextButton;
- }
-
- private createVideoLinks(view: azdata.ModelView): azdata.Component {
- const linksContainer = view.modelBuilder.flexContainer()
- .withLayout({
- flexFlow: 'column',
- width: '440px',
- height: '385px',
- justifyContent: 'flex-start',
- }).withProps({
- CSSStyles: {
- 'border': '1px solid rgba(0, 0, 0, 0.1)',
- 'padding': '10px',
- 'overflow': 'scroll',
- }
- }).component();
- const titleComponent = view.modelBuilder.text().withProps({
- value: loc.HELP_TITLE,
- CSSStyles: {
- ...styles.SECTION_HEADER_CSS
- }
- }).component();
-
- linksContainer.addItems([titleComponent], {
- CSSStyles: {
- 'margin-bottom': '16px'
- }
- });
-
- const links = [
- {
- title: localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio'),
- description: localize('sql.migration.dashboard.help.description.migrateUsingADS', 'The Azure SQL Migration extension for Azure Data Studio provides capabilities to assess, get right-sized Azure recommendations and migrate SQL Server databases to Azure.'),
- link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
- },
- {
- title: localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)'),
- description: localize('sql.migration.dashboard.help.description.mi', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises or Azure Virtual Machines) to Azure SQL Managed Instance with minimal downtime.'),
- link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
- },
- {
- title: localize('sql.migration.dashboard.help.link.vm', 'Tutorial: Migrate to SQL Server on Azure Virtual Machines (online)'),
- description: localize('sql.migration.dashboard.help.description.vm', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises) to SQL Server on Azure Virtual Machines with minimal downtime.'),
- link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
- },
- {
- title: localize('sql.migration.dashboard.help.link.dmsGuide', 'Azure Database Migration Guides'),
- description: localize('sql.migration.dashboard.help.description.dmsGuide', 'A hub of migration articles that provides step-by-step guidance for migrating and modernizing your data assets in Azure.'),
- link: 'https://docs.microsoft.com/data-migration/'
- },
- ];
-
- linksContainer.addItems(links.map(l => this.createLink(view, l)), {});
-
- const videoLinks: IActionMetadata[] = [];
- const videosContainer = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'row',
- width: maxWidth,
- }).component();
- videosContainer.addItems(videoLinks.map(l => this.createVideoLink(view, l)), {});
- linksContainer.addItem(videosContainer);
-
- return linksContainer;
- }
-
- private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
- const maxWidth = 400;
- const labelsContainer = view.modelBuilder.flexContainer().withProps({
- CSSStyles: {
- 'flex-direction': 'column',
- 'width': `${maxWidth}px`,
- 'justify-content': 'flex-start',
- 'margin-bottom': '12px'
- }
- }).component();
- const linkContainer = view.modelBuilder.flexContainer().withProps({
- CSSStyles: {
- 'flex-direction': 'row',
- 'width': `${maxWidth}px`,
- 'justify-content': 'flex-start',
- 'margin-bottom': '4px'
- }
-
- }).component();
- const descriptionComponent = view.modelBuilder.text().withProps({
- value: linkMetaData.description,
- width: maxWidth,
- CSSStyles: {
- ...styles.NOTE_CSS
- }
- }).component();
- const linkComponent = view.modelBuilder.hyperlink().withProps({
- label: linkMetaData.title!,
- url: linkMetaData.link!,
- showLinkIcon: true,
- CSSStyles: {
- ...styles.BODY_CSS
- }
- }).component();
- linkContainer.addItem(linkComponent);
- labelsContainer.addItems([linkContainer, descriptionComponent]);
- return labelsContainer;
- }
-
- private createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
- const maxWidth = 150;
- const videosContainer = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'column',
- width: maxWidth,
- justifyContent: 'flex-start'
- }).component();
- const video1Container = view.modelBuilder.divContainer().withProps({
- clickable: true,
- width: maxWidth,
- height: '100px'
- }).component();
- const descriptionComponent = view.modelBuilder.text().withProps({
- value: linkMetaData.description,
- width: maxWidth,
- height: '50px',
- CSSStyles: {
- ...styles.BODY_CSS
- }
- }).component();
- this._disposables.push(
- video1Container.onDidClick(async () => {
- if (linkMetaData.link) {
- await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
- }
- }));
- videosContainer.addItem(video1Container, {
- CSSStyles: {
- 'background-image': `url(${vscode.Uri.file(linkMetaData.iconPath?.light)})`,
- 'background-repeat': 'no-repeat',
- 'background-position': 'top',
- 'width': `${maxWidth}px`,
- 'height': '104px',
- 'background-size': `${maxWidth}px 120px`
- }
- });
- videosContainer.addItem(descriptionComponent);
- return videosContainer;
+ private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise {
+ await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
}
}
diff --git a/extensions/sql-migration/src/dashboard/tabBase.ts b/extensions/sql-migration/src/dashboard/tabBase.ts
new file mode 100644
index 0000000000..2d11c09bc4
--- /dev/null
+++ b/extensions/sql-migration/src/dashboard/tabBase.ts
@@ -0,0 +1,228 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { IconPathHelper } from '../constants/iconPathHelper';
+import { EOL } from 'os';
+import { DatabaseMigration } from '../api/azure';
+import { DashboardStatusBar } from './sqlServerDashboard';
+import { getSelectedServiceStatus } from '../models/migrationLocalStorage';
+import { util } from 'webpack';
+
+export const SqlMigrationExtensionId = 'microsoft.sql-migration';
+export const EmptySettingValue = '-';
+
+export enum AdsMigrationStatus {
+ ALL = 'all',
+ ONGOING = 'ongoing',
+ SUCCEEDED = 'succeeded',
+ FAILED = 'failed',
+ COMPLETING = 'completing'
+}
+
+export const MenuCommands = {
+ Cutover: 'sqlmigration.cutover',
+ ViewDatabase: 'sqlmigration.view.database',
+ ViewTarget: 'sqlmigration.view.target',
+ ViewService: 'sqlmigration.view.service',
+ CopyMigration: 'sqlmigration.copy.migration',
+ CancelMigration: 'sqlmigration.cancel.migration',
+ RetryMigration: 'sqlmigration.retry.migration',
+ StartMigration: 'sqlmigration.start',
+ IssueReporter: 'workbench.action.openIssueReporter',
+};
+
+export abstract class TabBase implements azdata.Tab, vscode.Disposable {
+ public content!: azdata.Component;
+ public title: string = '';
+ public id!: string;
+ public icon!: azdata.IconPath | undefined;
+
+ protected context!: vscode.ExtensionContext;
+ protected view!: azdata.ModelView;
+ protected disposables: vscode.Disposable[] = [];
+ protected isRefreshing: boolean = false;
+ protected openMigrationFcn!: (status: AdsMigrationStatus) => Promise;
+ protected statusBar!: DashboardStatusBar;
+
+ protected abstract initialize(view: azdata.ModelView): Promise;
+
+ public abstract refresh(): Promise;
+
+ dispose() {
+ this.disposables.forEach(
+ d => { try { d.dispose(); } catch { } });
+ }
+
+ protected numberCompare(number1: number | undefined, number2: number | undefined, sortDir: number): number {
+ if (!number1) {
+ return sortDir;
+ } else if (!number2) {
+ return -sortDir;
+ }
+ return util.comparators.compareNumbers(number1, number2) * -sortDir;
+ }
+
+ protected stringCompare(string1: string | undefined, string2: string | undefined, sortDir: number): number {
+ if (!string1) {
+ return sortDir;
+ } else if (!string2) {
+ return -sortDir;
+ }
+ return string1.localeCompare(string2) * -sortDir;
+ }
+
+ protected dateCompare(stringDate1: string | undefined, stringDate2: string | undefined, sortDir: number): number {
+ if (!stringDate1) {
+ return sortDir;
+ } else if (!stringDate2) {
+ return -sortDir;
+ }
+ return new Date(stringDate1) > new Date(stringDate2) ? -sortDir : sortDir;
+ }
+
+ protected async updateServiceContext(button: azdata.ButtonComponent): Promise {
+ const label = await getSelectedServiceStatus();
+ if (button.label !== label ||
+ button.title !== label) {
+
+ button.label = label;
+ button.title = label;
+
+ await this.refresh();
+ }
+ }
+
+ protected createNewMigrationButton(): azdata.ButtonComponent {
+ const newMigrationButton = this.view.modelBuilder.button()
+ .withProps({
+ buttonType: azdata.ButtonType.Normal,
+ label: loc.DESKTOP_MIGRATION_BUTTON_LABEL,
+ description: loc.DESKTOP_MIGRATION_BUTTON_DESCRIPTION,
+ height: 24,
+ iconHeight: 24,
+ iconWidth: 24,
+ iconPath: IconPathHelper.addNew,
+ }).component();
+ this.disposables.push(
+ newMigrationButton.onDidClick(async () => {
+ const actionId = MenuCommands.StartMigration;
+ const args = {
+ extensionId: SqlMigrationExtensionId,
+ issueTitle: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
+ };
+ return await vscode.commands.executeCommand(actionId, args);
+ }));
+ return newMigrationButton;
+ }
+
+ protected createNewSupportRequestButton(): azdata.ButtonComponent {
+ const newSupportRequestButton = this.view.modelBuilder.button()
+ .withProps({
+ buttonType: azdata.ButtonType.Normal,
+ label: loc.DESKTOP_SUPPORT_BUTTON_LABEL,
+ description: loc.DESKTOP_SUPPORT_BUTTON_DESCRIPTION,
+ height: 24,
+ iconHeight: 24,
+ iconWidth: 24,
+ iconPath: IconPathHelper.newSupportRequest,
+ }).component();
+ this.disposables.push(
+ newSupportRequestButton.onDidClick(async () => {
+ await vscode.env.openExternal(vscode.Uri.parse(
+ `https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
+ }));
+ return newSupportRequestButton;
+ }
+
+ protected createFeedbackButton(): azdata.ButtonComponent {
+ const feedbackButton = this.view.modelBuilder.button()
+ .withProps({
+ buttonType: azdata.ButtonType.Normal,
+ label: loc.DESKTOP_FEEDBACK_BUTTON_LABEL,
+ description: loc.DESKTOP_FEEDBACK_BUTTON_DESCRIPTION,
+ height: 24,
+ iconHeight: 24,
+ iconWidth: 24,
+ iconPath: IconPathHelper.sendFeedback,
+ }).component();
+ this.disposables.push(
+ feedbackButton.onDidClick(async () => {
+ const actionId = MenuCommands.IssueReporter;
+ const args = {
+ extensionId: SqlMigrationExtensionId,
+ issueTitle: loc.FEEDBACK_ISSUE_TITLE,
+ };
+ return await vscode.commands.executeCommand(actionId, args);
+ }));
+ return feedbackButton;
+ }
+
+ protected getMigrationErrors(migration: DatabaseMigration): string {
+ const errors = [];
+ errors.push(migration.properties.provisioningError);
+ errors.push(migration.properties.migrationFailureError?.message);
+ errors.push(...migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
+ errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason);
+
+ // remove undefined and duplicate error entries
+ return errors
+ .filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
+ .join(EOL);
+ }
+
+ protected showDialogMessage(
+ title: string,
+ statusMessage: string,
+ errorMessage: string,
+ ): void {
+ const tab = azdata.window.createTab(title);
+ tab.registerContent(async (view) => {
+ const flex = view.modelBuilder.flexContainer()
+ .withItems([
+ view.modelBuilder.text()
+ .withProps({ value: statusMessage })
+ .component(),
+ ])
+ .withLayout({
+ flexFlow: 'column',
+ width: 420,
+ })
+ .withProps({ CSSStyles: { 'margin': '0 15px' } })
+ .component();
+
+ if (errorMessage.length > 0) {
+ flex.addItem(
+ view.modelBuilder.inputBox()
+ .withProps({
+ value: errorMessage,
+ readOnly: true,
+ multiline: true,
+ inputType: 'text',
+ height: 100,
+ CSSStyles: { 'overflow': 'hidden auto' },
+ })
+ .component()
+ );
+ }
+
+ await view.initializeModel(flex);
+ });
+
+ const dialog = azdata.window.createModelViewDialog(
+ title,
+ 'messageDialog',
+ 450,
+ 'normal');
+ dialog.content = [tab];
+ dialog.okButton.hidden = true;
+ dialog.cancelButton.focused = true;
+ dialog.cancelButton.label = loc.CLOSE;
+
+ azdata.window.openDialog(dialog);
+ }
+}
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
index bfc91795e9..25c5448b78 100644
--- a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
@@ -11,7 +11,8 @@ import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure'
import { IconPathHelper } from '../../constants/iconPathHelper';
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
import * as styles from '../../constants/styles';
-import { isBlobMigration } from '../../constants/helper';
+import { getMigrationTargetTypeEnum, isBlobMigration } from '../../constants/helper';
+import { MigrationTargetType, ServiceTier } from '../../models/stateMachine';
export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -32,7 +33,7 @@ export class ConfirmCutoverDialog {
}).component();
const sourceDatabaseText = view.modelBuilder.text().withProps({
- value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
+ value: this.migrationCutoverModel.migration.properties.sourceDatabaseName,
CSSStyles: {
...styles.SMALL_NOTE_CSS,
'margin': '4px 0px 8px'
@@ -53,7 +54,7 @@ export class ConfirmCutoverDialog {
}
}).component();
- const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
+ const fileContainer = isBlobMigration(this.migrationCutoverModel.migration)
? this.createBlobFileContainer()
: this.createNetworkShareFileContainer();
@@ -76,13 +77,13 @@ export class ConfirmCutoverDialog {
}).component();
let infoDisplay = 'none';
- if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
+ if (getMigrationTargetTypeEnum(this.migrationCutoverModel.migration) === MigrationTargetType.SQLMI) {
const targetInstance = await getMigrationTargetInstance(
- this.migrationCutoverModel._serviceConstext.azureAccount!,
- this.migrationCutoverModel._serviceConstext.subscription!,
- this.migrationCutoverModel._migration);
+ this.migrationCutoverModel.serviceConstext.azureAccount!,
+ this.migrationCutoverModel.serviceConstext.subscription!,
+ this.migrationCutoverModel.migration);
- if ((targetInstance)?.sku?.tier === 'BusinessCritical') {
+ if ((targetInstance)?.sku?.tier === ServiceTier.BusinessCritical) {
infoDisplay = 'inline';
}
}
@@ -116,7 +117,7 @@ export class ConfirmCutoverDialog {
await this.migrationCutoverModel.startCutover();
void vscode.window.showInformationMessage(
constants.CUTOVER_IN_PROGRESS(
- this.migrationCutoverModel._migration.properties.sourceDatabaseName));
+ this.migrationCutoverModel.migration.properties.sourceDatabaseName));
}));
const formBuilder = view.modelBuilder.formContainer().withFormItems(
@@ -163,7 +164,7 @@ export class ConfirmCutoverDialog {
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
- text: e.toString()
+ text: e.message
};
} finally {
refreshLoader.loading = false;
@@ -241,7 +242,7 @@ export class ConfirmCutoverDialog {
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
- text: e.toString()
+ text: e.message
};
} finally {
refreshLoader.loading = false;
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
deleted file mode 100644
index b34828b8b9..0000000000
--- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
+++ /dev/null
@@ -1,852 +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 { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
-import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
-import * as loc from '../../constants/strings';
-import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
-import { EOL } from 'os';
-import { ConfirmCutoverDialog } from './confirmCutoverDialog';
-import { logError, TelemetryViews } from '../../telemtery';
-import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
-import * as styles from '../../constants/styles';
-import { canRetryMigration, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper';
-import { DatabaseMigration, getResourceName } from '../../api/azure';
-
-const statusImageSize: number = 14;
-
-export class MigrationCutoverDialog {
- private _dialogObject!: azdata.window.Dialog;
- private _view!: azdata.ModelView;
- private _model: MigrationCutoverDialogModel;
-
- private _databaseTitleName!: azdata.TextComponent;
- private _cutoverButton!: azdata.ButtonComponent;
- private _refreshButton!: azdata.ButtonComponent;
- private _cancelButton!: azdata.ButtonComponent;
- private _refreshLoader!: azdata.LoadingComponent;
- private _copyDatabaseMigrationDetails!: azdata.ButtonComponent;
- private _newSupportRequest!: azdata.ButtonComponent;
- private _retryButton!: azdata.ButtonComponent;
-
- 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.DeclarativeTableComponent;
- private _disposables: vscode.Disposable[] = [];
- private _emptyTableFill!: azdata.FlexContainer;
-
- private isRefreshing = false;
-
- readonly _infoFieldWidth: string = '250px';
-
- constructor(
- private readonly _context: vscode.ExtensionContext,
- private readonly _serviceContext: MigrationServiceContext,
- private readonly _migration: DatabaseMigration,
- private readonly _onClosedCallback: () => Promise) {
-
- this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
- this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
- }
-
- async initialize(): Promise {
- let tab = azdata.window.createTab('');
- tab.registerContent(async (view: azdata.ModelView) => {
- try {
- this._view = view;
-
- this._fileCount = view.modelBuilder.text().withProps({
- width: '500px',
- CSSStyles: {
- ...styles.BODY_CSS
- }
- }).component();
-
- const rowCssStyle: azdata.CssStyles = {
- 'border': 'none',
- 'text-align': 'left',
- 'border-bottom': '1px solid',
- 'font-size': '12px'
- };
-
- const headerCssStyles: azdata.CssStyles = {
- 'border': 'none',
- 'text-align': 'left',
- 'border-bottom': '1px solid',
- 'font-weight': 'bold',
- 'padding-left': '0px',
- 'padding-right': '0px',
- 'font-size': '12px'
- };
-
- this._fileTable = view.modelBuilder.declarativeTable().withProps({
- ariaLabel: loc.ACTIVE_BACKUP_FILES,
- columns: [
- {
- displayName: loc.ACTIVE_BACKUP_FILES,
- valueType: azdata.DeclarativeDataType.string,
- width: '230px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: loc.TYPE,
- valueType: azdata.DeclarativeDataType.string,
- width: '90px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: loc.STATUS,
- valueType: azdata.DeclarativeDataType.string,
- width: '60px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: loc.DATA_UPLOADED,
- valueType: azdata.DeclarativeDataType.string,
- width: '120px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: loc.COPY_THROUGHPUT,
- valueType: azdata.DeclarativeDataType.string,
- width: '150px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: loc.BACKUP_START_TIME,
- valueType: azdata.DeclarativeDataType.string,
- width: '130px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: loc.FIRST_LSN,
- valueType: azdata.DeclarativeDataType.string,
- width: '120px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: loc.LAST_LSN,
- valueType: azdata.DeclarativeDataType.string,
- width: '120px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- }
- ],
- data: [],
- width: '1100px',
- height: '300px',
- CSSStyles: {
- ...styles.BODY_CSS,
- 'display': 'none',
- 'padding-left': '0px'
- }
- }).component();
-
- const _emptyTableImage = view.modelBuilder.image().withProps({
- iconPath: IconPathHelper.emptyTable,
- iconHeight: '100px',
- iconWidth: '100px',
- height: '100px',
- width: '100px',
- CSSStyles: {
- 'text-align': 'center'
- }
- }).component();
-
- const _emptyTableText = view.modelBuilder.text().withProps({
- value: loc.EMPTY_TABLE_TEXT,
- CSSStyles: {
- ...styles.NOTE_CSS,
- 'margin-top': '8px',
- 'text-align': 'center',
- 'width': '300px'
- }
- }).component();
-
- this._emptyTableFill = view.modelBuilder.flexContainer()
- .withLayout({
- flexFlow: 'column',
- alignItems: 'center'
- }).withItems([
- _emptyTableImage,
- _emptyTableText,
- ]).withProps({
- width: 1000,
- display: 'none'
- }).component();
-
- let formItems = [
- { component: this.migrationContainerHeader() },
- { component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
- { component: await this.migrationInfoGrid() },
- { component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
- { component: this._fileCount },
- { component: this._fileTable },
- { component: this._emptyTableFill }
- ];
-
- const formBuilder = view.modelBuilder.formContainer().withFormItems(
- formItems,
- { horizontal: false }
- );
- const form = formBuilder.withLayout({ width: '100%' }).component();
-
- this._disposables.push(
- this._view.onClosed(e => {
- this._disposables.forEach(
- d => { try { d.dispose(); } catch { } });
- }));
-
- await view.initializeModel(form);
- await this.refreshStatus();
- } catch (e) {
- logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
- }
- });
- this._dialogObject.content = [tab];
-
- this._dialogObject.cancelButton.hidden = true;
- this._dialogObject.okButton.label = loc.CLOSE;
-
- azdata.window.openDialog(this._dialogObject);
- }
-
- private migrationContainerHeader(): azdata.FlexContainer {
- const sqlDatbaseLogo = this._view.modelBuilder.image().withProps({
- iconPath: IconPathHelper.sqlDatabaseLogo,
- iconHeight: '32px',
- iconWidth: '32px',
- width: '32px',
- height: '32px'
- }).component();
-
- this._databaseTitleName = this._view.modelBuilder.text().withProps({
- CSSStyles: {
- ...styles.PAGE_TITLE_CSS
- },
- width: 950,
- value: this._model._migration.properties.sourceDatabaseName
- }).component();
-
- const databaseSubTitle = this._view.modelBuilder.text().withProps({
- CSSStyles: {
- ...styles.NOTE_CSS
- },
- width: 950,
- value: loc.DATABASE
- }).component();
-
- const titleContainer = this._view.modelBuilder.flexContainer().withItems([
- this._databaseTitleName,
- databaseSubTitle
- ]).withLayout({
- 'flexFlow': 'column'
- }).withProps({
- width: 950
- }).component();
-
- const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
- width: 1000
- }).component();
-
- titleLogoContainer.addItem(sqlDatbaseLogo, {
- flex: '0'
- });
- titleLogoContainer.addItem(titleContainer, {
- flex: '0',
- CSSStyles: {
- 'margin-left': '5px',
- 'width': '930px'
- }
- });
-
- const headerActions = this._view.modelBuilder.flexContainer().withLayout({
- }).withProps({
- width: 1000
- }).component();
-
- this._cutoverButton = this._view.modelBuilder.button().withProps({
- iconPath: IconPathHelper.cutover,
- iconHeight: '16px',
- iconWidth: '16px',
- label: loc.COMPLETE_CUTOVER,
- height: '20px',
- width: '140px',
- enabled: false,
- CSSStyles: {
- ...styles.BODY_CSS,
- 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
- }
- }).component();
-
- this._disposables.push(this._cutoverButton.onDidClick(async (e) => {
- await this.refreshStatus();
- const dialog = new ConfirmCutoverDialog(this._model);
- await dialog.initialize();
-
- if (this._model.CutoverError) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
- }
- }));
-
- headerActions.addItem(this._cutoverButton, { flex: '0' });
-
- this._cancelButton = this._view.modelBuilder.button().withProps({
- iconPath: IconPathHelper.cancel,
- iconHeight: '16px',
- iconWidth: '16px',
- label: loc.CANCEL_MIGRATION,
- height: '20px',
- width: '140px',
- enabled: false,
- CSSStyles: {
- ...styles.BODY_CSS,
- }
- }).component();
-
- this._disposables.push(this._cancelButton.onDidClick((e) => {
- void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, { modal: true }, loc.YES, loc.NO).then(async (v) => {
- if (v === loc.YES) {
- await this._model.cancelMigration();
- await this.refreshStatus();
- if (this._model.CancelMigrationError) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANCELLATION_ERROR, this._model.CancelMigrationError);
- }
- }
- });
- }));
-
- headerActions.addItem(this._cancelButton, {
- flex: '0'
- });
-
- this._retryButton = this._view.modelBuilder.button().withProps({
- label: loc.RETRY_MIGRATION,
- iconPath: IconPathHelper.retry,
- enabled: false,
- iconHeight: '16px',
- iconWidth: '16px',
- height: '20px',
- width: '120px',
- CSSStyles: {
- ...styles.BODY_CSS,
- }
- }).component();
- this._disposables.push(this._retryButton.onDidClick(
- async (e) => {
- await this.refreshStatus();
- const retryMigrationDialog = new RetryMigrationDialog(
- this._context,
- this._serviceContext,
- this._migration,
- this._onClosedCallback);
- await retryMigrationDialog.openDialog();
- }
- ));
- headerActions.addItem(this._retryButton, {
- flex: '0',
- });
-
- this._refreshButton = this._view.modelBuilder.button().withProps({
- iconPath: IconPathHelper.refresh,
- iconHeight: '16px',
- iconWidth: '16px',
- label: 'Refresh',
- height: '20px',
- width: '80px',
- CSSStyles: {
- ...styles.BODY_CSS,
- }
- }).component();
-
- this._disposables.push(
- this._refreshButton.onDidClick(async (e) => {
- this._refreshButton.enabled = false;
- await this.refreshStatus();
- this._refreshButton.enabled = true;
- }));
-
- headerActions.addItem(this._refreshButton, { flex: '0' });
-
- this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
- iconPath: IconPathHelper.copy,
- iconHeight: '16px',
- iconWidth: '16px',
- label: loc.COPY_MIGRATION_DETAILS,
- height: '20px',
- width: '160px',
- CSSStyles: {
- ...styles.BODY_CSS,
- }
- }).component();
-
- this._disposables.push(this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
- await this.refreshStatus();
- await vscode.env.clipboard.writeText(this.getMigrationDetails());
-
- void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
- }));
-
- headerActions.addItem(this._copyDatabaseMigrationDetails, {
- flex: '0',
- CSSStyles: { 'margin-left': '5px' }
- });
-
- // create new support request button. Hiding button until sql migration support has been setup.
- this._newSupportRequest = this._view.modelBuilder.button().withProps({
- label: loc.NEW_SUPPORT_REQUEST,
- iconPath: IconPathHelper.newSupportRequest,
- iconHeight: '16px',
- iconWidth: '16px',
- height: '20px',
- width: '160px',
- CSSStyles: {
- ...styles.BODY_CSS,
- }
- }).component();
-
- this._disposables.push(this._newSupportRequest.onDidClick(async (e) => {
- const serviceId = this._model._migration.properties.migrationService;
- const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
- await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
- }));
-
- headerActions.addItem(this._newSupportRequest, {
- flex: '0',
- CSSStyles: {
- 'margin-left': '5px'
- }
- });
-
- this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
- loading: false,
- CSSStyles: {
- 'height': '8px',
- 'margin-top': '4px'
- }
- }).component();
-
- headerActions.addItem(this._refreshLoader, {
- flex: '0',
- CSSStyles: {
- 'margin-left': '16px'
- }
- });
-
- const header = this._view.modelBuilder.flexContainer().withItems([
- titleLogoContainer
- ]).withLayout({
- flexFlow: 'column'
- }).withProps({
- CSSStyles: {
- width: 1000
- }
- }).component();
-
- header.addItem(headerActions, {
- 'CSSStyles': {
- 'margin-top': '16px'
- }
- });
-
- return header;
- }
-
- private async migrationInfoGrid(): Promise {
- const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
- container.addItem(infoField.flexContainer, {
- CSSStyles: {
- width: this._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 _isBlobMigration = isBlobMigration(this._model._migration);
- 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, '', _isBlobMigration);
- 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, '', _isBlobMigration);
- this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
- this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration);
- this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration);
- addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
- addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
- addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
- addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
-
- const flexInfoProps = {
- flex: '0',
- CSSStyles: {
- 'flex': '0',
- 'width': this._infoFieldWidth
- }
- };
-
- const flexInfo = this._view.modelBuilder.flexContainer().withProps({
- width: 1000
- }).component();
- flexInfo.addItem(flexServer, flexInfoProps);
- flexInfo.addItem(flexTarget, flexInfoProps);
- flexInfo.addItem(flexStatus, flexInfoProps);
- flexInfo.addItem(flexFile, flexInfoProps);
-
- return flexInfo;
- }
-
- private getMigrationDetails(): string {
- return JSON.stringify(this._model.migrationStatus, undefined, 2);
- }
-
- private async refreshStatus(): Promise {
- if (this.isRefreshing) {
- return;
- }
-
- try {
- clearDialogMessage(this._dialogObject);
-
- await this._cutoverButton.updateCssStyles(
- { 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' });
-
- this.isRefreshing = true;
- this._refreshLoader.loading = true;
- await this._model.fetchStatus();
- const errors = [];
- errors.push(this._model.migrationStatus.properties.provisioningError);
- errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
- errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
- errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.restoreBlockingReason);
- this._dialogObject.message = {
- // remove undefined and duplicate error entries
- text: errors
- .filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
- .join(EOL),
- level: this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress
- || this._model.migrationStatus.properties.migrationStatus === MigrationStatus.Completing
- ? azdata.window.MessageLevel.Warning
- : azdata.window.MessageLevel.Error,
- description: this.getMigrationDetails()
- };
- const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
- const sqlServerName = this._model._migration.properties.sourceServerName;
- const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName;
- const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
- const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
- const targetDatabaseName = this._model._migration.name;
- const targetServerName = getResourceName(this._model._migration.properties.scope);
- let targetServerVersion;
- if (this._model.migrationStatus.id.includes('managedInstances')) {
- targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
- } else {
- targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
- }
-
- let lastAppliedSSN: string;
- let lastAppliedBackupFileTakenOn: string;
-
- const tableData: ActiveBackupFileSchema[] = [];
-
- this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(
- (activeBackupSet) => {
- if (this._shouldDisplayBackupFileTable()) {
- 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) : '-',
- backupStartTime: activeBackupSet.backupStartDate,
- firstLSN: activeBackupSet.firstLSN,
- lastLSN: activeBackupSet.lastLSN
- };
- })
- );
- }
-
- if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
- lastAppliedSSN = activeBackupSet.lastLSN;
- lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
- }
- });
-
- 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;
-
- const migrationStatusTextValue = this._getMigrationStatus();
- this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-';
- this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue);
-
- this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
-
- let backupLocation;
- const _isBlobMigration = isBlobMigration(this._model._migration);
- // Displaying storage accounts and blob container for azure blob backups.
- if (_isBlobMigration) {
- const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
- const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
- backupLocation = storageAccountResourceId && blobContainerName
- ? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
- : undefined;
- } else {
- const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare;
- backupLocation = fileShare?.path! ?? '-';
- }
- this._backupLocationInfoField.text.value = backupLocation ?? '-';
-
- this._lastLSNInfoField.text.value = lastAppliedSSN! ?? '-';
- this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
- this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
-
- if (_isBlobMigration) {
- if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
- this._currentRestoringFileInfoField.text.value = '-';
- } else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
- this._currentRestoringFileInfoField.text.value = loc.ALL_BACKUPS_RESTORED;
- } else {
- this._currentRestoringFileInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename;
- }
- }
-
- if (this._shouldDisplayBackupFileTable()) {
- await this._fileCount.updateCssStyles({
- ...styles.SECTION_HEADER_CSS,
- display: 'inline'
- });
- await this._fileTable.updateCssStyles({
- 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';
- } 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);
-
- this._fileTable.data = tableData.map((row) => {
- return [
- row.fileName,
- row.type,
- row.status,
- row.dataUploaded,
- row.copyThroughput,
- convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
- row.firstLSN,
- row.lastLSN
- ];
- });
- }
- }
-
- this._cutoverButton.enabled = false;
- if (migrationStatusTextValue === MigrationStatus.InProgress) {
- if (_isBlobMigration) {
- if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
- this._cutoverButton.enabled = true;
- }
- } else {
- const restoredCount = this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(
- (a) => a.listOfBackupFiles[0].status === BackupFileInfoStatus.Restored)?.length ?? 0;
- if (restoredCount > 0) {
- this._cutoverButton.enabled = true;
- }
- }
- }
-
- this._cancelButton.enabled =
- migrationStatusTextValue === MigrationStatus.Creating ||
- migrationStatusTextValue === MigrationStatus.InProgress;
-
- this._retryButton.enabled = canRetryMigration(migrationStatusTextValue);
-
- } catch (e) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
- console.log(e);
- } finally {
- this.isRefreshing = false;
- this._refreshLoader.loading = false;
- }
- }
-
- private async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
- flexContainer: azdata.FlexContainer,
- text: azdata.TextComponent,
- icon?: azdata.ImageComponent
- }> {
- const flexContainer = this._view.modelBuilder.flexContainer()
- .withProps({
- CSSStyles: {
- 'flex-direction': 'column',
- 'padding-right': '12px'
- }
- }).component();
-
- if (defaultHidden) {
- await flexContainer.updateCssStyles({
- 'display': 'none'
- });
- }
-
- const labelComponent = this._view.modelBuilder.text().withProps({
- value: label,
- CSSStyles: {
- ...styles.LIGHT_LABEL_CSS,
- 'margin-bottom': '0',
- }
- }).component();
- flexContainer.addItem(labelComponent);
-
- const textComponent = this._view.modelBuilder.text().withProps({
- value: value,
- CSSStyles: {
- ...styles.BODY_CSS,
- 'margin': '4px 0 12px',
- 'width': '100%',
- 'overflow': 'visible',
- 'overflow-wrap': 'break-word'
- }
- }).component();
-
- let iconComponent;
- if (iconPath) {
- iconComponent = this._view.modelBuilder.image().withProps({
- iconPath: (iconPath === ' ') ? undefined : iconPath,
- iconHeight: statusImageSize,
- iconWidth: statusImageSize,
- height: statusImageSize,
- width: statusImageSize,
- CSSStyles: {
- 'margin': '7px 3px 0 0',
- 'padding': '0'
- }
- }).component();
-
- const iconTextComponent = this._view.modelBuilder.flexContainer()
- .withItems([
- iconComponent,
- textComponent
- ]).withProps({
- CSSStyles: {
- 'margin': '0',
- 'padding': '0'
- },
- display: 'inline-flex'
- }).component();
- flexContainer.addItem(iconTextComponent);
- } else {
- flexContainer.addItem(textComponent);
- }
-
- return {
- flexContainer: flexContainer,
- text: textComponent,
- icon: iconComponent
- };
- }
-
- private _shouldDisplayBackupFileTable(): boolean {
- return !isBlobMigration(this._model._migration);
- }
-
- private _getMigrationStatus(): string {
- return this._model.migrationStatus
- ? getMigrationStatus(this._model.migrationStatus)
- : getMigrationStatus(this._model._migration);
- }
-}
-
-interface ActiveBackupFileSchema {
- fileName: string,
- type: string,
- status: string,
- dataUploaded: string,
- copyThroughput: string,
- backupStartTime: string,
- firstLSN: string,
- lastLSN: string
-}
-
-interface InfoFieldSchema {
- flexContainer: azdata.FlexContainer,
- text: azdata.TextComponent,
- icon?: azdata.ImageComponent
-}
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
index 196b04b3a1..b80661f797 100644
--- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
+++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
@@ -7,53 +7,45 @@ import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
import * as constants from '../../constants/strings';
-import { EOL } from 'os';
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
export class MigrationCutoverDialogModel {
public CutoverError?: Error;
public CancelMigrationError?: Error;
- public migrationStatus!: DatabaseMigration;
-
constructor(
- public _serviceConstext: MigrationServiceContext,
- public _migration: DatabaseMigration
- ) {
- }
+ public serviceConstext: MigrationServiceContext,
+ public migration: DatabaseMigration) { }
public async fetchStatus(): Promise {
- this.migrationStatus = await getMigrationDetails(
- this._serviceConstext.azureAccount!,
- this._serviceConstext.subscription!,
- this._migration.id,
- this._migration.properties?.migrationOperationId);
+ const migrationStatus = await getMigrationDetails(
+ this.serviceConstext.azureAccount!,
+ this.serviceConstext.subscription!,
+ this.migration.id,
+ this.migration.properties?.migrationOperationId);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.MigrationStatus,
- {
- 'migrationStatus': this.migrationStatus.properties?.migrationStatus
- },
- {}
- );
- // Logging status to help debugging.
- console.log(this.migrationStatus);
+ { 'migrationStatus': migrationStatus.properties?.migrationStatus },
+ {});
+
+ this.migration = migrationStatus;
}
public async startCutover(): Promise {
try {
this.CutoverError = undefined;
- if (this._migration) {
+ if (this.migration) {
const cutover = await startMigrationCutover(
- this._serviceConstext.azureAccount!,
- this._serviceConstext.subscription!,
- this._migration!);
+ this.serviceConstext.azureAccount!,
+ this.serviceConstext.subscription!,
+ this.migration!);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CutoverMigration,
{
- ...this.getTelemetryProps(this._serviceConstext, this._migration),
+ ...this.getTelemetryProps(this.serviceConstext, this.migration),
'migrationEndTime': new Date().toString(),
},
{}
@@ -67,30 +59,21 @@ export class MigrationCutoverDialogModel {
return undefined!;
}
- public async fetchErrors(): Promise {
- const errors = [];
- await this.fetchStatus();
- errors.push(this.migrationStatus.properties.migrationFailureError?.message);
- return errors
- .filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
- .join(EOL);
- }
-
public async cancelMigration(): Promise {
try {
this.CancelMigrationError = undefined;
- if (this.migrationStatus) {
+ if (this.migration) {
const cutoverStartTime = new Date().toString();
await stopMigration(
- this._serviceConstext.azureAccount!,
- this._serviceConstext.subscription!,
- this.migrationStatus);
+ this.serviceConstext.azureAccount!,
+ this.serviceConstext.subscription!,
+ this.migration);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CancelMigration,
{
- ...this.getTelemetryProps(this._serviceConstext, this._migration),
- 'migrationMode': getMigrationMode(this._migration),
+ ...this.getTelemetryProps(this.serviceConstext, this.migration),
+ 'migrationMode': getMigrationMode(this.migration),
'cutoverStartTime': cutoverStartTime,
},
{}
@@ -104,7 +87,7 @@ export class MigrationCutoverDialogModel {
}
public confirmCutoverStepsString(): string {
- if (isBlobMigration(this.migrationStatus)) {
+ if (isBlobMigration(this.migration)) {
return `${constants.CUTOVER_HELP_STEP1}
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
@@ -116,16 +99,16 @@ export class MigrationCutoverDialogModel {
}
public getLastBackupFileRestoredName(): string | undefined {
- return this.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
+ return this.migration.properties.migrationStatusDetails?.lastRestoredFilename;
}
public getPendingLogBackupsCount(): number | undefined {
- return this.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount;
+ return this.migration.properties.migrationStatusDetails?.pendingLogBackupsCount;
}
public getPendingFiles(): BackupFileInfo[] {
const files: BackupFileInfo[] = [];
- this.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
+ this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
abs.listOfBackupFiles.forEach(f => {
if (f.status !== BackupFileInfoStatus.Restored) {
files.push(f);
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
deleted file mode 100644
index a9d684488c..0000000000
--- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
+++ /dev/null
@@ -1,593 +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 { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage';
-import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
-import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
-import * as loc from '../../constants/strings';
-import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage } from '../../api/utils';
-import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
-import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog';
-import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel';
-import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper';
-import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
-import { DatabaseMigration, getResourceName } from '../../api/azure';
-import { logError, TelemetryViews } from '../../telemtery';
-import { SelectMigrationServiceDialog } from '../selectMigrationService/selectMigrationServiceDialog';
-
-const MenuCommands = {
- Cutover: 'sqlmigration.cutover',
- ViewDatabase: 'sqlmigration.view.database',
- ViewTarget: 'sqlmigration.view.target',
- ViewService: 'sqlmigration.view.service',
- CopyMigration: 'sqlmigration.copy.migration',
- CancelMigration: 'sqlmigration.cancel.migration',
- RetryMigration: 'sqlmigration.retry.migration',
-};
-
-export class MigrationStatusDialog {
- private _context: vscode.ExtensionContext;
- private _model: MigrationStatusDialogModel;
- private _dialogObject!: azdata.window.Dialog;
- private _view!: azdata.ModelView;
- private _searchBox!: azdata.InputBoxComponent;
- private _refresh!: azdata.ButtonComponent;
- private _serviceContextButton!: azdata.ButtonComponent;
- private _statusDropdown!: azdata.DropDownComponent;
- private _statusTable!: azdata.TableComponent;
- private _refreshLoader!: azdata.LoadingComponent;
- private _disposables: vscode.Disposable[] = [];
- private _filteredMigrations: DatabaseMigration[] = [];
-
- private isRefreshing = false;
-
- constructor(
- context: vscode.ExtensionContext,
- private _filter: AdsMigrationStatus,
- private _onClosedCallback: () => Promise) {
-
- this._context = context;
- this._model = new MigrationStatusDialogModel([]);
- this._dialogObject = azdata.window.createModelViewDialog(
- loc.MIGRATION_STATUS,
- 'MigrationControllerDialog',
- 'wide');
- }
-
- async initialize() {
- let tab = azdata.window.createTab('');
- tab.registerContent(async (view: azdata.ModelView) => {
- this._view = view;
- this.registerCommands();
- const form = view.modelBuilder.formContainer()
- .withFormItems(
- [
- { component: await this.createSearchAndRefreshContainer() },
- { component: this.createStatusTable() }
- ],
- { horizontal: false }
- ).withLayout({ width: '100%' })
- .component();
- this._disposables.push(
- this._view.onClosed(async e => {
- this._disposables.forEach(
- d => { try { d.dispose(); } catch { } });
-
- await this._onClosedCallback();
- }));
-
- await view.initializeModel(form);
- return await this.refreshTable();
- });
- this._dialogObject.content = [tab];
- this._dialogObject.cancelButton.hidden = true;
- this._dialogObject.okButton.label = loc.CLOSE;
- azdata.window.openDialog(this._dialogObject);
- }
-
- private canCancelMigration = (status: string | undefined) => status &&
- (
- status === MigrationStatus.InProgress ||
- status === MigrationStatus.Creating ||
- status === MigrationStatus.Completing ||
- status === MigrationStatus.Canceling
- );
-
- private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress;
-
- private async createSearchAndRefreshContainer(): Promise {
- this._searchBox = this._view.modelBuilder.inputBox()
- .withProps({
- stopEnterPropagation: true,
- placeHolder: loc.SEARCH_FOR_MIGRATIONS,
- width: '360px'
- }).component();
- this._disposables.push(
- this._searchBox.onTextChanged(
- async (value) => await this.populateMigrationTable()));
-
- this._refresh = this._view.modelBuilder.button()
- .withProps({
- iconPath: IconPathHelper.refresh,
- iconHeight: '16px',
- iconWidth: '20px',
- label: loc.REFRESH_BUTTON_LABEL,
- }).component();
- this._disposables.push(
- this._refresh.onDidClick(
- async (e) => await this.refreshTable()));
-
- this._statusDropdown = this._view.modelBuilder.dropDown()
- .withProps({
- ariaLabel: loc.MIGRATION_STATUS_FILTER,
- values: this._model.statusDropdownValues,
- width: '220px'
- }).component();
- this._disposables.push(
- this._statusDropdown.onValueChanged(
- async (value) => await this.populateMigrationTable()));
-
- if (this._filter) {
- this._statusDropdown.value =
- (this._statusDropdown.values)
- .find(value => value.name === this._filter);
- }
-
- this._refreshLoader = this._view.modelBuilder.loadingComponent()
- .withProps({ loading: false })
- .component();
-
- const searchLabel = this._view.modelBuilder.text()
- .withProps({
- value: 'Status',
- CSSStyles: {
- 'font-size': '13px',
- 'font-weight': '600',
- 'margin': '3px 0 0 0',
- },
- }).component();
-
- const serviceContextLabel = await getSelectedServiceStatus();
- this._serviceContextButton = this._view.modelBuilder.button()
- .withProps({
- iconPath: IconPathHelper.sqlMigrationService,
- iconHeight: 22,
- iconWidth: 22,
- label: serviceContextLabel,
- title: serviceContextLabel,
- description: loc.MIGRATION_SERVICE_DESCRIPTION,
- buttonType: azdata.ButtonType.Informational,
- width: 270,
- }).component();
-
- const onDialogClosed = async (): Promise => {
- const label = await getSelectedServiceStatus();
- this._serviceContextButton.label = label;
- this._serviceContextButton.title = label;
- await this.refreshTable();
- };
-
- this._disposables.push(
- this._serviceContextButton.onDidClick(
- async () => {
- const dialog = new SelectMigrationServiceDialog(onDialogClosed);
- await dialog.initialize();
- }));
-
- const flexContainer = this._view.modelBuilder.flexContainer()
- .withProps({
- width: '100%',
- CSSStyles: {
- 'justify-content': 'left',
- 'align-items': 'center',
- 'padding': '0px',
- 'display': 'flex',
- 'flex-direction': 'row',
- },
- }).component();
-
- flexContainer.addItem(this._searchBox, { flex: '0' });
- flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
- flexContainer.addItem(searchLabel, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
- flexContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
- flexContainer.addItem(this._refresh, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
- flexContainer.addItem(this._refreshLoader, { flex: '0 0 auto', CSSStyles: { 'margin-left': '20px' } });
-
- const container = this._view.modelBuilder.flexContainer()
- .withProps({ width: 1245 })
- .component();
- container.addItem(flexContainer, { flex: '0 0 auto', });
- return container;
- }
-
- private registerCommands(): void {
- this._disposables.push(vscode.commands.registerCommand(
- MenuCommands.Cutover,
- async (migrationId: string) => {
- try {
- clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(
- migration => migration.id === migrationId);
-
- if (this.canCutoverMigration(migration?.properties.migrationStatus)) {
- const cutoverDialogModel = new MigrationCutoverDialogModel(
- await MigrationLocalStorage.getMigrationServiceContext(),
- migration!);
- await cutoverDialogModel.fetchStatus();
- const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
- await dialog.initialize();
- if (cutoverDialogModel.CutoverError) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, cutoverDialogModel.CutoverError);
- }
- } else {
- await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
- }
- } catch (e) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, e);
- console.log(e);
- }
- }));
-
- this._disposables.push(vscode.commands.registerCommand(
- MenuCommands.ViewDatabase,
- async (migrationId: string) => {
- try {
- const migration = this._model._migrations.find(migration => migration.id === migrationId);
- const dialog = new MigrationCutoverDialog(
- this._context,
- await MigrationLocalStorage.getMigrationServiceContext(),
- migration!,
- this._onClosedCallback);
- await dialog.initialize();
- } catch (e) {
- console.log(e);
- }
- }));
-
- this._disposables.push(vscode.commands.registerCommand(
- MenuCommands.ViewTarget,
- async (migrationId: string) => {
- try {
- const migration = this._model._migrations.find(migration => migration.id === migrationId);
- const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
- await vscode.env.openExternal(vscode.Uri.parse(url));
- } catch (e) {
- console.log(e);
- }
- }));
-
- this._disposables.push(vscode.commands.registerCommand(
- MenuCommands.ViewService,
- async (migrationId: string) => {
- try {
- const migration = this._model._migrations.find(migration => migration.id === migrationId);
- const dialog = new SqlMigrationServiceDetailsDialog(
- await MigrationLocalStorage.getMigrationServiceContext(),
- migration!);
- await dialog.initialize();
- } catch (e) {
- console.log(e);
- }
- }));
-
- this._disposables.push(vscode.commands.registerCommand(
- MenuCommands.CopyMigration,
- async (migrationId: string) => {
- try {
- clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(migration => migration.id === migrationId);
- const cutoverDialogModel = new MigrationCutoverDialogModel(
- await MigrationLocalStorage.getMigrationServiceContext(),
- migration!);
- await cutoverDialogModel.fetchStatus();
- await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
-
- await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
- } catch (e) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
- console.log(e);
- }
- }));
-
- this._disposables.push(vscode.commands.registerCommand(
- MenuCommands.CancelMigration,
- async (migrationId: string) => {
- try {
- clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(migration => migration.id === migrationId);
- if (this.canCancelMigration(migration?.properties.migrationStatus)) {
- void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
- if (v === loc.YES) {
- const cutoverDialogModel = new MigrationCutoverDialogModel(
- await MigrationLocalStorage.getMigrationServiceContext(),
- migration!);
- await cutoverDialogModel.fetchStatus();
- await cutoverDialogModel.cancelMigration();
-
- if (cutoverDialogModel.CancelMigrationError) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANNOT_CANCEL, cutoverDialogModel.CancelMigrationError);
- }
- }
- });
- } else {
- await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
- }
- } catch (e) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANCELLATION_ERROR, e);
- console.log(e);
- }
- }));
-
- this._disposables.push(vscode.commands.registerCommand(
- MenuCommands.RetryMigration,
- async (migrationId: string) => {
- try {
- clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(migration => migration.id === migrationId);
- if (canRetryMigration(migration?.properties.migrationStatus)) {
- let retryMigrationDialog = new RetryMigrationDialog(
- this._context,
- await MigrationLocalStorage.getMigrationServiceContext(),
- migration!,
- this._onClosedCallback);
- await retryMigrationDialog.openDialog();
- }
- else {
- await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
- }
- } catch (e) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_RETRY_ERROR, e);
- console.log(e);
- }
- }));
- }
-
- private async populateMigrationTable(): Promise {
- try {
- this._filteredMigrations = filterMigrations(
- this._model._migrations,
- (this._statusDropdown.value).name,
- this._searchBox.value!);
-
- this._filteredMigrations.sort((m1, m2) => {
- if (!m1.properties?.startedOn) {
- return 1;
- } else if (!m2.properties?.startedOn) {
- return -1;
- }
- return new Date(m1.properties?.startedOn) > new Date(m2.properties?.startedOn) ? -1 : 1;
- });
-
- const data: any[] = this._filteredMigrations.map((migration, index) => {
- return [
- {
- icon: IconPathHelper.sqlDatabaseLogo,
- title: migration.properties.sourceDatabaseName ?? '-',
- }, // database
- {
- icon: getMigrationStatusImage(migration.properties.migrationStatus),
- title: this._getMigrationStatus(migration),
- }, // statue
- getMigrationMode(migration), // mode
- getMigrationTargetType(migration), // targetType
- getResourceName(migration.id), // targetName
- getResourceName(migration.properties.migrationService), // migrationService
- this._getMigrationDuration(
- migration.properties.startedOn,
- migration.properties.endedOn), // duration
- this._getMigrationTime(migration.properties.startedOn), // startTime
- this._getMigrationTime(migration.properties.endedOn), // endTime
- ];
- });
-
- await this._statusTable.updateProperty('data', data);
- } catch (e) {
- logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
- }
- }
-
- private _getMigrationTime(migrationTime: string): string {
- return migrationTime
- ? new Date(migrationTime).toLocaleString()
- : '---';
- }
-
- private _getMigrationDuration(startDate: string, endDate: string): string {
- if (startDate) {
- if (endDate) {
- return convertTimeDifferenceToDuration(
- new Date(startDate),
- new Date(endDate));
- } else {
- return convertTimeDifferenceToDuration(
- new Date(startDate),
- new Date());
- }
- }
-
- return '---';
- }
-
- private _getMigrationStatus(migration: DatabaseMigration): string {
- const properties = migration.properties;
- const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
- let warningCount = 0;
-
- if (properties.migrationFailureError?.message) {
- warningCount++;
- }
- if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
- warningCount += properties.migrationStatusDetails?.fileUploadBlockingErrors.length;
- }
- if (properties.migrationStatusDetails?.restoreBlockingReason) {
- warningCount++;
- }
-
- return loc.STATUS_VALUE(migrationStatus, warningCount) + (loc.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
- }
-
- public openCalloutDialog(dialogHeading: string, dialogName?: string, calloutMessageText?: string): void {
- const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, {
- xPos: 0,
- yPos: 0,
- width: 20,
- height: 20
- });
- const tab: azdata.window.DialogTab = azdata.window.createTab('');
- tab.registerContent(async view => {
- const warningContentContainer = view.modelBuilder.divContainer().component();
- const messageTextComponent = view.modelBuilder.text().withProps({
- value: calloutMessageText,
- CSSStyles: {
- 'font-size': '12px',
- 'line-height': '16px',
- 'margin': '0 0 12px 0',
- 'display': '-webkit-box',
- '-webkit-box-orient': 'vertical',
- '-webkit-line-clamp': '5',
- 'overflow': 'hidden'
- }
- }).component();
- warningContentContainer.addItem(messageTextComponent);
-
- await view.initializeModel(warningContentContainer);
- });
-
- dialog.content = [tab];
-
- azdata.window.openDialog(dialog);
- }
-
- private async refreshTable(): Promise {
- if (this.isRefreshing) {
- return;
- }
-
- this.isRefreshing = true;
- try {
- clearDialogMessage(this._dialogObject);
- this._refreshLoader.loading = true;
- this._model._migrations = await getCurrentMigrations();
- await this.populateMigrationTable();
- } catch (e) {
- displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
- console.log(e);
- } finally {
- this.isRefreshing = false;
- this._refreshLoader.loading = false;
- }
- }
-
- private createStatusTable(): azdata.TableComponent {
- const headerCssStyles = undefined;
- const rowCssStyles = undefined;
-
- this._statusTable = this._view.modelBuilder.table().withProps({
- ariaLabel: loc.MIGRATION_STATUS,
- data: [],
- forceFitColumns: azdata.ColumnSizingMode.ForceFit,
- height: '600px',
- width: '1095px',
- display: 'grid',
- columns: [
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.DATABASE,
- value: 'database',
- width: 190,
- type: azdata.ColumnType.hyperlink,
- icon: IconPathHelper.sqlDatabaseLogo,
- showText: true,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.STATUS_COLUMN,
- value: 'status',
- width: 120,
- type: azdata.ColumnType.hyperlink,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.MIGRATION_MODE,
- value: 'mode',
- width: 85,
- type: azdata.ColumnType.text,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.AZURE_SQL_TARGET,
- value: 'targetType',
- width: 120,
- type: azdata.ColumnType.text,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
- value: 'targetName',
- width: 125,
- type: azdata.ColumnType.text,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.DATABASE_MIGRATION_SERVICE,
- value: 'migrationService',
- width: 140,
- type: azdata.ColumnType.text,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.DURATION,
- value: 'duration',
- width: 50,
- type: azdata.ColumnType.text,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.START_TIME,
- value: 'startTime',
- width: 115,
- type: azdata.ColumnType.text,
- },
- {
- cssClass: rowCssStyles,
- headerCssClass: headerCssStyles,
- name: loc.FINISH_TIME,
- value: 'finishTime',
- width: 115,
- type: azdata.ColumnType.text,
- },
- ]
- }).component();
-
- this._disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
- const buttonState = rowState;
- switch (buttonState?.column) {
- case 0:
- case 1:
- const migration = this._filteredMigrations[rowState.row];
- const dialog = new MigrationCutoverDialog(
- this._context,
- await MigrationLocalStorage.getMigrationServiceContext(),
- migration,
- this._onClosedCallback);
- await dialog.initialize();
- break;
- }
- }));
-
- return this._statusTable;
- }
-}
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
deleted file mode 100644
index f01e971254..0000000000
--- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
+++ /dev/null
@@ -1,44 +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 { DatabaseMigration } from '../../api/azure';
-import * as loc from '../../constants/strings';
-
-export class MigrationStatusDialogModel {
- public statusDropdownValues: azdata.CategoryValue[] = [
- {
- displayName: loc.STATUS_ALL,
- name: AdsMigrationStatus.ALL
- }, {
- displayName: loc.STATUS_ONGOING,
- name: AdsMigrationStatus.ONGOING
- }, {
- displayName: loc.STATUS_COMPLETING,
- name: AdsMigrationStatus.COMPLETING
- }, {
- displayName: loc.STATUS_SUCCEEDED,
- name: AdsMigrationStatus.SUCCEEDED
- }, {
- displayName: loc.STATUS_FAILED,
- name: AdsMigrationStatus.FAILED
- }
- ];
-
- constructor(public _migrations: DatabaseMigration[]) {
- }
-
-}
-
-/**
- * This enum is used to categorize migrations internally in ADS. A migration has 2 statuses: Provisioning Status and Migration Status. The values from both the statuses are mapped to different values in this enum
- */
-export enum AdsMigrationStatus {
- ALL = 'all',
- ONGOING = 'ongoing',
- SUCCEEDED = 'succeeded',
- FAILED = 'failed',
- COMPLETING = 'completing'
-}
diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts
index b7475d5b18..59b8242dc5 100644
--- a/extensions/sql-migration/src/main.ts
+++ b/extensions/sql-migration/src/main.ts
@@ -79,7 +79,7 @@ class SQLMigration {
}),
azdata.tasks.registerTask(
'sqlmigration.refreshmigrations',
- async (e) => await widget?.refreshMigrations()),
+ async (e) => await widget.refresh()),
];
this.context.subscriptions.push(...commandDisposables);
diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts
index f05c938002..b763cee84b 100644
--- a/extensions/sql-migration/src/models/migrationLocalStorage.ts
+++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts
@@ -95,7 +95,8 @@ export enum MigrationStatus {
Canceled = 'Canceled',
Completing = 'Completing',
Creating = 'Creating',
- Canceling = 'Canceling'
+ Canceling = 'Canceling',
+ Retriable = 'Retriable',
}
export enum ProvisioningState {
@@ -110,6 +111,6 @@ export enum BackupFileInfoStatus {
Uploaded = 'Uploaded',
Restoring = 'Restoring',
Restored = 'Restored',
- Canceled = 'Canceled',
+ Cancelled = 'Cancelled',
Ignored = 'Ignored'
}
diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts
index 6ce7879474..d1491b3ad3 100644
--- a/extensions/sql-migration/src/models/stateMachine.ts
+++ b/extensions/sql-migration/src/models/stateMachine.ts
@@ -34,6 +34,11 @@ export enum State {
EXIT,
}
+export enum ServiceTier {
+ GeneralPurpose = 'GeneralPurpose',
+ BusinessCritical = 'BusinessCritical',
+}
+
export enum MigrationTargetType {
SQLVM = 'AzureSqlVirtualMachine',
SQLMI = 'AzureSqlManagedInstance',
diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts
index 4e75520810..35d73abac9 100644
--- a/extensions/sql-migration/src/telemtery.ts
+++ b/extensions/sql-migration/src/telemtery.ts
@@ -21,6 +21,8 @@ export enum TelemetryViews {
IntegrationRuntimePage = 'IntegrationRuntimePage',
MigrationCutoverDialog = 'MigrationCutoverDialog',
MigrationStatusDialog = 'MigrationStatusDialog',
+ DashboardTab = 'DashboardTab',
+ MigrationsTab = 'MigrationsTab',
MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage',
MigrationWizardSkuRecommendationPage = 'MigrationWizardSkuRecommendationPage',
MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage',