Add new tabbed dashboard, monitoring with breadcrumb navigation (#19995)

* SQL DB monitoring and  Dashboard refactor

* Merge remote-tracking branch 'origin/main' into dev/brih/feature/sql-migration-dashboard-tabs

* update filter text and optimize page load

* update migration column order, names and statusbox

* add column table sorting

* add new migration and pipeline status values, etc

* address review feedback
This commit is contained in:
brian-harris
2022-07-25 10:06:17 -07:00
committed by GitHub
parent db39571394
commit 78b7c3cfd4
27 changed files with 4192 additions and 2382 deletions

View File

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

View File

@@ -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<T>(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:

View File

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

View File

@@ -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'),
};
}
}

View File

@@ -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<T> {
[key: string]: T;
}
export const StatusLookup: LookupTable<string | undefined> = {
['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<string | undefined> = {
// 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<string | undefined> = {
[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');

View File

@@ -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<DashboardTab> {
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<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async create(
view: azdata.ModelView,
openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>,
statusBar: DashboardStatusBar): Promise<DashboardTab> {
this.view = view;
this.openMigrationFcn = openMigrationsFcn;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
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<void> {
const container = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
const toolbar = view.modelBuilder.toolbarContainer();
toolbar.addToolbarItems([
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton() },
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
<azdata.ToolbarComponent>{ 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(<string>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<azdata.Component> {
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<azdata.Component> {
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(<string>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<azdata.FlexContainer> {
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<azdata.Component> {
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<void> {
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;
}
}

View File

@@ -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<MigrationDetailsBlobContainerTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
constructor() {
super();
this.id = MigrationDetailsBlobContainerTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
onClosedCallback: () => Promise<void>,
statusBar: DashboardStatusBar,
): Promise<MigrationDetailsBlobContainerTab> {
this.view = view;
this.context = context;
this.onClosedCallback = onClosedCallback;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
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<void> {
try {
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
];
this.content = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withLayout({ width: '100%', padding: '0 0 0 15px' })
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
.component();
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected override async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: { 'flex': '0', 'width': infoFieldWidth }
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
}

View File

@@ -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<MigrationDetailsFileShareTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _fullBackupFileOnInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastLSNInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.TableComponent;
private _emptyTableFill!: azdata.FlexContainer;
constructor() {
super();
this.id = MigrationDetailsFileShareTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
onClosedCallback: () => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
this.view = view;
this.context = context;
this.onClosedCallback = onClosedCallback;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
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<void> {
try {
this._fileCount = this.view.modelBuilder.text()
.withProps({
width: '500px',
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._fileTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
CSSStyles: { 'padding-left': '0px', 'max-width': '1020px' },
data: [],
height: '300px',
columns: [
{
value: 'files',
name: loc.ACTIVE_BACKUP_FILES,
type: azdata.ColumnType.text,
width: 230,
},
{
value: 'type',
name: loc.TYPE,
width: 90,
type: azdata.ColumnType.text,
},
{
value: 'status',
name: loc.STATUS,
width: 60,
type: azdata.ColumnType.text,
},
{
value: 'uploaded',
name: loc.DATA_UPLOADED,
width: 120,
type: azdata.ColumnType.text,
},
{
value: 'throughput',
name: loc.COPY_THROUGHPUT,
width: 150,
type: azdata.ColumnType.text,
},
{
value: 'starttime',
name: loc.BACKUP_START_TIME,
width: 130,
type: azdata.ColumnType.text,
},
{
value: 'firstlsn',
name: loc.FIRST_LSN,
width: 120,
type: azdata.ColumnType.text,
},
{
value: 'lastlsn',
name: loc.LAST_LSN,
width: 120,
type: azdata.ColumnType.text,
}
],
}).component();
const emptyTableImage = this.view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.emptyTable,
iconHeight: '100px',
iconWidth: '100px',
height: '100px',
width: '100px',
CSSStyles: { 'text-align': 'center' }
}).component();
const emptyTableText = this.view.modelBuilder.text()
.withProps({
value: loc.EMPTY_TABLE_TEXT,
CSSStyles: {
...styles.NOTE_CSS,
'margin-top': '8px',
'text-align': 'center',
'width': '300px'
}
}).component();
this._emptyTableFill = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
alignItems: 'center'
}).withItems([
emptyTableImage,
emptyTableText,
]).withProps({
width: '100%',
display: 'none'
}).component();
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
{ component: this._fileCount },
{ component: this._fileTable },
{ component: this._emptyTableFill }
];
const formContainer = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withLayout({ width: '100%', padding: '0 0 0 15px' })
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
.component();
this.content = formContainer;
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', false);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', false);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', false);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: {
'flex': '0',
'width': infoFieldWidth
}
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
}

View File

@@ -0,0 +1,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<string> = {
[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<T> extends TabBase<T> {
protected model!: MigrationCutoverDialogModel;
protected databaseLabel!: azdata.TextComponent;
protected serviceContext!: MigrationServiceContext;
protected onClosedCallback!: () => Promise<void>;
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<void>, statusBar: DashboardStatusBar): Promise<T>;
protected abstract migrationInfoGrid(): Promise<azdata.FlexContainer>;
constructor() {
super();
this.title = '';
}
public async setMigrationContext(
serviceContext: MigrationServiceContext,
migration: DatabaseMigration): Promise<void> {
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([
<azdata.ToolbarComponent>{ component: this.cutoverButton },
<azdata.ToolbarComponent>{ component: this.cancelButton },
<azdata.ToolbarComponent>{ component: this.retryButton },
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.refreshButton },
<azdata.ToolbarComponent>{ 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<azdata.FlexContainer> {
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<void> {
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);
}
}

View File

@@ -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<MigrationDetailsTableTab> {
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<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsTableTab> {
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<void> {
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<void> {
const migration = this.model?.migration;
await this.showMigrationErrors(this.model?.migration);
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
const sqlServerName = migration?.properties.sourceServerName;
const sourceDatabaseName = migration?.properties.sourceDatabaseName;
const sqlServerInfo = await 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<number> = {};
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<number> = {}): Promise<void> {
if (this._progressTable.data.length > 0) {
await this._progressTable.updateProperty('data', []);
}
// Sort table data
this._sortTableMigrations(
this._progressDetail,
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true);
const data = this._progressDetail.map((d) => {
hashSet[d.status] = (hashSet[d.status] ?? 0) + 1;
return [
d.tableName,
<azdata.HyperlinkColumnCellValue>{
icon: getPipelineStatusImage(d.status),
title: loc.PipelineRunStatus[d.status] ?? d.status?.toUpperCase(),
},
formatSizeBytes(d.dataRead),
formatSizeBytes(d.dataWritten),
formatNumber(d.rowsRead),
formatNumber(d.rowsCopied),
formatSizeKb(d.copyThroughput),
formatTime((d.copyDuration ?? 0) * 1000),
loc.ParallelCopyType[d.parallelCopyType] ?? d.parallelCopyType,
d.usedParallelCopies,
formatDateTimeString(d.copyStart),
];
}) ?? [];
// Filter tableData
const filteredData = this._filterTables(data, this._tableFilterInputBox.value);
await this._progressTable.updateProperty('data', filteredData);
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
this._progressTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
CSSStyles: {
'padding-left': '0px',
'max-width': '1111px'
},
data: [],
height: '300px',
columns: [
{
value: TableColumns.tableName,
name: loc.SQLDB_COL_TABLE_NAME,
type: azdata.ColumnType.text,
width: 170,
},
<azdata.HyperlinkColumn>{
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<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
{ component: await this._createStatusBar() },
{ component: await this._createTableFilter() },
{ component: this._progressTable },
];
this.disposables.push(
this._progressTable.onCellAction!(
async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>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<azdata.FlexContainer> {
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: <azdata.CategoryValue>{ name: TableColumns.copyStart, displayName: loc.START_TIME },
values: [
<azdata.CategoryValue>{ name: TableColumns.tableName, displayName: loc.SQLDB_COL_TABLE_NAME },
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS },
<azdata.CategoryValue>{ name: TableColumns.dataRead, displayName: loc.SQLDB_COL_DATA_READ },
<azdata.CategoryValue>{ name: TableColumns.dataWritten, displayName: loc.SQLDB_COL_DATA_WRITTEN },
<azdata.CategoryValue>{ name: TableColumns.rowsRead, displayName: loc.SQLDB_COL_ROWS_READ },
<azdata.CategoryValue>{ name: TableColumns.rowsCopied, displayName: loc.SQLDB_COL_ROWS_COPIED },
<azdata.CategoryValue>{ name: TableColumns.copyThroughput, displayName: loc.SQLDB_COL_COPY_THROUGHPUT },
<azdata.CategoryValue>{ name: TableColumns.copyDuration, displayName: loc.SQLDB_COL_COPY_DURATION },
<azdata.CategoryValue>{ name: TableColumns.parallelCopyType, displayName: loc.SQLDB_COL_PARRALEL_COPY_TYPE },
<azdata.CategoryValue>{ name: TableColumns.usedParallelCopies, displayName: loc.SQLDB_COL_USED_PARALLEL_COPIES },
<azdata.CategoryValue>{ 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<azdata.FlexContainer> {
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<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldLgWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._serverObjectsInfoField = await this.createInfoField(loc.SERVER_OBJECTS_FIELD_LABEL, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._serverObjectsInfoField, flexStatus);
const flexInfoProps = {
flex: '0',
CSSStyles: { 'flex': '0', 'width': infoFieldLgWidth }
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
return flexInfo;
}
}

View File

@@ -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<MigrationsListTab> {
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<void>;
private _migrations: DatabaseMigration[] = [];
constructor() {
super();
this.id = MigrationsListTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
statusBar: DashboardStatusBar,
): Promise<MigrationsListTab> {
this.view = view;
this.context = context;
this._openMigrationDetails = openMigrationDetails;
this.statusBar = statusBar;
await this.initialize();
return this;
}
public onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
.find(value => value.name === filter.toString());
this._statusDropdown.value = statusFilter;
}
}
public async refresh(): Promise<void> {
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<void> {
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([
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this._refresh },
<azdata.ToolbarComponent>{ component: this._refreshLoader },
]);
return toolbar.component();
}
private async _createSearchAndSortContainer(): Promise<azdata.FlexContainer> {
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<void> =>
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: <azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
values: [
<azdata.CategoryValue>{ name: TableColumns.sourceDatabase, displayName: loc.SRC_DATABASE },
<azdata.CategoryValue>{ name: TableColumns.sourceServer, displayName: loc.SRC_SERVER },
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS_COLUMN },
<azdata.CategoryValue>{ name: TableColumns.mode, displayName: loc.MIGRATION_MODE },
<azdata.CategoryValue>{ name: TableColumns.targetType, displayName: loc.AZURE_SQL_TARGET },
<azdata.CategoryValue>{ name: TableColumns.targetDatabse, displayName: loc.TARGET_DATABASE_COLUMN },
<azdata.CategoryValue>{ name: TableColumns.targetServer, displayName: loc.TARGET_SERVER_COLUMN },
<azdata.CategoryValue>{ name: TableColumns.duration, displayName: loc.DURATION },
<azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
<azdata.CategoryValue>{ 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<void> {
try {
this._filteredMigrations = filterMigrations(
this._migrations,
(<azdata.CategoryValue>this._statusDropdown.value).name,
this._searchBox.value!);
this._sortMigrations(
this._filteredMigrations,
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true);
const data: any[] = this._filteredMigrations.map((migration, index) => {
return [
<azdata.HyperlinkColumnCellValue>{
icon: IconPathHelper.sqlDatabaseLogo,
title: migration.properties.sourceDatabaseName ?? EmptySettingValue,
}, // sourceDatabase
migration.properties.sourceServerName ?? EmptySettingValue, // sourceServer
<azdata.HyperlinkColumnCellValue>{
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
<azdata.ContextMenuColumnCellValue>{
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: [
<azdata.HyperlinkColumn>{
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,
},
<azdata.HyperlinkColumn>{
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 = <azdata.ICellActionEventArgs>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 }
];
}

View File

@@ -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<MigrationsTab> {
private _tab!: azdata.DivContainer;
private _migrationsListTab!: MigrationsListTab;
private _migrationDetailsTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsFileShareTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>;
private _selectedTabId: string | undefined = undefined;
constructor() {
super();
this.title = loc.DESKTOP_MIGRATIONS_TAB_TITLE;
this.id = MigrationsTabId;
}
public onDialogClosed = async (): Promise<void> =>
await this._migrationsListTab.onDialogClosed();
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
statusBar: DashboardStatusBar): Promise<MigrationsTab> {
this.context = context;
this.view = view;
this.statusBar = statusBar;
await this.initialize(view);
await this._openTab(this._migrationsListTab);
return this;
}
public async refresh(): Promise<void> {
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<void> {
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<void> {
await this._migrationsListTab?.setMigrationFilter(filter);
await this._openTab(this._migrationsListTab);
await this._migrationsListTab?.setMigrationFilter(filter);
}
private async _openMigrationDetails(migration: DatabaseMigration): Promise<void> {
switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) {
case FileStorageType.AzureBlob:
this._migrationDetailsTab = this._migrationDetailsBlobTab;
break;
case FileStorageType.FileShare:
this._migrationDetailsTab = this._migrationDetailsFileShareTab;
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<void> {
await this.statusBar.clearError();
await this._openTab(this._migrationsListTab);
}
private async _openTab(tab: azdata.Tab): Promise<void> {
if (tab.id === this._selectedTabId) {
return;
}
this._tab.clearItems();
this._tab.addItem(tab.content);
this._selectedTabId = tab.id;
}
}

View File

@@ -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<void>;
clearError: () => Promise<void>;
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<void> => {
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<void> {
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<void> {
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(<string>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<void> => {
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> {
void this._migrationsTab.refresh();
await this._dashboardTab.refresh();
}
private async createTasks(view: azdata.ModelView): Promise<azdata.Component> {
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<void> {
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<void> {
if (this.isRefreshing) {
protected async openErrorDialog(): Promise<void> {
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<azdata.Component> {
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<azdata.FlexContainer> {
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<void> {
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<azdata.Component> {
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(<string>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<void> {
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
}
}

View File

@@ -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<T> 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<void>;
protected statusBar!: DashboardStatusBar;
protected abstract initialize(view: azdata.ModelView): Promise<void>;
public abstract refresh(): Promise<void>;
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<void> {
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);
}
}

View File

@@ -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 ((<SqlManagedInstance>targetInstance)?.sku?.tier === 'BusinessCritical') {
if ((<SqlManagedInstance>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;

View File

@@ -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<void>) {
this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
}
async initialize(): Promise<void> {
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<azdata.FlexContainer> {
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<void> {
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
}

View File

@@ -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<void> {
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<DatabaseMigration | undefined> {
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<string> {
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<void> {
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);

View File

@@ -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<void>) {
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<azdata.FlexContainer> {
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 =
(<azdata.CategoryValue[]>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<void> => {
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<void> {
try {
this._filteredMigrations = filterMigrations(
this._model._migrations,
(<azdata.CategoryValue>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 [
<azdata.HyperlinkColumnCellValue>{
icon: IconPathHelper.sqlDatabaseLogo,
title: migration.properties.sourceDatabaseName ?? '-',
}, // database
<azdata.HyperlinkColumnCellValue>{
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<void> {
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: [
<azdata.HyperlinkColumn>{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.DATABASE,
value: 'database',
width: 190,
type: azdata.ColumnType.hyperlink,
icon: IconPathHelper.sqlDatabaseLogo,
showText: true,
},
<azdata.HyperlinkColumn>{
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 = <azdata.ICellActionEventArgs>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;
}
}

View File

@@ -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'
}

View File

@@ -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);

View File

@@ -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'
}

View File

@@ -34,6 +34,11 @@ export enum State {
EXIT,
}
export enum ServiceTier {
GeneralPurpose = 'GeneralPurpose',
BusinessCritical = 'BusinessCritical',
}
export enum MigrationTargetType {
SQLVM = 'AzureSqlVirtualMachine',
SQLMI = 'AzureSqlManagedInstance',

View File

@@ -21,6 +21,8 @@ export enum TelemetryViews {
IntegrationRuntimePage = 'IntegrationRuntimePage',
MigrationCutoverDialog = 'MigrationCutoverDialog',
MigrationStatusDialog = 'MigrationStatusDialog',
DashboardTab = 'DashboardTab',
MigrationsTab = 'MigrationsTab',
MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage',
MigrationWizardSkuRecommendationPage = 'MigrationWizardSkuRecommendationPage',
MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage',