mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 11:01:37 -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:
3
extensions/sql-migration/images/addNew.svg
Normal file
3
extensions/sql-migration/images/addNew.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.999 7.5V8.5H7.99902V15.5H6.99902V8.5H-0.000976562V7.5H6.99902V0.5H7.99902V7.5H14.999Z" fill="#0078D4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 220 B |
3
extensions/sql-migration/images/allTables.svg
Normal file
3
extensions/sql-migration/images/allTables.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 2H14V14H1V2ZM2 4H13V3H2V4ZM5 7V5H2V7H5ZM2 8V10H5V8H2ZM5 13V11H2V13H5ZM9 7V5H6V7H9ZM6 8V10H9V8H6ZM9 13V11H6V13H9ZM13 7V5H10V7H13ZM13 10V8H10V10H13ZM13 13V11H10V13H13Z" fill="#323130"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 299 B |
3
extensions/sql-migration/images/breadCrumb.svg
Normal file
3
extensions/sql-migration/images/breadCrumb.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
|
<defs><style>.cls-1{fill:#231f20;}</style></defs><title>chevron_right</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path d="M10.54,8l-7-7,1-1,8,8-8,8-1-1Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 299 B |
@@ -2,7 +2,7 @@
|
|||||||
"name": "sql-migration",
|
"name": "sql-migration",
|
||||||
"displayName": "%displayName%",
|
"displayName": "%displayName%",
|
||||||
"description": "%description%",
|
"description": "%description%",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"publisher": "Microsoft",
|
"publisher": "Microsoft",
|
||||||
"preview": false,
|
"preview": false,
|
||||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||||
@@ -147,32 +147,7 @@
|
|||||||
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud && mssql:engineedition != 8",
|
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud && mssql:engineedition != 8",
|
||||||
"hideRefreshTask": true,
|
"hideRefreshTask": true,
|
||||||
"container": {
|
"container": {
|
||||||
"grid-container": [
|
"modelview-container": null
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"row": 0,
|
|
||||||
"col": 0,
|
|
||||||
"widget": {
|
|
||||||
"tasks-widget": [
|
|
||||||
"sqlmigration.start",
|
|
||||||
"sqlmigration.newsupportrequest",
|
|
||||||
"sqlmigration.sendfeedback"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"row": 0,
|
|
||||||
"col": 1,
|
|
||||||
"rowspan": 2.5,
|
|
||||||
"colspan": 3.5,
|
|
||||||
"widget": {
|
|
||||||
"modelview": {
|
|
||||||
"id": "migration.dashboard"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -526,6 +526,22 @@ export interface MigrationStatusDetails {
|
|||||||
lastRestoredFilename: string;
|
lastRestoredFilename: string;
|
||||||
pendingLogBackupsCount: number;
|
pendingLogBackupsCount: number;
|
||||||
invalidFiles: string[];
|
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 {
|
export interface SqlConnectionInfo {
|
||||||
|
|||||||
@@ -5,13 +5,16 @@
|
|||||||
|
|
||||||
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
|
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
|
||||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||||
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
|
|
||||||
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
|
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import * as azure from './azure';
|
import * as azure from './azure';
|
||||||
import { azureResource, Tenant } from 'azurecore';
|
import { azureResource, Tenant } from 'azurecore';
|
||||||
import * as constants from '../constants/strings';
|
import * as constants from '../constants/strings';
|
||||||
import { logError, TelemetryViews } from '../telemtery';
|
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 {
|
export function deepClone<T>(obj: T): T {
|
||||||
if (!obj || typeof obj !== 'object') {
|
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[] {
|
export function getMigrationTime(migrationTime: string): string {
|
||||||
let filteredMigration: azure.DatabaseMigration[] = [];
|
return migrationTime
|
||||||
if (statusFilter === AdsMigrationStatus.ALL) {
|
? new Date(migrationTime).toLocaleString()
|
||||||
filteredMigration = databaseMigrations;
|
: DefaultSettingValue;
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
if (databaseNameFilter) {
|
|
||||||
const filter = databaseNameFilter.toLowerCase();
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
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;
|
return filteredMigration;
|
||||||
}
|
}
|
||||||
@@ -208,12 +245,61 @@ export function decorate(decorator: (fn: Function, key: string) => Function): Fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSessionIdHeader(sessionId: string): { [key: string]: string } {
|
export function getSessionIdHeader(sessionId: string): { [key: string]: string } {
|
||||||
return {
|
return { 'SqlMigrationSessionId': sessionId };
|
||||||
'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) {
|
switch (status) {
|
||||||
case MigrationStatus.InProgress:
|
case MigrationStatus.InProgress:
|
||||||
return IconPathHelper.inProgressMigration;
|
return IconPathHelper.inProgressMigration;
|
||||||
@@ -223,7 +309,10 @@ export function getMigrationStatusImage(status: string): IconPath {
|
|||||||
return IconPathHelper.notStartedMigration;
|
return IconPathHelper.notStartedMigration;
|
||||||
case MigrationStatus.Completing:
|
case MigrationStatus.Completing:
|
||||||
return IconPathHelper.completingCutover;
|
return IconPathHelper.completingCutover;
|
||||||
|
case MigrationStatus.Retriable:
|
||||||
|
return IconPathHelper.retry;
|
||||||
case MigrationStatus.Canceling:
|
case MigrationStatus.Canceling:
|
||||||
|
case MigrationStatus.Canceled:
|
||||||
return IconPathHelper.cancel;
|
return IconPathHelper.cancel;
|
||||||
case MigrationStatus.Failed:
|
case MigrationStatus.Failed:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -15,8 +15,84 @@ export enum SQLTargetAssetType {
|
|||||||
SQLDB = 'Microsoft.Sql/servers',
|
SQLDB = 'Microsoft.Sql/servers',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMigrationTargetType(migration: DatabaseMigration): string {
|
export const ParallelCopyTypeCodes = {
|
||||||
const id = migration.id?.toLowerCase();
|
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) {
|
if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) {
|
||||||
return loc.SQL_MANAGED_INSTANCE;
|
return loc.SQL_MANAGED_INSTANCE;
|
||||||
}
|
}
|
||||||
@@ -29,8 +105,8 @@ export function getMigrationTargetType(migration: DatabaseMigration): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined {
|
export function getMigrationTargetTypeEnum(migration: DatabaseMigration | undefined): MigrationTargetType | undefined {
|
||||||
switch (migration.type) {
|
switch (migration?.type) {
|
||||||
case SQLTargetAssetType.SQLMI:
|
case SQLTargetAssetType.SQLMI:
|
||||||
return MigrationTargetType.SQLMI;
|
return MigrationTargetType.SQLMI;
|
||||||
case SQLTargetAssetType.SQLVM:
|
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)
|
return isOfflineMigation(migration)
|
||||||
? loc.OFFLINE
|
? loc.OFFLINE
|
||||||
: loc.ONLINE;
|
: loc.ONLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode {
|
export function getMigrationModeEnum(migration: DatabaseMigration | undefined): MigrationMode {
|
||||||
return isOfflineMigation(migration)
|
return isOfflineMigation(migration)
|
||||||
? MigrationMode.OFFLINE
|
? MigrationMode.OFFLINE
|
||||||
: MigrationMode.ONLINE;
|
: MigrationMode.ONLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOfflineMigation(migration: DatabaseMigration): boolean {
|
export function isOfflineMigation(migration: DatabaseMigration | undefined): boolean {
|
||||||
return migration.properties.offlineConfiguration?.offline === true;
|
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;
|
return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMigrationStatus(migration: DatabaseMigration): string {
|
export function getMigrationStatus(migration: DatabaseMigration | undefined): string | undefined {
|
||||||
return migration.properties.migrationStatus
|
return migration?.properties.migrationStatus
|
||||||
?? migration.properties.provisioningState;
|
?? 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 {
|
export function canCancelMigration(migration: DatabaseMigration | undefined): boolean {
|
||||||
return status === undefined ||
|
const status = getMigrationStatus(migration);
|
||||||
status === MigrationStatus.Failed ||
|
return hasMigrationOperationId(migration)
|
||||||
status === MigrationStatus.Succeeded ||
|
&& (status === MigrationStatus.InProgress ||
|
||||||
status === MigrationStatus.Canceled;
|
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[][] {
|
export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] {
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ export class IconPathHelper {
|
|||||||
public static stop: IconPath;
|
public static stop: IconPath;
|
||||||
public static view: IconPath;
|
public static view: IconPath;
|
||||||
public static sqlMigrationService: IconPath;
|
public static sqlMigrationService: IconPath;
|
||||||
|
public static addNew: IconPath;
|
||||||
|
public static breadCrumb: IconPath;
|
||||||
|
public static allTables: IconPath;
|
||||||
|
|
||||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||||
IconPathHelper.copy = {
|
IconPathHelper.copy = {
|
||||||
@@ -183,5 +186,17 @@ export class IconPathHelper {
|
|||||||
light: context.asAbsolutePath('images/sqlMigrationService.svg'),
|
light: context.asAbsolutePath('images/sqlMigrationService.svg'),
|
||||||
dark: 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 { EOL } from 'os';
|
||||||
import { MigrationStatus } from '../models/migrationLocalStorage';
|
import { MigrationStatus } from '../models/migrationLocalStorage';
|
||||||
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
|
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
|
||||||
|
import { ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
|
||||||
const localize = nls.loadMessageBundle();
|
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.");
|
export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Failed to open the migration notebook.");
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
export function DASHBOARD_REFRESH_MIGRATIONS(error: string): string {
|
export const DASHBOARD_REFRESH_MIGRATIONS_TITLE = localize('sql.migration.refresh.migrations.error.title', 'An error has occured while refreshing the migrations list.');
|
||||||
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_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_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_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");
|
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");
|
export const FEEDBACK_ISSUE_TITLE = localize('sql.migration.feedback.issue.title', "Feedback on the migration experience");
|
||||||
|
|
||||||
//Migration cutover dialog
|
//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 MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
|
||||||
export const COMPLETE_CUTOVER = localize('sql.migration.complete.cutover', "Complete cutover");
|
export const COMPLETE_CUTOVER = localize('sql.migration.complete.cutover', "Complete cutover");
|
||||||
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database name");
|
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_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 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 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
|
//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.");
|
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 FILE_NAME = localize('sql.migration.file.name', "File name");
|
||||||
export const SIZE_COLUMN_HEADER = localize('sql.migration.size.column.header', "Size");
|
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.");
|
export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "No pending backups. Click refresh to check current status.");
|
||||||
|
|
||||||
//Migration status dialog
|
//Migration status dialog
|
||||||
export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account");
|
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.");
|
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_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing");
|
||||||
export const STATUS_SUCCEEDED = localize('sql.migration.status.dropdown.succeeded', "Status: Succeeded");
|
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 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 ONLINE = localize('sql.migration.online', "Online");
|
||||||
export const OFFLINE = localize('sql.migration.offline', "Offline");
|
export const OFFLINE = localize('sql.migration.offline', "Offline");
|
||||||
export const DATABASE = localize('sql.migration.database', "Database");
|
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 DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
|
||||||
export const DURATION = localize('sql.migration.duration', "Duration");
|
export const DURATION = localize('sql.migration.duration', "Duration");
|
||||||
export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type");
|
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_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine");
|
||||||
export const SQL_DATABASE = localize('sql.migration.sql.database', "SQL Database");
|
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 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 START_TIME = localize('sql.migration.start.time', "Start time");
|
||||||
export const FINISH_TIME = localize('sql.migration.finish.time', "Finish 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);
|
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> {
|
export interface LookupTable<T> {
|
||||||
[key: string]: T;
|
[key: string]: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const StatusLookup: LookupTable<string | undefined> = {
|
export const StatusLookup: LookupTable<string | undefined> = {
|
||||||
['InProgress']: localize('sql.migration.status.inprogress', 'In progress'),
|
[MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'),
|
||||||
['Succeeded']: localize('sql.migration.status.succeeded', 'Succeeded'),
|
[MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'),
|
||||||
['Creating']: localize('sql.migration.status.creating', 'Creating'),
|
[MigrationStatus.Creating]: localize('sql.migration.status.creating', 'Creating'),
|
||||||
['Completing']: localize('sql.migration.status.completing', 'Completing'),
|
[MigrationStatus.Completing]: localize('sql.migration.status.completing', 'Completing'),
|
||||||
['Canceling']: localize('sql.migration.status.canceling', 'Canceling'),
|
[MigrationStatus.Retriable]: localize('sql.migration.status.retriable', 'Retriable'),
|
||||||
['Failed']: localize('sql.migration.status.failed', 'Failed'),
|
[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
|
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 {
|
export function STATUS_WARNING_COUNT(status: string, count: number): string | undefined {
|
||||||
if (status === MigrationStatus.InProgress ||
|
if (status === MigrationStatus.InProgress ||
|
||||||
status === MigrationStatus.Creating ||
|
status === MigrationStatus.Creating ||
|
||||||
@@ -699,6 +750,27 @@ export function SEC(sec: number): string {
|
|||||||
return localize('sql.migration.sec', "{0} sec", sec);
|
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.
|
// 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_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");
|
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 AUTHENTICATION_TYPE = localize('sql.migration.authentication.type', "Authentication type");
|
||||||
|
|
||||||
export const REFRESH_BUTTON_LABEL = localize('sql.migration.status.refresh.label', 'Refresh');
|
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
|
// Saved Assessment Dialog
|
||||||
export const NEXT_LABEL = localize('sql.migration.saved.assessment.next', "Next");
|
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);
|
return localize('sql.migration.service.prompt', '{0} (change)', serviceName);
|
||||||
}
|
}
|
||||||
export const MIGRATION_SERVICE_DESCRIPTION = localize('sql.migration.select.service.description', 'Azure Database Migration Service');
|
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 azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { logError, TelemetryViews } from '../telemtery';
|
|
||||||
import * as loc from '../constants/strings';
|
import * as loc from '../constants/strings';
|
||||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
import { DashboardTab } from './dashboardTab';
|
||||||
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
|
import { MigrationsTab, MigrationsTabId } from './migrationsTab';
|
||||||
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
|
import { AdsMigrationStatus } from './tabBase';
|
||||||
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();
|
|
||||||
|
|
||||||
interface IActionMetadata {
|
export interface DashboardStatusBar {
|
||||||
title?: string,
|
showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise<void>;
|
||||||
description?: string,
|
clearError: () => Promise<void>;
|
||||||
link?: string,
|
errorTitle: string;
|
||||||
iconPath?: azdata.ThemedIconPath,
|
errorLabel: string;
|
||||||
command?: string;
|
errorDescription: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxWidth = 800;
|
export class DashboardWidget implements DashboardStatusBar {
|
||||||
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 {
|
|
||||||
private _context: vscode.ExtensionContext;
|
private _context: vscode.ExtensionContext;
|
||||||
private _migrationStatusCardsContainer!: azdata.FlexContainer;
|
|
||||||
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
|
|
||||||
private _view!: azdata.ModelView;
|
private _view!: azdata.ModelView;
|
||||||
private _inProgressMigrationButton!: StatusCard;
|
private _tabs!: azdata.TabbedPanelComponent;
|
||||||
private _inProgressWarningMigrationButton!: StatusCard;
|
private _statusInfoBox!: azdata.InfoBoxComponent;
|
||||||
private _allMigrationButton!: StatusCard;
|
private _dashboardTab!: DashboardTab;
|
||||||
private _successfulMigrationButton!: StatusCard;
|
private _migrationsTab!: MigrationsTab;
|
||||||
private _failedMigrationButton!: StatusCard;
|
|
||||||
private _completingMigrationButton!: StatusCard;
|
|
||||||
private _selectServiceText!: azdata.TextComponent;
|
|
||||||
private _serviceContextButton!: azdata.ButtonComponent;
|
|
||||||
private _refreshButton!: azdata.ButtonComponent;
|
|
||||||
|
|
||||||
private _disposables: vscode.Disposable[] = [];
|
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) {
|
constructor(context: vscode.ExtensionContext) {
|
||||||
this._context = context;
|
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 {
|
public register(): void {
|
||||||
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
|
azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => {
|
||||||
this._view = 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._disposables.push(
|
||||||
this._view.onClosed(e => {
|
this._view.onClosed(e => {
|
||||||
this._disposables.forEach(
|
this._disposables.forEach(
|
||||||
d => { try { d.dispose(); } catch { } });
|
d => { try { d.dispose(); } catch { } });
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await view.initializeModel(container);
|
const openMigrationFcn = async (filter: AdsMigrationStatus): Promise<void> => {
|
||||||
await this.refreshMigrations();
|
this._tabs.selectTab(MigrationsTabId);
|
||||||
});
|
await this._migrationsTab.setMigrationFilter(filter);
|
||||||
}
|
|
||||||
|
|
||||||
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: 'sqlmigration.start'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const preRequisiteListTitle = view.modelBuilder.text().withProps({
|
this._dashboardTab = await new DashboardTab().create(
|
||||||
value: loc.PRE_REQ_TITLE,
|
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: {
|
CSSStyles: {
|
||||||
...styles.BODY_CSS,
|
|
||||||
'margin': '0px',
|
'margin': '0px',
|
||||||
|
'padding': '0px',
|
||||||
|
'width': '100%'
|
||||||
}
|
}
|
||||||
}).component();
|
})
|
||||||
|
.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(
|
this._disposables.push(
|
||||||
buttonContainer.onDidClick(async () => {
|
this._tabs.onTabChanged(
|
||||||
if (taskMetaData.command) {
|
async id => {
|
||||||
await vscode.commands.executeCommand(taskMetaData.command);
|
await this.clearError();
|
||||||
}
|
await this.onDialogClosed();
|
||||||
}));
|
}));
|
||||||
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refreshMigrations(): Promise<void> {
|
public async refresh(): Promise<void> {
|
||||||
if (this.isRefreshing) {
|
void this._migrationsTab.refresh();
|
||||||
|
await this._dashboardTab.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onDialogClosed(): Promise<void> {
|
||||||
|
await this._dashboardTab.onDialogClosed();
|
||||||
|
await this._migrationsTab.onDialogClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _errorDialogIsOpen: boolean = false;
|
||||||
|
|
||||||
|
protected async openErrorDialog(): Promise<void> {
|
||||||
|
if (this._errorDialogIsOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isRefreshing = true;
|
|
||||||
this._migrationStatusCardLoadingContainer.loading = true;
|
|
||||||
let migrations: DatabaseMigration[] = [];
|
|
||||||
try {
|
try {
|
||||||
migrations = await getCurrentMigrations();
|
const tab = azdata.window.createTab(this.errorTitle);
|
||||||
} catch (e) {
|
tab.registerContent(async (view) => {
|
||||||
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
|
const flex = view.modelBuilder.flexContainer()
|
||||||
void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
}
|
|
||||||
}).component();
|
|
||||||
|
|
||||||
warningContainer = this._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 = 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([
|
.withItems([
|
||||||
statusContainerTitle,
|
view.modelBuilder.text()
|
||||||
this._refreshButton,
|
.withProps({ value: this.errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
|
||||||
]).withLayout({
|
.component(),
|
||||||
alignContent: 'center',
|
view.modelBuilder.inputBox()
|
||||||
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({
|
.withProps({
|
||||||
CSSStyles: {
|
value: this.errorDescription,
|
||||||
'justify-content': 'left',
|
readOnly: true,
|
||||||
'align-iems': 'center',
|
multiline: true,
|
||||||
},
|
inputType: 'text',
|
||||||
|
rows: 20,
|
||||||
|
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
|
||||||
})
|
})
|
||||||
.component();
|
.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({
|
.withLayout({
|
||||||
flexFlow: 'column',
|
flexFlow: 'column',
|
||||||
height: '272px',
|
width: 420,
|
||||||
})
|
})
|
||||||
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
|
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
|
||||||
.component();
|
.component();
|
||||||
|
|
||||||
await this._updateSummaryStatus();
|
await view.initializeModel(flex);
|
||||||
|
|
||||||
// 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 = [
|
const dialog = azdata.window.createModelViewDialog(
|
||||||
{
|
this.errorTitle,
|
||||||
title: localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio'),
|
'errorDialog',
|
||||||
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.'),
|
450,
|
||||||
link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
|
'flyout');
|
||||||
},
|
dialog.content = [tab];
|
||||||
{
|
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
|
||||||
title: localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)'),
|
dialog.okButton.focused = true;
|
||||||
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.'),
|
dialog.cancelButton.label = loc.CLOSE;
|
||||||
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(
|
this._disposables.push(
|
||||||
video1Container.onDidClick(async () => {
|
dialog.onClosed(async e => {
|
||||||
if (linkMetaData.link) {
|
if (e === 'ok') {
|
||||||
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
|
await this.clearError();
|
||||||
}
|
}
|
||||||
|
this._errorDialogIsOpen = false;
|
||||||
}));
|
}));
|
||||||
videosContainer.addItem(video1Container, {
|
|
||||||
CSSStyles: {
|
azdata.window.openDialog(dialog);
|
||||||
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
|
} catch (error) {
|
||||||
'background-repeat': 'no-repeat',
|
this._errorDialogIsOpen = false;
|
||||||
'background-position': 'top',
|
}
|
||||||
'width': `${maxWidth}px`,
|
}
|
||||||
'height': '104px',
|
|
||||||
'background-size': `${maxWidth}px 120px`
|
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> {
|
||||||
}
|
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
|
||||||
});
|
|
||||||
videosContainer.addItem(descriptionComponent);
|
|
||||||
return videosContainer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 { IconPathHelper } from '../../constants/iconPathHelper';
|
||||||
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
|
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
|
||||||
import * as styles from '../../constants/styles';
|
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 {
|
export class ConfirmCutoverDialog {
|
||||||
private _dialogObject!: azdata.window.Dialog;
|
private _dialogObject!: azdata.window.Dialog;
|
||||||
private _view!: azdata.ModelView;
|
private _view!: azdata.ModelView;
|
||||||
@@ -32,7 +33,7 @@ export class ConfirmCutoverDialog {
|
|||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
const sourceDatabaseText = view.modelBuilder.text().withProps({
|
const sourceDatabaseText = view.modelBuilder.text().withProps({
|
||||||
value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
|
value: this.migrationCutoverModel.migration.properties.sourceDatabaseName,
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
...styles.SMALL_NOTE_CSS,
|
...styles.SMALL_NOTE_CSS,
|
||||||
'margin': '4px 0px 8px'
|
'margin': '4px 0px 8px'
|
||||||
@@ -53,7 +54,7 @@ export class ConfirmCutoverDialog {
|
|||||||
}
|
}
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
|
const fileContainer = isBlobMigration(this.migrationCutoverModel.migration)
|
||||||
? this.createBlobFileContainer()
|
? this.createBlobFileContainer()
|
||||||
: this.createNetworkShareFileContainer();
|
: this.createNetworkShareFileContainer();
|
||||||
|
|
||||||
@@ -76,13 +77,13 @@ export class ConfirmCutoverDialog {
|
|||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
let infoDisplay = 'none';
|
let infoDisplay = 'none';
|
||||||
if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
|
if (getMigrationTargetTypeEnum(this.migrationCutoverModel.migration) === MigrationTargetType.SQLMI) {
|
||||||
const targetInstance = await getMigrationTargetInstance(
|
const targetInstance = await getMigrationTargetInstance(
|
||||||
this.migrationCutoverModel._serviceConstext.azureAccount!,
|
this.migrationCutoverModel.serviceConstext.azureAccount!,
|
||||||
this.migrationCutoverModel._serviceConstext.subscription!,
|
this.migrationCutoverModel.serviceConstext.subscription!,
|
||||||
this.migrationCutoverModel._migration);
|
this.migrationCutoverModel.migration);
|
||||||
|
|
||||||
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === 'BusinessCritical') {
|
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === ServiceTier.BusinessCritical) {
|
||||||
infoDisplay = 'inline';
|
infoDisplay = 'inline';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ export class ConfirmCutoverDialog {
|
|||||||
await this.migrationCutoverModel.startCutover();
|
await this.migrationCutoverModel.startCutover();
|
||||||
void vscode.window.showInformationMessage(
|
void vscode.window.showInformationMessage(
|
||||||
constants.CUTOVER_IN_PROGRESS(
|
constants.CUTOVER_IN_PROGRESS(
|
||||||
this.migrationCutoverModel._migration.properties.sourceDatabaseName));
|
this.migrationCutoverModel.migration.properties.sourceDatabaseName));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||||
@@ -163,7 +164,7 @@ export class ConfirmCutoverDialog {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._dialogObject.message = {
|
this._dialogObject.message = {
|
||||||
level: azdata.window.MessageLevel.Error,
|
level: azdata.window.MessageLevel.Error,
|
||||||
text: e.toString()
|
text: e.message
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
refreshLoader.loading = false;
|
refreshLoader.loading = false;
|
||||||
@@ -241,7 +242,7 @@ export class ConfirmCutoverDialog {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._dialogObject.message = {
|
this._dialogObject.message = {
|
||||||
level: azdata.window.MessageLevel.Error,
|
level: azdata.window.MessageLevel.Error,
|
||||||
text: e.toString()
|
text: e.message
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
refreshLoader.loading = false;
|
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 { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||||
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
|
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
|
||||||
import * as constants from '../../constants/strings';
|
import * as constants from '../../constants/strings';
|
||||||
import { EOL } from 'os';
|
|
||||||
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
|
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
|
||||||
|
|
||||||
export class MigrationCutoverDialogModel {
|
export class MigrationCutoverDialogModel {
|
||||||
public CutoverError?: Error;
|
public CutoverError?: Error;
|
||||||
public CancelMigrationError?: Error;
|
public CancelMigrationError?: Error;
|
||||||
public migrationStatus!: DatabaseMigration;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public _serviceConstext: MigrationServiceContext,
|
public serviceConstext: MigrationServiceContext,
|
||||||
public _migration: DatabaseMigration
|
public migration: DatabaseMigration) { }
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public async fetchStatus(): Promise<void> {
|
public async fetchStatus(): Promise<void> {
|
||||||
this.migrationStatus = await getMigrationDetails(
|
const migrationStatus = await getMigrationDetails(
|
||||||
this._serviceConstext.azureAccount!,
|
this.serviceConstext.azureAccount!,
|
||||||
this._serviceConstext.subscription!,
|
this.serviceConstext.subscription!,
|
||||||
this._migration.id,
|
this.migration.id,
|
||||||
this._migration.properties?.migrationOperationId);
|
this.migration.properties?.migrationOperationId);
|
||||||
|
|
||||||
sendSqlMigrationActionEvent(
|
sendSqlMigrationActionEvent(
|
||||||
TelemetryViews.MigrationCutoverDialog,
|
TelemetryViews.MigrationCutoverDialog,
|
||||||
TelemetryAction.MigrationStatus,
|
TelemetryAction.MigrationStatus,
|
||||||
{
|
{ 'migrationStatus': migrationStatus.properties?.migrationStatus },
|
||||||
'migrationStatus': this.migrationStatus.properties?.migrationStatus
|
{});
|
||||||
},
|
|
||||||
{}
|
this.migration = migrationStatus;
|
||||||
);
|
|
||||||
// Logging status to help debugging.
|
|
||||||
console.log(this.migrationStatus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async startCutover(): Promise<DatabaseMigration | undefined> {
|
public async startCutover(): Promise<DatabaseMigration | undefined> {
|
||||||
try {
|
try {
|
||||||
this.CutoverError = undefined;
|
this.CutoverError = undefined;
|
||||||
if (this._migration) {
|
if (this.migration) {
|
||||||
const cutover = await startMigrationCutover(
|
const cutover = await startMigrationCutover(
|
||||||
this._serviceConstext.azureAccount!,
|
this.serviceConstext.azureAccount!,
|
||||||
this._serviceConstext.subscription!,
|
this.serviceConstext.subscription!,
|
||||||
this._migration!);
|
this.migration!);
|
||||||
sendSqlMigrationActionEvent(
|
sendSqlMigrationActionEvent(
|
||||||
TelemetryViews.MigrationCutoverDialog,
|
TelemetryViews.MigrationCutoverDialog,
|
||||||
TelemetryAction.CutoverMigration,
|
TelemetryAction.CutoverMigration,
|
||||||
{
|
{
|
||||||
...this.getTelemetryProps(this._serviceConstext, this._migration),
|
...this.getTelemetryProps(this.serviceConstext, this.migration),
|
||||||
'migrationEndTime': new Date().toString(),
|
'migrationEndTime': new Date().toString(),
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -67,30 +59,21 @@ export class MigrationCutoverDialogModel {
|
|||||||
return undefined!;
|
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> {
|
public async cancelMigration(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.CancelMigrationError = undefined;
|
this.CancelMigrationError = undefined;
|
||||||
if (this.migrationStatus) {
|
if (this.migration) {
|
||||||
const cutoverStartTime = new Date().toString();
|
const cutoverStartTime = new Date().toString();
|
||||||
await stopMigration(
|
await stopMigration(
|
||||||
this._serviceConstext.azureAccount!,
|
this.serviceConstext.azureAccount!,
|
||||||
this._serviceConstext.subscription!,
|
this.serviceConstext.subscription!,
|
||||||
this.migrationStatus);
|
this.migration);
|
||||||
sendSqlMigrationActionEvent(
|
sendSqlMigrationActionEvent(
|
||||||
TelemetryViews.MigrationCutoverDialog,
|
TelemetryViews.MigrationCutoverDialog,
|
||||||
TelemetryAction.CancelMigration,
|
TelemetryAction.CancelMigration,
|
||||||
{
|
{
|
||||||
...this.getTelemetryProps(this._serviceConstext, this._migration),
|
...this.getTelemetryProps(this.serviceConstext, this.migration),
|
||||||
'migrationMode': getMigrationMode(this._migration),
|
'migrationMode': getMigrationMode(this.migration),
|
||||||
'cutoverStartTime': cutoverStartTime,
|
'cutoverStartTime': cutoverStartTime,
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -104,7 +87,7 @@ export class MigrationCutoverDialogModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public confirmCutoverStepsString(): string {
|
public confirmCutoverStepsString(): string {
|
||||||
if (isBlobMigration(this.migrationStatus)) {
|
if (isBlobMigration(this.migration)) {
|
||||||
return `${constants.CUTOVER_HELP_STEP1}
|
return `${constants.CUTOVER_HELP_STEP1}
|
||||||
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
|
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
|
||||||
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
|
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
|
||||||
@@ -116,16 +99,16 @@ export class MigrationCutoverDialogModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getLastBackupFileRestoredName(): string | undefined {
|
public getLastBackupFileRestoredName(): string | undefined {
|
||||||
return this.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
|
return this.migration.properties.migrationStatusDetails?.lastRestoredFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPendingLogBackupsCount(): number | undefined {
|
public getPendingLogBackupsCount(): number | undefined {
|
||||||
return this.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount;
|
return this.migration.properties.migrationStatusDetails?.pendingLogBackupsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPendingFiles(): BackupFileInfo[] {
|
public getPendingFiles(): BackupFileInfo[] {
|
||||||
const files: BackupFileInfo[] = [];
|
const files: BackupFileInfo[] = [];
|
||||||
this.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
|
this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
|
||||||
abs.listOfBackupFiles.forEach(f => {
|
abs.listOfBackupFiles.forEach(f => {
|
||||||
if (f.status !== BackupFileInfoStatus.Restored) {
|
if (f.status !== BackupFileInfoStatus.Restored) {
|
||||||
files.push(f);
|
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(
|
azdata.tasks.registerTask(
|
||||||
'sqlmigration.refreshmigrations',
|
'sqlmigration.refreshmigrations',
|
||||||
async (e) => await widget?.refreshMigrations()),
|
async (e) => await widget.refresh()),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.context.subscriptions.push(...commandDisposables);
|
this.context.subscriptions.push(...commandDisposables);
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ export enum MigrationStatus {
|
|||||||
Canceled = 'Canceled',
|
Canceled = 'Canceled',
|
||||||
Completing = 'Completing',
|
Completing = 'Completing',
|
||||||
Creating = 'Creating',
|
Creating = 'Creating',
|
||||||
Canceling = 'Canceling'
|
Canceling = 'Canceling',
|
||||||
|
Retriable = 'Retriable',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProvisioningState {
|
export enum ProvisioningState {
|
||||||
@@ -110,6 +111,6 @@ export enum BackupFileInfoStatus {
|
|||||||
Uploaded = 'Uploaded',
|
Uploaded = 'Uploaded',
|
||||||
Restoring = 'Restoring',
|
Restoring = 'Restoring',
|
||||||
Restored = 'Restored',
|
Restored = 'Restored',
|
||||||
Canceled = 'Canceled',
|
Cancelled = 'Cancelled',
|
||||||
Ignored = 'Ignored'
|
Ignored = 'Ignored'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export enum State {
|
|||||||
EXIT,
|
EXIT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ServiceTier {
|
||||||
|
GeneralPurpose = 'GeneralPurpose',
|
||||||
|
BusinessCritical = 'BusinessCritical',
|
||||||
|
}
|
||||||
|
|
||||||
export enum MigrationTargetType {
|
export enum MigrationTargetType {
|
||||||
SQLVM = 'AzureSqlVirtualMachine',
|
SQLVM = 'AzureSqlVirtualMachine',
|
||||||
SQLMI = 'AzureSqlManagedInstance',
|
SQLMI = 'AzureSqlManagedInstance',
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export enum TelemetryViews {
|
|||||||
IntegrationRuntimePage = 'IntegrationRuntimePage',
|
IntegrationRuntimePage = 'IntegrationRuntimePage',
|
||||||
MigrationCutoverDialog = 'MigrationCutoverDialog',
|
MigrationCutoverDialog = 'MigrationCutoverDialog',
|
||||||
MigrationStatusDialog = 'MigrationStatusDialog',
|
MigrationStatusDialog = 'MigrationStatusDialog',
|
||||||
|
DashboardTab = 'DashboardTab',
|
||||||
|
MigrationsTab = 'MigrationsTab',
|
||||||
MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage',
|
MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage',
|
||||||
MigrationWizardSkuRecommendationPage = 'MigrationWizardSkuRecommendationPage',
|
MigrationWizardSkuRecommendationPage = 'MigrationWizardSkuRecommendationPage',
|
||||||
MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage',
|
MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage',
|
||||||
|
|||||||
Reference in New Issue
Block a user