mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-08 09:38:26 -05:00
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:
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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[][] {
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
789
extensions/sql-migration/src/dashboard/dashboardTab.ts
Normal file
789
extensions/sql-migration/src/dashboard/dashboardTab.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
771
extensions/sql-migration/src/dashboard/migrationsListTab.ts
Normal file
771
extensions/sql-migration/src/dashboard/migrationsListTab.ts
Normal 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 }
|
||||
];
|
||||
}
|
||||
148
extensions/sql-migration/src/dashboard/migrationsTab.ts
Normal file
148
extensions/sql-migration/src/dashboard/migrationsTab.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
228
extensions/sql-migration/src/dashboard/tabBase.ts
Normal file
228
extensions/sql-migration/src/dashboard/tabBase.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ export enum State {
|
||||
EXIT,
|
||||
}
|
||||
|
||||
export enum ServiceTier {
|
||||
GeneralPurpose = 'GeneralPurpose',
|
||||
BusinessCritical = 'BusinessCritical',
|
||||
}
|
||||
|
||||
export enum MigrationTargetType {
|
||||
SQLVM = 'AzureSqlVirtualMachine',
|
||||
SQLMI = 'AzureSqlManagedInstance',
|
||||
|
||||
@@ -21,6 +21,8 @@ export enum TelemetryViews {
|
||||
IntegrationRuntimePage = 'IntegrationRuntimePage',
|
||||
MigrationCutoverDialog = 'MigrationCutoverDialog',
|
||||
MigrationStatusDialog = 'MigrationStatusDialog',
|
||||
DashboardTab = 'DashboardTab',
|
||||
MigrationsTab = 'MigrationsTab',
|
||||
MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage',
|
||||
MigrationWizardSkuRecommendationPage = 'MigrationWizardSkuRecommendationPage',
|
||||
MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage',
|
||||
|
||||
Reference in New Issue
Block a user