Add SQL DB offline migration wizard experience (#20403)

* sql db wizard with target selection

* add database table selection

* add sqldb to service and IR page

* Code complete

* navigation bug fixes

* fix target db selection

* improve sqldb error and status reporting

* fix error count bug

* remove table status inference

* address review feedback

* update resource strings and content

* fix migraton status string, use localized value

* fix ux navigation issues

* fix back/fwd w/o changes from changing data
This commit is contained in:
brian-harris
2022-08-19 18:12:34 -07:00
committed by GitHub
parent c0b09dcedd
commit 7a736b76fa
42 changed files with 5716 additions and 4209 deletions

View File

@@ -8,12 +8,12 @@ import * as azdata from 'azdata';
import * as azurecore from 'azurecore'; import * as azurecore from 'azurecore';
import * as constants from '../constants/strings'; import * as constants from '../constants/strings';
import { getSessionIdHeader } from './utils'; import { getSessionIdHeader } from './utils';
import { ProvisioningState } from '../models/migrationLocalStorage';
import { URL } from 'url'; import { URL } from 'url';
const ARM_MGMT_API_VERSION = '2021-04-01'; const ARM_MGMT_API_VERSION = '2021-04-01';
const SQL_VM_API_VERSION = '2021-11-01-preview'; const SQL_VM_API_VERSION = '2021-11-01-preview';
const SQL_MI_API_VERSION = '2021-11-01-preview'; const SQL_MI_API_VERSION = '2021-11-01-preview';
const SQL_SQLDB_API_VERSION = '2021-11-01-preview';
const DMSV2_API_VERSION = '2022-03-30-preview'; const DMSV2_API_VERSION = '2022-03-30-preview';
async function getAzureCoreAPI(): Promise<azurecore.IExtension> { async function getAzureCoreAPI(): Promise<azurecore.IExtension> {
@@ -93,6 +93,100 @@ export async function getAvailableSqlServers(account: azdata.Account, subscripti
return result.resources; return result.resources;
} }
export interface SKU {
name: string,
tier: 'GeneralPurpose' | 'BusinessCritical',
family: string,
capacity: number,
}
export interface AzureSqlDatabase {
id: string,
name: string,
location: string,
tags: any,
type: string,
sku: SKU,
kind: string,
properties: {
collation: string,
maxSizeBytes: number,
status: string,
databaseId: string,
creationDate: string,
currentServiceObjectiveName: string,
requestedServiceObjectiveName: string,
defaultSecondaryLocation: string,
catalogCollation: string,
zoneRedundant: boolean,
earliestRestoreDate: string,
readScale: string,
currentSku: SKU,
currentBackupStorageRedundancy: string,
requestedBackupStorageRedundancy: string,
maintenanceConfigurationId: string,
isLedgerOn: boolean
isInfraEncryptionEnabled: boolean,
licenseType: string,
maxLogSizeBytes: number,
},
}
export interface ServerAdministrators {
administratorType: string,
azureADOnlyAuthentication: boolean,
login: string,
principalType: string,
sid: string,
tenantId: string,
}
export interface PrivateEndpointProperty {
id?: string;
}
export interface PrivateLinkServiceConnectionStateProperty {
status: string;
description: string;
readonly actionsRequired?: string;
}
export interface PrivateEndpointConnectionProperties {
groupIds: string[];
privateEndpoint?: PrivateEndpointProperty;
privateLinkServiceConnectionState?: PrivateLinkServiceConnectionStateProperty;
readonly provisioningState?: string;
}
export interface ServerPrivateEndpointConnection {
readonly id?: string;
readonly properties?: PrivateEndpointConnectionProperties;
}
export interface AzureSqlDatabaseServer {
id: string,
name: string,
kind: string,
location: string,
tags?: { [propertyName: string]: string; };
type: string,
// sku: SKU,
// subscriptionId: string,
// tenantId: string,
// fullName: string,
properties: {
administratorLogin: string,
administrators: ServerAdministrators,
fullyQualifiedDomainName: string,
minimalTlsVersion: string,
privateEndpointConnections: ServerPrivateEndpointConnection[],
publicNetworkAccess: string,
restrictOutboundNetworkAccess: string,
state: string,
version: string,
},
}
export type SqlVMServer = { export type SqlVMServer = {
properties: { properties: {
virtualMachineResourceId: string, virtualMachineResourceId: string,
@@ -108,6 +202,31 @@ export type SqlVMServer = {
tenantId: string, tenantId: string,
subscriptionId: string subscriptionId: string
}; };
export async function getAvailableSqlDatabaseServers(account: azdata.Account, subscription: Subscription): Promise<AzureSqlDatabaseServer[]> {
const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.Sql/servers?api-version=${SQL_SQLDB_API_VERSION}`);
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
}
export async function getAvailableSqlDatabases(account: azdata.Account, subscription: Subscription, resourceGroupName: string, serverName: string): Promise<AzureSqlDatabase[]> {
const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/servers/${serverName}/databases?api-version=${SQL_SQLDB_API_VERSION}`);
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
}
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> { export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=${SQL_VM_API_VERSION}`); const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=${SQL_VM_API_VERSION}`);
@@ -203,10 +322,10 @@ export async function getSqlMigrationServices(account: azdata.Account, subscript
export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise<SqlMigrationService> { export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise<SqlMigrationService> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=${DMSV2_API_VERSION}`); const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=${DMSV2_API_VERSION}`);
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const requestBody = { const requestBody = {
'location': regionName 'location': regionName
}; };
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host, getSessionIdHeader(sessionId));
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
@@ -219,9 +338,9 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
for (i = 0; i < maxRetry; i++) { for (i = 0; i < maxRetry; i++) {
const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncPath, azurecore.HttpRequestMethod.GET, undefined, true, host); const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncPath, azurecore.HttpRequestMethod.GET, undefined, true, host);
const creationStatus = asyncResponse.response.data.status; const creationStatus = asyncResponse.response.data.status;
if (creationStatus === ProvisioningState.Succeeded) { if (creationStatus === constants.ProvisioningState.Succeeded) {
break; break;
} else if (creationStatus === ProvisioningState.Failed) { } else if (creationStatus === constants.ProvisioningState.Failed) {
throw new Error(asyncResponse.errors.toString()); throw new Error(asyncResponse.errors.toString());
} }
await new Promise(resolve => setTimeout(resolve, 5000)); //adding 5 sec delay before getting creation status await new Promise(resolve => setTimeout(resolve, 5000)); //adding 5 sec delay before getting creation status
@@ -287,7 +406,15 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou
return response.response.data; return response.response.data;
} }
export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest, sessionId: string): Promise<StartDatabaseMigrationResponse> { export async function startDatabaseMigration(
account: azdata.Account,
subscription: Subscription,
regionName: string,
targetServer: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer,
targetDatabaseName: string,
requestBody: StartDatabaseMigrationRequest,
sessionId: string): Promise<StartDatabaseMigrationResponse> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=${DMSV2_API_VERSION}`); const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=${DMSV2_API_VERSION}`);
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint; const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
@@ -438,10 +565,10 @@ export interface SqlMigrationServiceProperties {
} }
export interface SqlMigrationService { export interface SqlMigrationService {
properties: SqlMigrationServiceProperties;
location: string;
id: string; id: string;
name: string; name: string;
location: string;
properties: SqlMigrationServiceProperties;
error: { error: {
code: string, code: string,
message: string message: string
@@ -485,14 +612,29 @@ export interface StartDatabaseMigrationRequest {
}, },
sourceLocation?: SourceLocation sourceLocation?: SourceLocation
}, },
sourceSqlConnection: { targetSqlConnection?: {
authentication: string,
dataSource: string, dataSource: string,
username: string, authentication: string,
password: string userName: string,
password: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
}, },
sourceSqlConnection: {
dataSource: string,
authentication: string,
userName: string,
password: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
},
sqlDataCopyThresholds?: {
cidxrowthreshold: number,
cidxkbsthreshold: number,
},
tableList?: string[],
scope: string, scope: string,
offlineConfiguration: OfflineConfiguration, offlineConfiguration?: OfflineConfiguration,
} }
} }
@@ -503,10 +645,10 @@ export interface StartDatabaseMigrationResponse {
} }
export interface DatabaseMigration { export interface DatabaseMigration {
properties: DatabaseMigrationProperties;
id: string; id: string;
name: string; name: string;
type: string; type: string;
properties: DatabaseMigrationProperties;
} }
export interface DatabaseMigrationProperties { export interface DatabaseMigrationProperties {
@@ -525,6 +667,7 @@ export interface DatabaseMigrationProperties {
backupConfiguration: BackupConfiguration; backupConfiguration: BackupConfiguration;
offlineConfiguration: OfflineConfiguration; offlineConfiguration: OfflineConfiguration;
migrationFailureError: ErrorInfo; migrationFailureError: ErrorInfo;
tableList: string[];
} }
export interface MigrationStatusDetails { export interface MigrationStatusDetails {
@@ -543,6 +686,7 @@ export interface MigrationStatusDetails {
pendingLogBackupsCount: number; pendingLogBackupsCount: number;
invalidFiles: string[]; invalidFiles: string[];
listOfCopyProgressDetails: CopyProgressDetail[]; listOfCopyProgressDetails: CopyProgressDetail[];
sqlDataCopyErrors: string[];
} }
export interface CopyProgressDetail { export interface CopyProgressDetail {

View File

@@ -0,0 +1,282 @@
/*---------------------------------------------------------------------------------------------
* 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 { azureResource } from 'azurecore';
import { AzureSqlDatabase, AzureSqlDatabaseServer } from './azure';
import { generateGuid } from './utils';
import * as utils from '../api/utils';
const query_database_tables_sql = `
SELECT
DB_NAME() as database_name,
QUOTENAME(SCHEMA_NAME(o.schema_id)) + '.' + QUOTENAME(o.name) AS table_name,
SUM(p.Rows) AS row_count
FROM
sys.objects AS o
INNER JOIN sys.partitions AS p
ON o.object_id = p.object_id
WHERE
o.type = 'U'
AND o.is_ms_shipped = 0x0
AND index_id < 2
GROUP BY
o.schema_id,
o.name
ORDER BY table_name;`;
const query_target_databases_sql = `
SELECT
('servername') as server_name,
SERVERPROPERTY ('collation') as server_collation,
db.database_id as database_id,
db.name as database_name,
db.collation_name as database_collation,
CASE WHEN 'A' = 'a' THEN 0 ELSE 1 END as is_server_case_sensitive,
db.state as database_state,
db.is_read_only as is_read_only
FROM sys.databases db
WHERE
db.name not in ('master', 'tempdb', 'model', 'msdb')
AND is_distributor <> 1
ORDER BY db.name;`;
export const excludeDatabses: string[] = [
'master',
'tempdb',
'msdb',
'model'
];
export enum AuthenticationType {
Integrated = 'Integrated',
SqlLogin = 'SqlLogin'
}
export interface TableInfo {
databaseName: string;
tableName: string;
rowCount: number;
selectedForMigration: boolean;
}
export interface TargetDatabaseInfo {
serverName: string;
serverCollation: string;
databaseId: string;
databaseName: string;
databaseCollation: string;
isServerCaseSensitive: boolean;
databaseState: number;
isReadOnly: boolean;
sourceTables: Map<string, TableInfo>;
targetTables: Map<string, TableInfo>;
}
function getSqlDbConnectionProfile(
serverName: string,
tenantId: string,
databaseName: string,
userName: string,
password: string): azdata.IConnectionProfile {
return {
id: generateGuid(),
providerName: 'MSSQL',
connectionName: '',
serverName: serverName,
databaseName: databaseName,
userName: userName,
password: password,
authenticationType: AuthenticationType.SqlLogin,
savePassword: false,
saveProfile: false,
options: {
conectionName: '',
server: serverName,
database: databaseName,
authenticationType: AuthenticationType.SqlLogin,
user: userName,
password: password,
connectionTimeout: 60,
columnEncryptionSetting: 'Enabled',
encrypt: true,
trustServerCertificate: false,
connectRetryCount: '1',
connectRetryInterval: '10',
applicationName: 'azdata',
azureTenantId: tenantId,
originalDatabase: databaseName,
databaseDisplayName: databaseName,
},
};
}
function getConnectionProfile(
serverName: string,
azureResourceId: string,
userName: string,
password: string): azdata.IConnectionProfile {
return {
serverName: serverName,
id: generateGuid(),
connectionName: undefined,
azureResourceId: azureResourceId,
userName: userName,
password: password,
authenticationType: AuthenticationType.SqlLogin,
savePassword: false,
groupFullName: '',
groupId: '',
providerName: 'MSSQL',
saveProfile: false,
options: {
conectionName: '',
server: serverName,
authenticationType: AuthenticationType.SqlLogin,
user: userName,
password: password,
connectionTimeout: 60,
columnEncryptionSetting: 'Enabled',
encrypt: true,
trustServerCertificate: false,
connectRetryCount: '1',
connectRetryInterval: '10',
applicationName: 'azdata',
},
};
}
export async function collectSourceDatabaseTableInfo(sourceConnectionId: string, sourceDatabase: string): Promise<TableInfo[]> {
const ownerUri = await azdata.connection.getUriForConnection(sourceConnectionId);
const connectionProvider = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(
'MSSQL',
azdata.DataProviderType.ConnectionProvider);
await connectionProvider.changeDatabase(ownerUri, sourceDatabase);
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
'MSSQL',
azdata.DataProviderType.QueryProvider);
const results = await queryProvider.runQueryAndReturn(
ownerUri,
query_database_tables_sql);
return results.rows.map(row => {
return {
databaseName: getSqlString(row[0]),
tableName: getSqlString(row[1]),
rowCount: getSqlNumber(row[2]),
selectedForMigration: false,
};
}) ?? [];
}
export async function collectTargetDatabaseTableInfo(
targetServer: AzureSqlDatabaseServer,
targetDatabaseName: string,
tenantId: string,
userName: string,
password: string): Promise<TableInfo[]> {
const connectionProfile = getSqlDbConnectionProfile(
targetServer.properties.fullyQualifiedDomainName,
tenantId,
targetDatabaseName,
userName,
password);
const result = await azdata.connection.connect(connectionProfile, false, false);
if (result.connected && result.connectionId) {
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
'MSSQL',
azdata.DataProviderType.QueryProvider);
const ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
const results = await queryProvider.runQueryAndReturn(
ownerUri,
query_database_tables_sql);
return results.rows.map(row => {
return {
databaseName: getSqlString(row[0]),
tableName: getSqlString(row[1]),
rowCount: getSqlNumber(row[2]),
selectedForMigration: false,
};
}) ?? [];
}
throw new Error(result.errorMessage);
}
export async function collectTargetDatabaseInfo(
targetServer: AzureSqlDatabaseServer,
userName: string,
password: string): Promise<TargetDatabaseInfo[]> {
const connectionProfile = getConnectionProfile(
targetServer.properties.fullyQualifiedDomainName,
targetServer.id,
userName,
password);
const result = await azdata.connection.connect(connectionProfile, false, false);
if (result.connected && result.connectionId) {
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
'MSSQL',
azdata.DataProviderType.QueryProvider);
const ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
const results = await queryProvider.runQueryAndReturn(
ownerUri,
query_target_databases_sql);
return results.rows.map(row => {
return {
serverName: getSqlString(row[0]),
serverCollation: getSqlString(row[1]),
databaseId: getSqlString(row[2]),
databaseName: getSqlString(row[3]),
databaseCollation: getSqlString(row[4]),
isServerCaseSensitive: getSqlBoolean(row[5]),
databaseState: getSqlNumber(row[6]),
isReadOnly: getSqlBoolean(row[7]),
sourceTables: new Map(),
targetTables: new Map(),
};
}) ?? [];
}
throw new Error(result.errorMessage);
}
export async function collectAzureTargetDatabases(
account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
resourceGroup: string,
targetServerName: string,
): Promise<AzureSqlDatabase[]> {
const databaseList: AzureSqlDatabase[] = [];
if (resourceGroup && targetServerName) {
databaseList.push(...
await utils.getAzureSqlDatabases(
account,
subscription,
resourceGroup,
targetServerName));
}
return databaseList.filter(
database => !excludeDatabses.includes(database.name)) ?? [];
}
export function getSqlString(value: azdata.DbCellValue): string {
return value.isNull ? '' : value.displayValue;
}
export function getSqlNumber(value: azdata.DbCellValue): number {
return value.isNull ? 0 : parseInt(value.displayValue);
}
export function getSqlBoolean(value: azdata.DbCellValue): boolean {
return value.isNull ? false : value.displayValue === '1';
}

View File

@@ -3,10 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata'; import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath, DisplayType, Component } from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
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';
@@ -15,8 +14,26 @@ import { logError, TelemetryViews } from '../telemtery';
import { AdsMigrationStatus } from '../dashboard/tabBase'; import { AdsMigrationStatus } from '../dashboard/tabBase';
import { getMigrationMode, getMigrationStatus, getMigrationTargetType, PipelineStatusCodes } from '../constants/helper'; import { getMigrationMode, getMigrationStatus, getMigrationTargetType, PipelineStatusCodes } from '../constants/helper';
export type TargetServerType = azure.SqlVMServer | azureResource.AzureSqlManagedInstance | azure.AzureSqlDatabaseServer;
export const SqlMigrationExtensionId = 'microsoft.sql-migration';
export const DefaultSettingValue = '---'; export const DefaultSettingValue = '---';
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',
OpenNotebooks: 'sqlmigration.openNotebooks',
NewSupportRequest: 'sqlmigration.newsupportrequest',
SendFeedback: 'sqlmigration.sendfeedback',
};
export function deepClone<T>(obj: T): T { export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') { if (!obj || typeof obj !== 'object') {
return obj; return obj;
@@ -145,19 +162,19 @@ export function filterMigrations(databaseMigrations: azure.DatabaseMigration[],
return filteredMigration.filter( return filteredMigration.filter(
value => { value => {
const status = getMigrationStatus(value); const status = getMigrationStatus(value);
return status === MigrationStatus.InProgress return status === constants.MigrationStatus.InProgress
|| status === MigrationStatus.Retriable || status === constants.MigrationStatus.Retriable
|| status === MigrationStatus.Creating; || status === constants.MigrationStatus.Creating;
}); });
case AdsMigrationStatus.SUCCEEDED: case AdsMigrationStatus.SUCCEEDED:
return filteredMigration.filter( return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Succeeded); value => getMigrationStatus(value) === constants.MigrationStatus.Succeeded);
case AdsMigrationStatus.FAILED: case AdsMigrationStatus.FAILED:
return filteredMigration.filter( return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Failed); value => getMigrationStatus(value) === constants.MigrationStatus.Failed);
case AdsMigrationStatus.COMPLETING: case AdsMigrationStatus.COMPLETING:
return filteredMigration.filter( return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Completing); value => getMigrationStatus(value) === constants.MigrationStatus.Completing);
} }
return filteredMigration; return filteredMigration;
} }
@@ -192,6 +209,8 @@ export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?:
selectedIndex = -1; selectedIndex = -1;
} }
selectDropDownIndex(dropDown, selectedIndex > -1 ? selectedIndex : 0); selectDropDownIndex(dropDown, selectedIndex > -1 ? selectedIndex : 0);
} else {
dropDown.value = undefined;
} }
} }
@@ -251,19 +270,22 @@ export function getSessionIdHeader(sessionId: string): { [key: string]: string }
export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration): string { export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration): string {
const properties = migration.properties; const properties = migration.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState; const migrationStatus = getMigrationStatus(migration) ?? '';
let warningCount = 0;
if (properties.migrationFailureError?.message) { // provisioning error
warningCount++; let warningCount = properties.provisioningError?.length > 0 ? 1 : 0;
}
if (properties.migrationStatusDetails?.fileUploadBlockingErrors) { // migration failure error
const blockingErrors = properties.migrationStatusDetails?.fileUploadBlockingErrors.length ?? 0; warningCount += properties.migrationFailureError?.message?.length > 0 ? 1 : 0;
warningCount += blockingErrors;
} // file upload blocking errors
if (properties.migrationStatusDetails?.restoreBlockingReason) { warningCount += properties.migrationStatusDetails?.fileUploadBlockingErrors?.length ?? 0;
warningCount++;
} // restore blocking reason
warningCount += properties.migrationStatusDetails?.restoreBlockingReason ? 1 : 0;
// sql data copy errors
warningCount += properties.migrationStatusDetails?.sqlDataCopyErrors?.length ?? 0;
return constants.STATUS_VALUE(migrationStatus, warningCount) return constants.STATUS_VALUE(migrationStatus, warningCount)
+ (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? ''); + (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
@@ -302,20 +324,20 @@ export function getPipelineStatusImage(status: string | undefined): IconPath {
export function getMigrationStatusImage(migration: azure.DatabaseMigration): IconPath { export function getMigrationStatusImage(migration: azure.DatabaseMigration): IconPath {
const status = getMigrationStatus(migration); const status = getMigrationStatus(migration);
switch (status) { switch (status) {
case MigrationStatus.InProgress: case constants.MigrationStatus.InProgress:
return IconPathHelper.inProgressMigration; return IconPathHelper.inProgressMigration;
case MigrationStatus.Succeeded: case constants.MigrationStatus.Succeeded:
return IconPathHelper.completedMigration; return IconPathHelper.completedMigration;
case MigrationStatus.Creating: case constants.MigrationStatus.Creating:
return IconPathHelper.notStartedMigration; return IconPathHelper.notStartedMigration;
case MigrationStatus.Completing: case constants.MigrationStatus.Completing:
return IconPathHelper.completingCutover; return IconPathHelper.completingCutover;
case MigrationStatus.Retriable: case constants.MigrationStatus.Retriable:
return IconPathHelper.retry; return IconPathHelper.retry;
case MigrationStatus.Canceling: case constants.MigrationStatus.Canceling:
case MigrationStatus.Canceled: case constants.MigrationStatus.Canceled:
return IconPathHelper.cancel; return IconPathHelper.cancel;
case MigrationStatus.Failed: case constants.MigrationStatus.Failed:
default: default:
return IconPathHelper.error; return IconPathHelper.error;
} }
@@ -379,34 +401,7 @@ export async function getAzureAccountsDropdownValues(accounts: Account[]): Promi
} }
export function getAzureTenants(account?: Account): Tenant[] { export function getAzureTenants(account?: Account): Tenant[] {
let tenants: Tenant[] = []; return account?.properties.tenants || [];
try {
if (account) {
tenants = account.properties.tenants;
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getAzureTenants', e);
}
return tenants;
}
export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise<CategoryValue[]> {
let tenantsValues: CategoryValue[] = [];
tenants.forEach((tenant) => {
tenantsValues.push({
name: tenant.id,
displayName: tenant.displayName
});
});
if (tenantsValues.length === 0) {
tenantsValues = [
{
displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR,
name: ''
}
];
}
return tenantsValues;
} }
export async function getAzureSubscriptions(account?: Account): Promise<azureResource.AzureResourceSubscription[]> { export async function getAzureSubscriptions(account?: Account): Promise<azureResource.AzureResourceSubscription[]> {
@@ -441,172 +436,58 @@ export async function getAzureSubscriptionsDropdownValues(subscriptions: azureRe
return subscriptionsValues; return subscriptionsValues;
} }
export async function getSqlManagedInstanceLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, managedInstances?: azureResource.AzureSqlManagedInstance[]): Promise<azureResource.AzureLocation[]> { export async function getResourceLocations(
let locations: azureResource.AzureLocation[] = []; account?: Account,
subscription?: azureResource.AzureResourceSubscription,
resources?: { location: string }[]): Promise<azureResource.AzureLocation[]> {
try { try {
if (account && subscription && managedInstances) { if (account && subscription && resources) {
locations = await azure.getLocations(account, subscription); const locations = await azure.getLocations(account, subscription);
locations = locations.filter((loc, i) => managedInstances.some(mi => mi.location.toLowerCase() === loc.name.toLowerCase())); return locations
.filter((loc, i) => resources.some(resource => resource.location.toLowerCase() === loc.name.toLowerCase()))
.sort((a, b) => a.displayName.localeCompare(b.displayName));
} }
} catch (e) { } catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceLocations', e); logError(TelemetryViews.Utils, 'utils.getResourceLocations', e);
} }
locations.sort((a, b) => a.displayName.localeCompare(b.displayName)); return [];
return locations;
} }
export async function getSqlVirtualMachineLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, virtualMachines?: azure.SqlVMServer[]): Promise<azureResource.AzureLocation[]> { export function getServiceResourceGroupsByLocation(
let locations: azureResource.AzureLocation[] = []; resources: { location: string, id: string, tenantId?: string }[],
try { location: azureResource.AzureLocation): azureResource.AzureResourceResourceGroup[] {
if (account && subscription && virtualMachines) {
locations = await azure.getLocations(account, subscription);
locations = locations.filter((loc, i) => virtualMachines.some(vm => vm.location.toLowerCase() === loc.name.toLowerCase()));
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlVirtualMachineLocations', e);
}
locations.sort((a, b) => a.displayName.localeCompare(b.displayName));
return locations;
}
export async function getSqlMigrationServiceLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, migrationServices?: azure.SqlMigrationService[]): Promise<azureResource.AzureLocation[]> {
let locations: azureResource.AzureLocation[] = [];
try {
if (account && subscription && migrationServices) {
locations = await azure.getLocations(account, subscription);
locations = locations.filter((loc, i) => migrationServices.some(dms => dms.location.toLowerCase() === loc.name.toLowerCase()));
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlMigrationServiceLocations', e);
}
locations.sort((a, b) => a.displayName.localeCompare(b.displayName));
return locations;
}
export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise<CategoryValue[]> {
let locationValues: CategoryValue[] = [];
locations.forEach((loc) => {
locationValues.push({
name: loc.name,
displayName: loc.displayName
});
});
if (locationValues.length === 0) {
locationValues = [
{
displayName: constants.NO_LOCATION_FOUND,
name: ''
}
];
}
return locationValues;
}
export async function getSqlManagedInstanceResourceGroups(managedInstances?: azureResource.AzureSqlManagedInstance[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
try { if (resources && location) {
if (managedInstances && location) { const locationName = location.name.toLowerCase();
resourceGroups = managedInstances resourceGroups = resources
.filter((mi) => mi.location.toLowerCase() === location.name.toLowerCase()) .filter(resource => resource.location.toLowerCase() === locationName)
.map((mi) => { .map(resource => {
return <azureResource.AzureResourceResourceGroup>{ return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(mi.id), id: azure.getFullResourceGroupFromId(resource.id),
name: azure.getResourceGroupFromId(mi.id), name: azure.getResourceGroupFromId(resource.id),
subscription: { subscription: { id: getSubscriptionIdFromResourceId(resource.id) },
id: mi.subscriptionId tenant: resource.tenantId
},
tenant: mi.tenantId
}; };
}); });
} }
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceResourceGroups', e);
}
// remove duplicates // remove duplicates
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); return resourceGroups
resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); .filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i)
return resourceGroups; .sort((a, b) => a.name.localeCompare(b.name));
} }
export async function getSqlVirtualMachineResourceGroups(virtualMachines?: azure.SqlVMServer[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> { export function getSubscriptionIdFromResourceId(resourceId: string): string | undefined {
let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; let parts = resourceId?.split('/subscriptions/');
try { if (parts?.length > 1) {
if (virtualMachines && location) { parts = parts[1]?.split('/resourcegroups/');
resourceGroups = virtualMachines if (parts?.length > 0) {
.filter((vm) => vm.location.toLowerCase() === location.name.toLowerCase()) return parts[0];
.map((vm) => {
return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(vm.id),
name: azure.getResourceGroupFromId(vm.id),
subscription: {
id: vm.subscriptionId
},
tenant: vm.tenantId
};
});
} }
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlVirtualMachineResourceGroups', e);
} }
return undefined;
// remove duplicates
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i);
resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
return resourceGroups;
}
export async function getStorageAccountResourceGroups(storageAccounts?: azure.StorageAccount[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
try {
if (storageAccounts && location) {
resourceGroups = storageAccounts
.filter((sa) => sa.location.toLowerCase() === location.name.toLowerCase())
.map((sa) => {
return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(sa.id),
name: azure.getResourceGroupFromId(sa.id),
subscription: {
id: sa.subscriptionId
},
tenant: sa.tenantId
};
});
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getStorageAccountResourceGroups', e);
}
// remove duplicates
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i);
resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
return resourceGroups;
}
export async function getSqlMigrationServiceResourceGroups(migrationServices?: azure.SqlMigrationService[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
try {
if (migrationServices && location) {
resourceGroups = migrationServices
.filter((dms) => dms.properties.provisioningState === ProvisioningState.Succeeded && dms.location.toLowerCase() === location.name.toLowerCase())
.map((dms) => {
return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(dms.id),
name: azure.getResourceGroupFromId(dms.id),
subscription: {
id: dms.properties.subscriptionId
},
};
});
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlMigrationServiceResourceGroups', e);
}
// remove duplicates
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i);
resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
return resourceGroups;
} }
export async function getAllResourceGroups(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azureResource.AzureResourceResourceGroup[]> { export async function getAllResourceGroups(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azureResource.AzureResourceResourceGroup[]> {
@@ -622,25 +503,6 @@ export async function getAllResourceGroups(account?: Account, subscription?: azu
return resourceGroups; return resourceGroups;
} }
export async function getAzureResourceGroupsDropdownValues(resourceGroups: azureResource.AzureResourceResourceGroup[]): Promise<CategoryValue[]> {
let resourceGroupValues: CategoryValue[] = [];
resourceGroups.forEach((rg) => {
resourceGroupValues.push({
name: rg.id,
displayName: rg.name
});
});
if (resourceGroupValues.length === 0) {
resourceGroupValues = [
{
displayName: constants.RESOURCE_GROUP_NOT_FOUND,
name: ''
}
];
}
return resourceGroupValues;
}
export async function getManagedInstances(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azureResource.AzureSqlManagedInstance[]> { export async function getManagedInstances(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azureResource.AzureSqlManagedInstance[]> {
let managedInstances: azureResource.AzureSqlManagedInstance[] = []; let managedInstances: azureResource.AzureSqlManagedInstance[] = [];
try { try {
@@ -687,6 +549,31 @@ export async function getManagedInstancesDropdownValues(managedInstances: azureR
return managedInstancesValues; return managedInstancesValues;
} }
export async function getAzureSqlDatabaseServers(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.AzureSqlDatabaseServer[]> {
let sqlDatabaseServers: azure.AzureSqlDatabaseServer[] = [];
try {
if (account && subscription) {
sqlDatabaseServers = await azure.getAvailableSqlDatabaseServers(account, subscription);
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getAzureSqlDatabaseServers', e);
}
sqlDatabaseServers.sort((a, b) => a.name.localeCompare(b.name));
return sqlDatabaseServers;
}
export async function getAzureSqlDatabases(account?: Account, subscription?: azureResource.AzureResourceSubscription, resourceGroupName?: string, serverName?: string): Promise<azure.AzureSqlDatabase[]> {
if (account && subscription && resourceGroupName && serverName) {
try {
const databases = await azure.getAvailableSqlDatabases(account, subscription, resourceGroupName, serverName);
return databases.sort((a, b) => a.name.localeCompare(b.name));
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getAzureSqlDatabases', e);
}
}
return [];
}
export async function getVirtualMachines(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.SqlVMServer[]> { export async function getVirtualMachines(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.SqlVMServer[]> {
let virtualMachines: azure.SqlVMServer[] = []; let virtualMachines: azure.SqlVMServer[] = [];
try { try {
@@ -705,30 +592,6 @@ export async function getVirtualMachines(account?: Account, subscription?: azure
return virtualMachines; return virtualMachines;
} }
export async function getVirtualMachinesDropdownValues(virtualMachines: azure.SqlVMServer[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<CategoryValue[]> {
let virtualMachineValues: CategoryValue[] = [];
if (location && resourceGroup) {
virtualMachines.forEach((virtualMachine) => {
if (virtualMachine.location.toLowerCase() === location.name.toLowerCase() && azure.getResourceGroupFromId(virtualMachine.id).toLowerCase() === resourceGroup.name.toLowerCase()) {
virtualMachineValues.push({
name: virtualMachine.id,
displayName: virtualMachine.name
});
}
});
}
if (virtualMachineValues.length === 0) {
virtualMachineValues = [
{
displayName: constants.NO_VIRTUAL_MACHINE_FOUND,
name: ''
}
];
}
return virtualMachineValues;
}
export async function getStorageAccounts(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.StorageAccount[]> { export async function getStorageAccounts(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.StorageAccount[]> {
let storageAccounts: azure.StorageAccount[] = []; let storageAccounts: azure.StorageAccount[] = [];
try { try {
@@ -742,65 +605,18 @@ export async function getStorageAccounts(account?: Account, subscription?: azure
return storageAccounts; return storageAccounts;
} }
export async function getStorageAccountsDropdownValues(storageAccounts: azure.StorageAccount[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<CategoryValue[]> {
let storageAccountValues: CategoryValue[] = [];
storageAccounts.forEach((storageAccount) => {
if (storageAccount.location.toLowerCase() === location.name.toLowerCase() && storageAccount.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase()) {
storageAccountValues.push({
name: storageAccount.id,
displayName: storageAccount.name
});
}
});
if (storageAccountValues.length === 0) {
storageAccountValues = [
{
displayName: constants.NO_STORAGE_ACCOUNT_FOUND,
name: ''
}
];
}
return storageAccountValues;
}
export async function getAzureSqlMigrationServices(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.SqlMigrationService[]> { export async function getAzureSqlMigrationServices(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.SqlMigrationService[]> {
let sqlMigrationServices: azure.SqlMigrationService[] = [];
try { try {
if (account && subscription) { if (account && subscription) {
sqlMigrationServices = (await azure.getSqlMigrationServices(account, subscription)).filter(dms => { const services = await azure.getSqlMigrationServices(account, subscription);
return dms.properties.provisioningState === ProvisioningState.Succeeded; return services
}); .filter(dms => dms.properties.provisioningState === constants.ProvisioningState.Succeeded)
.sort((a, b) => a.name.localeCompare(b.name));
} }
} catch (e) { } catch (e) {
logError(TelemetryViews.Utils, 'utils.getAzureSqlMigrationServices', e); logError(TelemetryViews.Utils, 'utils.getAzureSqlMigrationServices', e);
} }
sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name)); return [];
return sqlMigrationServices;
}
export async function getAzureSqlMigrationServicesDropdownValues(sqlMigrationServices: azure.SqlMigrationService[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<CategoryValue[]> {
let SqlMigrationServicesValues: CategoryValue[] = [];
if (location && resourceGroup) {
sqlMigrationServices.forEach((sqlMigrationService) => {
if (sqlMigrationService.location.toLowerCase() === location.name.toLowerCase() && sqlMigrationService.properties.resourceGroup.toLowerCase() === resourceGroup.name.toLowerCase()) {
SqlMigrationServicesValues.push({
name: sqlMigrationService.id,
displayName: sqlMigrationService.name
});
}
});
}
if (SqlMigrationServicesValues.length === 0) {
SqlMigrationServicesValues = [
{
displayName: constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR,
name: ''
}
];
}
return SqlMigrationServicesValues;
} }
export async function getBlobContainer(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount): Promise<azureResource.BlobContainer[]> { export async function getBlobContainer(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount): Promise<azureResource.BlobContainer[]> {
@@ -816,25 +632,6 @@ export async function getBlobContainer(account?: Account, subscription?: azureRe
return blobContainers; return blobContainers;
} }
export async function getBlobContainersValues(blobContainers: azureResource.BlobContainer[]): Promise<CategoryValue[]> {
let blobContainersValues: CategoryValue[] = [];
blobContainers.forEach((blobContainer) => {
blobContainersValues.push({
name: blobContainer.id,
displayName: blobContainer.name
});
});
if (blobContainersValues.length === 0) {
blobContainersValues = [
{
displayName: constants.NO_BLOBCONTAINERS_FOUND,
name: ''
}
];
}
return blobContainersValues;
}
export async function getBlobLastBackupFileNames(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount, blobContainer?: azureResource.BlobContainer): Promise<azureResource.Blob[]> { export async function getBlobLastBackupFileNames(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount, blobContainer?: azureResource.BlobContainer): Promise<azureResource.Blob[]> {
let lastFileNames: azureResource.Blob[] = []; let lastFileNames: azureResource.Blob[] = [];
try { try {
@@ -848,39 +645,91 @@ export async function getBlobLastBackupFileNames(account?: Account, subscription
return lastFileNames; return lastFileNames;
} }
export async function getBlobLastBackupFileNamesValues(lastFileNames: azureResource.Blob[]): Promise<CategoryValue[]> { export function getAzureResourceDropdownValues(
let lastFileNamesValues: CategoryValue[] = []; azureResources: { location: string, id: string, name: string }[],
lastFileNames.forEach((lastFileName) => { location: azureResource.AzureLocation | undefined,
lastFileNamesValues.push({ resourceGroup: string | undefined,
name: lastFileName.name, resourceNotFoundMessage: string): CategoryValue[] {
displayName: lastFileName.name
if (location?.name && resourceGroup && azureResources?.length > 0) {
const locationName = location.name.toLowerCase();
const resourceGroupName = resourceGroup.toLowerCase();
return azureResources
.filter(resource =>
resource.location?.toLowerCase() === locationName &&
azure.getResourceGroupFromId(resource.id)?.toLowerCase() === resourceGroupName)
.map(resource => {
return { name: resource.id, displayName: resource.name };
}); });
});
if (lastFileNamesValues.length === 0) {
lastFileNamesValues = [
{
displayName: constants.NO_BLOBFILES_FOUND,
name: ''
} }
];
return [{ name: '', displayName: resourceNotFoundMessage }];
}
export function getResourceDropdownValues(resources: { id: string, name: string }[], resourceNotFoundMessage: string): CategoryValue[] {
return resources?.map(resource => { return { name: resource.id, displayName: resource.name }; })
|| [{ name: '', displayName: resourceNotFoundMessage }];
}
export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise<CategoryValue[]> {
return tenants?.map(tenant => { return { name: tenant.id, displayName: tenant.displayName }; })
|| [{ name: '', displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR }];
}
export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise<CategoryValue[]> {
return locations?.map(location => { return { name: location.name, displayName: location.displayName }; })
|| [{ name: '', displayName: constants.NO_LOCATION_FOUND }];
}
export async function getBlobLastBackupFileNamesValues(blobs: azureResource.Blob[]): Promise<CategoryValue[]> {
return blobs?.map(blob => { return { name: blob.name, displayName: blob.name }; })
|| [{ name: '', displayName: constants.NO_BLOBFILES_FOUND }];
}
export async function updateControlDisplay(control: Component, visible: boolean, displayStyle: DisplayType = 'inline'): Promise<void> {
const display = visible ? displayStyle : 'none';
control.display = display;
await control.updateCssStyles({ 'display': display });
await control.updateProperties({ 'display': display });
}
export function generateGuid(): string {
const hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
let oct: string = '';
let tmp: number;
/* tslint:disable:no-bitwise */
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
} }
return lastFileNamesValues;
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
const clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
return `${oct.substr(0, 8)}-${oct.substr(9, 4)}-4${oct.substr(13, 3)}-${clockSequenceHi}${oct.substr(16, 3)}-${oct.substr(19, 12)}`;
/* tslint:enable:no-bitwise */
} }
export async function promptUserForFolder(): Promise<string> { export async function promptUserForFolder(): Promise<string> {
let path = ''; const options: vscode.OpenDialogOptions = {
let options: vscode.OpenDialogOptions = {
defaultUri: vscode.Uri.file(getUserHome()!), defaultUri: vscode.Uri.file(getUserHome()!),
canSelectFiles: false, canSelectFiles: false,
canSelectFolders: true, canSelectFolders: true,
canSelectMany: false, canSelectMany: false,
}; };
let fileUris = await vscode.window.showOpenDialog(options); const fileUris = await vscode.window.showOpenDialog(options);
if (fileUris && fileUris?.length > 0 && fileUris[0]) { if (fileUris && fileUris.length > 0 && fileUris[0]) {
path = fileUris[0].fsPath; return fileUris[0].fsPath;
} }
return path; return '';
} }

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { DatabaseMigration } from '../api/azure'; import { DatabaseMigration } from '../api/azure';
import { MigrationStatus } from '../models/migrationLocalStorage'; import { DefaultSettingValue } from '../api/utils';
import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine'; import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine';
import * as loc from './strings'; import * as loc from './strings';
@@ -143,6 +143,11 @@ export function getMigrationStatus(migration: DatabaseMigration | undefined): st
?? migration?.properties.provisioningState; ?? migration?.properties.provisioningState;
} }
export function getMigrationStatusString(migration: DatabaseMigration | undefined): string {
const migrationStatus = getMigrationStatus(migration) ?? DefaultSettingValue;
return loc.StatusLookup[migrationStatus] ?? migrationStatus;
}
export function hasMigrationOperationId(migration: DatabaseMigration | undefined): boolean { export function hasMigrationOperationId(migration: DatabaseMigration | undefined): boolean {
const migrationId = migration?.id ?? ''; const migrationId = migration?.id ?? '';
const migationOperationId = migration?.properties?.migrationOperationId ?? ''; const migationOperationId = migration?.properties?.migrationOperationId ?? '';
@@ -153,41 +158,41 @@ export function hasMigrationOperationId(migration: DatabaseMigration | undefined
export function canCancelMigration(migration: DatabaseMigration | undefined): boolean { export function canCancelMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration); const status = getMigrationStatus(migration);
return hasMigrationOperationId(migration) return hasMigrationOperationId(migration)
&& (status === MigrationStatus.InProgress || && (status === loc.MigrationStatus.InProgress ||
status === MigrationStatus.Retriable || status === loc.MigrationStatus.Retriable ||
status === MigrationStatus.Creating); status === loc.MigrationStatus.Creating);
} }
export function canDeleteMigration(migration: DatabaseMigration | undefined): boolean { export function canDeleteMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration); const status = getMigrationStatus(migration);
return status === MigrationStatus.Canceled return status === loc.MigrationStatus.Canceled
|| status === MigrationStatus.Failed || status === loc.MigrationStatus.Failed
|| status === MigrationStatus.Retriable || status === loc.MigrationStatus.Retriable
|| status === MigrationStatus.Succeeded; || status === loc.MigrationStatus.Succeeded;
} }
export function canRetryMigration(migration: DatabaseMigration | undefined): boolean { export function canRetryMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration); const status = getMigrationStatus(migration);
return status === MigrationStatus.Canceled return status === loc.MigrationStatus.Canceled
|| status === MigrationStatus.Retriable || status === loc.MigrationStatus.Retriable
|| status === MigrationStatus.Failed || status === loc.MigrationStatus.Failed
|| status === MigrationStatus.Succeeded; || status === loc.MigrationStatus.Succeeded;
} }
export function canCutoverMigration(migration: DatabaseMigration | undefined): boolean { export function canCutoverMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration); const status = getMigrationStatus(migration);
return hasMigrationOperationId(migration) return hasMigrationOperationId(migration)
&& status === MigrationStatus.InProgress && status === loc.MigrationStatus.InProgress
&& isOnlineMigration(migration) && isOnlineMigration(migration)
&& isFullBackupRestored(migration); && isFullBackupRestored(migration);
} }
export function isActiveMigration(migration: DatabaseMigration | undefined): boolean { export function isActiveMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration); const status = getMigrationStatus(migration);
return status === MigrationStatus.Completing return status === loc.MigrationStatus.Completing
|| status === MigrationStatus.Retriable || status === loc.MigrationStatus.Retriable
|| status === MigrationStatus.Creating || status === loc.MigrationStatus.Creating
|| status === MigrationStatus.InProgress; || status === loc.MigrationStatus.InProgress;
} }
export function isFullBackupRestored(migration: DatabaseMigration | undefined): boolean { export function isFullBackupRestored(migration: DatabaseMigration | undefined): boolean {

View File

@@ -6,11 +6,36 @@
import { AzureAccount } from 'azurecore'; import { AzureAccount } from 'azurecore';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { EOL } from 'os'; import { EOL } from 'os';
import { MigrationStatus } from '../models/migrationLocalStorage';
import { MigrationSourceAuthenticationType } from '../models/stateMachine'; import { MigrationSourceAuthenticationType } from '../models/stateMachine';
import { ParallelCopyTypeCodes, PipelineStatusCodes } from './helper'; import { formatNumber, ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export enum MigrationStatus {
Failed = 'Failed',
Succeeded = 'Succeeded',
InProgress = 'InProgress',
Canceled = 'Canceled',
Completing = 'Completing',
Creating = 'Creating',
Canceling = 'Canceling',
Retriable = 'Retriable',
}
export enum ProvisioningState {
Failed = 'Failed',
Succeeded = 'Succeeded',
Creating = 'Creating'
}
export enum BackupFileInfoStatus {
Arrived = 'Arrived',
Uploading = 'Uploading',
Uploaded = 'Uploaded',
Restoring = 'Restoring',
Restored = 'Restored',
Cancelled = 'Cancelled',
Ignored = 'Ignored'
}
// #region wizard // #region wizard
export function WIZARD_TITLE(instanceName: string): string { export function WIZARD_TITLE(instanceName: string): string {
@@ -67,12 +92,13 @@ export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.a
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target"); export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target");
export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance"); export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance");
export const SKU_RECOMMENDATION_DB_CARD_TEXT = localize('sql.migration.sku.db.card.title', "Azure SQL Database"); export const SKU_RECOMMENDATION_SQLDB_CARD_TEXT = localize('sql.migration.sku.sqldb.card.title', "Azure SQL Database");
export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine"); export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine");
export const SELECT_AZURE_MI = localize('sql.migration.select.azure.mi', "Select your target Azure subscription and your target Azure SQL Managed Instance."); export const SELECT_AZURE_MI = localize('sql.migration.select.azure.mi', "Select your target Azure subscription and your target Azure SQL Managed Instance.");
export const SELECT_AZURE_VM = localize('sql.migration.select.azure.vm', "Select your target Azure Subscription and your target SQL Server on Azure Virtual Machine for your target."); export const SELECT_AZURE_VM = localize('sql.migration.select.azure.vm', "Select your target Azure Subscription and your target SQL Server on Azure Virtual Machine for your target.");
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI = localize('sql.migration.sku.recommendation.view.assessment.mi', "To migrate to Azure SQL Managed Instance, view assessment results and select one or more databases."); export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI = localize('sql.migration.sku.recommendation.view.assessment.mi', "To migrate to Azure SQL Managed Instance, view assessment results and select one or more databases.");
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM = localize('sql.migration.sku.recommendation.view.assessment.vm', "To migrate to SQL Server on Azure Virtual Machine, view assessment results and select one or more databases."); export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM = localize('sql.migration.sku.recommendation.view.assessment.vm', "To migrate to SQL Server on Azure Virtual Machine, view assessment results and select one or more databases.");
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_SQLDB = localize('sql.migration.sku.recommendation.view.assessment.sqldb', "To migrate to Azure SQL Database, view assessment results and select one or more databases.");
export const VIEW_SELECT_BUTTON_LABEL = localize('sql.migration.view.select.button.label', "View/Select"); export const VIEW_SELECT_BUTTON_LABEL = localize('sql.migration.view.select.button.label', "View/Select");
export function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string { export function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string {
return localize('total.databases.selected', "{0} of {1} databases selected", selectedDbCount, totalDbCount); return localize('total.databases.selected', "{0} of {1} databases selected", selectedDbCount, totalDbCount);
@@ -148,7 +174,11 @@ export function ASSESSED_DBS(totalDbs: number): string {
return localize('sql.migration.assessed.databases', "(for {0} assessed databases)", totalDbs); return localize('sql.migration.assessed.databases', "(for {0} assessed databases)", totalDbs);
} }
export function RECOMMENDATIONS_AVAILABLE(totalDbs: number): string { export function RECOMMENDATIONS_AVAILABLE(totalDbs: number): string {
return localize('sql.migration.sku.available.recommendations', "{0} recommendations available", totalDbs); if (totalDbs === 1) {
return localize('sql.migration.sku.available.recommendations.one', "{0} recommendation available", totalDbs);
} else {
return localize('sql.migration.sku.available.recommendations.many', "{0} recommendations available", totalDbs);
}
} }
export const RECOMMENDATIONS = localize('sql.migration.sku.recommendations', "Recommendations"); export const RECOMMENDATIONS = localize('sql.migration.sku.recommendations', "Recommendations");
export const LOADING_RECOMMENDATIONS = localize('sql.migration.sku.recommendations.loading', "Loading..."); export const LOADING_RECOMMENDATIONS = localize('sql.migration.sku.recommendations.loading', "Loading...");
@@ -160,8 +190,11 @@ export function VM_CONFIGURATION(vmSize: string, vCPU: number): string {
export function VM_CONFIGURATION_PREVIEW(dataDisk: string, logDisk: string, temp: string): string { export function VM_CONFIGURATION_PREVIEW(dataDisk: string, logDisk: string, temp: string): string {
return localize('sql.migration.sku.azureConfiguration.vmPreview', "Data: {0}, Log: {1}, tempdb: {2}", dataDisk, logDisk, temp); return localize('sql.migration.sku.azureConfiguration.vmPreview', "Data: {0}, Log: {1}, tempdb: {2}", dataDisk, logDisk, temp);
} }
export function DB_CONFIGURATION(computeTier: string, vCore: number): string { export function SQLDB_CONFIGURATION(computeTier: string, vCore: number): string {
return localize('sql.migration.sku.azureConfiguration.db', "{0} - {1} vCore", computeTier, vCore); return localize('sql.migration.sku.azureConfiguration.sqldb', "{0} - {1} vCore", computeTier, vCore);
}
export function SQLDB_CONFIGURATION_PREVIEW(hardwareType: string, computeTier: string, vCore: number, storage: number): string {
return localize('sql.migration.sku.azureConfiguration.sqldbPreview', "{0} - {1} - {2} vCore - {3} GB", hardwareType, computeTier, vCore, storage);
} }
export function MI_CONFIGURATION(hardwareType: string, computeTier: string, vCore: number): string { export function MI_CONFIGURATION(hardwareType: string, computeTier: string, vCore: number): string {
return localize('sql.migration.sku.azureConfiguration.mi', "{0} - {1} - {2} vCore", hardwareType, computeTier, vCore); return localize('sql.migration.sku.azureConfiguration.mi', "{0} - {1} - {2} vCore", hardwareType, computeTier, vCore);
@@ -253,6 +286,40 @@ export function AZURE_SQL_TARGET_PAGE_DESCRIPTION(targetInstance: string = 'inst
return localize('sql.migration.wizard.target.description', "Select an Azure account and your target {0}.", targetInstance); return localize('sql.migration.wizard.target.description', "Select an Azure account and your target {0}.", targetInstance);
} }
export const AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE = localize('sql.migration.wizard.connection.error.title', "An error occurred while conneting to the target server.");
export function SQL_TARGET_CONNECTION_ERROR(message: string): string {
return localize('sql.migration.wizard.target.connection.error', "Connection error: {0}", message);
}
export function SQL_TARGET_CONNECTION_SUCCESS(databaseCount: string): string {
return localize('sql.migration.wizard.target.connection.success', "Connection was successful. Target databases found: {0}", databaseCount);
}
export const SQL_TARGET_MISSING_SOURCE_DATABASES = localize('sql.migration.wizard.source.missing', 'Connection was successful but did not find any target databases.');
export const SQL_TARGET_MAPPING_ERROR_MISSING_TARGET = localize(
'sql.migration.wizard.target.missing',
'Database mapping error. Missing target databases to migrate. Please configure the target server connection and click connect to collect the list of available database migration targets.');
export function SQL_TARGET_CONNECTION_DUPLICATE_TARGET_MAPPING(
targetDatabaseName: string,
sourceDatabaseName: string,
mappedSourceDatabaseName: string,
): string {
return localize(
'sql.migration.wizard.target.mapping.error.duplicate',
"Database mapping error. Target database '{0}' cannot be selected to as a migration target for database '{1}'. Target database '${targetDatabaseName}' is already selected as a migration target for database '{2}'. Please select a different target database.",
targetDatabaseName,
sourceDatabaseName,
mappedSourceDatabaseName);
}
//`Database mapping error. Source database '${sourceDatabaseName}' is not mapped to a target database. Please select a target database to migrate to.`
export function SQL_TARGET_CONNECTION_SOURCE_NOT_MAPPED(sourceDatabaseName: string): string {
return localize(
'sql.migration.wizard.target.source.mapping.error',
"Database mapping error. Source database '{0}' is not mapped to a target database. Please select a target database to migrate to.",
sourceDatabaseName);
}
// Managed Instance // Managed Instance
export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Managed Instance"); export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Managed Instance");
export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instances found."); export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instances found.");
@@ -261,7 +328,6 @@ export function UNAVAILABLE_TARGET_PREFIX(targetName: string): string {
return localize('sql.migration.unavailable.target', "(Unavailable) {0}", targetName); return localize('sql.migration.unavailable.target', "(Unavailable) {0}", targetName);
} }
// Virtual Machine // Virtual Machine
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "SQL Server on Azure Virtual Machines"); export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "SQL Server on Azure Virtual Machines");
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE_SHORT = localize('sql.migration.azure.sql.database.virtual.machine.short', "SQL Server on Azure VM"); export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE_SHORT = localize('sql.migration.azure.sql.database.virtual.machine.short', "SQL Server on Azure VM");
@@ -269,7 +335,10 @@ export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachin
export const INVALID_VIRTUAL_MACHINE_ERROR = localize('sql.migration.invalid.virtualMachine.error', "To continue, select a valid virtual machine."); export const INVALID_VIRTUAL_MACHINE_ERROR = localize('sql.migration.invalid.virtualMachine.error', "To continue, select a valid virtual machine.");
// Azure SQL Database // Azure SQL Database
export const AZURE_SQL_DATABASE = localize('sql.migration.azure.sql.database', "Azure SQL Database"); export const AZURE_SQL_DATABASE = localize('sql.migration.azure.sql.database', "Azure SQL Database Server");
export const NO_SQL_DATABASE_SERVER_FOUND = localize('sql.migration.no.sqldatabaseserver.found', "No Azure SQL database servers found.");
export const NO_SQL_DATABASE_FOUND = localize('sql.migration.no.sqldatabase.found', "No Azure SQL databases found.");
export const INVALID_SQL_DATABASE_ERROR = localize('sql.migration.invalid.sqldatabase.error', "To continue, select a valid Azure SQL Database server.");
// Target info tooltip // Target info tooltip
export const TARGET_SUBSCRIPTION_INFO = localize('sql.migration.sku.subscription', "Subscription name for your Azure SQL target"); export const TARGET_SUBSCRIPTION_INFO = localize('sql.migration.sku.subscription', "Subscription name for your Azure SQL target");
@@ -313,6 +382,10 @@ export function MI_NOT_READY_ERROR(miName: string, state: string): string {
return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state); return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state);
} }
export function SQLDB_NOT_READY_ERROR(sqldbName: string, state: string): string {
return localize('sql.migration.sqldb.not.ready', "The SQL database server '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available SQL database server.", sqldbName, state);
}
export const SELECT_AN_ACCOUNT = localize('sql.migration.select.service.select.a.', "Sign into Azure and select an account"); export const SELECT_AN_ACCOUNT = localize('sql.migration.select.service.select.a.', "Sign into Azure and select an account");
export const SELECT_A_TENANT = localize('sql.migration.select.service.select.a.tenant', "Select a tenant"); export const SELECT_A_TENANT = localize('sql.migration.select.service.select.a.tenant', "Select a tenant");
export const SELECT_A_SUBSCRIPTION = localize('sql.migration.select.service.select.a.subscription', "Select a subscription"); export const SELECT_A_SUBSCRIPTION = localize('sql.migration.select.service.select.a.subscription', "Select a subscription");
@@ -331,6 +404,7 @@ export function ACCOUNT_CREDENTIALS_REFRESH(accountName: string): string {
"{0} (requires credentials refresh)", "{0} (requires credentials refresh)",
accountName); accountName);
} }
export const SELECT_SERVICE_PLACEHOLDER = localize('sql.migration.select.service.select.migration.target', "Select a target server.");
// database backup page // database backup page
export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database backup"); export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database backup");
@@ -361,6 +435,18 @@ export const DATABASE_BACKUP_BLOB_STORAGE_TABLE_HELP_TEXT = localize('sql.migrat
export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Subscription"); export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Subscription");
export const DATABASE_BACKUP_MIGRATION_MODE_LABEL = localize('sql.migration.database.migration.mode.label', "Migration mode"); export const DATABASE_BACKUP_MIGRATION_MODE_LABEL = localize('sql.migration.database.migration.mode.label', "Migration mode");
export const DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION = localize('sql.migration.database.migration.mode.description', "To migrate to the Azure SQL target, choose a migration mode based on your downtime requirements."); export const DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION = localize('sql.migration.database.migration.mode.description', "To migrate to the Azure SQL target, choose a migration mode based on your downtime requirements.");
export const DATABASE_TABLE_SELECTION_LABEL = localize('sql.migration.database.table.selection.label', "Migration table selection");
export const DATABASE_TABLE_SELECTION_DESCRIPTION = localize('sql.migration.database.table.selection.description', "To migrate to the Azure SQL target, select tables in each database for migration.");
export const DATABASE_TABLE_REFRESH_LABEL = localize('sql.migration.database.table.refresh.label', "Refresh");
export const DATABASE_TABLE_SOURCE_DATABASE_COLUMN_LABEL = localize('sql.migration.database.table.source.column.label', "Source database");
export const DATABASE_TABLE_TARGET_DATABASE_COLUMN_LABEL = localize('sql.migration.database.table.target.column.label', "Target database");
export const DATABASE_TABLE_SELECTED_TABLES_COLUMN_LABEL = localize('sql.migration.database.table.tables.column.label', "Select tables");
export const DATABASE_TABLE_CONNECTION_ERROR = localize('sql.migration.database.connection.error', "An error occurred while connecting to target migration database.");
export function DATABASE_TABLE_CONNECTION_ERROR_MESSAGE(message: string): string {
return localize('sql.migration.database.connection.error.message', "Connection error:{0} {1}", EOL, message);
}
export const DATABASE_TABLE_DATA_LOADING = localize('sql.migration.database.loading', "Loading database table list..");
export const DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE = localize('sql.migration.database.validate.selection', "Please select target database tables to migrate to. At least one database with one table is required.");
export const DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL = localize('sql.migration.database.migration.mode.online.label', "Online migration"); export const DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL = localize('sql.migration.database.migration.mode.online.label', "Online migration");
export const DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION = localize('sql.migration.database.migration.mode.online.description', "Application downtime is limited to cutover at the end of migration."); export const DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION = localize('sql.migration.database.migration.mode.online.description', "Application downtime is limited to cutover at the end of migration.");
export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migration.database.migration.mode.offline.label', "Offline migration"); export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migration.database.migration.mode.offline.label', "Offline migration");
@@ -378,6 +464,22 @@ export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscr
export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "To continue, select a valid location."); export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "To continue, select a valid location.");
export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "To continue, select a valid resource group."); export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "To continue, select a valid resource group.");
export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "To continue, select a valid storage account."); export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "To continue, select a valid storage account.");
export const MISSING_TARGET_USERNAME_ERROR = localize('sql.migration.missing.targetUserName.error', "To continue, enter a valid target user name.");
export const MISSING_TARGET_PASSWORD_ERROR = localize('sql.migration.missing.targetPassword.error', "To continue, enter a valid target password.");
export const TARGET_TABLE_NOT_EMPTY = localize('sql.migration.target.table.not.empty', "Target table is not empty.");
export const TARGET_TABLE_MISSING = localize('sql.migration.target.table.missing', "Target table does not exist");
export const TARGET_USERNAME_LAbEL = localize('sql.migration.username.label', "Target user name");
export const TARGET_USERNAME_PLACEHOLDER = localize('sql.migration.username.placeholder', "Enter the target user name");
export const TARGET_PASSWORD_LAbEL = localize('sql.migration.password.label', "Target password");
export const TARGET_PASSWORD_PLACEHOLDER = localize('sql.migration.password.placeholder', "Enter the target password");
export const TARGET_CONNECTION_LABEL = localize('sql.migration.connection.label', "Connect");
export const MAP_SOURCE_TARGET_HEADING = localize('sql.migration.map.target.heading', "Map selected source databases to target databases for migration");
export const MAP_SOURCE_TARGET_DESCRIPTION = localize('sql.migration.map.target.description', "Select the target database where you would like to migrate your source database to. You can choose a target database for only one source database.");
export const MAP_SOURCE_COLUMN = localize('sql.migration.map.source.column', "Source database");
export const MAP_TARGET_COLUMN = localize('sql.migration.map.target.column', "Target database");
export const MAP_TARGET_PLACEHOLDER = localize('sql.migration.map.target.placeholder', "Select a target database");
export function INVALID_BLOB_RESOURCE_GROUP_ERROR(sourceDb: string): string { export function INVALID_BLOB_RESOURCE_GROUP_ERROR(sourceDb: string): string {
return localize('sql.migration.invalid.blob.resourceGroup.error', "To continue, select a valid resource group for source database '{0}'.", sourceDb); return localize('sql.migration.invalid.blob.resourceGroup.error', "To continue, select a valid resource group for source database '{0}'.", sourceDb);
} }
@@ -406,6 +508,26 @@ export const SELECT_RESOURCE_GROUP_PROMPT = localize('sql.migration.blob.resourc
export const SELECT_STORAGE_ACCOUNT = localize('sql.migration.blob.storageAccount.select', "Select a storage account value first."); export const SELECT_STORAGE_ACCOUNT = localize('sql.migration.blob.storageAccount.select', "Select a storage account value first.");
export const SELECT_BLOB_CONTAINER = localize('sql.migration.blob.container.select', "Select a blob container value first."); export const SELECT_BLOB_CONTAINER = localize('sql.migration.blob.container.select', "Select a blob container value first.");
export function SELECT_DATABASE_TABLES_TITLE(targetDatabaseName: string): string {
return localize('sql.migration.table.select.label', "Select tables for {0}", targetDatabaseName);
}
export const TABLE_SELECTION_EDIT = localize('sql.migration.table.selection.edit', "Edit");
export function TABLE_SELECTION_COUNT(selectedCount: number, rowCount: number): string {
return localize('sql.migration.table.selection.count', "{0} of {1}", selectedCount, rowCount);
}
export function TABLE_SELECTED_COUNT(selectedCount: number, rowCount: number): string {
return localize('sql.migration.table.selected.count', "{0} of {1} tables selected", selectedCount, rowCount);
}
export const DATABASE_MISSING_TABLES = localize('sql.migratino.database.missing.tables', "0 tables found.");
export const DATABASE_LOADING_TABLES = localize('sql.migratino.database.loading.tables', "Loading tables list...");
export const TABLE_SELECTION_FILTER = localize('sql.migratino.table.selection.filter', "Filter tables");
export const TABLE_SELECTION_UPDATE_BUTTON = localize('sql.migratino.table.selection.update.button', "Update");
export const TABLE_SELECTION_CANCEL_BUTTON = localize('sql.migratino.table.selection.update.cancel', "Cancel");
export const TABLE_SELECTION_TABLENAME_COLUMN = localize('sql.migratino.table.selection.tablename.column', "Table name");
export const TABLE_SELECTION_HASROWS_COLUMN = localize('sql.migratino.table.selection.status.column', "Has rows");
// integration runtime page // integration runtime page
export const SELECT_RESOURCE_GROUP = localize('sql.migration.blob.resourceGroup.select', "Select a resource group."); export const SELECT_RESOURCE_GROUP = localize('sql.migration.blob.resourceGroup.select', "Select a resource group.");
export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service"); export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service");
@@ -503,6 +625,7 @@ export const START_MIGRATION_TEXT = localize('sql.migration.start.migration.butt
export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', "Summary"); export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', "Summary");
export const SUMMARY_MI_TYPE = localize('sql.migration.summary.mi.type', "Azure SQL Managed Instance"); export const SUMMARY_MI_TYPE = localize('sql.migration.summary.mi.type', "Azure SQL Managed Instance");
export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "SQL Server on Azure Virtual Machine"); export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "SQL Server on Azure Virtual Machine");
export const SUMMARY_SQLDB_TYPE = localize('sql.migration.summary.sqldb.type', "Azure SQL Database");
export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Databases for migration"); export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Databases for migration");
export const SUMMARY_AZURE_STORAGE_SUBSCRIPTION = localize('sql.migration.summary.azure.storage.subscription', "Azure storage subscription"); export const SUMMARY_AZURE_STORAGE_SUBSCRIPTION = localize('sql.migration.summary.azure.storage.subscription', "Azure storage subscription");
export const SUMMARY_AZURE_STORAGE = localize('sql.migration.summary.azure.storage', "Azure storage"); export const SUMMARY_AZURE_STORAGE = localize('sql.migration.summary.azure.storage', "Azure storage");
@@ -521,6 +644,9 @@ export const DATABASE_TO_BE_MIGRATED = localize('sql.migration.database.to.be.mi
export function COUNT_DATABASES(count: number): string { export function COUNT_DATABASES(count: number): string {
return (count === 1) ? localize('sql.migration.count.database.single', "{0} database", count) : localize('sql.migration.count.database.multiple', "{0} databases", count); return (count === 1) ? localize('sql.migration.count.database.single', "{0} database", count) : localize('sql.migration.count.database.multiple', "{0} databases", count);
} }
export function TOTAL_TABLES_SELECTED(selected: number, total: number): string {
return localize('total.tables.selected.of.total', "{0} of {1}", formatNumber(selected), formatNumber(total));
}
// Open notebook quick pick string // Open notebook quick pick string
export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pick.placeholder', "Select the operation you'd like to perform."); export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pick.placeholder', "Select the operation you'd like to perform.");
@@ -565,6 +691,7 @@ export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source
export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server"); export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version"); export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
export const TARGET_DATABASE_NAME = localize('sql.migration.target.database.name', "Target database name"); export const TARGET_DATABASE_NAME = localize('sql.migration.target.database.name', "Target database name");
export const TARGET_TABLE_COUNT_NAME = localize('sql.migration.target.table.count.name', "Tables selected");
export const TARGET_SERVER = localize('sql.migration.target.server', "Target server"); export const TARGET_SERVER = localize('sql.migration.target.server', "Target server");
export const TARGET_VERSION = localize('sql.migration.target.version', "Target version"); export const TARGET_VERSION = localize('sql.migration.target.version', "Target version");
export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status"); export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status");
@@ -683,12 +810,12 @@ export const OPEN_MIGRATION_TARGET_ERROR = localize('sql.migration.open.migratio
export const OPEN_MIGRATION_SERVICE_ERROR = localize('sql.migration.open.migration.service.error', "Error opening migration service dialog"); 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 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 const ERROR_DIALOG_CLEAR_BUTTON_LABEL = localize('sql.migration.error.dialog.clear.button.label', "Clear");
export const ERROR_DIALOG_ARIA_CLICK_VIEW_ERROR_DETAILS = localize('sql.migration.error.aria.view.details', 'Click to view error details');
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> = {
[MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'), [MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'),
[MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'), [MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'),
@@ -794,7 +921,7 @@ export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration
export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source credentials"); export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source credentials");
export const ENTER_YOUR_SQL_CREDS = localize('sql.migration.enter.your.sql.cred', "Enter the credentials for the source SQL Server instance. These credentials will be used while migrating databases to Azure SQL."); export const ENTER_YOUR_SQL_CREDS = localize('sql.migration.enter.your.sql.cred', "Enter the credentials for the source SQL Server instance. These credentials will be used while migrating databases to Azure SQL.");
export const SERVER = localize('sql.migration.server', "Server"); export const SERVER = localize('sql.migration.server', "Server");
export const USERNAME = localize('sql.migration.username', "Username"); export const USERNAME = localize('sql.migration.username', "User name");
export const SIZE = localize('sql.migration.size', "Size (MB)"); export const SIZE = localize('sql.migration.size', "Size (MB)");
export const LAST_BACKUP = localize('sql.migration.last.backup', "Last backup"); export const LAST_BACKUP = localize('sql.migration.last.backup', "Last backup");
export const DATABASE_MIGRATE_TEXT = localize('sql.migrate.text', "Select the databases that you want to migrate to Azure SQL."); export const DATABASE_MIGRATE_TEXT = localize('sql.migrate.text', "Select the databases that you want to migrate to Azure SQL.");
@@ -819,7 +946,8 @@ export const WARNINGS_DETAILS = localize('sql.migration.warnings.details', "Warn
export const ISSUES_DETAILS = localize('sql.migration.issues.details', "Issue details"); export const ISSUES_DETAILS = localize('sql.migration.issues.details', "Issue details");
export const SELECT_DB_PROMPT = localize('sql.migration.select.prompt', "Click on SQL Server instance or any of the databases on the left to view its details."); export const SELECT_DB_PROMPT = localize('sql.migration.select.prompt', "Click on SQL Server instance or any of the databases on the left to view its details.");
export const NO_ISSUES_FOUND_VM = localize('sql.migration.no.issues.vm', "No issues found for migrating to SQL Server on Azure Virtual Machine."); export const NO_ISSUES_FOUND_VM = localize('sql.migration.no.issues.vm', "No issues found for migrating to SQL Server on Azure Virtual Machine.");
export const NO_ISSUES_FOUND_MI = localize('sql.migration.no.issues.mi', "No issues found for migrating to SQL Server on Azure SQL Managed Instance."); export const NO_ISSUES_FOUND_MI = localize('sql.migration.no.issues.mi', "No issues found for migrating to Azure SQL Managed Instance.");
export const NO_ISSUES_FOUND_SQLDB = localize('sql.migration.no.issues.sqldb', "No issues found for migrating to Azure SQL Database.");
export const NO_RESULTS_AVAILABLE = localize('sql.migration.no.results', 'Assessment results are unavailable.'); export const NO_RESULTS_AVAILABLE = localize('sql.migration.no.results', 'Assessment results are unavailable.');
export function IMPACT_OBJECT_TYPE(objectType?: string): string { export function IMPACT_OBJECT_TYPE(objectType?: string): string {

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* 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';
export interface ErrorEvent {
connectionId: string;
title: string;
label: string;
message: string;
}
export class DashboardStatusBar implements vscode.Disposable {
private _errorTitle: string = '';
private _errorLabel: string = '';
private _errorDescription: string = '';
private _errorDialogIsOpen: boolean = false;
private _statusInfoBox: azdata.InfoBoxComponent;
private _context: vscode.ExtensionContext;
private _errorEvent: vscode.EventEmitter<ErrorEvent> = new vscode.EventEmitter<ErrorEvent>();
private _disposables: vscode.Disposable[] = [];
constructor(context: vscode.ExtensionContext, connectionId: string, statusInfoBox: azdata.InfoBoxComponent, errorEvent: vscode.EventEmitter<ErrorEvent>) {
this._context = context;
this._statusInfoBox = statusInfoBox;
this._errorEvent = errorEvent;
this._disposables.push(
this._errorEvent.event(
async (e) => {
if (e.connectionId === connectionId) {
return (e.title.length > 0 && e.label.length > 0)
? await this.showError(e.title, e.label, e.message)
: await this.clearError();
}
}));
}
dispose() {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}
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;
this._statusInfoBox.ariaLabel = 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 async openErrorDialog(): Promise<void> {
if (this._errorDialogIsOpen) {
return;
}
try {
const tab = azdata.window.createTab(this._errorTitle);
tab.registerContent(async (view) => {
const flex = view.modelBuilder.flexContainer()
.withItems([
view.modelBuilder.text()
.withProps({ value: this._errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
.component(),
view.modelBuilder.inputBox()
.withProps({
value: this._errorDescription,
readOnly: true,
multiline: true,
height: 400,
inputType: 'text',
display: 'inline-block',
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
})
.component()])
.withLayout({ flexFlow: 'column', width: 420, })
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
.component();
await view.initializeModel(flex);
});
const dialog = azdata.window.createModelViewDialog(
this._errorTitle,
'errorDialog',
450,
'flyout');
dialog.content = [tab];
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
dialog.okButton.focused = true;
dialog.cancelButton.label = loc.CLOSE;
this._context.subscriptions.push(
dialog.onClosed(async e => {
if (e === 'ok') {
await this.clearError();
}
this._errorDialogIsOpen = false;
}));
azdata.window.openDialog(dialog);
} catch (error) {
this._errorDialogIsOpen = false;
}
}
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> {
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
}
}

View File

@@ -8,13 +8,13 @@ import * as vscode from 'vscode';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper'; import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { filterMigrations } from '../api/utils'; import { filterMigrations, MenuCommands } from '../api/utils';
import { DatabaseMigration } from '../api/azure'; import { DatabaseMigration } from '../api/azure';
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage'; import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog'; import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
import { logError, TelemetryViews } from '../telemtery'; import { logError, TelemetryViews } from '../telemtery';
import { AdsMigrationStatus, MenuCommands, TabBase } from './tabBase'; import { AdsMigrationStatus, ServiceContextChangeEvent, TabBase } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard'; import { DashboardStatusBar } from './DashboardStatusBar';
interface IActionMetadata { interface IActionMetadata {
title?: string, title?: string,
@@ -62,16 +62,15 @@ export class DashboardTab extends TabBase<DashboardTab> {
this.icon = IconPathHelper.sqlMigrationLogo; this.icon = IconPathHelper.sqlMigrationLogo;
} }
public onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async create( public async create(
view: azdata.ModelView, view: azdata.ModelView,
openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>, openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>,
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
statusBar: DashboardStatusBar): Promise<DashboardTab> { statusBar: DashboardStatusBar): Promise<DashboardTab> {
this.view = view; this.view = view;
this.openMigrationFcn = openMigrationsFcn; this.openMigrationsFcn = openMigrationsFcn;
this.serviceContextChangedEvent = serviceContextChangedEvent;
this.statusBar = statusBar; this.statusBar = statusBar;
await this.initialize(this.view); await this.initialize(this.view);
@@ -80,23 +79,16 @@ export class DashboardTab extends TabBase<DashboardTab> {
} }
public async refresh(): Promise<void> { public async refresh(): Promise<void> {
if (this.isRefreshing) { if (this.isRefreshing || this._migrationStatusCardLoadingContainer === undefined) {
return; return;
} }
this.isRefreshing = true;
this._migrationStatusCardLoadingContainer.loading = true;
let migrations: DatabaseMigration[] = [];
try { try {
this.isRefreshing = true;
this._refreshButton.enabled = false;
this._migrationStatusCardLoadingContainer.loading = true;
await this.statusBar.clearError(); await this.statusBar.clearError();
migrations = await getCurrentMigrations(); const 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); const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
let warningCount = 0; let warningCount = 0;
@@ -125,8 +117,17 @@ export class DashboardTab extends TabBase<DashboardTab> {
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true); this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
await this._updateSummaryStatus(); await this._updateSummaryStatus();
this.isRefreshing = false; } catch (e) {
await this.statusBar.showError(
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
e.message);
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
} finally {
this._migrationStatusCardLoadingContainer.loading = false; this._migrationStatusCardLoadingContainer.loading = false;
this._refreshButton.enabled = true;
this.isRefreshing = false;
}
} }
protected async initialize(view: azdata.ModelView): Promise<void> { protected async initialize(view: azdata.ModelView): Promise<void> {
@@ -616,11 +617,8 @@ export class DashboardTab extends TabBase<DashboardTab> {
}).component(); }).component();
this.disposables.push( this.disposables.push(
this._refreshButton.onDidClick(async (e) => { this._refreshButton.onDidClick(
this._refreshButton.enabled = false; async (e) => await this.refresh()));
await this.refresh();
this._refreshButton.enabled = true;
}));
const buttonContainer = view.modelBuilder.flexContainer() const buttonContainer = view.modelBuilder.flexContainer()
.withProps({ .withProps({
@@ -668,7 +666,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_IN_PROGRESS); loc.MIGRATION_IN_PROGRESS);
this.disposables.push( this.disposables.push(
this._inProgressMigrationButton.container.onDidClick( this._inProgressMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING))); async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ONGOING)));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._inProgressMigrationButton.container, this._inProgressMigrationButton.container,
{ flex: '0 0 auto' }); { flex: '0 0 auto' });
@@ -681,7 +679,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
true); true);
this.disposables.push( this.disposables.push(
this._inProgressWarningMigrationButton.container.onDidClick( this._inProgressWarningMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING))); async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ONGOING)));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._inProgressWarningMigrationButton.container, this._inProgressWarningMigrationButton.container,
{ flex: '0 0 auto' }); { flex: '0 0 auto' });
@@ -693,7 +691,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_COMPLETED); loc.MIGRATION_COMPLETED);
this.disposables.push( this.disposables.push(
this._successfulMigrationButton.container.onDidClick( this._successfulMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.SUCCEEDED))); async (e) => await this.openMigrationsFcn(AdsMigrationStatus.SUCCEEDED)));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._successfulMigrationButton.container, this._successfulMigrationButton.container,
{ flex: '0 0 auto' }); { flex: '0 0 auto' });
@@ -705,7 +703,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_CUTOVER_CARD); loc.MIGRATION_CUTOVER_CARD);
this.disposables.push( this.disposables.push(
this._completingMigrationButton.container.onDidClick( this._completingMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.COMPLETING))); async (e) => await this.openMigrationsFcn(AdsMigrationStatus.COMPLETING)));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._completingMigrationButton.container, this._completingMigrationButton.container,
{ flex: '0 0 auto' }); { flex: '0 0 auto' });
@@ -717,7 +715,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_FAILED); loc.MIGRATION_FAILED);
this.disposables.push( this.disposables.push(
this._failedMigrationButton.container.onDidClick( this._failedMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.FAILED))); async (e) => await this.openMigrationsFcn(AdsMigrationStatus.FAILED)));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._failedMigrationButton.container, this._failedMigrationButton.container,
{ flex: '0 0 auto' }); { flex: '0 0 auto' });
@@ -729,7 +727,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.VIEW_ALL); loc.VIEW_ALL);
this.disposables.push( this.disposables.push(
this._allMigrationButton.container.onDidClick( this._allMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ALL))); async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ALL)));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._allMigrationButton.container, this._allMigrationButton.container,
{ flex: '0 0 auto' }); { flex: '0 0 auto' });
@@ -759,9 +757,21 @@ export class DashboardTab extends TabBase<DashboardTab> {
}) })
.component(); .component();
const connectionProfile = await azdata.connection.getCurrentConnection();
this.disposables.push(
this.serviceContextChangedEvent.event(
async (e) => {
if (e.connectionId === connectionProfile.connectionId) {
await this.updateServiceContext(this._serviceContextButton);
await this.refresh();
}
}
));
await this.updateServiceContext(this._serviceContextButton);
this.disposables.push( this.disposables.push(
this._serviceContextButton.onDidClick(async () => { this._serviceContextButton.onDidClick(async () => {
const dialog = new SelectMigrationServiceDialog(async () => await this.onDialogClosed()); const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
await dialog.initialize(); await dialog.initialize();
})); }));

View File

@@ -8,11 +8,11 @@ import * as vscode from 'vscode';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { getSqlServerName, getMigrationStatusImage } from '../api/utils'; import { getSqlServerName, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemtery'; import { logError, TelemetryViews } from '../telemtery';
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper'; import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { getResourceName } from '../api/azure'; import { getResourceName } from '../api/azure';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase'; import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { EmptySettingValue } from './tabBase'; import { EmptySettingValue } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard'; import { DashboardStatusBar } from './DashboardStatusBar';
const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab'; const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab';
@@ -36,13 +36,13 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
public async create( public async create(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
view: azdata.ModelView, view: azdata.ModelView,
onClosedCallback: () => Promise<void>, openMigrationsListFcn: () => Promise<void>,
statusBar: DashboardStatusBar, statusBar: DashboardStatusBar,
): Promise<MigrationDetailsBlobContainerTab> { ): Promise<MigrationDetailsBlobContainerTab> {
this.view = view; this.view = view;
this.context = context; this.context = context;
this.onClosedCallback = onClosedCallback; this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar; this.statusBar = statusBar;
await this.initialize(this.view); await this.initialize(this.view);
@@ -51,12 +51,14 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
} }
public async refresh(): Promise<void> { public async refresh(): Promise<void> {
if (this.isRefreshing || this.model?.migration === undefined) { if (this.isRefreshing ||
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return; return;
} }
this.isRefreshing = true; this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true; this.refreshLoader.loading = true;
await this.statusBar.clearError(); await this.statusBar.clearError();
@@ -95,7 +97,7 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
this._targetServerInfoField.text.value = targetServerName; this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion; this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue; this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration); this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId; const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
@@ -114,9 +116,8 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
this.cancelButton.enabled = canCancelMigration(migration); this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration); this.retryButton.enabled = canRetryMigration(migration);
this.isRefreshing = false;
this.refreshLoader.loading = false; this.refreshLoader.loading = false;
this.refreshButton.enabled = true; this.isRefreshing = false;
} }
protected async initialize(view: azdata.ModelView): Promise<void> { protected async initialize(view: azdata.ModelView): Promise<void> {

View File

@@ -10,11 +10,11 @@ import * as loc from '../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage } from '../api/utils'; import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemtery'; import { logError, TelemetryViews } from '../telemtery';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper'; import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { getResourceName } from '../api/azure'; import { getResourceName } from '../api/azure';
import { EmptySettingValue } from './tabBase'; import { EmptySettingValue } from './tabBase';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase'; import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { DashboardStatusBar } from './sqlServerDashboard'; import { DashboardStatusBar } from './DashboardStatusBar';
const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab'; const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab';
@@ -43,7 +43,6 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
private _lastAppliedBackupInfoField!: InfoFieldSchema; private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema; private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema; private _currentRestoringFileInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent; private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.TableComponent; private _fileTable!: azdata.TableComponent;
private _emptyTableFill!: azdata.FlexContainer; private _emptyTableFill!: azdata.FlexContainer;
@@ -56,12 +55,12 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
public async create( public async create(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
view: azdata.ModelView, view: azdata.ModelView,
onClosedCallback: () => Promise<void>, openMigrationsListFcn: () => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> { statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
this.view = view; this.view = view;
this.context = context; this.context = context;
this.onClosedCallback = onClosedCallback; this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar; this.statusBar = statusBar;
await this.initialize(this.view); await this.initialize(this.view);
@@ -70,24 +69,20 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
} }
public async refresh(): Promise<void> { public async refresh(): Promise<void> {
if (this.isRefreshing || this.model?.migration === undefined) { if (this.isRefreshing ||
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return; return;
} }
try {
this.isRefreshing = true; this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true; this.refreshLoader.loading = true;
await this.statusBar.clearError(); await this.statusBar.clearError();
await this._fileTable.updateProperty('data', []); await this._fileTable.updateProperty('data', []);
try {
await this.model.fetchStatus(); 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; const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles( await this.cutoverButton.updateCssStyles(
@@ -142,7 +137,7 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
this._targetServerInfoField.text.value = targetServerName; this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion; this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue; this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration); this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue; this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
@@ -187,9 +182,15 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
this.cutoverButton.enabled = canCutoverMigration(migration); this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration); this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration); this.retryButton.enabled = canRetryMigration(migration);
this.isRefreshing = false; } catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
} finally {
this.refreshLoader.loading = false; this.refreshLoader.loading = false;
this.refreshButton.enabled = true; this.isRefreshing = false;
}
} }
protected async initialize(view: azdata.ModelView): Promise<void> { protected async initialize(view: azdata.ModelView): Promise<void> {

View File

@@ -15,7 +15,7 @@ import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migratio
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog'; import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog'; import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { MigrationTargetType } from '../models/stateMachine'; import { MigrationTargetType } from '../models/stateMachine';
import { DashboardStatusBar } from './sqlServerDashboard'; import { DashboardStatusBar } from './DashboardStatusBar';
export const infoFieldLgWidth: string = '330px'; export const infoFieldLgWidth: string = '330px';
export const infoFieldWidth: string = '250px'; export const infoFieldWidth: string = '250px';
@@ -38,8 +38,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
protected model!: MigrationCutoverDialogModel; protected model!: MigrationCutoverDialogModel;
protected databaseLabel!: azdata.TextComponent; protected databaseLabel!: azdata.TextComponent;
protected serviceContext!: MigrationServiceContext; protected serviceContext!: MigrationServiceContext;
protected onClosedCallback!: () => Promise<void>; protected openMigrationsListFcn!: () => Promise<void>;
protected cutoverButton!: azdata.ButtonComponent; protected cutoverButton!: azdata.ButtonComponent;
protected refreshButton!: azdata.ButtonComponent; protected refreshButton!: azdata.ButtonComponent;
protected cancelButton!: azdata.ButtonComponent; protected cancelButton!: azdata.ButtonComponent;
@@ -49,7 +48,11 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
protected retryButton!: azdata.ButtonComponent; protected retryButton!: azdata.ButtonComponent;
protected summaryTextComponent: azdata.TextComponent[] = []; protected summaryTextComponent: azdata.TextComponent[] = [];
public abstract create(context: vscode.ExtensionContext, view: azdata.ModelView, onClosedCallback: () => Promise<void>, statusBar: DashboardStatusBar): Promise<T>; public abstract create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
openMigrationsListFcn: () => Promise<void>,
statusBar: DashboardStatusBar): Promise<T>;
protected abstract migrationInfoGrid(): Promise<azdata.FlexContainer>; protected abstract migrationInfoGrid(): Promise<azdata.FlexContainer>;
@@ -80,7 +83,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
.component(); .component();
this.disposables.push( this.disposables.push(
migrationsTabLink.onDidClick( migrationsTabLink.onDidClick(
async (e) => await this.onClosedCallback())); async (e) => await this.openMigrationsListFcn()));
const breadCrumbImage = this.view.modelBuilder.image() const breadCrumbImage = this.view.modelBuilder.image()
.withProps({ .withProps({
@@ -202,7 +205,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
this.context, this.context,
this.serviceContext, this.serviceContext,
this.model.migration, this.model.migration,
this.onClosedCallback); this.serviceContextChangedEvent);
await retryMigrationDialog.openDialog(); await retryMigrationDialog.openDialog();
} }
)); ));
@@ -254,12 +257,10 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
async (e) => await this.refresh())); async (e) => await this.refresh()));
this.refreshLoader = this.view.modelBuilder.loadingComponent() this.refreshLoader = this.view.modelBuilder.loadingComponent()
.withItem(this.refreshButton)
.withProps({ .withProps({
loading: false, loading: false,
CSSStyles: { CSSStyles: { 'height': '8px', 'margin-top': '4px' }
'height': '8px',
'margin-top': '4px'
}
}).component(); }).component();
toolbarContainer.addToolbarItems([ toolbarContainer.addToolbarItems([
@@ -268,7 +269,6 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
<azdata.ToolbarComponent>{ component: this.retryButton }, <azdata.ToolbarComponent>{ component: this.retryButton },
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true }, <azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true }, <azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.refreshButton },
<azdata.ToolbarComponent>{ component: this.refreshLoader }, <azdata.ToolbarComponent>{ component: this.refreshLoader },
]); ]);

View File

@@ -8,13 +8,12 @@ import * as vscode from 'vscode';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils'; import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils';
import { logError, TelemetryViews } from '../telemtery'; import { logError, TelemetryViews } from '../telemtery';
import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper'; import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
import { CopyProgressDetail, getResourceName } from '../api/azure'; import { CopyProgressDetail, getResourceName } from '../api/azure';
import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase'; import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { EmptySettingValue } from './tabBase';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
import { DashboardStatusBar } from './sqlServerDashboard';
import { EOL } from 'os'; import { EOL } from 'os';
import { DashboardStatusBar } from './DashboardStatusBar';
const MigrationDetailsTableTabId = 'MigrationDetailsTableTab'; const MigrationDetailsTableTabId = 'MigrationDetailsTableTab';
@@ -63,12 +62,12 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
public async create( public async create(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
view: azdata.ModelView, view: azdata.ModelView,
onClosedCallback: () => Promise<void>, openMigrationsListFcn: () => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsTableTab> { statusBar: DashboardStatusBar): Promise<MigrationDetailsTableTab> {
this.view = view; this.view = view;
this.context = context; this.context = context;
this.onClosedCallback = onClosedCallback; this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar; this.statusBar = statusBar;
await this.initialize(this.view); await this.initialize(this.view);
@@ -78,16 +77,17 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
@debounce(500) @debounce(500)
public async refresh(): Promise<void> { public async refresh(): Promise<void> {
if (this.isRefreshing) { if (this.isRefreshing ||
this.refreshLoader === undefined) {
return; return;
} }
try {
this.isRefreshing = true; this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true; this.refreshLoader.loading = true;
await this.statusBar.clearError(); await this.statusBar.clearError();
try {
await this.model.fetchStatus(); await this.model.fetchStatus();
await this._loadData(); await this._loadData();
} catch (e) { } catch (e) {
@@ -95,11 +95,10 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
loc.MIGRATION_STATUS_REFRESH_ERROR, loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR, loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message); e.message);
} } finally {
this.isRefreshing = false;
this.refreshLoader.loading = false; this.refreshLoader.loading = false;
this.refreshButton.enabled = true; this.isRefreshing = false;
}
} }
private async _loadData(): Promise<void> { private async _loadData(): Promise<void> {
@@ -120,8 +119,9 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
const targetType = getMigrationTargetTypeEnum(migration); const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? '']; const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
const hashSet: loc.LookupTable<number> = {};
this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? []; this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
const hashSet: loc.LookupTable<number> = {};
await this._populateTableData(hashSet); await this._populateTableData(hashSet);
const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0; const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0;
@@ -138,7 +138,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
(hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) + (hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) +
(hashSet[PipelineStatusCodes.InProgress] ?? 0); (hashSet[PipelineStatusCodes.InProgress] ?? 0);
const totalCount = migration.properties.migrationStatusDetails?.listOfCopyProgressDetails.length ?? 0; const totalCount = this._progressDetail.length;
this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount); this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount);
this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount); this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount);
@@ -155,7 +155,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
this._targetServerInfoField.text.value = targetServerName; this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion; this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue; this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration); this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._serverObjectsInfoField.text.value = totalCount.toLocaleString(); this._serverObjectsInfoField.text.value = totalCount.toLocaleString();

View File

@@ -6,20 +6,16 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage } from '../models/migrationLocalStorage'; import { getCurrentMigrations, getSelectedServiceStatus } from '../models/migrationLocalStorage';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils'; import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime, MenuCommands } from '../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog'; import { getMigrationTargetType, getMigrationMode, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
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 { DatabaseMigration, getResourceName } from '../api/azure';
import { logError, TelemetryViews } from '../telemtery'; import { logError, TelemetryViews } from '../telemtery';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog'; import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase'; import { AdsMigrationStatus, EmptySettingValue, ServiceContextChangeEvent, TabBase } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
import { MigrationMode } from '../models/stateMachine'; import { MigrationMode } from '../models/stateMachine';
import { DashboardStatusBar } from './DashboardStatusBar';
export const MigrationsListTabId = 'MigrationsListTab'; export const MigrationsListTabId = 'MigrationsListTab';
@@ -58,12 +54,14 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
view: azdata.ModelView, view: azdata.ModelView,
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>, openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
statusBar: DashboardStatusBar, statusBar: DashboardStatusBar,
): Promise<MigrationsListTab> { ): Promise<MigrationsListTab> {
this.view = view; this.view = view;
this.context = context; this.context = context;
this._openMigrationDetails = openMigrationDetails; this._openMigrationDetails = openMigrationDetails;
this.serviceContextChangedEvent = serviceContextChangedEvent;
this.statusBar = statusBar; this.statusBar = statusBar;
await this.initialize(); await this.initialize();
@@ -71,29 +69,28 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
return this; return this;
} }
public onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> { public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) { if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values) const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
.find(value => value.name === filter.toString()); .find(value => value.name === filter.toString());
this._statusDropdown.value = statusFilter; await this._statusDropdown.updateProperties({ 'value': statusFilter });
} }
} }
public async refresh(): Promise<void> { public async refresh(): Promise<void> {
if (this.isRefreshing) { if (this.isRefreshing ||
this._refreshLoader === undefined) {
return; return;
} }
try {
this.isRefreshing = true; this.isRefreshing = true;
this._refresh.enabled = false;
this._refreshLoader.loading = true; this._refreshLoader.loading = true;
await this.statusBar.clearError(); await this.statusBar.clearError();
try {
await this._statusTable.updateProperty('data', []); await this._statusTable.updateProperty('data', []);
this._migrations = await getCurrentMigrations(); this._migrations = await getCurrentMigrations();
await this._populateMigrationTable(); await this._populateMigrationTable();
@@ -105,26 +102,22 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e); logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
} finally { } finally {
this._refreshLoader.loading = false; this._refreshLoader.loading = false;
this._refresh.enabled = true;
this.isRefreshing = false; this.isRefreshing = false;
} }
} }
protected async initialize(): Promise<void> { protected async initialize(): Promise<void> {
this._registerCommands(); this._createStatusTable();
this.content = this.view.modelBuilder.flexContainer() this.content = this.view.modelBuilder.flexContainer()
.withItems( .withItems(
[ [
this._createToolbar(), this._createToolbar(),
await this._createSearchAndSortContainer(), await this._createSearchAndSortContainer(),
this._createStatusTable() this._statusTable,
], ],
{ CSSStyles: { 'width': '100%' } } { CSSStyles: { 'width': '100%' } }
).withLayout({ ).withLayout({ width: '100%', flexFlow: 'column' })
width: '100%', .withProps({ CSSStyles: { 'padding': '0px' } })
flexFlow: 'column',
}).withProps({ CSSStyles: { 'padding': '0px' } })
.component(); .component();
} }
@@ -144,20 +137,16 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
async (e) => await this.refresh())); async (e) => await this.refresh()));
this._refreshLoader = this.view.modelBuilder.loadingComponent() this._refreshLoader = this.view.modelBuilder.loadingComponent()
.withItem(this._refresh)
.withProps({ .withProps({
loading: false, loading: false,
CSSStyles: { CSSStyles: { 'height': '8px', 'margin-top': '6px' }
'height': '8px', }).component();
'margin-top': '6px'
}
})
.component();
toolbar.addToolbarItems([ toolbar.addToolbarItems([
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true }, <azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() }, <azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true }, <azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this._refresh },
<azdata.ToolbarComponent>{ component: this._refreshLoader }, <azdata.ToolbarComponent>{ component: this._refreshLoader },
]); ]);
@@ -178,16 +167,25 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
width: 230, width: 230,
}).component(); }).component();
const onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
this.disposables.push( this.disposables.push(
this._serviceContextButton.onDidClick( this._serviceContextButton.onDidClick(
async () => { async () => {
const dialog = new SelectMigrationServiceDialog(onDialogClosed); const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
await dialog.initialize(); await dialog.initialize();
})); }));
const connectionProfile = await azdata.connection.getCurrentConnection();
this.disposables.push(
this.serviceContextChangedEvent.event(
async (e) => {
if (e.connectionId === connectionProfile.connectionId) {
await this.updateServiceContext(this._serviceContextButton);
await this.refresh();
}
}
));
await this.updateServiceContext(this._serviceContextButton);
this._searchBox = this.view.modelBuilder.inputBox() this._searchBox = this.view.modelBuilder.inputBox()
.withProps({ .withProps({
stopEnterPropagation: true, stopEnterPropagation: true,
@@ -212,7 +210,9 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
.withProps({ .withProps({
ariaLabel: loc.MIGRATION_STATUS_FILTER, ariaLabel: loc.MIGRATION_STATUS_FILTER,
values: this._statusDropdownValues, values: this._statusDropdownValues,
width: '150px' width: '150px',
fireOnTextChange: true,
value: this._statusDropdownValues[0],
}).component(); }).component();
this.disposables.push( this.disposables.push(
this._statusDropdown.onValueChanged( this._statusDropdown.onValueChanged(
@@ -311,173 +311,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
return container; 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 { private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
const sortDir = ascending ? -1 : 1; const sortDir = ascending ? -1 : 1;
switch (columnName) { switch (columnName) {
@@ -575,6 +408,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
(<azdata.CategoryValue>this._columnSortDropdown.value).name, (<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true); this._columnSortCheckbox.checked === true);
const connectionProfile = await azdata.connection.getCurrentConnection();
const data: any[] = this._filteredMigrations.map((migration, index) => { const data: any[] = this._filteredMigrations.map((migration, index) => {
return [ return [
<azdata.HyperlinkColumnCellValue>{ <azdata.HyperlinkColumnCellValue>{
@@ -597,7 +431,11 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
getMigrationTime(migration.properties.endedOn), // finishTime getMigrationTime(migration.properties.endedOn), // finishTime
<azdata.ContextMenuColumnCellValue>{ <azdata.ContextMenuColumnCellValue>{
title: '', title: '',
context: migration.id, context: {
connectionId: connectionProfile.connectionId,
migrationId: migration.id,
migrationOperationId: migration.properties.migrationOperationId,
},
commands: this._getMenuCommands(migration), // context menu commands: this._getMenuCommands(migration), // context menu
}, },
]; ];
@@ -632,7 +470,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
value: 'sourceDatabase', value: 'sourceDatabase',
width: 190, width: 190,
type: azdata.ColumnType.hyperlink, type: azdata.ColumnType.hyperlink,
showText: true,
}, },
{ {
cssClass: rowCssStyles, cssClass: rowCssStyles,
@@ -717,7 +554,8 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
] ]
}).component(); }).component();
this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => { this.disposables.push(
this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState; const buttonState = <azdata.ICellActionEventArgs>rowState;
const migration = this._filteredMigrations[rowState.row]; const migration = this._filteredMigrations[rowState.row];
switch (buttonState?.column) { switch (buttonState?.column) {

View File

@@ -6,16 +6,16 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { AdsMigrationStatus, TabBase } from './tabBase'; import { AdsMigrationStatus, MigrationDetailsEvent, ServiceContextChangeEvent, TabBase } from './tabBase';
import { MigrationsListTab, MigrationsListTabId } from './migrationsListTab'; import { MigrationsListTab, MigrationsListTabId } from './migrationsListTab';
import { DatabaseMigration } from '../api/azure'; import { DatabaseMigration, getMigrationDetails } from '../api/azure';
import { MigrationLocalStorage } from '../models/migrationLocalStorage'; import { MigrationLocalStorage } from '../models/migrationLocalStorage';
import { FileStorageType } from '../models/stateMachine'; import { FileStorageType } from '../models/stateMachine';
import { MigrationDetailsTabBase } from './migrationDetailsTabBase'; import { MigrationDetailsTabBase } from './migrationDetailsTabBase';
import { MigrationDetailsFileShareTab } from './migrationDetailsFileShareTab'; import { MigrationDetailsFileShareTab } from './migrationDetailsFileShareTab';
import { MigrationDetailsBlobContainerTab } from './migrationDetailsBlobContainerTab'; import { MigrationDetailsBlobContainerTab } from './migrationDetailsBlobContainerTab';
import { MigrationDetailsTableTab } from './migrationDetailsTableTab'; import { MigrationDetailsTableTab } from './migrationDetailsTableTab';
import { DashboardStatusBar } from './sqlServerDashboard'; import { DashboardStatusBar } from './DashboardStatusBar';
export const MigrationsTabId = 'MigrationsTab'; export const MigrationsTabId = 'MigrationsTab';
@@ -27,6 +27,7 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>; private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>; private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>;
private _selectedTabId: string | undefined = undefined; private _selectedTabId: string | undefined = undefined;
private _migrationDetailsEvent!: vscode.EventEmitter<MigrationDetailsEvent>;
constructor() { constructor() {
super(); super();
@@ -34,16 +35,17 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
this.id = MigrationsTabId; this.id = MigrationsTabId;
} }
public onDialogClosed = async (): Promise<void> =>
await this._migrationsListTab.onDialogClosed();
public async create( public async create(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
view: azdata.ModelView, view: azdata.ModelView,
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
migrationDetailsEvent: vscode.EventEmitter<MigrationDetailsEvent>,
statusBar: DashboardStatusBar): Promise<MigrationsTab> { statusBar: DashboardStatusBar): Promise<MigrationsTab> {
this.context = context; this.context = context;
this.view = view; this.view = view;
this.serviceContextChangedEvent = serviceContextChangedEvent;
this._migrationDetailsEvent = migrationDetailsEvent;
this.statusBar = statusBar; this.statusBar = statusBar;
await this.initialize(view); await this.initialize(view);
@@ -56,9 +58,9 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
switch (this._selectedTabId) { switch (this._selectedTabId) {
case undefined: case undefined:
case MigrationsListTabId: case MigrationsListTabId:
return await this._migrationsListTab?.refresh(); return this._migrationsListTab.refresh();
default: default:
return await this._migrationDetailsTab?.refresh(); return this._migrationDetailsTab.refresh();
} }
} }
@@ -77,41 +79,58 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
this._migrationsListTab = await new MigrationsListTab().create( this._migrationsListTab = await new MigrationsListTab().create(
this.context, this.context,
this.view, this.view,
async (migration) => await this._openMigrationDetails(migration), async (migration) => await this.openMigrationDetails(migration),
this.serviceContextChangedEvent,
this.statusBar); this.statusBar);
this.disposables.push(this._migrationsListTab); this.disposables.push(this._migrationsListTab);
const openMigrationsListTab = async (): Promise<void> => {
await this.statusBar.clearError();
await this._openTab(this._migrationsListTab);
};
this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create( this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create(
this.context, this.context,
this.view, this.view,
async () => await this._openMigrationsListTab(), openMigrationsListTab,
this.statusBar); this.statusBar);
this.disposables.push(this._migrationDetailsBlobTab); this.disposables.push(this._migrationDetailsBlobTab);
this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create( this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create(
this.context, this.context,
this.view, this.view,
async () => await this._openMigrationsListTab(), openMigrationsListTab,
this.statusBar); this.statusBar);
this.disposables.push(this._migrationDetailsFileShareTab); this.disposables.push(this._migrationDetailsFileShareTab);
this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create( this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create(
this.context, this.context,
this.view, this.view,
async () => await this._openMigrationsListTab(), openMigrationsListTab,
this.statusBar); this.statusBar);
this.disposables.push(this._migrationDetailsFileShareTab); this.disposables.push(this._migrationDetailsFileShareTab);
const connectionProfile = await azdata.connection.getCurrentConnection();
const connectionId = connectionProfile.connectionId;
this.disposables.push(
this._migrationDetailsEvent.event(async e => {
if (e.connectionId === connectionId) {
const migration = await this._getMigrationDetails(e.migrationId, e.migrationOperationId);
if (migration) {
await this.openMigrationDetails(migration);
}
}
}));
this.content = this._tab; this.content = this._tab;
} }
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> { public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
await this._migrationsListTab?.setMigrationFilter(filter);
await this._openTab(this._migrationsListTab); await this._openTab(this._migrationsListTab);
await this._migrationsListTab?.setMigrationFilter(filter); await this._migrationsListTab.setMigrationFilter(filter);
} }
private async _openMigrationDetails(migration: DatabaseMigration): Promise<void> { public async openMigrationDetails(migration: DatabaseMigration): Promise<void> {
switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) { switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) {
case FileStorageType.AzureBlob: case FileStorageType.AzureBlob:
this._migrationDetailsTab = this._migrationDetailsBlobTab; this._migrationDetailsTab = this._migrationDetailsBlobTab;
@@ -128,12 +147,21 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
await MigrationLocalStorage.getMigrationServiceContext(), await MigrationLocalStorage.getMigrationServiceContext(),
migration); migration);
const promise = this._migrationDetailsTab.refresh();
await this._openTab(this._migrationDetailsTab); await this._openTab(this._migrationDetailsTab);
await promise;
} }
private async _openMigrationsListTab(): Promise<void> { private async _getMigrationDetails(migrationId: string, migrationOperationId: string): Promise<DatabaseMigration | undefined> {
await this.statusBar.clearError(); const context = await MigrationLocalStorage.getMigrationServiceContext();
await this._openTab(this._migrationsListTab); if (context.azureAccount && context.subscription) {
return getMigrationDetails(
context.azureAccount,
context.subscription,
migrationId,
migrationOperationId);
}
return undefined;
} }
private async _openTab(tab: azdata.Tab): Promise<void> { private async _openTab(tab: azdata.Tab): Promise<void> {
@@ -141,6 +169,7 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
return; return;
} }
await this.statusBar.clearError();
this._tab.clearItems(); this._tab.clearItems();
this._tab.addItem(tab.content); this._tab.addItem(tab.content);
this._selectedTabId = tab.id; this._selectedTabId = tab.id;

View File

@@ -5,82 +5,121 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as mssql from 'mssql';
import { promises as fs } from 'fs';
import { DatabaseMigration, getMigrationDetails } from '../api/azure';
import { MenuCommands, SqlMigrationExtensionId } from '../api/utils';
import { canCancelMigration, canRetryMigration } from '../constants/helper';
import { IconPathHelper } from '../constants/iconPathHelper';
import { MigrationNotebookInfo, NotebookPathHelper } from '../constants/notebookPathHelper';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { SavedAssessmentDialog } from '../dialog/assessmentResults/savedAssessmentDialog';
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
import { MigrationStateModel, SavedInfo } from '../models/stateMachine';
import { logError, TelemetryViews } from '../telemtery';
import { WizardController } from '../wizard/wizardController';
import { DashboardStatusBar, ErrorEvent } from './DashboardStatusBar';
import { DashboardTab } from './dashboardTab'; import { DashboardTab } from './dashboardTab';
import { MigrationsTab, MigrationsTabId } from './migrationsTab'; import { MigrationsTab, MigrationsTabId } from './migrationsTab';
import { AdsMigrationStatus } from './tabBase'; import { AdsMigrationStatus, MigrationDetailsEvent, ServiceContextChangeEvent } from './tabBase';
export interface DashboardStatusBar { export interface MenuCommandArgs {
showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise<void>; connectionId: string,
clearError: () => Promise<void>; migrationId: string,
errorTitle: string; migrationOperationId: string,
errorLabel: string;
errorDescription: string;
} }
export class DashboardWidget implements DashboardStatusBar { export class DashboardWidget {
private _context: vscode.ExtensionContext; public stateModel!: MigrationStateModel;
private _view!: azdata.ModelView; private readonly _context: vscode.ExtensionContext;
private _tabs!: azdata.TabbedPanelComponent; private readonly _onServiceContextChanged: vscode.EventEmitter<ServiceContextChangeEvent>;
private _statusInfoBox!: azdata.InfoBoxComponent; private readonly _migrationDetailsEvent: vscode.EventEmitter<MigrationDetailsEvent>;
private _dashboardTab!: DashboardTab; private readonly _errorEvent: vscode.EventEmitter<ErrorEvent>;
private _migrationsTab!: MigrationsTab;
private _disposables: vscode.Disposable[] = [];
constructor(context: vscode.ExtensionContext) { constructor(context: vscode.ExtensionContext) {
this._context = context; this._context = context;
NotebookPathHelper.setExtensionContext(context);
IconPathHelper.setExtensionContext(context);
MigrationLocalStorage.setExtensionContext(context);
this._onServiceContextChanged = new vscode.EventEmitter<ServiceContextChangeEvent>();
this._errorEvent = new vscode.EventEmitter<ErrorEvent>();
this._migrationDetailsEvent = new vscode.EventEmitter<MigrationDetailsEvent>();
context.subscriptions.push(this._onServiceContextChanged);
context.subscriptions.push(this._errorEvent);
context.subscriptions.push(this._migrationDetailsEvent);
} }
public errorTitle: string = ''; public async register(): Promise<void> {
public errorLabel: string = ''; await this._registerCommands();
public errorDescription: string = '';
public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise<void> {
this.errorTitle = errorTitle;
this.errorLabel = errorLabel;
this.errorDescription = errorDescription;
this._statusInfoBox.style = 'error';
this._statusInfoBox.text = errorTitle;
await this._updateStatusDisplay(this._statusInfoBox, true);
}
public async clearError(): Promise<void> {
await this._updateStatusDisplay(this._statusInfoBox, false);
this.errorTitle = '';
this.errorLabel = '';
this.errorDescription = '';
this._statusInfoBox.style = 'success';
this._statusInfoBox.text = '';
}
public register(): void {
azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => { azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => {
this._view = view; const disposables: vscode.Disposable[] = [];
this._disposables.push( const _view = view;
this._view.onClosed(e => {
this._disposables.forEach( const statusInfoBox = view.modelBuilder.infoBox()
d => { try { d.dispose(); } catch { } }); .withProps({
})); style: 'error',
text: '',
clickableButtonAriaLabel: loc.ERROR_DIALOG_ARIA_CLICK_VIEW_ERROR_DETAILS,
announceText: true,
isClickable: true,
display: 'none',
CSSStyles: { 'font-size': '14px', 'display': 'none', },
}).component();
const connectionProfile = await azdata.connection.getCurrentConnection();
const statusBar = new DashboardStatusBar(
this._context,
connectionProfile.connectionId,
statusInfoBox,
this._errorEvent);
disposables.push(
statusInfoBox.onDidClick(
async e => await statusBar.openErrorDialog()));
disposables.push(
_view.onClosed(e =>
disposables.forEach(
d => { try { d.dispose(); } catch { } })));
const openMigrationFcn = async (filter: AdsMigrationStatus): Promise<void> => { const openMigrationFcn = async (filter: AdsMigrationStatus): Promise<void> => {
this._tabs.selectTab(MigrationsTabId); if (!migrationsTabInitialized) {
await this._migrationsTab.setMigrationFilter(filter); migrationsTabInitialized = true;
tabs.selectTab(MigrationsTabId);
await migrationsTab.setMigrationFilter(AdsMigrationStatus.ALL);
await migrationsTab.refresh();
await migrationsTab.setMigrationFilter(filter);
} else {
const promise = migrationsTab.setMigrationFilter(filter);
tabs.selectTab(MigrationsTabId);
await promise;
}
}; };
this._dashboardTab = await new DashboardTab().create( const dashboardTab = await new DashboardTab().create(
view, view,
async (filter: AdsMigrationStatus) => await openMigrationFcn(filter), async (filter: AdsMigrationStatus) => await openMigrationFcn(filter),
this); this._onServiceContextChanged,
this._disposables.push(this._dashboardTab); statusBar);
disposables.push(dashboardTab);
this._migrationsTab = await new MigrationsTab().create( const migrationsTab = await new MigrationsTab().create(
this._context, this._context,
view, view,
this); this._onServiceContextChanged,
this._disposables.push(this._migrationsTab); this._migrationDetailsEvent,
statusBar);
disposables.push(migrationsTab);
this._tabs = view.modelBuilder.tabbedPanel() const tabs = view.modelBuilder.tabbedPanel()
.withTabs([this._dashboardTab, this._migrationsTab]) .withTabs([dashboardTab, migrationsTab])
.withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal }) .withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal })
.withProps({ .withProps({
CSSStyles: { CSSStyles: {
@@ -91,107 +130,338 @@ export class DashboardWidget implements DashboardStatusBar {
}) })
.component(); .component();
this._disposables.push( let migrationsTabInitialized = false;
this._tabs.onTabChanged( disposables.push(
async id => { tabs.onTabChanged(async tabId => {
await this.clearError(); const connectionProfile = await azdata.connection.getCurrentConnection();
await this.onDialogClosed(); await this.clearError(connectionProfile.connectionId);
if (tabId === MigrationsTabId && !migrationsTabInitialized) {
migrationsTabInitialized = true;
await migrationsTab.refresh();
}
})); }));
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() const flexContainer = view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' }) .withLayout({ flexFlow: 'column' })
.withItems([this._statusInfoBox, this._tabs]) .withItems([statusInfoBox, tabs])
.component(); .component();
await view.initializeModel(flexContainer); await view.initializeModel(flexContainer);
await dashboardTab.refresh();
await this.refresh();
}); });
} }
public async refresh(): Promise<void> { private async _registerCommands(): Promise<void> {
void this._migrationsTab.refresh(); this._context.subscriptions.push(
await this._dashboardTab.refresh(); vscode.commands.registerCommand(
} MenuCommands.Cutover,
async (args: MenuCommandArgs) => {
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;
}
try { try {
const tab = azdata.window.createTab(this.errorTitle); await this.clearError(args.connectionId);
tab.registerContent(async (view) => { const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
const flex = view.modelBuilder.flexContainer() if (canRetryMigration(migration)) {
.withItems([ const cutoverDialogModel = new MigrationCutoverDialogModel(
view.modelBuilder.text() await MigrationLocalStorage.getMigrationServiceContext(),
.withProps({ value: this.errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } }) migration!);
.component(), await cutoverDialogModel.fetchStatus();
view.modelBuilder.inputBox() const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
.withProps({ await dialog.initialize();
value: this.errorDescription, if (cutoverDialogModel.CutoverError) {
readOnly: true, void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
multiline: true, logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
inputType: 'text', }
rows: 20, } else {
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' }, await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
}) }
.component() } catch (e) {
]) await this.showError(
.withLayout({ args.connectionId,
flexFlow: 'column', loc.MIGRATION_CUTOVER_ERROR,
width: 420, loc.MIGRATION_CUTOVER_ERROR,
}) e.message);
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
.component(); logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
await view.initializeModel(flex);
});
const dialog = azdata.window.createModelViewDialog(
this.errorTitle,
'errorDialog',
450,
'flyout');
dialog.content = [tab];
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
dialog.okButton.focused = true;
dialog.cancelButton.label = loc.CLOSE;
this._disposables.push(
dialog.onClosed(async e => {
if (e === 'ok') {
await this.clearError();
} }
this._errorDialogIsOpen = false;
})); }));
azdata.window.openDialog(dialog); this._context.subscriptions.push(
} catch (error) { vscode.commands.registerCommand(
this._errorDialogIsOpen = false; MenuCommands.ViewDatabase,
async (args: MenuCommandArgs) => {
try {
await this.clearError(args.connectionId);
this._migrationDetailsEvent.fire({
connectionId: args.connectionId,
migrationId: args.migrationId,
migrationOperationId: args.migrationOperationId,
});
} catch (e) {
await this.showError(
args.connectionId,
loc.OPEN_MIGRATION_DETAILS_ERROR,
loc.OPEN_MIGRATION_DETAILS_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
}
}));
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.ViewTarget,
async (args: MenuCommandArgs) => {
try {
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
await vscode.env.openExternal(vscode.Uri.parse(url));
} catch (e) {
await this.showError(
args.connectionId,
loc.OPEN_MIGRATION_TARGET_ERROR,
loc.OPEN_MIGRATION_TARGET_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
}
}));
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.ViewService,
async (args: MenuCommandArgs) => {
try {
await this.clearError(args.connectionId);
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
const dialog = new SqlMigrationServiceDetailsDialog(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await dialog.initialize();
} catch (e) {
await this.showError(
args.connectionId,
loc.OPEN_MIGRATION_SERVICE_ERROR,
loc.OPEN_MIGRATION_SERVICE_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
}
}));
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.CopyMigration,
async (args: MenuCommandArgs) => {
await this.clearError(args.connectionId);
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
if (migration) {
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration);
try {
await cutoverDialogModel.fetchStatus();
} catch (e) {
await this.showError(
args.connectionId,
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._context.subscriptions.push(vscode.commands.registerCommand(
MenuCommands.CancelMigration,
async (args: MenuCommandArgs) => {
try {
await this.clearError(args.connectionId);
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
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.showError(
args.connectionId,
loc.MIGRATION_CANCELLATION_ERROR,
loc.MIGRATION_CANCELLATION_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
}
}));
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.RetryMigration,
async (args: MenuCommandArgs) => {
try {
await this.clearError(args.connectionId);
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
if (canRetryMigration(migration)) {
const retryMigrationDialog = new RetryMigrationDialog(
this._context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration!,
this._onServiceContextChanged);
await retryMigrationDialog.openDialog();
}
else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
}
} catch (e) {
await this.showError(
args.connectionId,
loc.MIGRATION_RETRY_ERROR,
loc.MIGRATION_RETRY_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
}
}));
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.StartMigration,
async () => await this.launchMigrationWizard()));
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.OpenNotebooks,
async () => {
const input = vscode.window.createQuickPick<MigrationNotebookInfo>();
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
input.items = NotebookPathHelper.getAllMigrationNotebooks();
this._context.subscriptions.push(
input.onDidAccept(async (e) => {
const selectedNotebook = input.selectedItems[0];
if (selectedNotebook) {
try {
await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
preview: false,
initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
initialDirtyState: false
});
} catch (e) {
void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
}
input.hide();
}
}));
input.show();
}));
this._context.subscriptions.push(azdata.tasks.registerTask(
MenuCommands.StartMigration,
async () => await this.launchMigrationWizard()));
this._context.subscriptions.push(
azdata.tasks.registerTask(
MenuCommands.NewSupportRequest,
async () => await this.launchNewSupportRequest()));
this._context.subscriptions.push(
azdata.tasks.registerTask(
MenuCommands.SendFeedback,
async () => {
const actionId = MenuCommands.IssueReporter;
const args = {
extensionId: SqlMigrationExtensionId,
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
};
return await vscode.commands.executeCommand(actionId, args);
}));
}
private async clearError(connectionId: string): Promise<void> {
this._errorEvent.fire({
connectionId: connectionId,
title: '',
label: '',
message: '',
});
}
private async showError(connectionId: string, title: string, label: string, message: string): Promise<void> {
this._errorEvent.fire({
connectionId: connectionId,
title: title,
label: label,
message: message,
});
}
private async _getMigrationById(migrationId: string, migrationOperationId: string): Promise<DatabaseMigration | undefined> {
const context = await MigrationLocalStorage.getMigrationServiceContext();
if (context.azureAccount && context.subscription) {
return getMigrationDetails(
context.azureAccount,
context.subscription,
migrationId,
migrationOperationId);
}
return undefined;
}
public async launchMigrationWizard(): Promise<void> {
const activeConnection = await azdata.connection.getCurrentConnection();
let connectionId: string = '';
let serverName: string = '';
if (!activeConnection) {
const connection = await azdata.connection.openConnectionDialog();
if (connection) {
connectionId = connection.connectionId;
serverName = connection.options.server;
}
} else {
connectionId = activeConnection.connectionId;
serverName = activeConnection.serverName;
}
if (serverName) {
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
if (api) {
this.stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
this._context.subscriptions.push(this.stateModel);
const savedInfo = this.checkSavedInfo(serverName);
if (savedInfo) {
this.stateModel.savedInfo = savedInfo;
this.stateModel.serverName = serverName;
const savedAssessmentDialog = new SavedAssessmentDialog(
this._context,
this.stateModel,
this._onServiceContextChanged);
await savedAssessmentDialog.openDialog();
} else {
const wizardController = new WizardController(
this._context,
this.stateModel,
this._onServiceContextChanged);
await wizardController.openWizard(connectionId);
}
}
} }
} }
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> { private checkSavedInfo(serverName: string): SavedInfo | undefined {
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' }); return this._context.globalState.get<SavedInfo>(`${this.stateModel.mementoString}.${serverName}`);
}
public async launchNewSupportRequest(): Promise<void> {
await vscode.env.openExternal(vscode.Uri.parse(
`https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
} }
} }

View File

@@ -9,10 +9,10 @@ import * as loc from '../constants/strings';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
import { EOL } from 'os'; import { EOL } from 'os';
import { DatabaseMigration } from '../api/azure'; import { DatabaseMigration } from '../api/azure';
import { DashboardStatusBar } from './sqlServerDashboard';
import { getSelectedServiceStatus } from '../models/migrationLocalStorage'; import { getSelectedServiceStatus } from '../models/migrationLocalStorage';
import { MenuCommands, SqlMigrationExtensionId } from '../api/utils';
import { DashboardStatusBar } from './DashboardStatusBar';
export const SqlMigrationExtensionId = 'microsoft.sql-migration';
export const EmptySettingValue = '-'; export const EmptySettingValue = '-';
export enum AdsMigrationStatus { export enum AdsMigrationStatus {
@@ -23,17 +23,15 @@ export enum AdsMigrationStatus {
COMPLETING = 'completing' COMPLETING = 'completing'
} }
export const MenuCommands = { export interface ServiceContextChangeEvent {
Cutover: 'sqlmigration.cutover', connectionId: string;
ViewDatabase: 'sqlmigration.view.database', }
ViewTarget: 'sqlmigration.view.target',
ViewService: 'sqlmigration.view.service', export interface MigrationDetailsEvent {
CopyMigration: 'sqlmigration.copy.migration', connectionId: string,
CancelMigration: 'sqlmigration.cancel.migration', migrationId: string,
RetryMigration: 'sqlmigration.retry.migration', migrationOperationId: string,
StartMigration: 'sqlmigration.start', }
IssueReporter: 'workbench.action.openIssueReporter',
};
export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable { export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
public content!: azdata.Component; public content!: azdata.Component;
@@ -45,7 +43,8 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
protected view!: azdata.ModelView; protected view!: azdata.ModelView;
protected disposables: vscode.Disposable[] = []; protected disposables: vscode.Disposable[] = [];
protected isRefreshing: boolean = false; protected isRefreshing: boolean = false;
protected openMigrationFcn!: (status: AdsMigrationStatus) => Promise<void>; protected openMigrationsFcn!: (status: AdsMigrationStatus) => Promise<void>;
protected serviceContextChangedEvent!: vscode.EventEmitter<ServiceContextChangeEvent>;
protected statusBar!: DashboardStatusBar; protected statusBar!: DashboardStatusBar;
protected abstract initialize(view: azdata.ModelView): Promise<void>; protected abstract initialize(view: azdata.ModelView): Promise<void>;
@@ -165,8 +164,9 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
const errors = []; const errors = [];
errors.push(migration.properties.provisioningError); errors.push(migration.properties.provisioningError);
errors.push(migration.properties.migrationFailureError?.message); errors.push(migration.properties.migrationFailureError?.message);
errors.push(...migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []); errors.push(migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason); errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason);
errors.push(migration.properties.migrationStatusDetails?.sqlDataCopyErrors);
// remove undefined and duplicate error entries // remove undefined and duplicate error entries
return errors return errors

View File

@@ -118,12 +118,16 @@ export class AssessmentResultsDialog {
this._model._miDbs = selectedDbs; this._model._miDbs = selectedDbs;
break; break;
} }
case MigrationTargetType.SQLVM: { case MigrationTargetType.SQLVM: {
this.didUpdateDatabasesForMigration(this._model._vmDbs, selectedDbs); this.didUpdateDatabasesForMigration(this._model._vmDbs, selectedDbs);
this._model._vmDbs = selectedDbs; this._model._vmDbs = selectedDbs;
break; break;
} }
case MigrationTargetType.SQLDB: {
this.didUpdateDatabasesForMigration(this._model._sqldbDbs, selectedDbs);
this._model._sqldbDbs = selectedDbs;
break;
}
} }
await this._skuRecommendationPage.refreshCardText(); await this._skuRecommendationPage.refreshCardText();
this.model.refreshDatabaseBackupPage = true; this.model.refreshDatabaseBackupPage = true;

View File

@@ -9,25 +9,28 @@ import * as constants from '../../constants/strings';
import { MigrationStateModel } from '../../models/stateMachine'; import { MigrationStateModel } from '../../models/stateMachine';
import { WizardController } from '../../wizard/wizardController'; import { WizardController } from '../../wizard/wizardController';
import * as styles from '../../constants/styles'; import * as styles from '../../constants/styles';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
export class SavedAssessmentDialog { export class SavedAssessmentDialog {
private static readonly OkButtonText: string = constants.NEXT_LABEL; private static readonly OkButtonText: string = constants.NEXT_LABEL;
private static readonly CancelButtonText: string = constants.CANCEL_LABEL; private static readonly CancelButtonText: string = constants.CANCEL_LABEL;
private _isOpen: boolean = false;
private dialog: azdata.window.Dialog | undefined; private dialog: azdata.window.Dialog | undefined;
private _rootContainer!: azdata.FlexContainer;
private stateModel: MigrationStateModel; private stateModel: MigrationStateModel;
private context: vscode.ExtensionContext; private context: vscode.ExtensionContext;
private _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private _isOpen: boolean = false;
private _rootContainer!: azdata.FlexContainer;
constructor( constructor(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
stateModel: MigrationStateModel, stateModel: MigrationStateModel,
private readonly _onClosedCallback: () => Promise<void>) { serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
this.stateModel = stateModel; this.stateModel = stateModel;
this.context = context; this.context = context;
this._serviceContextChangedEvent = serviceContextChangedEvent;
} }
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> { private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
@@ -36,18 +39,18 @@ export class SavedAssessmentDialog {
try { try {
this._rootContainer = this.initializePageContent(view); this._rootContainer = this.initializePageContent(view);
await view.initializeModel(this._rootContainer); await view.initializeModel(this._rootContainer);
this._disposables.push(dialog.okButton.onClick(async e => { this._disposables.push(
await this.execute(); dialog.okButton.onClick(
})); async e => await this.execute()));
this._disposables.push(dialog.cancelButton.onClick(e => {
this.cancel(); this._disposables.push(
})); dialog.cancelButton.onClick(
e => this.cancel()));
this._disposables.push(
view.onClosed(
e => this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
this._disposables.push(view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } }
);
}));
resolve(); resolve();
} catch (ex) { } catch (ex) {
reject(ex); reject(ex);
@@ -83,7 +86,7 @@ export class SavedAssessmentDialog {
const wizardController = new WizardController( const wizardController = new WizardController(
this.context, this.context,
this.stateModel, this.stateModel,
this._onClosedCallback); this._serviceContextChangedEvent);
await wizardController.openWizard(this.stateModel.sourceConnectionId); await wizardController.openWizard(this.stateModel.sourceConnectionId);
this._isOpen = false; this._isOpen = false;
@@ -100,44 +103,39 @@ export class SavedAssessmentDialog {
public initializePageContent(view: azdata.ModelView): azdata.FlexContainer { public initializePageContent(view: azdata.ModelView): azdata.FlexContainer {
const buttonGroup = 'resumeMigration'; const buttonGroup = 'resumeMigration';
const radioStart = view.modelBuilder.radioButton().withProps({ const radioStart = view.modelBuilder.radioButton()
.withProps({
label: constants.START_NEW_SESSION, label: constants.START_NEW_SESSION,
name: buttonGroup, name: buttonGroup,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '8px' },
...styles.BODY_CSS,
'margin-bottom': '8px'
},
checked: true checked: true
}).component(); }).component();
this._disposables.push(radioStart.onDidChangeCheckedState((e) => { this._disposables.push(
if (e) { radioStart.onDidChangeCheckedState(checked => {
if (checked) {
this.stateModel.resumeAssessment = false; this.stateModel.resumeAssessment = false;
} }
})); }));
const radioContinue = view.modelBuilder.radioButton().withProps({ const radioContinue = view.modelBuilder.radioButton()
.withProps({
label: constants.RESUME_SESSION, label: constants.RESUME_SESSION,
name: buttonGroup, name: buttonGroup,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS },
...styles.BODY_CSS,
},
checked: false checked: false
}).component(); }).component();
this._disposables.push(radioContinue.onDidChangeCheckedState((e) => { this._disposables.push(
if (e) { radioContinue.onDidChangeCheckedState(checked => {
if (checked) {
this.stateModel.resumeAssessment = true; this.stateModel.resumeAssessment = true;
} }
})); }));
const flex = view.modelBuilder.flexContainer() const flex = view.modelBuilder.flexContainer()
.withLayout({ .withLayout({ flexFlow: 'column', })
flexFlow: 'column', .withProps({ CSSStyles: { 'padding': '20px 15px', } })
}).withProps({ .component();
CSSStyles: {
'padding': '20px 15px',
}
}).component();
flex.addItem(radioStart, { flex: '0 0 auto' }); flex.addItem(radioStart, { flex: '0 0 auto' });
flex.addItem(radioContinue, { flex: '0 0 auto' }); flex.addItem(radioContinue, { flex: '0 0 auto' });

View File

@@ -91,7 +91,14 @@ export class SqlDatabaseTree {
const selectDbMessage = this.createSelectDbMessage(); const selectDbMessage = this.createSelectDbMessage();
this._resultComponent = await this.createComponentResult(view); this._resultComponent = await this.createComponentResult(view);
const treeComponent = await this.createComponent(view, this._targetType === MigrationTargetType.SQLVM ? this._model._vmDbs : this._model._miDbs); const treeComponent = await this.createComponent(
view,
(this._targetType === MigrationTargetType.SQLVM)
? this._model._vmDbs
: (this._targetType === MigrationTargetType.SQLMI)
? this._model._miDbs
: this._model._sqldbDbs);
this._rootContainer = view.modelBuilder.flexContainer().withLayout({ this._rootContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row', flexFlow: 'row',
height: '100%', height: '100%',
@@ -101,7 +108,8 @@ export class SqlDatabaseTree {
this._rootContainer.addItem(this._resultComponent, { flex: '0 0 auto' }); this._rootContainer.addItem(this._resultComponent, { flex: '0 0 auto' });
this._rootContainer.addItem(selectDbMessage, { flex: '1 1 auto' }); this._rootContainer.addItem(selectDbMessage, { flex: '1 1 auto' });
if (this._targetType === MigrationTargetType.SQLMI) { if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
if (!!this._model._assessmentResults?.issues.find(value => value.databaseRestoreFails) || if (!!this._model._assessmentResults?.issues.find(value => value.databaseRestoreFails) ||
!!this._model._assessmentResults?.databaseAssessments.find(d => !!d.issues.find(issue => issue.databaseRestoreFails))) { !!this._model._assessmentResults?.databaseAssessments.find(d => !!d.issues.find(issue => issue.databaseRestoreFails))) {
dialog.message = { dialog.message = {
@@ -192,7 +200,8 @@ export class SqlDatabaseTree {
})); }));
this._disposables.push(this._databaseTable.onRowSelected(async (e) => { this._disposables.push(this._databaseTable.onRowSelected(async (e) => {
if (this._targetType === MigrationTargetType.SQLMI) { if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
this._activeIssues = this._model._assessmentResults?.databaseAssessments[e.row].issues; this._activeIssues = this._model._assessmentResults?.databaseAssessments[e.row].issues;
} else { } else {
this._activeIssues = []; this._activeIssues = [];
@@ -306,7 +315,8 @@ export class SqlDatabaseTree {
}); });
this._recommendation.value = constants.WARNINGS_DETAILS; this._recommendation.value = constants.WARNINGS_DETAILS;
this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues?.length); this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues?.length);
if (this._targetType === MigrationTargetType.SQLMI) { if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
await this.refreshResults(); await this.refreshResults();
} }
})); }));
@@ -388,42 +398,34 @@ export class SqlDatabaseTree {
} }
private createNoIssuesText(): azdata.FlexContainer { private createNoIssuesText(): azdata.FlexContainer {
let message: azdata.TextComponent;
const failedAssessment = this.handleFailedAssessment(); const failedAssessment = this.handleFailedAssessment();
if (this._targetType === MigrationTargetType.SQLVM) {
message = this._view.modelBuilder.text().withProps({
value: failedAssessment
? constants.NO_RESULTS_AVAILABLE
: constants.NO_ISSUES_FOUND_VM,
CSSStyles: {
...styles.BODY_CSS
}
}).component();
} else {
message = this._view.modelBuilder.text().withProps({
value: failedAssessment
? constants.NO_RESULTS_AVAILABLE
: constants.NO_ISSUES_FOUND_MI,
CSSStyles: {
...styles.BODY_CSS
}
}).component();
}
//TODO: will need to add a SQL DB condition here in the future
this._noIssuesContainer = this._view.modelBuilder.flexContainer().withItems([message]).withProps({
CSSStyles: { const value = failedAssessment
'margin-top': '8px', ? constants.NO_RESULTS_AVAILABLE
'display': 'none' : (this._targetType === MigrationTargetType.SQLVM)
} ? constants.NO_ISSUES_FOUND_VM
: (this._targetType === MigrationTargetType.SQLMI)
? constants.NO_ISSUES_FOUND_MI
: constants.NO_ISSUES_FOUND_SQLDB;
const message = this._view.modelBuilder.text()
.withProps({
value: value,
CSSStyles: { ...styles.BODY_CSS }
}).component(); }).component();
this._noIssuesContainer = this._view.modelBuilder.flexContainer()
.withItems([message])
.withProps({ CSSStyles: { 'margin-top': '8px', 'display': 'none' } })
.component();
return this._noIssuesContainer; return this._noIssuesContainer;
} }
private handleFailedAssessment(): boolean { private handleFailedAssessment(): boolean {
const failedAssessment: boolean = this._model._assessmentResults?.assessmentError !== undefined const failedAssessment: boolean = this._model._assessmentResults?.assessmentError !== undefined
|| (this._model._assessmentResults?.errors?.length || 0) > 0; || (this._model._assessmentResults?.errors?.length ?? 0) > 0;
if (failedAssessment) { if (failedAssessment) {
this._dialog.message = { this._dialog.message = {
level: azdata.window.MessageLevel.Warning, level: azdata.window.MessageLevel.Warning,
@@ -471,16 +473,12 @@ export class SqlDatabaseTree {
private createAssessmentContainer(): azdata.FlexContainer { private createAssessmentContainer(): azdata.FlexContainer {
const title = this.createAssessmentTitle(); const title = this.createAssessmentTitle();
const bottomContainer = this.createDescriptionContainer(); const bottomContainer = this.createDescriptionContainer();
const container = this._view.modelBuilder.flexContainer()
const container = this._view.modelBuilder.flexContainer().withItems([title, bottomContainer]).withLayout({ .withItems([title, bottomContainer])
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).withProps({ .withProps({ CSSStyles: { 'margin-left': '24px' } })
CSSStyles: { .component();
'margin-left': '24px'
}
}).component();
return container; return container;
} }
@@ -488,14 +486,10 @@ export class SqlDatabaseTree {
private createDescriptionContainer(): azdata.FlexContainer { private createDescriptionContainer(): azdata.FlexContainer {
const description = this.createDescription(); const description = this.createDescription();
const impactedObjects = this.createImpactedObjectsDescription(); const impactedObjects = this.createImpactedObjectsDescription();
const container = this._view.modelBuilder.flexContainer()
const container = this._view.modelBuilder.flexContainer().withLayout({ .withLayout({ flexFlow: 'row' })
flexFlow: 'row' .withProps({ CSSStyles: { 'height': '100%' } })
}).withProps({ .component();
CSSStyles: {
'height': '100%'
}
}).component();
container.addItem(description, { flex: '0 0 auto', CSSStyles: { 'width': '200px', 'margin-right': '35px' } }); container.addItem(description, { flex: '0 0 auto', CSSStyles: { 'width': '200px', 'margin-right': '35px' } });
container.addItem(impactedObjects, { flex: '0 0 auto', CSSStyles: { 'width': '280px' } }); container.addItem(impactedObjects, { flex: '0 0 auto', CSSStyles: { 'width': '280px' } });
@@ -541,19 +535,8 @@ export class SqlDatabaseTree {
rowCssStyles: rowStyle rowCssStyles: rowStyle
}, },
], ],
dataValues: [ dataValues: [[{ value: '' }, { value: '' }]],
[ CSSStyles: { 'margin-top': '12px' }
{
value: ''
},
{
value: ''
}
]
],
CSSStyles: {
'margin-top': '12px'
}
} }
).component(); ).component();
@@ -562,7 +545,8 @@ export class SqlDatabaseTree {
this.refreshImpactedObject(impactedObject); this.refreshImpactedObject(impactedObject);
})); }));
const objectDetailsTitle = this._view.modelBuilder.text().withProps({ const objectDetailsTitle = this._view.modelBuilder.text()
.withProps({
value: constants.OBJECT_DETAILS, value: constants.OBJECT_DETAILS,
CSSStyles: { CSSStyles: {
...styles.LIGHT_LABEL_CSS, ...styles.LIGHT_LABEL_CSS,
@@ -574,24 +558,34 @@ export class SqlDatabaseTree {
'margin': '5px 0px 0px 0px', 'margin': '5px 0px 0px 0px',
'word-wrap': 'break-word' 'word-wrap': 'break-word'
}; };
this._objectDetailsType = this._view.modelBuilder.text().withProps({ this._objectDetailsType = this._view.modelBuilder.text()
.withProps({
value: constants.TYPES_LABEL, value: constants.TYPES_LABEL,
CSSStyles: objectDescriptionStyle CSSStyles: objectDescriptionStyle
}).component(); }).component();
this._objectDetailsName = this._view.modelBuilder.text().withProps({ this._objectDetailsName = this._view.modelBuilder.text()
.withProps({
value: constants.NAMES_LABEL, value: constants.NAMES_LABEL,
CSSStyles: objectDescriptionStyle CSSStyles: objectDescriptionStyle
}).component(); }).component();
this._objectDetailsSample = this._view.modelBuilder.text().withProps({ this._objectDetailsSample = this._view.modelBuilder.text()
.withProps({
value: '', value: '',
CSSStyles: objectDescriptionStyle CSSStyles: objectDescriptionStyle
}).component(); }).component();
const container = this._view.modelBuilder.flexContainer().withItems([impactedObjectsTitle, this._impactedObjectsTable, objectDetailsTitle, this._objectDetailsType, this._objectDetailsName, this._objectDetailsSample]).withLayout({ const container = this._view.modelBuilder.flexContainer()
flexFlow: 'column' .withItems([
}).component(); impactedObjectsTitle,
this._impactedObjectsTable,
objectDetailsTitle,
this._objectDetailsType,
this._objectDetailsName,
this._objectDetailsSample])
.withLayout({ flexFlow: 'column' })
.component();
return container; return container;
} }
@@ -607,26 +601,32 @@ export class SqlDatabaseTree {
'width': '200px', 'width': '200px',
'word-wrap': 'break-word' 'word-wrap': 'break-word'
}; };
const descriptionTitle = this._view.modelBuilder.text().withProps({ const descriptionTitle = this._view.modelBuilder.text()
.withProps({
value: constants.DESCRIPTION, value: constants.DESCRIPTION,
CSSStyles: LABEL_CSS CSSStyles: LABEL_CSS
}).component(); }).component();
this._descriptionText = this._view.modelBuilder.text().withProps({ this._descriptionText = this._view.modelBuilder.text()
.withProps({
CSSStyles: textStyle CSSStyles: textStyle
}).component(); }).component();
const recommendationTitle = this._view.modelBuilder.text().withProps({ const recommendationTitle = this._view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDATION, value: constants.RECOMMENDATION,
CSSStyles: LABEL_CSS CSSStyles: LABEL_CSS
}).component(); }).component();
this._recommendationText = this._view.modelBuilder.text().withProps({ this._recommendationText = this._view.modelBuilder.text()
.withProps({
CSSStyles: textStyle CSSStyles: textStyle
}).component(); }).component();
const moreInfo = this._view.modelBuilder.text().withProps({ const moreInfo = this._view.modelBuilder.text()
.withProps({
value: constants.MORE_INFO, value: constants.MORE_INFO,
CSSStyles: LABEL_CSS CSSStyles: LABEL_CSS
}).component(); }).component();
this._moreInfo = this._view.modelBuilder.hyperlink().withProps({ this._moreInfo = this._view.modelBuilder.hyperlink()
.withProps({
label: '', label: '',
url: '', url: '',
CSSStyles: textStyle, CSSStyles: textStyle,
@@ -634,15 +634,22 @@ export class SqlDatabaseTree {
showLinkIcon: true showLinkIcon: true
}).component(); }).component();
const container = this._view.modelBuilder.flexContainer().withItems([descriptionTitle, this._descriptionText, recommendationTitle, this._recommendationText, moreInfo, this._moreInfo]).withLayout({ const container = this._view.modelBuilder.flexContainer()
flexFlow: 'column' .withItems([descriptionTitle,
}).component(); this._descriptionText,
recommendationTitle,
this._recommendationText,
moreInfo,
this._moreInfo])
.withLayout({ flexFlow: 'column' })
.component();
return container; return container;
} }
private createAssessmentTitle(): azdata.TextComponent { private createAssessmentTitle(): azdata.TextComponent {
this._assessmentTitle = this._view.modelBuilder.text().withProps({ this._assessmentTitle = this._view.modelBuilder.text()
.withProps({
value: '', value: '',
CSSStyles: { CSSStyles: {
...styles.LABEL_CSS, ...styles.LABEL_CSS,
@@ -657,26 +664,28 @@ export class SqlDatabaseTree {
} }
private createTitleComponent(): azdata.TextComponent { private createTitleComponent(): azdata.TextComponent {
const title = this._view.modelBuilder.text().withProps({ return this._view.modelBuilder.text()
.withProps({
value: constants.TARGET_PLATFORM, value: constants.TARGET_PLATFORM,
CSSStyles: { CSSStyles: {
...styles.BODY_CSS, ...styles.BODY_CSS,
'margin': '0 0 4px 0' 'margin': '0 0 4px 0'
} }
}); }).component();
return title.component();
} }
private createPlatformComponent(): azdata.TextComponent { private createPlatformComponent(): azdata.TextComponent {
const impact = this._view.modelBuilder.text().withProps({ const target = (this._targetType === MigrationTargetType.SQLVM)
value: (this._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, ? constants.SUMMARY_VM_TYPE
CSSStyles: { : (this._targetType === MigrationTargetType.SQLMI)
...styles.PAGE_SUBTITLE_CSS ? constants.SUMMARY_MI_TYPE
} : constants.SUMMARY_SQLDB_TYPE;
});
return impact.component(); return this._view.modelBuilder.text()
.withProps({
value: target,
CSSStyles: { ...styles.PAGE_SUBTITLE_CSS }
}).component();
} }
private createRecommendationComponent(): azdata.TextComponent { private createRecommendationComponent(): azdata.TextComponent {
@@ -718,7 +727,6 @@ export class SqlDatabaseTree {
} }
private createImpactedObjectsTable(): azdata.FlexContainer { private createImpactedObjectsTable(): azdata.FlexContainer {
const headerStyle: azdata.CssStyles = { const headerStyle: azdata.CssStyles = {
'border': 'none', 'border': 'none',
'text-align': 'left' 'text-align': 'left'
@@ -732,13 +740,11 @@ export class SqlDatabaseTree {
'overflow': 'hidden', 'overflow': 'hidden',
}; };
this._assessmentResultsTable = this._view.modelBuilder.declarativeTable().withProps( this._assessmentResultsTable = this._view.modelBuilder.declarativeTable()
{ .withProps({
enableRowSelection: true, enableRowSelection: true,
width: '200px', width: '200px',
CSSStyles: { CSSStyles: { 'table-layout': 'fixed' },
'table-layout': 'fixed'
},
columns: [ columns: [
{ {
displayName: '', displayName: '',
@@ -765,14 +771,14 @@ export class SqlDatabaseTree {
await this.refreshAssessmentDetails(selectedIssue); await this.refreshAssessmentDetails(selectedIssue);
})); }));
const container = this._view.modelBuilder.flexContainer().withItems([this._assessmentResultsTable]).withLayout({ const container = this._view.modelBuilder.flexContainer()
.withItems([this._assessmentResultsTable])
.withLayout({
flexFlow: 'column', flexFlow: 'column',
height: '100%' height: '100%'
}).withProps({ })
CSSStyles: { .withProps({ CSSStyles: { 'border-right': 'solid 1px' } })
'border-right': 'solid 1px' .component();
}
}).component();
return container; return container;
} }
@@ -788,42 +794,23 @@ export class SqlDatabaseTree {
} }
public async refreshResults(): Promise<void> { public async refreshResults(): Promise<void> {
if (this._targetType === MigrationTargetType.SQLMI) { if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
if (this._activeIssues?.length === 0) { if (this._activeIssues?.length === 0) {
/// show no issues here /// show no issues here
await this._assessmentsTable.updateCssStyles({ await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' });
'display': 'none', await this._assessmentContainer.updateCssStyles({ 'display': 'none' });
'border-right': 'none' await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' });
});
await this._assessmentContainer.updateCssStyles({
'display': 'none'
});
await this._noIssuesContainer.updateCssStyles({
'display': 'flex'
});
} else { } else {
await this._assessmentContainer.updateCssStyles({ await this._assessmentContainer.updateCssStyles({ 'display': 'flex' });
'display': 'flex' await this._assessmentsTable.updateCssStyles({ 'display': 'flex', 'border-right': 'solid 1px' });
}); await this._noIssuesContainer.updateCssStyles({ 'display': 'none' });
await this._assessmentsTable.updateCssStyles({
'display': 'flex',
'border-right': 'solid 1px'
});
await this._noIssuesContainer.updateCssStyles({
'display': 'none'
});
} }
} else { } else {
await this._assessmentsTable.updateCssStyles({ await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' });
'display': 'none', await this._assessmentContainer.updateCssStyles({ 'display': 'none' });
'border-right': 'none' await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' });
});
await this._assessmentContainer.updateCssStyles({
'display': 'none'
});
await this._noIssuesContainer.updateCssStyles({
'display': 'flex'
});
this._recommendationTitle.value = constants.ASSESSMENT_RESULTS; this._recommendationTitle.value = constants.ASSESSMENT_RESULTS;
this._recommendation.value = ''; this._recommendation.value = '';
} }
@@ -868,7 +855,8 @@ export class SqlDatabaseTree {
this._impactedObjects = selectedIssue?.impactedObjects || []; this._impactedObjects = selectedIssue?.impactedObjects || [];
this._recommendationText.value = selectedIssue?.message || constants.NA; this._recommendationText.value = selectedIssue?.message || constants.NA;
await this._impactedObjectsTable.setDataValues(this._impactedObjects.map( await this._impactedObjectsTable.setDataValues(
this._impactedObjects.map(
(object) => [{ value: object.objectType }, { value: object.name }])); (object) => [{ value: object.objectType }, { value: object.name }]));
this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1; this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1;
@@ -884,12 +872,16 @@ export class SqlDatabaseTree {
let instanceTableValues: azdata.DeclarativeTableCellValue[][] = []; let instanceTableValues: azdata.DeclarativeTableCellValue[][] = [];
this._databaseTableValues = []; this._databaseTableValues = [];
this._dbNames = this._model._databasesForAssessment; this._dbNames = this._model._databasesForAssessment;
const selectedDbs = (this._targetType === MigrationTargetType.SQLVM) ? this._model._vmDbs : this._model._miDbs; const selectedDbs = (this._targetType === MigrationTargetType.SQLVM)
? this._model._vmDbs
: (this._targetType === MigrationTargetType.SQLMI)
? this._model._miDbs
: this._model._sqldbDbs;
this._serverName = (await this._model.getSourceConnectionProfile()).serverName; this._serverName = (await this._model.getSourceConnectionProfile()).serverName;
if (this._targetType === MigrationTargetType.SQLVM || !this._model._assessmentResults) { if (this._targetType === MigrationTargetType.SQLVM || !this._model._assessmentResults) {
instanceTableValues = [ instanceTableValues = [[
[
{ {
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName), value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
style: styleLeft style: styleLeft
@@ -898,11 +890,9 @@ export class SqlDatabaseTree {
value: '0', value: '0',
style: styleRight style: styleRight
} }
] ]];
];
this._dbNames.forEach((db) => { this._dbNames.forEach((db) => {
this._databaseTableValues.push( this._databaseTableValues.push([
[
{ {
value: selectedDbs.includes(db), value: selectedDbs.includes(db),
style: styleLeft style: styleLeft
@@ -915,12 +905,10 @@ export class SqlDatabaseTree {
value: '0', value: '0',
style: styleRight style: styleRight
} }
] ]);
);
}); });
} else { } else {
instanceTableValues = [ instanceTableValues = [[
[
{ {
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName), value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
style: styleLeft style: styleLeft
@@ -929,11 +917,10 @@ export class SqlDatabaseTree {
value: this._model._assessmentResults?.issues?.length, value: this._model._assessmentResults?.issues?.length,
style: styleRight style: styleRight
} }
] ]];
]; this._model._assessmentResults?.databaseAssessments
this._model._assessmentResults?.databaseAssessments.sort((db1, db2) => { .sort((db1, db2) => db2.issues?.length - db1.issues?.length);
return db2.issues?.length - db1.issues?.length;
});
// Reset the dbName list so that it is in sync with the table // Reset the dbName list so that it is in sync with the table
this._dbNames = this._model._assessmentResults?.databaseAssessments.map(da => da.name); this._dbNames = this._model._assessmentResults?.databaseAssessments.map(da => da.name);
this._model._assessmentResults?.databaseAssessments.forEach((db) => { this._model._assessmentResults?.databaseAssessments.forEach((db) => {
@@ -941,8 +928,7 @@ export class SqlDatabaseTree {
if (db.issues.find(item => item.databaseRestoreFails)) { if (db.issues.find(item => item.databaseRestoreFails)) {
selectable = false; selectable = false;
} }
this._databaseTableValues.push( this._databaseTableValues.push([
[
{ {
value: selectedDbs.includes(db.name), value: selectedDbs.includes(db.name),
style: styleLeft, style: styleLeft,
@@ -956,8 +942,7 @@ export class SqlDatabaseTree {
value: db.issues?.length, value: db.issues?.length,
style: styleRight style: styleRight
} }
] ]);
);
}); });
} }
await this._instanceTable.setDataValues(instanceTableValues); await this._instanceTable.setDataValues(instanceTableValues);
@@ -973,47 +958,7 @@ export class SqlDatabaseTree {
}); });
} }
// undo when bug #16445 is fixed
private createIconTextCell(icon: IconPath, text: string): string { private createIconTextCell(icon: IconPath, text: string): string {
return text; return text;
} }
// private createIconTextCell(icon: IconPath, text: string): azdata.FlexContainer {
// const cellContainer = this._view.modelBuilder.flexContainer().withProps({
// CSSStyles: {
// 'justify-content': 'left'
// }
// }).component();
// const iconComponent = this._view.modelBuilder.image().withProps({
// iconPath: icon,
// iconWidth: '16px',
// iconHeight: '16px',
// width: '20px',
// height: '20px'
// }).component();
// cellContainer.addItem(iconComponent, {
// flex: '0',
// CSSStyles: {
// 'width': '32px'
// }
// });
// const textComponent = this._view.modelBuilder.text().withProps({
// value: text,
// title: text,
// CSSStyles: {
// 'margin': '0px',
// 'width': '100%',
// }
// }).component();
// cellContainer.addItem(textComponent, {
// CSSStyles: {
// 'width': 'auto'
// }
// });
// return cellContainer;
// }
// undo when bug #16445 is fixed
} }

View File

@@ -390,8 +390,12 @@ export class CreateSqlMigrationServiceDialog {
private async populateResourceGroups(): Promise<void> { private async populateResourceGroups(): Promise<void> {
this.migrationServiceResourceGroupDropdown.loading = true; this.migrationServiceResourceGroupDropdown.loading = true;
try { try {
this._resourceGroups = await utils.getAllResourceGroups(this._model._azureAccount, this._model._targetSubscription); this._resourceGroups = await utils.getAllResourceGroups(
this.migrationServiceResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups); this._model._azureAccount,
this._model._targetSubscription);
this.migrationServiceResourceGroupDropdown.values = utils.getResourceDropdownValues(
this._resourceGroups,
constants.RESOURCE_GROUP_NOT_FOUND);
const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.name.toLowerCase() === this._resourceGroupPreset.toLowerCase()); const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.name.toLowerCase() === this._resourceGroupPreset.toLowerCase());
this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0]; this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0];

View File

@@ -156,9 +156,10 @@ export class ConfirmCutoverDialog {
height: 20, height: 20,
label: constants.REFRESH, label: constants.REFRESH,
}).component(); }).component();
this._disposables.push(refreshButton.onDidClick(async e => { this._disposables.push(
refreshLoader.loading = true; refreshButton.onDidClick(async e => {
try { try {
refreshLoader.loading = true;
await this.migrationCutoverModel.fetchStatus(); await this.migrationCutoverModel.fetchStatus();
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0); containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
} catch (e) { } catch (e) {
@@ -232,9 +233,10 @@ export class ConfirmCutoverDialog {
headingRow.addItem(containerHeading, { flex: '0' }); headingRow.addItem(containerHeading, { flex: '0' });
this._disposables.push(refreshButton.onDidClick(async e => { this._disposables.push(
refreshLoader.loading = true; refreshButton.onDidClick(async e => {
try { try {
refreshLoader.loading = true;
await this.migrationCutoverModel.fetchStatus(); await this.migrationCutoverModel.fetchStatus();
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0); containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())); lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date()));

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure'; import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure';
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage'; import { 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 { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper'; import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
@@ -110,7 +110,7 @@ export class MigrationCutoverDialogModel {
const files: BackupFileInfo[] = []; const files: BackupFileInfo[] = [];
this.migration.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 !== constants.BackupFileInfoStatus.Restored) {
files.push(f); files.push(f);
} }
}); });

View File

@@ -13,6 +13,7 @@ import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { WizardController } from '../../wizard/wizardController'; import { WizardController } from '../../wizard/wizardController';
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper'; import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
import * as constants from '../../constants/strings'; import * as constants from '../../constants/strings';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
export class RetryMigrationDialog { export class RetryMigrationDialog {
@@ -20,15 +21,20 @@ export class RetryMigrationDialog {
private readonly _context: vscode.ExtensionContext, private readonly _context: vscode.ExtensionContext,
private readonly _serviceContext: MigrationServiceContext, private readonly _serviceContext: MigrationServiceContext,
private readonly _migration: DatabaseMigration, private readonly _migration: DatabaseMigration,
private readonly _onClosedCallback: () => Promise<void>) { private readonly _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
} }
private async createMigrationStateModel(serviceContext: MigrationServiceContext, migration: DatabaseMigration, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): Promise<MigrationStateModel> { private async createMigrationStateModel(
let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration); serviceContext: MigrationServiceContext,
migration: DatabaseMigration,
connectionId: string,
serverName: string,
api: mssql.IExtension,
location: azureResource.AzureLocation): Promise<MigrationStateModel> {
const stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
const sourceDatabaseName = migration.properties.sourceDatabaseName; const sourceDatabaseName = migration.properties.sourceDatabaseName;
let savedInfo: SavedInfo; const savedInfo: SavedInfo = {
savedInfo = {
closedPage: 0, closedPage: 0,
// DatabaseSelector // DatabaseSelector
@@ -142,7 +148,7 @@ export class RetryMigrationDialog {
} }
}); });
let activeConnection = await azdata.connection.getCurrentConnection(); const activeConnection = await azdata.connection.getCurrentConnection();
let connectionId: string = ''; let connectionId: string = '';
let serverName: string = ''; let serverName: string = '';
if (!activeConnection) { if (!activeConnection) {
@@ -163,7 +169,7 @@ export class RetryMigrationDialog {
const wizardController = new WizardController( const wizardController = new WizardController(
this._context, this._context,
stateModel, stateModel,
this._onClosedCallback); this._serviceContextChangedEvent);
await wizardController.openWizard(stateModel.sourceConnectionId); await wizardController.openWizard(stateModel.sourceConnectionId);
} else { } else {
void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY); void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY);

View File

@@ -12,6 +12,7 @@ import * as constants from '../../constants/strings';
import * as utils from '../../api/utils'; import * as utils from '../../api/utils';
import { SqlMigrationService } from '../../api/azure'; import { SqlMigrationService } from '../../api/azure';
import { logError, TelemetryViews } from '../../telemtery'; import { logError, TelemetryViews } from '../../telemtery';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
const CONTROL_MARGIN = '20px'; const CONTROL_MARGIN = '20px';
const INPUT_COMPONENT_WIDTH = '100%'; const INPUT_COMPONENT_WIDTH = '100%';
@@ -56,7 +57,7 @@ export class SelectMigrationServiceDialog {
private _deleteButton!: azdata.window.Button; private _deleteButton!: azdata.window.Button;
constructor( constructor(
private readonly _onClosedCallback: () => Promise<void>) { private readonly onServiceContextChanged: vscode.EventEmitter<ServiceContextChangeEvent>) {
this._dialog = azdata.window.createModelViewDialog( this._dialog = azdata.window.createModelViewDialog(
constants.MIGRATION_SERVICE_SELECT_TITLE, constants.MIGRATION_SERVICE_SELECT_TITLE,
'SelectMigraitonServiceDialog', 'SelectMigraitonServiceDialog',
@@ -85,10 +86,10 @@ export class SelectMigrationServiceDialog {
'left'); 'left');
this._disposables.push( this._disposables.push(
this._deleteButton.onClick(async (value) => { this._deleteButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext({}); await MigrationLocalStorage.saveMigrationServiceContext({}, this.onServiceContextChanged);
await this._onClosedCallback();
azdata.window.closeDialog(this._dialog); azdata.window.closeDialog(this._dialog);
})); }));
this._dialog.customButtons = [this._deleteButton]; this._dialog.customButtons = [this._deleteButton];
azdata.window.openDialog(this._dialog); azdata.window.openDialog(this._dialog);
@@ -262,7 +263,7 @@ export class SelectMigrationServiceDialog {
? utils.deepClone(selectedLocation) ? utils.deepClone(selectedLocation)
: undefined!; : undefined!;
await this._populateResourceGroupDropdown(); await this._populateResourceGroupDropdown();
await this._populateMigrationServiceDropdown(); this._populateMigrationServiceDropdown();
} }
})); }));
@@ -290,7 +291,7 @@ export class SelectMigrationServiceDialog {
this._serviceContext.resourceGroup = (selectedResourceGroup) this._serviceContext.resourceGroup = (selectedResourceGroup)
? utils.deepClone(selectedResourceGroup) ? utils.deepClone(selectedResourceGroup)
: undefined!; : undefined!;
await this._populateMigrationServiceDropdown(); this._populateMigrationServiceDropdown();
} }
})); }));
@@ -323,10 +324,10 @@ export class SelectMigrationServiceDialog {
})); }));
this._disposables.push( this._disposables.push(
this._dialog.okButton.onClick(async (value) => { this._dialog.okButton.onClick(async (value) =>
await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext); await MigrationLocalStorage.saveMigrationServiceContext(
await this._onClosedCallback(); this._serviceContext,
})); this.onServiceContextChanged)));
return this._view.modelBuilder.flexContainer() return this._view.modelBuilder.flexContainer()
.withItems([ .withItems([
@@ -417,8 +418,14 @@ export class SelectMigrationServiceDialog {
private async _populateLocationDropdown(): Promise<void> { private async _populateLocationDropdown(): Promise<void> {
try { try {
this._azureLocationDropdown.loading = true; this._azureLocationDropdown.loading = true;
this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this._serviceContext.azureAccount, this._serviceContext.subscription); this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(
this._locations = await utils.getSqlMigrationServiceLocations(this._serviceContext.azureAccount, this._serviceContext.subscription, this._sqlMigrationServices); this._serviceContext.azureAccount,
this._serviceContext.subscription);
this._locations = await utils.getResourceLocations(
this._serviceContext.azureAccount,
this._serviceContext.subscription,
this._sqlMigrationServices);
this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this._locations); this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this._locations);
if (this._azureLocationDropdown.values.length > 0) { if (this._azureLocationDropdown.values.length > 0) {
utils.selectDefaultDropdownValue( utils.selectDefaultDropdownValue(
@@ -439,8 +446,13 @@ export class SelectMigrationServiceDialog {
private async _populateResourceGroupDropdown(): Promise<void> { private async _populateResourceGroupDropdown(): Promise<void> {
try { try {
this._azureResourceGroupDropdown.loading = true; this._azureResourceGroupDropdown.loading = true;
this._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this._sqlMigrationServices, this._serviceContext.location!); this._resourceGroups = utils.getServiceResourceGroupsByLocation(
this._azureResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups); this._sqlMigrationServices,
this._serviceContext.location!);
this._azureResourceGroupDropdown.values = utils.getResourceDropdownValues(
this._resourceGroups,
constants.RESOURCE_GROUP_NOT_FOUND);
if (this._azureResourceGroupDropdown.values.length > 0) { if (this._azureResourceGroupDropdown.values.length > 0) {
utils.selectDefaultDropdownValue( utils.selectDefaultDropdownValue(
this._azureResourceGroupDropdown, this._azureResourceGroupDropdown,
@@ -457,10 +469,15 @@ export class SelectMigrationServiceDialog {
} }
} }
private async _populateMigrationServiceDropdown(): Promise<void> { private _populateMigrationServiceDropdown(): void {
try { try {
this._azureServiceDropdown.loading = true; this._azureServiceDropdown.loading = true;
this._azureServiceDropdown.values = await utils.getAzureSqlMigrationServicesDropdownValues(this._sqlMigrationServices, this._serviceContext.location!, this._serviceContext.resourceGroup!); this._azureServiceDropdown.values = utils.getAzureResourceDropdownValues(
this._sqlMigrationServices,
this._serviceContext.location!,
this._serviceContext.resourceGroup?.name,
constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR);
if (this._azureServiceDropdown.values.length > 0) { if (this._azureServiceDropdown.values.length > 0) {
utils.selectDefaultDropdownValue( utils.selectDefaultDropdownValue(
this._azureServiceDropdown, this._azureServiceDropdown,

View File

@@ -111,9 +111,11 @@ export class GetAzureRecommendationDialog {
'margin': '0' 'margin': '0'
}, },
}).component(); }).component();
this._disposables.push(collectDataButton.onDidChangeCheckedState(async (e) => { this._disposables.push(
if (e) { collectDataButton.onDidChangeCheckedState(async checked => {
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.CollectData); if (checked) {
await this.switchDataSourceContainerFields(
PerformanceDataSourceOptions.CollectData);
} }
})); }));
@@ -122,81 +124,72 @@ export class GetAzureRecommendationDialog {
name: buttonGroup, name: buttonGroup,
label: constants.AZURE_RECOMMENDATION_OPEN_EXISTING, label: constants.AZURE_RECOMMENDATION_OPEN_EXISTING,
checked: this._performanceDataSource === PerformanceDataSourceOptions.OpenExisting, checked: this._performanceDataSource === PerformanceDataSourceOptions.OpenExisting,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin': '0 12px' }
...styles.BODY_CSS,
'margin': '0 12px',
}
}).component(); }).component();
this._disposables.push(openExistingButton.onDidChangeCheckedState(async (e) => { this._disposables.push(
if (e) { openExistingButton.onDidChangeCheckedState(async checked => {
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.OpenExisting); if (checked) {
await this.switchDataSourceContainerFields(
PerformanceDataSourceOptions.OpenExisting);
} }
})); }));
radioButtonContainer.addItems([ radioButtonContainer.addItems([
collectDataButton, collectDataButton,
openExistingButton openExistingButton]);
]);
this._collectDataContainer = this.createCollectDataContainer(_view); this._collectDataContainer = this.createCollectDataContainer(_view);
this._openExistingContainer = this.createOpenExistingContainer(_view); this._openExistingContainer = this.createOpenExistingContainer(_view);
const container = _view.modelBuilder.flexContainer().withLayout({ const container = _view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).withItems([ .withItems([
chooseMethodText, chooseMethodText,
radioButtonContainer, radioButtonContainer,
this._openExistingContainer, this._openExistingContainer,
this._collectDataContainer, this._collectDataContainer])
]).component(); .component();
return container; return container;
} }
private createCollectDataContainer(_view: azdata.ModelView): azdata.FlexContainer { private createCollectDataContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({ const container = _view.modelBuilder.flexContainer()
CSSStyles: { .withProps(
'flex-direction': 'column', { CSSStyles: { 'flex-direction': 'column', 'display': 'inline' } })
'display': 'inline', .component();
}
}).component();
const instructions = _view.modelBuilder.text().withProps({ const instructions = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER, value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, 'margin-bottom': '8px' }
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component(); }).component();
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({ const selectFolderContainer = _view.modelBuilder.flexContainer()
CSSStyles: { .withProps(
'flex-direction': 'row', { CSSStyles: { 'flex-direction': 'row', 'align-items': 'center' } })
'align-items': 'center', .component();
}
}).component();
this._collectDataFolderInput = _view.modelBuilder.inputBox().withProps({ this._collectDataFolderInput = _view.modelBuilder.inputBox()
.withProps({
placeHolder: constants.FOLDER_NAME, placeHolder: constants.FOLDER_NAME,
readOnly: true, readOnly: true,
width: 320, width: 320,
CSSStyles: { CSSStyles: { 'margin-right': '12px' },
'margin-right': '12px'
},
}).component(); }).component();
this._disposables.push(this._collectDataFolderInput.onTextChanged(async (value) => { this._disposables.push(
this._collectDataFolderInput.onTextChanged(async (value) => {
if (value) { if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim(); this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true; this.dialog!.okButton.enabled = true;
} }
})); }));
const browseButton = _view.modelBuilder.button().withProps({ const browseButton = _view.modelBuilder.button()
.withProps({
label: constants.BROWSE, label: constants.BROWSE,
width: 100, width: 100,
CSSStyles: { CSSStyles: { 'margin': '0' }
'margin': '0'
}
}).component(); }).component();
this._disposables.push(browseButton.onDidClick(async (e) => { this._disposables.push(browseButton.onDidClick(async (e) => {
let folder = await utils.promptUserForFolder(); let folder = await utils.promptUserForFolder();
@@ -205,74 +198,61 @@ export class GetAzureRecommendationDialog {
selectFolderContainer.addItems([ selectFolderContainer.addItems([
this._collectDataFolderInput, this._collectDataFolderInput,
browseButton, browseButton]);
]);
container.addItems([ container.addItems([
instructions, instructions,
selectFolderContainer, selectFolderContainer]);
]);
return container; return container;
} }
private createOpenExistingContainer(_view: azdata.ModelView): azdata.FlexContainer { private createOpenExistingContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({ const container = _view.modelBuilder.flexContainer()
CSSStyles: { .withProps(
'flex-direction': 'column', { CSSStyles: { 'flex-direction': 'column', 'display': 'none', } })
'display': 'none', .component();
}
}).component();
const instructions = _view.modelBuilder.text().withProps({ const instructions = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER, value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, 'margin-bottom': '8px' }
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component(); }).component();
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({ const selectFolderContainer = _view.modelBuilder.flexContainer()
CSSStyles: { .withProps(
'flex-direction': 'row', { CSSStyles: { 'flex-direction': 'row', 'align-items': 'center' } })
'align-items': 'center', .component();
}
}).component();
this._openExistingFolderInput = _view.modelBuilder.inputBox().withProps({ this._openExistingFolderInput = _view.modelBuilder.inputBox().withProps({
placeHolder: constants.FOLDER_NAME, placeHolder: constants.FOLDER_NAME,
readOnly: true, readOnly: true,
width: 320, width: 320,
CSSStyles: { CSSStyles: { 'margin-right': '12px' },
'margin-right': '12px'
},
}).component(); }).component();
this._disposables.push(this._openExistingFolderInput.onTextChanged(async (value) => { this._disposables.push(
this._openExistingFolderInput.onTextChanged(async (value) => {
if (value) { if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim(); this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true; this.dialog!.okButton.enabled = true;
} }
})); }));
const openButton = _view.modelBuilder.button().withProps({ const openButton = _view.modelBuilder.button()
.withProps({
label: constants.OPEN, label: constants.OPEN,
width: 100, width: 100,
CSSStyles: { CSSStyles: { 'margin': '0' }
'margin': '0'
}
}).component(); }).component();
this._disposables.push(openButton.onDidClick(async (e) => { this._disposables.push(
let folder = await utils.promptUserForFolder(); openButton.onDidClick(
this._openExistingFolderInput.value = folder; async (e) => this._openExistingFolderInput.value = await utils.promptUserForFolder()));
}));
selectFolderContainer.addItems([ selectFolderContainer.addItems([
this._openExistingFolderInput, this._openExistingFolderInput,
openButton, openButton]);
]);
container.addItems([ container.addItems([
instructions, instructions,
selectFolderContainer, selectFolderContainer]);
]);
return container; return container;
} }
@@ -281,53 +261,56 @@ export class GetAzureRecommendationDialog {
let okButtonEnabled = false; let okButtonEnabled = false;
switch (containerType) { switch (containerType) {
case PerformanceDataSourceOptions.CollectData: { case PerformanceDataSourceOptions.CollectData:
await this._collectDataContainer.updateCssStyles({ 'display': 'inline' }); await utils.updateControlDisplay(this._collectDataContainer, true);
await this._openExistingContainer.updateCssStyles({ 'display': 'none' }); await utils.updateControlDisplay(this._openExistingContainer, false);
if (this._collectDataFolderInput.value) { if (this._collectDataFolderInput.value) {
okButtonEnabled = true; okButtonEnabled = true;
} }
break; break;
} case PerformanceDataSourceOptions.OpenExisting:
case PerformanceDataSourceOptions.OpenExisting: { await utils.updateControlDisplay(this._collectDataContainer, false);
await this._collectDataContainer.updateCssStyles({ 'display': 'none' }); await utils.updateControlDisplay(this._openExistingContainer, true);
await this._openExistingContainer.updateCssStyles({ 'display': 'inline' });
if (this._openExistingFolderInput.value) { if (this._openExistingFolderInput.value) {
okButtonEnabled = true; okButtonEnabled = true;
} }
break; break;
} }
}
this.dialog!.okButton.enabled = okButtonEnabled; this.dialog!.okButton.enabled = okButtonEnabled;
} }
public async openDialog(dialogName?: string) { public async openDialog(dialogName?: string) {
if (!this._isOpen) { if (!this._isOpen) {
this._isOpen = true; this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(constants.GET_AZURE_RECOMMENDATION, 'GetAzureRecommendationsDialog', 'narrow'); this.dialog = azdata.window.createModelViewDialog(
constants.GET_AZURE_RECOMMENDATION,
'GetAzureRecommendationsDialog',
'narrow');
this.dialog.okButton.label = GetAzureRecommendationDialog.StartButtonText; this.dialog.okButton.label = GetAzureRecommendationDialog.StartButtonText;
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute())); this._disposables.push(
this._disposables.push(this.dialog.cancelButton.onClick(() => this._isOpen = false)); this.dialog.okButton.onClick(
async () => await this.execute()));
const dialogSetupPromises: Thenable<void>[] = []; this._disposables.push(
dialogSetupPromises.push(this.initializeDialog(this.dialog)); this.dialog.cancelButton.onClick(
() => this._isOpen = false));
const promise = this.initializeDialog(this.dialog);
azdata.window.openDialog(this.dialog); azdata.window.openDialog(this.dialog);
await Promise.all(dialogSetupPromises); await promise;
// if data source was previously selected, default folder value to previously selected // if data source was previously selected, default folder value to previously selected
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) { switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
case PerformanceDataSourceOptions.CollectData: { case PerformanceDataSourceOptions.CollectData:
this._collectDataFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation; this._collectDataFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
break; break;
} case PerformanceDataSourceOptions.OpenExisting:
case PerformanceDataSourceOptions.OpenExisting: {
this._openExistingFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation; this._openExistingFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
break; break;
} }
}
await this.switchDataSourceContainerFields(this._performanceDataSource); await this.switchDataSourceContainerFields(this._performanceDataSource);
} }
@@ -338,16 +321,14 @@ export class GetAzureRecommendationDialog {
this.migrationStateModel._skuRecommendationPerformanceDataSource = this._performanceDataSource; this.migrationStateModel._skuRecommendationPerformanceDataSource = this._performanceDataSource;
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) { switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
case PerformanceDataSourceOptions.CollectData: { case PerformanceDataSourceOptions.CollectData:
await this.migrationStateModel.startPerfDataCollection( await this.migrationStateModel.startPerfDataCollection(
this.migrationStateModel._skuRecommendationPerformanceLocation, this.migrationStateModel._skuRecommendationPerformanceLocation,
this.migrationStateModel._performanceDataQueryIntervalInSeconds, this.migrationStateModel._performanceDataQueryIntervalInSeconds,
this.migrationStateModel._staticDataQueryIntervalInSeconds, this.migrationStateModel._staticDataQueryIntervalInSeconds,
this.migrationStateModel._numberOfPerformanceDataQueryIterations, this.migrationStateModel._numberOfPerformanceDataQueryIterations,
this.skuRecommendationPage this.skuRecommendationPage);
);
break; break;
}
case PerformanceDataSourceOptions.OpenExisting: { case PerformanceDataSourceOptions.OpenExisting: {
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName; const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
const errors: string[] = []; const errors: string[] = [];

View File

@@ -25,7 +25,10 @@ export class SkuEditParametersDialog {
private _targetPercentileDropdown!: azdata.DropDownComponent; private _targetPercentileDropdown!: azdata.DropDownComponent;
private _enablePreviewValue!: boolean; private _enablePreviewValue!: boolean;
constructor(public skuRecommendationPage: SKURecommendationPage, public migrationStateModel: MigrationStateModel) { constructor(
public skuRecommendationPage: SKURecommendationPage,
public migrationStateModel: MigrationStateModel) {
this._enablePreviewValue = true; this._enablePreviewValue = true;
} }
@@ -35,10 +38,10 @@ export class SkuEditParametersDialog {
try { try {
const flex = this.createContainer(view); const flex = this.createContainer(view);
this._disposables.push(view.onClosed(e => { this._disposables.push(
view.onClosed(e =>
this._disposables.forEach( this._disposables.forEach(
d => { try { d.dispose(); } catch { } }); d => { try { d.dispose(); } catch { } })));
}));
await view.initializeModel(flex); await view.initializeModel(flex);
resolve(); resolve();
@@ -50,38 +53,33 @@ export class SkuEditParametersDialog {
} }
private createContainer(_view: azdata.ModelView): azdata.FlexContainer { private createContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({ const container = _view.modelBuilder.flexContainer()
CSSStyles: { .withProps(
'margin': '8px 16px', { CSSStyles: { 'margin': '8px 16px', 'flex-direction': 'column' } })
'flex-direction': 'column', .component();
}
}).component();
const description = _view.modelBuilder.text().withProps({ const description = _view.modelBuilder.text()
.withProps({
value: constants.EDIT_PARAMETERS_TEXT, value: constants.EDIT_PARAMETERS_TEXT,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS }
...styles.BODY_CSS, })
} .component();
}).component();
const WIZARD_INPUT_COMPONENT_WIDTH = '300px'; const WIZARD_INPUT_COMPONENT_WIDTH = '300px';
const scaleFactorLabel = _view.modelBuilder.text().withProps({ const scaleFactorLabel = _view.modelBuilder.text()
.withProps({
value: constants.SCALE_FACTOR, value: constants.SCALE_FACTOR,
description: constants.SCALE_FACTOR_TOOLTIP, description: constants.SCALE_FACTOR_TOOLTIP,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true, requiredIndicator: true,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS }
...styles.LABEL_CSS
}
}).component(); }).component();
this._scaleFactorInput = _view.modelBuilder.inputBox().withProps({ this._scaleFactorInput = _view.modelBuilder.inputBox()
.withProps({
required: true, required: true,
validationErrorMessage: constants.INVALID_SCALE_FACTOR, validationErrorMessage: constants.INVALID_SCALE_FACTOR,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { CSSStyles: { 'margin-top': '-1em', 'margin-bottom': '8px' },
'margin-top': '-1em',
'margin-bottom': '8px',
},
}).withValidation(c => { }).withValidation(c => {
if (Number(c.value) && Number(c.value) > 0) { if (Number(c.value) && Number(c.value) > 0) {
return true; return true;
@@ -89,17 +87,16 @@ export class SkuEditParametersDialog {
return false; return false;
}).component(); }).component();
const targetPercentileLabel = _view.modelBuilder.text().withProps({ const targetPercentileLabel = _view.modelBuilder.text()
.withProps({
value: constants.PERCENTAGE_UTILIZATION, value: constants.PERCENTAGE_UTILIZATION,
description: constants.PERCENTAGE_UTILIZATION_TOOLTIP, description: constants.PERCENTAGE_UTILIZATION_TOOLTIP,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true, requiredIndicator: true,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS }
...styles.LABEL_CSS,
}
}).component(); }).component();
const createPercentageValues = () => { const createPercentageValues = () => {
let values: azdata.CategoryValue[] = []; const values: azdata.CategoryValue[] = [];
TARGET_PERCENTILE_VALUES.forEach(n => { TARGET_PERCENTILE_VALUES.forEach(n => {
const val = n.toString(); const val = n.toString();
values.push({ values.push({
@@ -109,7 +106,8 @@ export class SkuEditParametersDialog {
}); });
return values; return values;
}; };
this._targetPercentileDropdown = _view.modelBuilder.dropDown().withProps({ this._targetPercentileDropdown = _view.modelBuilder.dropDown()
.withProps({
values: createPercentageValues(), values: createPercentageValues(),
ariaLabel: constants.PERCENTAGE_UTILIZATION, ariaLabel: constants.PERCENTAGE_UTILIZATION,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
@@ -122,13 +120,12 @@ export class SkuEditParametersDialog {
}, },
}).component(); }).component();
const enablePreviewLabel = _view.modelBuilder.text().withProps({ const enablePreviewLabel = _view.modelBuilder.text()
.withProps({
value: constants.ENABLE_PREVIEW_SKU, value: constants.ENABLE_PREVIEW_SKU,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true, requiredIndicator: true,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, }
...styles.LABEL_CSS,
}
}).component(); }).component();
const buttonGroup = 'enablePreviewSKUs'; const buttonGroup = 'enablePreviewSKUs';
const enablePreviewRadioButtonContainer = _view.modelBuilder.flexContainer() const enablePreviewRadioButtonContainer = _view.modelBuilder.flexContainer()
@@ -151,8 +148,9 @@ export class SkuEditParametersDialog {
'margin': '0' 'margin': '0'
}, },
}).component(); }).component();
this._disposables.push(enablePreviewButton.onDidChangeCheckedState(async (e) => { this._disposables.push(
if (e) { enablePreviewButton.onDidChangeCheckedState(async checked => {
if (checked) {
this._enablePreviewValue = true; this._enablePreviewValue = true;
} }
})); }));
@@ -167,23 +165,21 @@ export class SkuEditParametersDialog {
'margin': '0 12px', 'margin': '0 12px',
} }
}).component(); }).component();
this._disposables.push(disablePreviewButton.onDidChangeCheckedState(async (e) => { this._disposables.push(
if (e) { disablePreviewButton.onDidChangeCheckedState(checked => {
if (checked) {
this._enablePreviewValue = false; this._enablePreviewValue = false;
} }
})); }));
enablePreviewRadioButtonContainer.addItems([ enablePreviewRadioButtonContainer.addItems([
enablePreviewButton, enablePreviewButton,
disablePreviewButton disablePreviewButton]);
]);
const enablePreviewInfoBox = _view.modelBuilder.infoBox() const enablePreviewInfoBox = _view.modelBuilder.infoBox()
.withProps({ .withProps({
text: constants.ENABLE_PREVIEW_SKU_INFO, text: constants.ENABLE_PREVIEW_SKU_INFO,
style: 'information', style: 'information',
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, }
...styles.BODY_CSS,
}
}).component(); }).component();
container.addItems([ container.addItems([
@@ -202,12 +198,19 @@ export class SkuEditParametersDialog {
public async openDialog(dialogName?: string) { public async openDialog(dialogName?: string) {
if (!this._isOpen) { if (!this._isOpen) {
this._isOpen = true; this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(constants.EDIT_RECOMMENDATION_PARAMETERS, 'SkuEditParametersDialog', 'narrow'); this.dialog = azdata.window.createModelViewDialog(
constants.EDIT_RECOMMENDATION_PARAMETERS,
'SkuEditParametersDialog',
'narrow');
this.dialog.okButton.label = SkuEditParametersDialog.UpdateButtonText; this.dialog.okButton.label = SkuEditParametersDialog.UpdateButtonText;
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute())); this._disposables.push(
this.dialog.okButton.onClick(
async () => await this.execute()));
this._disposables.push(this.dialog.cancelButton.onClick(() => this._isOpen = false)); this._disposables.push(
this.dialog.cancelButton.onClick(
() => this._isOpen = false));
const dialogSetupPromises: Thenable<void>[] = []; const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog)); dialogSetupPromises.push(this.initializeDialog(this.dialog));

View File

@@ -34,15 +34,13 @@ export class SkuRecommendationResultsDialog {
constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) { constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) {
switch (this._targetType) { switch (this._targetType) {
case MigrationTargetType.SQLMI: case MigrationTargetType.SQLMI:
this.targetName = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE; this.targetName = constants.SKU_RECOMMENDATION_MI_CARD_TEXT;
break; break;
case MigrationTargetType.SQLVM: case MigrationTargetType.SQLVM:
this.targetName = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE; this.targetName = constants.SKU_RECOMMENDATION_VM_CARD_TEXT;
break; break;
case MigrationTargetType.SQLDB: case MigrationTargetType.SQLDB:
this.targetName = constants.AZURE_SQL_DATABASE; this.targetName = constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT;
break; break;
} }
@@ -79,7 +77,9 @@ export class SkuRecommendationResultsDialog {
this.targetRecommendations?.forEach((recommendation, index) => { this.targetRecommendations?.forEach((recommendation, index) => {
if (index > 0) { if (index > 0) {
const separator = _view.modelBuilder.separator().withProps({ width: 750 }).component(); const separator = _view.modelBuilder.separator()
.withProps({ width: 750 })
.component();
container.addItem(separator); container.addItem(separator);
} }
@@ -101,7 +101,9 @@ export class SkuRecommendationResultsDialog {
recommendation = <mssql.IaaSSkuRecommendationResultItem>recommendationItem; recommendation = <mssql.IaaSSkuRecommendationResultItem>recommendationItem;
if (recommendation.targetSku) { if (recommendation.targetSku) {
configuration = constants.VM_CONFIGURATION(recommendation.targetSku.virtualMachineSize!.azureSkuName, recommendation.targetSku.virtualMachineSize!.vCPUsAvailable); configuration = constants.VM_CONFIGURATION(
recommendation.targetSku.virtualMachineSize!.azureSkuName,
recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
storageSection = this.createSqlVmTargetStorageSection(_view, recommendation); storageSection = this.createSqlVmTargetStorageSection(_view, recommendation);
} }
@@ -123,31 +125,31 @@ export class SkuRecommendationResultsDialog {
: constants.PREMIUM_SERIES_MEMORY_OPTIMIZED; : constants.PREMIUM_SERIES_MEMORY_OPTIMIZED;
configuration = this._targetType === MigrationTargetType.SQLDB configuration = this._targetType === MigrationTargetType.SQLDB
? constants.DB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!) ? constants.SQLDB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!)
: constants.MI_CONFIGURATION(hardwareType, serviceTier, recommendation.targetSku.computeSize!); : constants.MI_CONFIGURATION(hardwareType, serviceTier, recommendation.targetSku.computeSize!);
const storageLabel = _view.modelBuilder.text().withProps({ const storageLabel = _view.modelBuilder.text()
.withProps({
value: constants.STORAGE_HEADER, value: constants.STORAGE_HEADER,
CSSStyles: { CSSStyles: {
...styles.LABEL_CSS, ...styles.LABEL_CSS,
'margin': '12px 0 0', 'margin': '12px 0 0',
} }
}).component(); }).component();
const storageValue = _view.modelBuilder.text().withProps({ const storageValue = _view.modelBuilder.text()
.withProps({
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024), value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, }
...styles.BODY_CSS,
}
}).component(); }).component();
storageSection.addItems([ storageSection.addItems([
storageLabel, storageLabel,
storageValue, storageValue]);
]);
} }
break; break;
} }
const recommendationContainer = _view.modelBuilder.flexContainer().withProps({ const recommendationContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: { CSSStyles: {
'margin-bottom': '20px', 'margin-bottom': '20px',
'flex-direction': 'column', 'flex-direction': 'column',
@@ -155,52 +157,41 @@ export class SkuRecommendationResultsDialog {
}).component(); }).component();
if (this._targetType === MigrationTargetType.SQLDB) { if (this._targetType === MigrationTargetType.SQLDB) {
const databaseNameLabel = _view.modelBuilder.text().withProps({ const databaseNameLabel = _view.modelBuilder.text()
.withProps({
value: recommendation.databaseName!, value: recommendation.databaseName!,
CSSStyles: { CSSStyles: { ...styles.SECTION_HEADER_CSS, }
...styles.SECTION_HEADER_CSS,
}
}).component(); }).component();
recommendationContainer.addItem(databaseNameLabel); recommendationContainer.addItem(databaseNameLabel);
} }
const targetDeploymentTypeLabel = _view.modelBuilder.text().withProps({ const targetDeploymentTypeLabel = _view.modelBuilder.text()
.withProps({
value: constants.TARGET_DEPLOYMENT_TYPE, value: constants.TARGET_DEPLOYMENT_TYPE,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', }
...styles.LABEL_CSS,
'margin': '0',
}
}).component(); }).component();
const targetDeploymentTypeValue = _view.modelBuilder.text().withProps({ const targetDeploymentTypeValue = _view.modelBuilder.text()
.withProps({
value: this.targetName, value: this.targetName,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
...styles.BODY_CSS,
'margin': '0',
}
}).component(); }).component();
const azureConfigurationLabel = _view.modelBuilder.text().withProps({ const azureConfigurationLabel = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_CONFIGURATION, value: constants.AZURE_CONFIGURATION,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0', }
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
}).component(); }).component();
const azureConfigurationValue = _view.modelBuilder.text().withProps({ const azureConfigurationValue = _view.modelBuilder.text()
.withProps({
value: configuration, value: configuration,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
...styles.BODY_CSS,
'margin': '0',
}
}).component(); }).component();
recommendationContainer.addItems([ recommendationContainer.addItems([
targetDeploymentTypeLabel, targetDeploymentTypeLabel,
targetDeploymentTypeValue, targetDeploymentTypeValue,
targetDeploymentTypeLabel, targetDeploymentTypeLabel,
targetDeploymentTypeValue, targetDeploymentTypeValue,
azureConfigurationLabel, azureConfigurationLabel,
azureConfigurationValue, azureConfigurationValue,
@@ -209,23 +200,21 @@ export class SkuRecommendationResultsDialog {
const recommendationsReasonSection = _view.modelBuilder.text().withProps({ const recommendationsReasonSection = _view.modelBuilder.text().withProps({
value: constants.RECOMMENDATION_REASON, value: constants.RECOMMENDATION_REASON,
CSSStyles: { CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin': '12px 0 0' }
...styles.SECTION_HEADER_CSS,
'margin': '12px 0 0'
}
}).component(); }).component();
const reasonsContainer = _view.modelBuilder.flexContainer().withLayout({ const reasonsContainer = _view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).component(); .component();
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications) || [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications)
|| [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
justifications?.forEach(text => { justifications?.forEach(text => {
reasonsContainer.addItem( reasonsContainer.addItem(
_view.modelBuilder.text().withProps({ _view.modelBuilder.text().withProps({
value: text, value: text,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, }
...styles.BODY_CSS,
}
}).component() }).component()
); );
}); });
@@ -235,25 +224,22 @@ export class SkuRecommendationResultsDialog {
recommendationContainer.addItems([ recommendationContainer.addItems([
recommendationsReasonSection, recommendationsReasonSection,
reasonsContainer, reasonsContainer,
storagePropertiesContainer, storagePropertiesContainer]);
]);
return recommendationContainer; return recommendationContainer;
} }
private createSqlVmTargetStorageSection(_view: azdata.ModelView, recommendation: mssql.IaaSSkuRecommendationResultItem): azdata.FlexContainer { private createSqlVmTargetStorageSection(_view: azdata.ModelView, recommendation: mssql.IaaSSkuRecommendationResultItem): azdata.FlexContainer {
const recommendedTargetStorageSection = _view.modelBuilder.text().withProps({ const recommendedTargetStorageSection = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION, value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
CSSStyles: { CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
}).component(); }).component();
const recommendedTargetStorageInfo = _view.modelBuilder.text().withProps({
const recommendedTargetStorageInfo = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO, value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, }
...styles.BODY_CSS,
}
}).component(); }).component();
const headerCssStyle = { const headerCssStyle = {
@@ -333,20 +319,21 @@ export class SkuRecommendationResultsDialog {
logDiskTableRow, logDiskTableRow,
]; ];
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({ const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION, ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns, columns: columns,
dataValues: storageConfigurationTableRows, dataValues: storageConfigurationTableRows,
width: 700 width: 700
}).component(); }).component();
const container = _view.modelBuilder.flexContainer().withLayout({ const container = _view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).withItems([ .withItems([
recommendedTargetStorageSection, recommendedTargetStorageSection,
recommendedTargetStorageInfo, recommendedTargetStorageInfo,
storageConfigurationTable, storageConfigurationTable])
]).component(); .component();
return container; return container;
} }
@@ -375,18 +362,15 @@ export class SkuRecommendationResultsDialog {
break; break;
case MigrationTargetType.SQLDB: case MigrationTargetType.SQLDB:
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements.filter(d => { instanceRequirements = this.instanceRequirements?.databaseLevelRequirements
return databaseName === d.databaseName; .filter((d) => databaseName === d.databaseName)[0]!;
})[0]!;
break; break;
} }
const storagePropertiesSection = _view.modelBuilder.text().withProps({ const storagePropertiesSection = _view.modelBuilder.text()
.withProps({
value: constants.SOURCE_PROPERTIES, value: constants.SOURCE_PROPERTIES,
CSSStyles: { CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
}).component(); }).component();
const headerCssStyle = { const headerCssStyle = {
@@ -407,7 +391,7 @@ export class SkuRecommendationResultsDialog {
}; };
const columnWidth = 80; const columnWidth = 80;
let columns: azdata.DeclarativeTableColumn[] = [ const columns: azdata.DeclarativeTableColumn[] = [
{ {
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.DIMENSION, displayName: constants.DIMENSION,
@@ -450,19 +434,18 @@ export class SkuRecommendationResultsDialog {
ioLatencyRow, ioLatencyRow,
]; ];
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({ const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION, ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns, columns: columns,
dataValues: storagePropertiesTableRows, dataValues: storagePropertiesTableRows,
width: 300 width: 300
}).component(); }).component();
const container = _view.modelBuilder.flexContainer().withLayout({ const container = _view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).withItems([ .withItems([storagePropertiesSection, storagePropertiesTable])
storagePropertiesSection, .component();
storagePropertiesTable,
]).component();
return container; return container;
} }
@@ -537,10 +520,9 @@ export class SkuRecommendationResultsDialog {
})); }));
this.dialog.customButtons = [this._saveButton]; this.dialog.customButtons = [this._saveButton];
const dialogSetupPromises: Thenable<void>[] = []; const promise = this.initializeDialog(this.dialog);
dialogSetupPromises.push(this.initializeDialog(this.dialog));
azdata.window.openDialog(this.dialog); azdata.window.openDialog(this.dialog);
await Promise.all(dialogSetupPromises); await promise;
} }
} }

View File

@@ -0,0 +1,324 @@
/*---------------------------------------------------------------------------------------------
* 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 constants from '../../constants/strings';
import { AzureSqlDatabaseServer } from '../../api/azure';
import { collectSourceDatabaseTableInfo, collectTargetDatabaseTableInfo, TableInfo } from '../../api/sqlUtils';
import { MigrationStateModel } from '../../models/stateMachine';
const DialogName = 'TableMigrationSelection';
export class TableMigrationSelectionDialog {
private _dialog: azdata.window.Dialog | undefined;
private _headingText!: azdata.TextComponent;
private _filterInputBox!: azdata.InputBoxComponent;
private _tableSelectionTable!: azdata.TableComponent;
private _tableLoader!: azdata.LoadingComponent;
private _disposables: vscode.Disposable[] = [];
private _isOpen: boolean = false;
private _model: MigrationStateModel;
private _sourceDatabaseName: string;
private _tableSelectionMap!: Map<string, TableInfo>;
private _targetTableMap!: Map<string, TableInfo>;
private _onSaveCallback: () => Promise<void>;
constructor(
model: MigrationStateModel,
sourceDatabaseName: string,
onSaveCallback: () => Promise<void>
) {
this._model = model;
this._sourceDatabaseName = sourceDatabaseName;
this._onSaveCallback = onSaveCallback;
}
private async _loadData(): Promise<void> {
try {
this._tableLoader.loading = true;
const targetDatabaseInfo = this._model._sourceTargetMapping.get(this._sourceDatabaseName);
if (targetDatabaseInfo) {
const sourceTableList: TableInfo[] = await collectSourceDatabaseTableInfo(
this._model.sourceConnectionId,
this._sourceDatabaseName);
this._tableSelectionMap = new Map();
sourceTableList.forEach(table => {
const sourceTable = targetDatabaseInfo.sourceTables.get(table.tableName);
const isSelected = sourceTable?.selectedForMigration === true;
const tableInfo: TableInfo = {
databaseName: table.databaseName,
rowCount: table.rowCount,
selectedForMigration: isSelected,
tableName: table.tableName,
};
this._tableSelectionMap.set(table.tableName, tableInfo);
});
const targetTableList: TableInfo[] = await collectTargetDatabaseTableInfo(
this._model._targetServerInstance as AzureSqlDatabaseServer,
targetDatabaseInfo.databaseName,
this._model._azureTenant.id,
this._model._targetUserName,
this._model._targetPassword);
this._targetTableMap = new Map();
targetTableList.forEach(table =>
this._targetTableMap.set(
table.tableName, {
databaseName: table.databaseName,
rowCount: table.rowCount,
selectedForMigration: false,
tableName: table.tableName,
}));
}
} catch (error) {
this._dialog!.message = {
text: constants.DATABASE_TABLE_CONNECTION_ERROR,
description: constants.DATABASE_TABLE_CONNECTION_ERROR_MESSAGE(error.message),
level: azdata.window.MessageLevel.Error
};
} finally {
this._tableLoader.loading = false;
await this._loadControls();
}
}
private async _loadControls(): Promise<void> {
const data: any[][] = [];
const filterText = this._filterInputBox.value ?? '';
const selectedItems: number[] = [];
let tableRow = 0;
this._tableSelectionMap.forEach(sourceTable => {
if (filterText?.length === 0 || sourceTable.tableName.indexOf(filterText) > -1) {
let tableStatus = constants.TARGET_TABLE_MISSING;
const targetTable = this._targetTableMap.get(sourceTable.tableName);
if (targetTable) {
const targetTableRowCount = targetTable?.rowCount ?? 0;
tableStatus = targetTableRowCount > 0
? constants.TARGET_TABLE_NOT_EMPTY
: '--';
}
data.push([
sourceTable.selectedForMigration,
sourceTable.tableName,
tableStatus]);
if (sourceTable.selectedForMigration && targetTable) {
selectedItems.push(tableRow);
}
tableRow++;
}
});
await this._tableSelectionTable.updateProperty('data', data);
this._tableSelectionTable.selectedRows = selectedItems;
this._updateRowSelection();
}
private async _initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
dialog.registerContent(async (view) => {
this._filterInputBox = view.modelBuilder.inputBox()
.withProps({
inputType: 'search',
placeHolder: constants.TABLE_SELECTION_FILTER,
width: 268,
}).component();
this._disposables.push(
this._filterInputBox.onTextChanged(
async e => await this._loadControls()));
this._headingText = view.modelBuilder.text()
.withProps({ value: constants.DATABASE_LOADING_TABLES })
.component();
this._tableSelectionTable = await this._createSelectionTable(view);
this._tableLoader = view.modelBuilder.loadingComponent()
.withItem(this._tableSelectionTable)
.withProps({
loading: false,
loadingText: constants.DATABASE_TABLE_DATA_LOADING
}).component();
const flex = view.modelBuilder.flexContainer()
.withItems([
this._filterInputBox,
this._headingText,
this._tableLoader],
{ flex: '0 0 auto' })
.withProps({ CSSStyles: { 'margin': '0 0 0 15px' } })
.withLayout({
flexFlow: 'column',
height: '100%',
width: 565,
}).component();
this._disposables.push(
view.onClosed(e =>
this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
await view.initializeModel(flex);
await this._loadData();
});
}
public async openDialog(dialogTitle: string) {
if (!this._isOpen) {
this._isOpen = true;
this._dialog = azdata.window.createModelViewDialog(
dialogTitle,
DialogName,
600);
this._dialog.okButton.label = constants.TABLE_SELECTION_UPDATE_BUTTON;
this._disposables.push(
this._dialog.okButton.onClick(
async () => this._save()));
this._dialog.cancelButton.label = constants.TABLE_SELECTION_CANCEL_BUTTON;
this._disposables.push(
this._dialog.cancelButton.onClick(
async () => this._isOpen = false));
const promise = this._initializeDialog(this._dialog);
azdata.window.openDialog(this._dialog);
await promise;
}
}
private async _createSelectionTable(view: azdata.ModelView): Promise<azdata.TableComponent> {
const cssClass = 'no-borders';
const table = view.modelBuilder.table()
.withProps({
data: [],
width: 565,
height: '600px',
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [
<azdata.CheckboxColumn>{
value: '',
width: 10,
type: azdata.ColumnType.checkBox,
action: azdata.ActionOnCellCheckboxCheck.selectRow,
resizable: false,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.TABLE_SELECTION_TABLENAME_COLUMN,
value: 'tableName',
type: azdata.ColumnType.text,
width: 300,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.TABLE_SELECTION_HASROWS_COLUMN,
value: 'hasRows',
type: azdata.ColumnType.text,
width: 255,
cssClass: cssClass,
headerCssClass: cssClass,
}]
})
.withValidation(() => true)
.component();
let updating: boolean = false;
this._disposables.push(
table.onRowSelected(e => {
if (updating) {
return;
}
updating = true;
// collect table list selected for migration
const selectedRows = this._tableSelectionTable.selectedRows ?? [];
const keepSelectedRows: number[] = [];
// determine if selected rows have a matching target and can be selected
selectedRows.forEach(rowIndex => {
// get selected source table name
const sourceTableName = this._tableSelectionTable.data[rowIndex][1] as string;
// get source table info
const sourceTableInfo = this._tableSelectionMap.get(sourceTableName);
if (sourceTableInfo) {
// see if source table exists on target database
const targetTableInfo = this._targetTableMap.get(sourceTableName);
// keep source table selected
sourceTableInfo.selectedForMigration = targetTableInfo !== undefined;
// update table selection map with new selectedForMigration value
this._tableSelectionMap.set(sourceTableName, sourceTableInfo);
// keep row selected
if (sourceTableInfo.selectedForMigration) {
keepSelectedRows.push(rowIndex);
}
}
});
// if the selected rows are different, update the selectedRows property
if (!this._areEqual(this._tableSelectionTable.selectedRows ?? [], keepSelectedRows)) {
this._tableSelectionTable.selectedRows = keepSelectedRows;
}
this._updateRowSelection();
updating = false;
}));
return table;
}
private _areEqual(source: number[], target: number[]): boolean {
if (source.length === target.length) {
for (let i = 0; i < source.length; i++) {
if (source[i] !== target[i]) {
return false;
}
}
return true;
}
return false;
}
private _updateRowSelection(): void {
this._headingText.value = this._tableSelectionTable.data.length > 0
? constants.TABLE_SELECTED_COUNT(
this._tableSelectionTable.selectedRows?.length ?? 0,
this._tableSelectionTable.data.length)
: this._tableLoader.loading
? constants.DATABASE_LOADING_TABLES
: constants.DATABASE_MISSING_TABLES;
}
private async _save(): Promise<void> {
const targetDatabaseInfo = this._model._sourceTargetMapping.get(this._sourceDatabaseName);
if (targetDatabaseInfo) {
// collect table list selected for migration
const selectedRows = this._tableSelectionTable.selectedRows ?? [];
const selectedTables = new Map<String, TableInfo>();
selectedRows.forEach(rowIndex => {
const tableName = this._tableSelectionTable.data[rowIndex][1] as string;
const tableInfo = this._tableSelectionMap.get(tableName);
if (tableInfo) {
selectedTables.set(tableName, tableInfo);
}
});
// copy table map selection status from grid
this._tableSelectionMap.forEach(tableInfo => {
const selectedTableInfo = selectedTables.get(tableInfo.tableName);
tableInfo.selectedForMigration = selectedTableInfo?.selectedForMigration === true;
this._tableSelectionMap.set(tableInfo.tableName, tableInfo);
});
// save table selection changes to migration source target map
targetDatabaseInfo.sourceTables = this._tableSelectionMap;
this._model._sourceTargetMapping.set(this._sourceDatabaseName, targetDatabaseInfo);
}
await this._onSaveCallback();
this._isOpen = false;
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { MigrationMode, MigrationStateModel, NetworkContainerType } from '../../models/stateMachine'; import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType } from '../../models/stateMachine';
import * as constants from '../../constants/strings'; import * as constants from '../../constants/strings';
import * as styles from '../../constants/styles'; import * as styles from '../../constants/styles';
@@ -25,21 +25,19 @@ export class TargetDatabaseSummaryDialog {
this._dialogObject = azdata.window.createModelViewDialog( this._dialogObject = azdata.window.createModelViewDialog(
constants.DATABASE_TO_BE_MIGRATED, constants.DATABASE_TO_BE_MIGRATED,
'TargetDatabaseSummaryDialog', 'TargetDatabaseSummaryDialog',
dialogWidth dialogWidth);
);
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
let tab = azdata.window.createTab('sql.migration.CreateResourceGroupDialog'); const tab = azdata.window.createTab('sql.migration.CreateResourceGroupDialog');
tab.registerContent(async (view: azdata.ModelView) => { tab.registerContent(async (view: azdata.ModelView) => {
this._view = view; this._view = view;
const databaseCount = this._view.modelBuilder.text().withProps({ const isSqlDbMigration = this._model._targetType === MigrationTargetType.SQLDB;
const databaseCount = this._view.modelBuilder.text()
.withProps({
value: constants.COUNT_DATABASES(this._model._databasesForMigration.length), value: constants.COUNT_DATABASES(this._model._databasesForMigration.length),
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '20px' }
...styles.BODY_CSS,
'margin-bottom': '20px'
}
}).component(); }).component();
const headerCssStyle = { const headerCssStyle = {
@@ -61,7 +59,7 @@ export class TargetDatabaseSummaryDialog {
const columnWidth = 150; const columnWidth = 150;
let columns: azdata.DeclarativeTableColumn[] = [ const columns: azdata.DeclarativeTableColumn[] = [
{ {
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.SOURCE_DATABASE, displayName: constants.SOURCE_DATABASE,
@@ -70,7 +68,6 @@ export class TargetDatabaseSummaryDialog {
rowCssStyles: rowCssStyle, rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle headerCssStyles: headerCssStyle
}, },
{ {
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.TARGET_DATABASE_NAME, displayName: constants.TARGET_DATABASE_NAME,
@@ -78,39 +75,43 @@ export class TargetDatabaseSummaryDialog {
width: columnWidth, width: columnWidth,
rowCssStyles: rowCssStyle, rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle headerCssStyles: headerCssStyle
} }];
];
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
columns.push({ columns.push(
{
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.LOCATION, displayName: constants.LOCATION,
isReadOnly: true, isReadOnly: true,
width: columnWidth, width: columnWidth,
rowCssStyles: rowCssStyle, rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle headerCssStyles: headerCssStyle
}, { },
{
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.RESOURCE_GROUP, displayName: constants.RESOURCE_GROUP,
isReadOnly: true, isReadOnly: true,
width: columnWidth, width: columnWidth,
rowCssStyles: rowCssStyle, rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle headerCssStyles: headerCssStyle
}, { },
{
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.SUMMARY_AZURE_STORAGE, displayName: constants.SUMMARY_AZURE_STORAGE,
isReadOnly: true, isReadOnly: true,
width: columnWidth, width: columnWidth,
rowCssStyles: rowCssStyle, rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle headerCssStyles: headerCssStyle
}, { },
{
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER, displayName: constants.BLOB_CONTAINER,
isReadOnly: true, isReadOnly: true,
width: columnWidth, width: columnWidth,
rowCssStyles: rowCssStyle, rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle headerCssStyles: headerCssStyle
}, { },
{
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
isReadOnly: true, isReadOnly: true,
@@ -119,6 +120,15 @@ export class TargetDatabaseSummaryDialog {
headerCssStyles: headerCssStyle, headerCssStyles: headerCssStyle,
hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE
}); });
} else if (isSqlDbMigration) {
columns.push({
valueType: azdata.DeclarativeDataType.string,
displayName: constants.TARGET_TABLE_COUNT_NAME,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
});
} else { } else {
columns.push({ columns.push({
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
@@ -134,59 +144,54 @@ export class TargetDatabaseSummaryDialog {
this._model._databasesForMigration.forEach((db, index) => { this._model._databasesForMigration.forEach((db, index) => {
const tableRow: azdata.DeclarativeTableCellValue[] = []; const tableRow: azdata.DeclarativeTableCellValue[] = [];
tableRow.push({ tableRow.push(
value: db { value: db },
}, { { value: this._model._targetDatabaseNames[index] });
value: this._model._targetDatabaseNames[index]
});
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
tableRow.push({ tableRow.push(
value: this._model._databaseBackup.blobs[index].storageAccount.location { value: this._model._databaseBackup.blobs[index].storageAccount.location },
}, { { value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup! },
value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup! { value: this._model._databaseBackup.blobs[index].storageAccount.name },
}, { { value: this._model._databaseBackup.blobs[index].blobContainer.name });
value: this._model._databaseBackup.blobs[index].storageAccount.name
}, {
value: this._model._databaseBackup.blobs[index].blobContainer.name
});
if (this._model._databaseBackup.migrationMode === MigrationMode.OFFLINE) { if (this._model._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
tableRow.push({ tableRow.push(
value: this._model._databaseBackup.blobs[index].lastBackupFile! { value: this._model._databaseBackup.blobs[index].lastBackupFile! });
});
} }
} else if (isSqlDbMigration) {
const totalTables = this._model._sourceTargetMapping.get(db)?.sourceTables.size ?? 0;
let selectedTables = 0;
this._model._sourceTargetMapping.get(db)?.sourceTables.forEach(
tableInfo => selectedTables += tableInfo.selectedForMigration ? 1 : 0);
tableRow.push(
{ value: constants.TOTAL_TABLES_SELECTED(selectedTables, totalTables) });
} else { } else {
tableRow.push({ tableRow.push(
value: this._model._databaseBackup.networkShares[index].networkShareLocation { value: this._model._databaseBackup.networkShares[index].networkShareLocation });
});
} }
tableRows.push(tableRow); tableRows.push(tableRow);
}); });
const databaseTable: azdata.DeclarativeTableComponent = this._view.modelBuilder.declarativeTable().withProps({ const databaseTable: azdata.DeclarativeTableComponent = this._view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.DATABASE_TO_BE_MIGRATED, ariaLabel: constants.DATABASE_TO_BE_MIGRATED,
columns: columns, columns: columns,
dataValues: tableRows, dataValues: tableRows,
width: this._tableLength width: this._tableLength
}).component(); }).component();
const container = this._view.modelBuilder.flexContainer().withLayout({ const container = this._view.modelBuilder.flexContainer()
flexFlow: 'column', .withLayout({ flexFlow: 'column' })
}).withItems([ .withItems([databaseCount, databaseTable])
databaseCount, .component();
databaseTable const form = this._view.modelBuilder.formContainer()
]).component(); .withFormItems(
const formBuilder = this._view.modelBuilder.formContainer().withFormItems( [{ component: container }],
[ { horizontal: false })
{ .withLayout({ width: '100%' })
component: container .component();
}
],
{
horizontal: false
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form); return view.initializeModel(form);
}); });
this._dialogObject.content = [tab]; this._dialogObject.content = [tab];

View File

@@ -4,154 +4,14 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { WizardController } from './wizard/wizardController';
import * as mssql from 'mssql';
import { promises as fs } from 'fs';
import * as loc from './constants/strings';
import { MigrationNotebookInfo, NotebookPathHelper } from './constants/notebookPathHelper';
import { IconPathHelper } from './constants/iconPathHelper';
import { DashboardWidget } from './dashboard/sqlServerDashboard'; import { DashboardWidget } from './dashboard/sqlServerDashboard';
import { MigrationLocalStorage } from './models/migrationLocalStorage';
import { MigrationStateModel, SavedInfo } from './models/stateMachine';
import { SavedAssessmentDialog } from './dialog/assessmentResults/savedAssessmentDialog';
class SQLMigration {
public stateModel!: MigrationStateModel;
constructor(private readonly context: vscode.ExtensionContext) {
NotebookPathHelper.setExtensionContext(context);
IconPathHelper.setExtensionContext(context);
MigrationLocalStorage.setExtensionContext(context);
}
async start(): Promise<void> {
await this.registerCommands();
}
async registerCommands(): Promise<void> {
const commandDisposables: vscode.Disposable[] = [ // Array of disposables returned by registerCommand
vscode.commands.registerCommand(
'sqlmigration.start',
async () => await this.launchMigrationWizard()),
vscode.commands.registerCommand(
'sqlmigration.openNotebooks',
async () => {
const input = vscode.window.createQuickPick<MigrationNotebookInfo>();
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
input.items = NotebookPathHelper.getAllMigrationNotebooks();
this.context.subscriptions.push(input.onDidAccept(async (e) => {
const selectedNotebook = input.selectedItems[0];
if (selectedNotebook) {
try {
await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
preview: false,
initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
initialDirtyState: false
});
} catch (e) {
void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
}
input.hide();
}
}));
input.show();
}),
azdata.tasks.registerTask(
'sqlmigration.start',
async () => await this.launchMigrationWizard()),
azdata.tasks.registerTask(
'sqlmigration.newsupportrequest',
async () => await this.launchNewSupportRequest()),
azdata.tasks.registerTask(
'sqlmigration.sendfeedback',
async () => {
const actionId = 'workbench.action.openIssueReporter';
const args = {
extensionId: 'microsoft.sql-migration',
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
};
return await vscode.commands.executeCommand(actionId, args);
}),
azdata.tasks.registerTask(
'sqlmigration.refreshmigrations',
async (e) => await widget.refresh()),
];
this.context.subscriptions.push(...commandDisposables);
}
async launchMigrationWizard(): Promise<void> {
let activeConnection = await azdata.connection.getCurrentConnection();
let connectionId: string = '';
let serverName: string = '';
if (!activeConnection) {
const connection = await azdata.connection.openConnectionDialog();
if (connection) {
connectionId = connection.connectionId;
serverName = connection.options.server;
}
} else {
connectionId = activeConnection.connectionId;
serverName = activeConnection.serverName;
}
if (serverName) {
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
if (api) {
this.stateModel = new MigrationStateModel(this.context, connectionId, api.sqlMigration);
this.context.subscriptions.push(this.stateModel);
const savedInfo = this.checkSavedInfo(serverName);
if (savedInfo) {
this.stateModel.savedInfo = savedInfo;
this.stateModel.serverName = serverName;
const savedAssessmentDialog = new SavedAssessmentDialog(
this.context,
this.stateModel,
async () => await widget?.onDialogClosed());
await savedAssessmentDialog.openDialog();
} else {
const wizardController = new WizardController(
this.context,
this.stateModel,
async () => await widget?.onDialogClosed());
await wizardController.openWizard(connectionId);
}
}
}
}
private checkSavedInfo(serverName: string): SavedInfo | undefined {
let savedInfo: SavedInfo | undefined = this.context.globalState.get(`${this.stateModel.mementoString}.${serverName}`);
if (savedInfo) {
return savedInfo;
} else {
return undefined;
}
}
async launchNewSupportRequest(): Promise<void> {
await vscode.env.openExternal(vscode.Uri.parse(
`https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
}
stop(): void {
}
}
let sqlMigration: SQLMigration;
let widget: DashboardWidget; let widget: DashboardWidget;
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext): Promise<DashboardWidget> {
sqlMigration = new SQLMigration(context);
await sqlMigration.registerCommands();
widget = new DashboardWidget(context); widget = new DashboardWidget(context);
widget.register(); await widget.register();
return widget;
} }
export function deactivate(): void { export function deactivate(): void {
sqlMigration.stop();
} }

View File

@@ -8,6 +8,7 @@ import * as azurecore from 'azurecore';
import { DatabaseMigration, SqlMigrationService, getSubscriptions, getServiceMigrations } from '../api/azure'; import { DatabaseMigration, SqlMigrationService, getSubscriptions, getServiceMigrations } from '../api/azure';
import { deepClone } from '../api/utils'; import { deepClone } from '../api/utils';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { ServiceContextChangeEvent } from '../dashboard/tabBase';
export class MigrationLocalStorage { export class MigrationLocalStorage {
private static context: vscode.ExtensionContext; private static context: vscode.ExtensionContext;
@@ -26,15 +27,16 @@ export class MigrationLocalStorage {
return {}; return {};
} }
public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext): Promise<void> { public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext, serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {
const connectionProfile = await azdata.connection.getCurrentConnection(); const connectionProfile = await azdata.connection.getCurrentConnection();
if (connectionProfile) { if (connectionProfile) {
const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`; const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`;
return await this.context.globalState.update(serverContextKey, deepClone(serviceContext)); await this.context.globalState.update(serverContextKey, deepClone(serviceContext));
serviceContextChangedEvent.fire({ connectionId: connectionProfile.connectionId });
} }
} }
public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> { public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration, serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {
if (serviceContext.azureAccount?.isStale) { if (serviceContext.azureAccount?.isStale) {
const accounts = await azdata.accounts.getAllAccounts(); const accounts = await azdata.accounts.getAllAccounts();
const account = accounts.find(a => !a.isStale && a.key.accountId === serviceContext.azureAccount?.key.accountId); const account = accounts.find(a => !a.isStale && a.key.accountId === serviceContext.azureAccount?.key.accountId);
@@ -43,7 +45,7 @@ export class MigrationLocalStorage {
const subscription = subscriptions.find(s => s.id === serviceContext.subscription?.id); const subscription = subscriptions.find(s => s.id === serviceContext.subscription?.id);
if (subscription) { if (subscription) {
serviceContext.azureAccount = account; serviceContext.azureAccount = account;
await this.saveMigrationServiceContext(serviceContext); await this.saveMigrationServiceContext(serviceContext, serviceContextChangedEvent);
} }
} }
} }
@@ -87,30 +89,3 @@ export interface MigrationServiceContext {
resourceGroup?: azurecore.azureResource.AzureResourceResourceGroup, resourceGroup?: azurecore.azureResource.AzureResourceResourceGroup,
migrationService?: SqlMigrationService, migrationService?: SqlMigrationService,
} }
export enum MigrationStatus {
Failed = 'Failed',
Succeeded = 'Succeeded',
InProgress = 'InProgress',
Canceled = 'Canceled',
Completing = 'Completing',
Creating = 'Creating',
Canceling = 'Canceling',
Retriable = 'Retriable',
}
export enum ProvisioningState {
Failed = 'Failed',
Succeeded = 'Succeeded',
Creating = 'Creating'
}
export enum BackupFileInfoStatus {
Arrived = 'Arrived',
Uploading = 'Uploading',
Uploaded = 'Uploaded',
Restoring = 'Restoring',
Restored = 'Restored',
Cancelled = 'Cancelled',
Ignored = 'Ignored'
}

View File

@@ -80,6 +80,4 @@ export abstract class MigrationWizardPage {
const current = this.wizard.currentPage; const current = this.wizard.currentPage;
await this.wizard.setCurrentPage(current + 1); await this.wizard.setCurrentPage(current + 1);
} }
} }

View File

@@ -7,13 +7,14 @@ import * as azdata from 'azdata';
import * as azurecore from 'azurecore'; import * as azurecore from 'azurecore';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as mssql from 'mssql'; import * as mssql from 'mssql';
import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases } from '../api/azure'; import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer } from '../api/azure';
import * as constants from '../constants/strings'; import * as constants from '../constants/strings';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery'; import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
import { hashString, deepClone } from '../api/utils'; import { hashString, deepClone } from '../api/utils';
import { SKURecommendationPage } from '../wizard/skuRecommendationPage'; import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
import { excludeDatabses, TargetDatabaseInfo } from '../api/sqlUtils';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export enum State { export enum State {
@@ -136,7 +137,7 @@ export interface SavedInfo {
subscription: azurecore.azureResource.AzureResourceSubscription | null; subscription: azurecore.azureResource.AzureResourceSubscription | null;
location: azurecore.azureResource.AzureLocation | null; location: azurecore.azureResource.AzureLocation | null;
resourceGroup: azurecore.azureResource.AzureResourceResourceGroup | null; resourceGroup: azurecore.azureResource.AzureResourceResourceGroup | null;
targetServerInstance: azurecore.azureResource.AzureSqlManagedInstance | SqlVMServer | null; targetServerInstance: azurecore.azureResource.AzureSqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer | null;
migrationMode: MigrationMode | null; migrationMode: MigrationMode | null;
networkContainerType: NetworkContainerType | null; networkContainerType: NetworkContainerType | null;
networkShares: NetworkShare[]; networkShares: NetworkShare[];
@@ -176,7 +177,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _resourceGroup!: azurecore.azureResource.AzureResourceResourceGroup; public _resourceGroup!: azurecore.azureResource.AzureResourceResourceGroup;
public _targetManagedInstances!: SqlManagedInstance[]; public _targetManagedInstances!: SqlManagedInstance[];
public _targetSqlVirtualMachines!: SqlVMServer[]; public _targetSqlVirtualMachines!: SqlVMServer[];
public _targetServerInstance!: SqlManagedInstance | SqlVMServer; public _targetSqlDatabaseServers!: AzureSqlDatabaseServer[];
public _targetServerInstance!: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer;
public _databaseBackup!: DatabaseBackupModel; public _databaseBackup!: DatabaseBackupModel;
public _storageAccounts!: StorageAccount[]; public _storageAccounts!: StorageAccount[];
public _fileShares!: azurecore.azureResource.FileShare[]; public _fileShares!: azurecore.azureResource.FileShare[];
@@ -185,15 +187,15 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _sourceDatabaseNames!: string[]; public _sourceDatabaseNames!: string[];
public _targetDatabaseNames!: string[]; public _targetDatabaseNames!: string[];
public _targetUserName!: string;
public _targetPassword!: string;
public _sourceTargetMapping: Map<string, TargetDatabaseInfo | undefined> = new Map();
public _sqlMigrationServiceResourceGroup!: azurecore.azureResource.AzureResourceResourceGroup; public _sqlMigrationServiceResourceGroup!: azurecore.azureResource.AzureResourceResourceGroup;
public _sqlMigrationService!: SqlMigrationService | undefined; public _sqlMigrationService!: SqlMigrationService | undefined;
public _sqlMigrationServices!: SqlMigrationService[]; public _sqlMigrationServices!: SqlMigrationService[];
public _nodeNames!: string[]; public _nodeNames!: string[];
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
private _currentState: State;
private _gatheringInformationError: string | undefined;
public _databasesForAssessment!: string[]; public _databasesForAssessment!: string[];
public _assessmentResults!: ServerAssessment; public _assessmentResults!: ServerAssessment;
public _assessedDatabaseList!: string[]; public _assessedDatabaseList!: string[];
@@ -204,8 +206,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _databasesForMigration: string[] = []; public _databasesForMigration: string[] = [];
public _didUpdateDatabasesForMigration: boolean = false; public _didUpdateDatabasesForMigration: boolean = false;
public _didDatabaseMappingChange: boolean = false;
public _vmDbs: string[] = []; public _vmDbs: string[] = [];
public _miDbs: string[] = []; public _miDbs: string[] = [];
public _sqldbDbs: string[] = [];
public _targetType!: MigrationTargetType; public _targetType!: MigrationTargetType;
public _skuRecommendationResults!: SkuRecommendation; public _skuRecommendationResults!: SkuRecommendation;
@@ -213,10 +217,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
private _skuRecommendationApiResponse!: mssql.SkuRecommendationResult; private _skuRecommendationApiResponse!: mssql.SkuRecommendationResult;
public _skuRecommendationReportFilePaths: string[]; public _skuRecommendationReportFilePaths: string[];
public _skuRecommendationPerformanceLocation!: string; public _skuRecommendationPerformanceLocation!: string;
private _skuRecommendationRecommendedDatabaseList!: string[];
private _startPerfDataCollectionApiResponse!: mssql.StartPerfDataCollectionResult;
private _stopPerfDataCollectionApiResponse!: mssql.StopPerfDataCollectionResult;
private _refreshPerfDataCollectionApiResponse!: mssql.RefreshPerfDataCollectionResult;
public _perfDataCollectionStartDate!: Date | undefined; public _perfDataCollectionStartDate!: Date | undefined;
public _perfDataCollectionStopDate!: Date | undefined; public _perfDataCollectionStopDate!: Date | undefined;
public _perfDataCollectionLastRefreshedDate!: Date; public _perfDataCollectionLastRefreshedDate!: Date;
@@ -232,9 +233,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public readonly _recommendationTargetPlatforms = [MigrationTargetType.SQLDB, MigrationTargetType.SQLMI, MigrationTargetType.SQLVM]; public readonly _recommendationTargetPlatforms = [MigrationTargetType.SQLDB, MigrationTargetType.SQLMI, MigrationTargetType.SQLVM];
public refreshPerfDataCollectionFrequency = this._performanceDataQueryIntervalInSeconds * 1000; public refreshPerfDataCollectionFrequency = this._performanceDataQueryIntervalInSeconds * 1000;
private _autoRefreshPerfDataCollectionHandle!: NodeJS.Timeout;
public refreshGetSkuRecommendationFrequency = constants.TIME_IN_MINUTES(10); public refreshGetSkuRecommendationFrequency = constants.TIME_IN_MINUTES(10);
private _autoRefreshGetSkuRecommendationHandle!: NodeJS.Timeout;
public _skuScalingFactor!: number; public _skuScalingFactor!: number;
public _skuTargetPercentile!: number; public _skuTargetPercentile!: number;
@@ -246,15 +245,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public savedInfo!: SavedInfo; public savedInfo!: SavedInfo;
public closedPage!: number; public closedPage!: number;
public _sessionId: string = uuidv4(); public _sessionId: string = uuidv4();
public excludeDbs: string[] = [
'master',
'tempdb',
'msdb',
'model'
];
public serverName!: string; public serverName!: string;
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
private _currentState: State;
private _gatheringInformationError: string | undefined;
private _skuRecommendationRecommendedDatabaseList!: string[];
private _startPerfDataCollectionApiResponse!: mssql.StartPerfDataCollectionResult;
private _stopPerfDataCollectionApiResponse!: mssql.StopPerfDataCollectionResult;
private _refreshPerfDataCollectionApiResponse!: mssql.RefreshPerfDataCollectionResult;
private _autoRefreshPerfDataCollectionHandle!: NodeJS.Timeout;
private _autoRefreshGetSkuRecommendationHandle!: NodeJS.Timeout;
constructor( constructor(
public extensionContext: vscode.ExtensionContext, public extensionContext: vscode.ExtensionContext,
private readonly _sourceConnectionId: string, private readonly _sourceConnectionId: string,
@@ -288,8 +290,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._stateChangeEventEmitter.fire({ oldState, newState: this.currentState }); this._stateChangeEventEmitter.fire({ oldState, newState: this.currentState });
} }
public async getDatabases(): Promise<string[]> { public async getDatabases(): Promise<string[]> {
let temp = await azdata.connection.listDatabases(this.sourceConnectionId); const temp = await azdata.connection.listDatabases(this.sourceConnectionId);
let finalResult = temp.filter((name) => !this.excludeDbs.includes(name)); const finalResult = temp.filter((name) => !excludeDatabses.includes(name));
return finalResult; return finalResult;
} }
public hasRecommendedDatabaseListChanged(): boolean { public hasRecommendedDatabaseListChanged(): boolean {
@@ -304,7 +306,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
})); }));
} }
public async getDatabaseAssessments(targetType: MigrationTargetType): Promise<ServerAssessment> { public async getDatabaseAssessments(targetType: MigrationTargetType[]): Promise<ServerAssessment> {
const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId); const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId);
try { try {
const response = (await this.migrationService.getAssessments(ownerUri, this._databasesForAssessment))!; const response = (await this.migrationService.getAssessments(ownerUri, this._databasesForAssessment))!;
@@ -313,11 +315,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
if (response?.assessmentResult) { if (response?.assessmentResult) {
response.assessmentResult.items = response.assessmentResult.items?.filter( response.assessmentResult.items = response.assessmentResult.items?.filter(
issue => issue.appliesToMigrationTargetPlatform === targetType); issue => targetType.includes(
<MigrationTargetType>issue.appliesToMigrationTargetPlatform));
response.assessmentResult.databases?.forEach( response.assessmentResult.databases?.forEach(
database => database.items = database.items?.filter( database => database.items = database.items?.filter(
issue => issue.appliesToMigrationTargetPlatform === targetType)); issue => targetType.includes(
<MigrationTargetType>issue.appliesToMigrationTargetPlatform)));
this._assessmentResults = { this._assessmentResults = {
issues: this._assessmentApiResponse?.assessmentResult?.items || [], issues: this._assessmentApiResponse?.assessmentResult?.items || [],
databaseAssessments: this._assessmentApiResponse?.assessmentResult?.databases?.map(d => { databaseAssessments: this._assessmentApiResponse?.assessmentResult?.databases?.map(d => {
@@ -447,8 +452,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'sessionId': this._sessionId, 'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultItem?.targetSku) 'recommendedSku': JSON.stringify(resultItem?.targetSku)
}, },
{} {});
);
}); });
this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults?.forEach(resultItem => { this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults?.forEach(resultItem => {
@@ -460,8 +464,19 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'sessionId': this._sessionId, 'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultItem?.targetSku) 'recommendedSku': JSON.stringify(resultItem?.targetSku)
}, },
{} {});
); });
this._skuRecommendationResults?.recommendations?.sqlDbRecommendationResults?.forEach(resultItem => {
// Send telemetry for recommended SQLDB SKU
sendSqlMigrationActionEvent(
TelemetryViews.SkuRecommendationWizard,
TelemetryAction.GetSqlDbSkuRecommendation,
{
'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultItem?.targetSku)
},
{});
}); });
// Send Instance requirements used for calculating recommendations // Send Instance requirements used for calculating recommendations
@@ -513,8 +528,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB, 'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB,
'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile, 'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile,
'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed, 'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed,
} });
);
} catch (e) { } catch (e) {
logError(TelemetryViews.SkuRecommendationWizard, 'GetSkuRecommendationTelemetryFailed', e); logError(TelemetryViews.SkuRecommendationWizard, 'GetSkuRecommendationTelemetryFailed', e);
@@ -530,7 +544,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
try { try {
if (!this.performanceCollectionInProgress()) { if (!this.performanceCollectionInProgress()) {
const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId); const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId);
const response = await this.migrationService.startPerfDataCollection(ownerUri, dataFolder, perfQueryIntervalInSec, staticQueryIntervalInSec, numberOfIterations); const response = await this.migrationService.startPerfDataCollection(
ownerUri,
dataFolder,
perfQueryIntervalInSec,
staticQueryIntervalInSec,
numberOfIterations);
this._startPerfDataCollectionApiResponse = response!; this._startPerfDataCollectionApiResponse = response!;
this._perfDataCollectionStartDate = this._startPerfDataCollectionApiResponse.dateTimeStarted; this._perfDataCollectionStartDate = this._startPerfDataCollectionApiResponse.dateTimeStarted;
@@ -560,8 +579,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'sessionId': this._sessionId, 'sessionId': this._sessionId,
'timeDataCollectionStarted': this._perfDataCollectionStartDate?.toString() || '' 'timeDataCollectionStarted': this._perfDataCollectionStartDate?.toString() || ''
}, },
{} {});
);
} catch (e) { } catch (e) {
logError(TelemetryViews.DataCollectionWizard, 'StartDataCollectionTelemetryFailed', e); logError(TelemetryViews.DataCollectionWizard, 'StartDataCollectionTelemetryFailed', e);
@@ -574,13 +592,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
if (!this._autoRefreshPerfDataCollectionHandle) { if (!this._autoRefreshPerfDataCollectionHandle) {
clearInterval(this._autoRefreshPerfDataCollectionHandle); clearInterval(this._autoRefreshPerfDataCollectionHandle);
if (this.refreshPerfDataCollectionFrequency !== -1) { if (this.refreshPerfDataCollectionFrequency !== -1) {
this._autoRefreshPerfDataCollectionHandle = setInterval(async function () { this._autoRefreshPerfDataCollectionHandle = setInterval(
async function () {
await classVariable.refreshPerfDataCollection(); await classVariable.refreshPerfDataCollection();
if (await classVariable.isWaitingForFirstTimeRefresh()) { if (await classVariable.isWaitingForFirstTimeRefresh()) {
await page.refreshSkuRecommendationComponents(); // update timer await page.refreshSkuRecommendationComponents(); // update timer
} }
}, refreshIntervalInMs); },
refreshIntervalInMs);
} }
} }
@@ -588,9 +607,11 @@ export class MigrationStateModel implements Model, vscode.Disposable {
// start one-time timer to get SKU recommendation // start one-time timer to get SKU recommendation
clearTimeout(this._autoRefreshGetSkuRecommendationHandle); clearTimeout(this._autoRefreshGetSkuRecommendationHandle);
if (this.refreshGetSkuRecommendationFrequency !== -1) { if (this.refreshGetSkuRecommendationFrequency !== -1) {
this._autoRefreshGetSkuRecommendationHandle = setTimeout(async function () { this._autoRefreshGetSkuRecommendationHandle = setTimeout(
async function () {
await page.refreshAzureRecommendation(); await page.refreshAzureRecommendation();
}, this.refreshGetSkuRecommendationFrequency); },
this.refreshGetSkuRecommendationFrequency);
} }
} }
} }
@@ -612,7 +633,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
// Generate telemetry for stop data collection request // Generate telemetry for stop data collection request
this.generateStopDataCollectionTelemetry().catch(e => console.error(e)); this.generateStopDataCollectionTelemetry()
.catch(e => console.error(e));
return true; return true;
} }
@@ -625,8 +647,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'sessionId': this._sessionId, 'sessionId': this._sessionId,
'timeDataCollectionStopped': this._perfDataCollectionStopDate?.toString() || '' 'timeDataCollectionStopped': this._perfDataCollectionStopDate?.toString() || ''
}, },
{} {});
);
} catch (e) { } catch (e) {
logError(TelemetryViews.DataCollectionWizard, 'StopDataCollectionTelemetryFailed', e); logError(TelemetryViews.DataCollectionWizard, 'StopDataCollectionTelemetryFailed', e);
@@ -643,7 +664,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._perfDataCollectionIsCollecting = this._refreshPerfDataCollectionApiResponse.isCollecting; this._perfDataCollectionIsCollecting = this._refreshPerfDataCollectionApiResponse.isCollecting;
if (this._perfDataCollectionErrors?.length > 0) { if (this._perfDataCollectionErrors?.length > 0) {
void vscode.window.showInformationMessage(constants.PERF_DATA_COLLECTION_ERROR(this._assessmentApiResponse?.assessmentResult?.name, this._perfDataCollectionErrors)); void vscode.window.showInformationMessage(
constants.PERF_DATA_COLLECTION_ERROR(
this._assessmentApiResponse?.assessmentResult?.name,
this._perfDataCollectionErrors));
} }
} }
catch (error) { catch (error) {
@@ -661,33 +685,24 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
public performanceCollectionNotStarted(): boolean { public performanceCollectionNotStarted(): boolean {
if (!this._perfDataCollectionStartDate return !this._perfDataCollectionStartDate
&& !this._perfDataCollectionStopDate) { && !this._perfDataCollectionStopDate;
return true;
}
return false;
} }
public performanceCollectionInProgress(): boolean { public performanceCollectionInProgress(): boolean {
if (this._perfDataCollectionStartDate return this._perfDataCollectionStartDate !== undefined
&& !this._perfDataCollectionStopDate) { && this._perfDataCollectionStopDate === undefined;
return true;
}
return false;
} }
public performanceCollectionStopped(): boolean { public performanceCollectionStopped(): boolean {
if (this._perfDataCollectionStartDate return this._perfDataCollectionStartDate !== undefined
&& this._perfDataCollectionStopDate) { && this._perfDataCollectionStopDate !== undefined;
return true;
}
return false;
} }
private async generateAssessmentTelemetry(): Promise<void> { private async generateAssessmentTelemetry(): Promise<void> {
try { try {
let serverIssues = this._assessmentResults?.issues.map(i => { const serverIssues = this._assessmentResults?.issues.map(i => {
return { return {
ruleId: i.ruleId, ruleId: i.ruleId,
count: i.impactedObjects.length count: i.impactedObjects.length
@@ -696,18 +711,15 @@ export class MigrationStateModel implements Model, vscode.Disposable {
const serverAssessmentErrorsMap: Map<number, number> = new Map(); const serverAssessmentErrorsMap: Map<number, number> = new Map();
this._assessmentApiResponse?.assessmentResult?.errors?.forEach(e => { this._assessmentApiResponse?.assessmentResult?.errors?.forEach(e => {
serverAssessmentErrorsMap.set(e.errorId, serverAssessmentErrorsMap.get(e.errorId) ?? 0 + 1); serverAssessmentErrorsMap.set(
e.errorId,
serverAssessmentErrorsMap.get(e.errorId) ?? 0 + 1);
}); });
let serverErrors: { errorId: number, count: number }[] = []; const serverErrors: { errorId: number, count: number }[] = [];
serverAssessmentErrorsMap.forEach((v, k) => { serverAssessmentErrorsMap.forEach(
serverErrors.push( (v, k) => serverErrors.push(
{ { errorId: k, count: v }));
errorId: k,
count: v
}
);
});
const startTime = new Date(this._assessmentApiResponse?.startTime); const startTime = new Date(this._assessmentApiResponse?.startTime);
const endTime = new Date(this._assessmentApiResponse?.endedTime); const endTime = new Date(this._assessmentApiResponse?.endedTime);
@@ -759,27 +771,26 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'assessmentTimeMs': d.assessmentTimeInMilliseconds, 'assessmentTimeMs': d.assessmentTimeInMilliseconds,
'numberOfBlockerIssues': d.sqlManagedInstanceTargetReadiness.numOfBlockerIssues, 'numberOfBlockerIssues': d.sqlManagedInstanceTargetReadiness.numOfBlockerIssues,
'databaseSizeInMb': d.databaseSize 'databaseSizeInMb': d.databaseSize
} });
);
d.items.forEach(i => { d.items.forEach(i => {
databaseWarningsMap.set(i.ruleId, databaseWarningsMap.get(i.ruleId) ?? 0 + i.impactedObjects.length); databaseWarningsMap.set(
i.ruleId,
databaseWarningsMap.get(i.ruleId) ?? 0 + i.impactedObjects.length);
}); });
d.errors.forEach(e => { d.errors.forEach(
databaseErrorsMap.set(e.errorId, databaseErrorsMap.get(e.errorId) ?? 0 + 1); e => databaseErrorsMap.set(
}); e.errorId,
databaseErrorsMap.get(e.errorId) ?? 0 + 1));
}); });
let databaseWarnings: { warningId: string, count: number }[] = []; const databaseWarnings: { warningId: string, count: number }[] = [];
databaseWarningsMap.forEach((v, k) => { databaseWarningsMap.forEach(
databaseWarnings.push({ (v, k) => databaseWarnings.push(
warningId: k, { warningId: k, count: v }));
count: v
});
});
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.MigrationWizardTargetSelectionPage, TelemetryViews.MigrationWizardTargetSelectionPage,
@@ -788,16 +799,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'sessionId': this._sessionId, 'sessionId': this._sessionId,
'warnings': JSON.stringify(databaseWarnings) 'warnings': JSON.stringify(databaseWarnings)
}, },
{} {});
);
let databaseErrors: { errorId: number, count: number }[] = []; const databaseErrors: { errorId: number, count: number }[] = [];
databaseErrorsMap.forEach((v, k) => { databaseErrorsMap.forEach(
databaseErrors.push({ (v, k) => databaseErrors.push(
errorId: k, { errorId: k, count: v }));
count: v
});
});
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.MigrationWizardTargetSelectionPage, TelemetryViews.MigrationWizardTargetSelectionPage,
@@ -806,8 +813,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'sessionId': this._sessionId, 'sessionId': this._sessionId,
'errors': JSON.stringify(databaseErrors) 'errors': JSON.stringify(databaseErrors)
}, },
{} {});
);
} catch (e) { } catch (e) {
console.log('error during assessment telemetry:'); console.log('error during assessment telemetry:');
@@ -837,13 +843,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getSourceConnectionProfile(): Promise<azdata.connection.ConnectionProfile> { public async getSourceConnectionProfile(): Promise<azdata.connection.ConnectionProfile> {
const sqlConnections = await azdata.connection.getConnections(); const sqlConnections = await azdata.connection.getConnections();
return sqlConnections.find((value) => { return sqlConnections.find(
if (value.connectionId === this.sourceConnectionId) { value => value.connectionId === this.sourceConnectionId)!;
return true;
} else {
return false;
}
})!;
} }
public getLocationDisplayName(location: string): Promise<string> { public getLocationDisplayName(location: string): Promise<string> {
@@ -851,22 +852,20 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
public async getManagedDatabases(): Promise<string[]> { public async getManagedDatabases(): Promise<string[]> {
return (await getSqlManagedInstanceDatabases(this._azureAccount, return (
await getSqlManagedInstanceDatabases(this._azureAccount,
this._targetSubscription, this._targetSubscription,
<SqlManagedInstance>this._targetServerInstance)).map(t => t.name); <SqlManagedInstance>this._targetServerInstance)
).map(t => t.name);
} }
public async startMigration() { public async startMigration() {
const sqlConnections = await azdata.connection.getConnections(); const sqlConnections = await azdata.connection.getConnections();
const currentConnection = sqlConnections.find((value) => { const currentConnection = sqlConnections.find(
if (value.connectionId === this.sourceConnectionId) { value => value.connectionId === this.sourceConnectionId);
return true;
} else {
return false;
}
});
const isOfflineMigration = this._databaseBackup.migrationMode === MigrationMode.OFFLINE; const isOfflineMigration = this._databaseBackup.migrationMode === MigrationMode.OFFLINE;
const isSqlDbTarget = this._targetType === MigrationTargetType.SQLDB;
const requestBody: StartDatabaseMigrationRequest = { const requestBody: StartDatabaseMigrationRequest = {
location: this._sqlMigrationService?.location!, location: this._sqlMigrationService?.location!,
@@ -877,7 +876,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
sourceSqlConnection: { sourceSqlConnection: {
dataSource: currentConnection?.serverName!, dataSource: currentConnection?.serverName!,
authentication: this._authenticationType, authentication: this._authenticationType,
username: this._sqlServerUsername, userName: this._sqlServerUsername,
password: this._sqlServerPassword password: this._sqlServerPassword
}, },
scope: this._targetServerInstance.id, scope: this._targetServerInstance.id,
@@ -926,6 +925,55 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
}; };
break; break;
default:
if (isSqlDbTarget) {
const sourceDatabaseName = this._databasesForMigration[i];
const targetDatabaseInfo = this._sourceTargetMapping.get(sourceDatabaseName);
const totalTables = targetDatabaseInfo?.sourceTables.size ?? 0;
// skip databases that don't have tables
if (totalTables === 0) {
continue;
}
const sourceTables: string[] = [];
let selectedTables = 0;
targetDatabaseInfo?.sourceTables.forEach(sourceTableInfo => {
if (sourceTableInfo.selectedForMigration) {
selectedTables++;
sourceTables.push(sourceTableInfo.tableName);
}
});
// skip databases that don't have tables selected
if (selectedTables === 0) {
continue;
}
const sqlDbTarget = this._targetServerInstance as AzureSqlDatabaseServer;
requestBody.properties.offlineConfiguration = undefined;
requestBody.properties.sourceSqlConnection = {
dataSource: currentConnection?.serverName!,
authentication: this._authenticationType,
userName: this._sqlServerUsername,
password: this._sqlServerPassword,
encryptConnection: true,
trustServerCertificate: false,
};
requestBody.properties.targetSqlConnection = {
dataSource: sqlDbTarget.properties.fullyQualifiedDomainName,
authentication: MigrationSourceAuthenticationType.Sql,
userName: this._targetUserName,
password: this._targetPassword,
encryptConnection: true,
trustServerCertificate: false,
};
// send an empty array when 'all' tables are selected for migration
requestBody.properties.tableList = selectedTables === totalTables
? []
: sourceTables;
}
break;
} }
requestBody.properties.sourceDatabaseName = this._databasesForMigration[i]; requestBody.properties.sourceDatabaseName = this._databasesForMigration[i];
const response = await startDatabaseMigration( const response = await startDatabaseMigration(
@@ -969,8 +1017,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'wizardEntryPoint': wizardEntryPoint, 'wizardEntryPoint': wizardEntryPoint,
}, },
{ {
} });
);
void vscode.window.showInformationMessage( void vscode.window.showInformationMessage(
localize( localize(
@@ -982,7 +1029,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
} catch (e) { } catch (e) {
void vscode.window.showErrorMessage( void vscode.window.showErrorMessage(
localize('sql.migration.starting.migration.error', "An error occurred while starting the migration: '{0}'", e.message)); localize(
'sql.migration.starting.migration.error',
"An error occurred while starting the migration: '{0}'",
e.message));
logError(TelemetryViews.MigrationLocalStorage, 'StartMigrationFailed', e); logError(TelemetryViews.MigrationLocalStorage, 'StartMigrationFailed', e);
} }
finally { finally {
@@ -990,15 +1040,15 @@ export class MigrationStateModel implements Model, vscode.Disposable {
await this.refreshPerfDataCollection(); await this.refreshPerfDataCollection();
if ((!this.resumeAssessment || this.retryMigration) && this._perfDataCollectionIsCollecting) { if ((!this.resumeAssessment || this.retryMigration) && this._perfDataCollectionIsCollecting) {
void this.stopPerfDataCollection(); void this.stopPerfDataCollection();
void vscode.window.showInformationMessage(constants.AZURE_RECOMMENDATION_STOP_POPUP); void vscode.window.showInformationMessage(
constants.AZURE_RECOMMENDATION_STOP_POPUP);
} }
} }
} }
} }
public async saveInfo(serverName: string, currentPage: Page): Promise<void> { public async saveInfo(serverName: string, currentPage: Page): Promise<void> {
let saveInfo: SavedInfo; const saveInfo: SavedInfo = {
saveInfo = {
closedPage: currentPage, closedPage: currentPage,
databaseAssessment: [], databaseAssessment: [],
databaseList: [], databaseList: [],
@@ -1047,7 +1097,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
saveInfo.serverAssessment = this._assessmentResults; saveInfo.serverAssessment = this._assessmentResults;
if (this._skuRecommendationPerformanceDataSource) { if (this._skuRecommendationPerformanceDataSource) {
let skuRecommendation: SkuRecommendationSavedInfo = { const skuRecommendation: SkuRecommendationSavedInfo = {
skuRecommendationPerformanceDataSource: this._skuRecommendationPerformanceDataSource, skuRecommendationPerformanceDataSource: this._skuRecommendationPerformanceDataSource,
skuRecommendationPerformanceLocation: this._skuRecommendationPerformanceLocation, skuRecommendationPerformanceLocation: this._skuRecommendationPerformanceLocation,
perfDataCollectionStartDate: this._perfDataCollectionStartDate, perfDataCollectionStartDate: this._perfDataCollectionStartDate,
@@ -1072,6 +1122,9 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._databasesForAssessment = this.savedInfo.databaseAssessment; this._databasesForAssessment = this.savedInfo.databaseAssessment;
this._databasesForMigration = this.savedInfo.databaseList; this._databasesForMigration = this.savedInfo.databaseList;
this._didUpdateDatabasesForMigration = true; this._didUpdateDatabasesForMigration = true;
this._didDatabaseMappingChange = true;
this.refreshDatabaseBackupPage = true;
switch (this._targetType) { switch (this._targetType) {
case MigrationTargetType.SQLMI: case MigrationTargetType.SQLMI:
this._miDbs = this._databasesForMigration; this._miDbs = this._databasesForMigration;
@@ -1079,6 +1132,9 @@ export class MigrationStateModel implements Model, vscode.Disposable {
case MigrationTargetType.SQLVM: case MigrationTargetType.SQLVM:
this._vmDbs = this._databasesForMigration; this._vmDbs = this._databasesForMigration;
break; break;
case MigrationTargetType.SQLDB:
this._sqldbDbs = this._databasesForMigration;
break;
} }
this._azureAccount = this.savedInfo.azureAccount || undefined!; this._azureAccount = this.savedInfo.azureAccount || undefined!;
@@ -1091,7 +1147,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._databaseBackup.migrationMode = this.savedInfo.migrationMode || undefined!; this._databaseBackup.migrationMode = this.savedInfo.migrationMode || undefined!;
this.refreshDatabaseBackupPage = true;
this._sourceDatabaseNames = this._databasesForMigration; this._sourceDatabaseNames = this._databasesForMigration;
this._targetDatabaseNames = this.savedInfo.targetDatabaseNames; this._targetDatabaseNames = this.savedInfo.targetDatabaseNames;
this._databaseBackup.networkContainerType = this.savedInfo.networkContainerType || undefined!; this._databaseBackup.networkContainerType = this.savedInfo.networkContainerType || undefined!;

View File

@@ -57,6 +57,7 @@ export enum TelemetryAction {
OnPageLeave = 'OnPageLeave', OnPageLeave = 'OnPageLeave',
GetMISkuRecommendation = 'GetMISkuRecommendation', GetMISkuRecommendation = 'GetMISkuRecommendation',
GetVMSkuRecommendation = 'GetVMSkuRecommendation', GetVMSkuRecommendation = 'GetVMSkuRecommendation',
GetSqlDbSkuRecommendation = 'GetSqlDbSkuRecommendation',
GetInstanceRequirements = 'GetInstanceRequirements', GetInstanceRequirements = 'GetInstanceRequirements',
StartDataCollection = 'StartDataCollection', StartDataCollection = 'StartDataCollection',
StopDataCollection = 'StopDataCollection' StopDataCollection = 'StopDataCollection'

File diff suppressed because it is too large Load Diff

View File

@@ -219,9 +219,9 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
] ]
}).component(); }).component();
this._disposables.push(this._databaseSelectorTable.onRowSelected(async (e) => { this._disposables.push(
await this.updateValuesOnSelection(); this._databaseSelectorTable.onRowSelected(
})); async (e) => await this.updateValuesOnSelection()));
// load unfiltered table list and pre-select list of databases saved in state // load unfiltered table list and pre-select list of databases saved in state
await this._filterTableList('', this.migrationStateModel._databasesForAssessment); await this._filterTableList('', this.migrationStateModel._databasesForAssessment);

View File

@@ -6,7 +6,7 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import { MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog'; import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog';
import * as constants from '../constants/strings'; import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
@@ -17,20 +17,17 @@ import * as utils from '../api/utils';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
export class IntergrationRuntimePage extends MigrationWizardPage { export class IntergrationRuntimePage extends MigrationWizardPage {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
private _statusLoadingComponent!: azdata.LoadingComponent; private _statusLoadingComponent!: azdata.LoadingComponent;
private _subscription!: azdata.TextComponent; private _subscription!: azdata.TextComponent;
private _location!: azdata.TextComponent; private _location!: azdata.TextComponent;
private _resourceGroupDropdown!: azdata.DropDownComponent; private _resourceGroupDropdown!: azdata.DropDownComponent;
private _dmsDropdown!: azdata.DropDownComponent; private _dmsDropdown!: azdata.DropDownComponent;
private _dmsInfoContainer!: azdata.FlexContainer; private _dmsInfoContainer!: azdata.FlexContainer;
private _dmsStatusInfoBox!: azdata.InfoBoxComponent; private _dmsStatusInfoBox!: azdata.InfoBoxComponent;
private _authKeyTable!: azdata.DeclarativeTableComponent; private _authKeyTable!: azdata.DeclarativeTableComponent;
private _refreshButton!: azdata.ButtonComponent; private _refreshButton!: azdata.ButtonComponent;
private _connectionStatusLoader!: azdata.LoadingComponent; private _connectionStatusLoader!: azdata.LoadingComponent;
private _copy1!: azdata.ButtonComponent; private _copy1!: azdata.ButtonComponent;
private _copy2!: azdata.ButtonComponent; private _copy2!: azdata.ButtonComponent;
private _refresh1!: azdata.ButtonComponent; private _refresh1!: azdata.ButtonComponent;
@@ -55,31 +52,39 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
const form = view.modelBuilder.formContainer() const form = view.modelBuilder.formContainer()
.withFormItems([ .withFormItems([
{ component: this.migrationServiceDropdownContainer() }, { component: this.migrationServiceDropdownContainer() },
{ component: this._dmsInfoContainer } { component: this._dmsInfoContainer }])
])
.withProps({ CSSStyles: { 'padding-top': '0' } }) .withProps({ CSSStyles: { 'padding-top': '0' } })
.component(); .component();
this._disposables.push(this._view.onClosed(e => { this._disposables.push(
this._view.onClosed(e =>
this._disposables.forEach( this._disposables.forEach(
d => { try { d.dispose(); } catch { } }); d => { try { d.dispose(); } catch { } })));
}));
await view.initializeModel(form); await view.initializeModel(form);
} }
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> { public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
this._subscription.value = this.migrationStateModel._targetSubscription.name; this._subscription.value = this.migrationStateModel._targetSubscription.name;
this._location.value = await getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); this._location.value = await getLocationDisplayName(
this._dmsInfoContainer.display = (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._sqlMigrationService) ? 'inline' : 'none'; this.migrationStateModel._targetServerInstance.location);
await utils.updateControlDisplay(
this._dmsInfoContainer,
this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE);
await this.loadResourceGroupDropdown(); await this.loadResourceGroupDropdown();
this.wizard.registerNavigationValidator((pageChangeInfo) => { this.wizard.registerNavigationValidator((pageChangeInfo) => {
this.wizard.message = { text: '' };
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
this.wizard.message = {
text: ''
};
return true; return true;
} }
const state = this.migrationStateModel._sqlMigrationService?.properties?.integrationRuntimeState; const state = this.migrationStateModel._sqlMigrationService?.properties?.integrationRuntimeState;
if (!this.migrationStateModel._sqlMigrationService) { if (!this.migrationStateModel._sqlMigrationService) {
this.wizard.message = { this.wizard.message = {
@@ -88,153 +93,150 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
}; };
return false; return false;
} }
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && state !== 'Online') { if ((this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE)
&& state !== 'Online') {
this.wizard.message = { this.wizard.message = {
level: azdata.window.MessageLevel.Error, level: azdata.window.MessageLevel.Error,
text: constants.SERVICE_OFFLINE_ERROR text: constants.SERVICE_OFFLINE_ERROR
}; };
return false; return false;
} else {
this.wizard.message = {
text: ''
};
} }
return true; return true;
}); });
} }
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> { public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator((pageChangeInfo) => { this.wizard.registerNavigationValidator((pageChangeInfo) => true);
return true;
});
} }
protected async handleStateChange(e: StateChangeEvent): Promise<void> { protected async handleStateChange(e: StateChangeEvent): Promise<void> {
} }
private migrationServiceDropdownContainer(): azdata.FlexContainer { private migrationServiceDropdownContainer(): azdata.FlexContainer {
const descriptionText = this._view.modelBuilder.text().withProps({ const descriptionText = this._view.modelBuilder.text()
.withProps({
value: constants.IR_PAGE_DESCRIPTION, value: constants.IR_PAGE_DESCRIPTION,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '16px' }
...styles.BODY_CSS,
'margin-bottom': '16px'
}
}).component(); }).component();
const subscriptionLabel = this._view.modelBuilder.text().withProps({ const subscriptionLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SUBSCRIPTION, value: constants.SUBSCRIPTION,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS }
...styles.LABEL_CSS
}
}).component(); }).component();
this._subscription = this._view.modelBuilder.text().withProps({ this._subscription = this._view.modelBuilder.text()
.withProps({
enabled: false, enabled: false,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { CSSStyles: { 'margin': '0' }
'margin': '0'
}
}).component(); }).component();
const locationLabel = this._view.modelBuilder.text().withProps({ const locationLabel = this._view.modelBuilder.text()
.withProps({
value: constants.LOCATION, value: constants.LOCATION,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '1em' }
...styles.LABEL_CSS,
'margin-top': '1em'
}
}).component(); }).component();
this._location = this._view.modelBuilder.text().withProps({ this._location = this._view.modelBuilder.text()
.withProps({
enabled: false, enabled: false,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { CSSStyles: { 'margin': '0' }
'margin': '0'
}
}).component(); }).component();
const resourceGroupLabel = this._view.modelBuilder.text().withProps({ const resourceGroupLabel = this._view.modelBuilder.text()
.withProps({
value: constants.RESOURCE_GROUP, value: constants.RESOURCE_GROUP,
requiredIndicator: true, requiredIndicator: true,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS }
...styles.LABEL_CSS
}
}).component(); }).component();
this._resourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({ this._resourceGroupDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.RESOURCE_GROUP, ariaLabel: constants.RESOURCE_GROUP,
placeholder: constants.SELECT_RESOURCE_GROUP, placeholder: constants.SELECT_RESOURCE_GROUP,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true, editable: true,
required: true, required: true,
fireOnTextChange: true, fireOnTextChange: true,
CSSStyles: { CSSStyles: { 'margin-top': '-1em' }
'margin-top': '-1em'
}
}).component(); }).component();
this._disposables.push(this._resourceGroupDropdown.onValueChanged(async (value) => { this._disposables.push(
this._resourceGroupDropdown.onValueChanged(
async (value) => {
if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) { if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) {
const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value);
this.migrationStateModel._sqlMigrationServiceResourceGroup = (selectedResourceGroup) this.migrationStateModel._sqlMigrationServiceResourceGroup = (selectedResourceGroup)
? selectedResourceGroup ? selectedResourceGroup
: undefined!; : undefined!;
await this.populateDms(); this.populateDms();
} }
})); }));
const migrationServiceDropdownLabel = this._view.modelBuilder.text().withProps({ const migrationServiceDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.IR_PAGE_TITLE, value: constants.IR_PAGE_TITLE,
requiredIndicator: true, requiredIndicator: true,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS }
...styles.LABEL_CSS
}
}).component(); }).component();
this._dmsDropdown = this._view.modelBuilder.dropDown().withProps({ this._dmsDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.IR_PAGE_TITLE, ariaLabel: constants.IR_PAGE_TITLE,
placeholder: constants.SELECT_RESOURCE_GROUP_PROMPT, placeholder: constants.SELECT_RESOURCE_GROUP_PROMPT,
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true, editable: true,
required: true, required: true,
fireOnTextChange: true, fireOnTextChange: true,
CSSStyles: { CSSStyles: { 'margin-top': '-1em' }
'margin-top': '-1em'
}
}).component(); }).component();
this._disposables.push(this._dmsDropdown.onValueChanged(async (value) => { this._disposables.push(
this._dmsDropdown.onValueChanged(
async (value) => {
if (value && value !== 'undefined' && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) { if (value && value !== 'undefined' && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { this.wizard.message = { text: '' };
this._dmsInfoContainer.display = 'inline';
} await utils.updateControlDisplay(
this.wizard.message = { this._dmsInfoContainer,
text: '' this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
}; this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE);
const selectedDms = this.migrationStateModel._sqlMigrationServices.find(dms => dms.name === value && dms.properties.resourceGroup.toLowerCase() === this.migrationStateModel._sqlMigrationServiceResourceGroup.name.toLowerCase());
const selectedDms = this.migrationStateModel._sqlMigrationServices.find(
dms => dms.name === value && dms.properties.resourceGroup.toLowerCase() === this.migrationStateModel._sqlMigrationServiceResourceGroup.name.toLowerCase());
if (selectedDms) { if (selectedDms) {
this.migrationStateModel._sqlMigrationService = selectedDms; this.migrationStateModel._sqlMigrationService = selectedDms;
await this.loadMigrationServiceStatus(); await this.loadStatus();
} }
} else { } else {
this.migrationStateModel._sqlMigrationService = undefined; this.migrationStateModel._sqlMigrationService = undefined;
this._dmsInfoContainer.display = 'none'; await utils.updateControlDisplay(this._dmsInfoContainer, false);
} }
})); }));
const createNewMigrationService = this._view.modelBuilder.hyperlink().withProps({ const createNewMigrationService = this._view.modelBuilder.hyperlink()
.withProps({
label: constants.CREATE_NEW, label: constants.CREATE_NEW,
ariaLabel: constants.CREATE_NEW_MIGRATION_SERVICE, ariaLabel: constants.CREATE_NEW_MIGRATION_SERVICE,
url: '', url: '',
CSSStyles: { CSSStyles: { ...styles.BODY_CSS }
...styles.BODY_CSS
}
}).component(); }).component();
this._disposables.push(createNewMigrationService.onDidClick(async (e) => { this._disposables.push(
createNewMigrationService.onDidClick(
async (e) => {
const dialog = new CreateSqlMigrationServiceDialog(); const dialog = new CreateSqlMigrationServiceDialog();
const createdDmsResult = await dialog.createNewDms(this.migrationStateModel, (<azdata.CategoryValue>this._resourceGroupDropdown.value).displayName); const createdDmsResult = await dialog.createNewDms(
this.migrationStateModel,
(<azdata.CategoryValue>this._resourceGroupDropdown.value).displayName);
this.migrationStateModel._sqlMigrationServiceResourceGroup = createdDmsResult.resourceGroup; this.migrationStateModel._sqlMigrationServiceResourceGroup = createdDmsResult.resourceGroup;
this.migrationStateModel._sqlMigrationService = createdDmsResult.service; this.migrationStateModel._sqlMigrationService = createdDmsResult.service;
await this.loadResourceGroupDropdown(); await this.loadResourceGroupDropdown();
await this.populateDms(); this.populateDms();
})); }));
const flexContainer = this._view.modelBuilder.flexContainer().withItems([ return this._view.modelBuilder.flexContainer()
.withItems([
descriptionText, descriptionText,
subscriptionLabel, subscriptionLabel,
this._subscription, this._subscription,
@@ -244,27 +246,24 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this._resourceGroupDropdown, this._resourceGroupDropdown,
migrationServiceDropdownLabel, migrationServiceDropdownLabel,
this._dmsDropdown, this._dmsDropdown,
createNewMigrationService createNewMigrationService])
]).withLayout({ .withLayout({ flexFlow: 'column' })
flexFlow: 'column' .component();
}).component();
return flexContainer;
} }
private createDMSDetailsContainer(): azdata.FlexContainer { private createDMSDetailsContainer(): azdata.FlexContainer {
const container = this._view.modelBuilder.flexContainer().withLayout({ const container = this._view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).component(); .component();
const connectionStatusLabel = this._view.modelBuilder.text().withProps({ const connectionStatusLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SERVICE_CONNECTION_STATUS, value: constants.SERVICE_CONNECTION_STATUS,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, 'width': '130px' }
...styles.LABEL_CSS,
'width': '130px'
}
}).component(); }).component();
this._refreshButton = this._view.modelBuilder.button().withProps({ this._refreshButton = this._view.modelBuilder.button()
.withProps({
iconWidth: '18px', iconWidth: '18px',
iconHeight: '18px', iconHeight: '18px',
iconPath: IconPathHelper.refresh, iconPath: IconPathHelper.refresh,
@@ -273,73 +272,73 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
ariaLabel: constants.REFRESH, ariaLabel: constants.REFRESH,
}).component(); }).component();
this._disposables.push(this._refreshButton.onDidClick(async (e) => { this._disposables.push(
this._connectionStatusLoader.loading = true; this._refreshButton.onDidClick(
try { async (e) => this.loadStatus()));
await this.loadStatus();
} finally {
this._connectionStatusLoader.loading = false;
}
}));
const connectionLabelContainer = this._view.modelBuilder.flexContainer().component(); const connectionLabelContainer = this._view.modelBuilder.flexContainer()
connectionLabelContainer.addItem(connectionStatusLabel, { .component();
flex: '0' connectionLabelContainer.addItem(
}); connectionStatusLabel,
connectionLabelContainer.addItem(this._refreshButton, { { flex: '0' });
flex: '0', connectionLabelContainer.addItem(
CSSStyles: { 'margin-right': '10px' } this._refreshButton,
}); { flex: '0', CSSStyles: { 'margin-right': '10px' } });
const statusContainer = this._view.modelBuilder.flexContainer().withLayout({ const statusContainer = this._view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).component(); .component();
this._dmsStatusInfoBox = this._view.modelBuilder.infoBox().withProps({ this._dmsStatusInfoBox = this._view.modelBuilder.infoBox()
.withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH, width: WIZARD_INPUT_COMPONENT_WIDTH,
style: 'error', style: 'error',
text: '', text: '',
CSSStyles: { CSSStyles: { ...styles.BODY_CSS }
...styles.BODY_CSS
}
}).component(); }).component();
const authenticationKeysLabel = this._view.modelBuilder.text().withProps({ const authenticationKeysLabel = this._view.modelBuilder.text()
.withProps({
value: constants.AUTHENTICATION_KEYS, value: constants.AUTHENTICATION_KEYS,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS }
...styles.LABEL_CSS
}
}).component(); }).component();
this._copy1 = this._view.modelBuilder.button().withProps({ this._copy1 = this._view.modelBuilder.button()
.withProps({
title: constants.COPY_KEY1, title: constants.COPY_KEY1,
iconPath: IconPathHelper.copy, iconPath: IconPathHelper.copy,
ariaLabel: constants.COPY_KEY1, ariaLabel: constants.COPY_KEY1,
}).component(); }).component();
this._disposables.push(this._copy1.onDidClick(async (e) => { this._disposables.push(
this._copy1.onDidClick(
async (e) => {
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![0][1].value); await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![0][1].value);
void vscode.window.showInformationMessage(constants.SERVICE_KEY1_COPIED_HELP); void vscode.window.showInformationMessage(constants.SERVICE_KEY1_COPIED_HELP);
})); }));
this._copy2 = this._view.modelBuilder.button().withProps({ this._copy2 = this._view.modelBuilder.button()
.withProps({
title: constants.COPY_KEY2, title: constants.COPY_KEY2,
iconPath: IconPathHelper.copy, iconPath: IconPathHelper.copy,
ariaLabel: constants.COPY_KEY2, ariaLabel: constants.COPY_KEY2,
}).component(); }).component();
this._disposables.push(this._copy2.onDidClick(async (e) => { this._disposables.push(
this._copy2.onDidClick(async (e) => {
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![1][1].value); await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![1][1].value);
void vscode.window.showInformationMessage(constants.SERVICE_KEY2_COPIED_HELP); void vscode.window.showInformationMessage(constants.SERVICE_KEY2_COPIED_HELP);
})); }));
this._refresh1 = this._view.modelBuilder.button().withProps({ this._refresh1 = this._view.modelBuilder.button()
.withProps({
title: constants.REFRESH_KEY1, title: constants.REFRESH_KEY1,
iconPath: IconPathHelper.refresh, iconPath: IconPathHelper.refresh,
ariaLabel: constants.REFRESH_KEY1, ariaLabel: constants.REFRESH_KEY1,
}).component(); }).component();
this._refresh2 = this._view.modelBuilder.button().withProps({ this._refresh2 = this._view.modelBuilder.button()
.withProps({
title: constants.REFRESH_KEY2, title: constants.REFRESH_KEY2,
iconPath: IconPathHelper.refresh, iconPath: IconPathHelper.refresh,
ariaLabel: constants.REFRESH_KEY2, ariaLabel: constants.REFRESH_KEY2,
@@ -350,65 +349,69 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
statusContainer.addItems([ statusContainer.addItems([
this._dmsStatusInfoBox, this._dmsStatusInfoBox,
authenticationKeysLabel, authenticationKeysLabel,
this._authKeyTable this._authKeyTable]);
]);
this._connectionStatusLoader = this._view.modelBuilder.loadingComponent().withItem( this._connectionStatusLoader = this._view.modelBuilder.loadingComponent()
statusContainer .withItem(statusContainer)
).withProps({ .withProps({ loading: false })
loading: false .component();
}).component();
container.addItems( container.addItems([
[
connectionLabelContainer, connectionLabelContainer,
this._connectionStatusLoader this._connectionStatusLoader]);
]
);
return container; return container;
} }
public async loadResourceGroupDropdown(): Promise<void> { public async loadResourceGroupDropdown(): Promise<void> {
try {
this._resourceGroupDropdown.loading = true; this._resourceGroupDropdown.loading = true;
this._dmsDropdown.loading = true; this._dmsDropdown.loading = true;
try {
this.migrationStateModel._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription); this.migrationStateModel._sqlMigrationServices = await utils.getAzureSqlMigrationServices(
this.migrationStateModel._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this.migrationStateModel._sqlMigrationServices, this.migrationStateModel._location); this.migrationStateModel._azureAccount,
this._resourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); this.migrationStateModel._targetSubscription);
const resourceGroup = (this.migrationStateModel._sqlMigrationService)
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
this.migrationStateModel._sqlMigrationServices,
this.migrationStateModel._location);
this._resourceGroupDropdown.values = utils.getResourceDropdownValues(
this.migrationStateModel._resourceGroups,
constants.RESOURCE_GROUP_NOT_FOUND);
const resourceGroup = this.migrationStateModel._sqlMigrationService
? getFullResourceGroupFromId(this.migrationStateModel._sqlMigrationService?.id) ? getFullResourceGroupFromId(this.migrationStateModel._sqlMigrationService?.id)
: undefined; : undefined;
utils.selectDefaultDropdownValue(this._resourceGroupDropdown, resourceGroup, false); utils.selectDefaultDropdownValue(this._resourceGroupDropdown, resourceGroup, false);
} finally { } finally {
this._dmsDropdown.loading = false;
this._resourceGroupDropdown.loading = false; this._resourceGroupDropdown.loading = false;
this._dmsDropdown.loading = false;
} }
} }
public async populateDms(): Promise<void> { public populateDms(): void {
try {
this._dmsDropdown.loading = true; this._dmsDropdown.loading = true;
try { this._dmsDropdown.values = utils.getAzureResourceDropdownValues(
this._dmsDropdown.values = await utils.getAzureSqlMigrationServicesDropdownValues(this.migrationStateModel._sqlMigrationServices, this.migrationStateModel._location, this.migrationStateModel._sqlMigrationServiceResourceGroup); this.migrationStateModel._sqlMigrationServices,
utils.selectDefaultDropdownValue(this._dmsDropdown, this.migrationStateModel._sqlMigrationService?.id, false); this.migrationStateModel._location,
this.migrationStateModel._sqlMigrationServiceResourceGroup.name,
constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR);
utils.selectDefaultDropdownValue(
this._dmsDropdown,
this.migrationStateModel._sqlMigrationService?.id,
false);
} finally { } finally {
this._dmsDropdown.loading = false; this._dmsDropdown.loading = false;
} }
} }
private async loadMigrationServiceStatus(): Promise<void> {
this._statusLoadingComponent.loading = true;
try {
await this.loadStatus();
} catch (error) {
logError(TelemetryViews.MigrationWizardIntegrationRuntimePage, 'ErrorLoadingMigrationServiceStatus', error);
} finally {
this._statusLoadingComponent.loading = false;
}
}
private async loadStatus(): Promise<void> { private async loadStatus(): Promise<void> {
try { try {
this._statusLoadingComponent.loading = true;
if (this.migrationStateModel._sqlMigrationService) { if (this.migrationStateModel._sqlMigrationService) {
const migrationService = await getSqlMigrationService( const migrationService = await getSqlMigrationService(
this.migrationStateModel._azureAccount, this.migrationStateModel._azureAccount,
@@ -436,12 +439,15 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
const state = migrationService.properties.integrationRuntimeState; const state = migrationService.properties.integrationRuntimeState;
if (state === 'Online') { if (state === 'Online') {
await this._dmsStatusInfoBox.updateProperties(<azdata.InfoBoxComponentProperties>{ await this._dmsStatusInfoBox.updateProperties(<azdata.InfoBoxComponentProperties>{
text: constants.SERVICE_READY(this.migrationStateModel._sqlMigrationService!.name, this.migrationStateModel._nodeNames.join(', ')), text: constants.SERVICE_READY(
this.migrationStateModel._sqlMigrationService!.name,
this.migrationStateModel._nodeNames.join(', ')),
style: 'success' style: 'success'
}); });
} else { } else {
await this._dmsStatusInfoBox.updateProperties(<azdata.InfoBoxComponentProperties>{ await this._dmsStatusInfoBox.updateProperties(<azdata.InfoBoxComponentProperties>{
text: constants.SERVICE_NOT_READY(this.migrationStateModel._sqlMigrationService!.name), text: constants.SERVICE_NOT_READY(
this.migrationStateModel._sqlMigrationService!.name),
style: 'error' style: 'error'
}); });
} }
@@ -464,19 +470,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
.withItems([this._copy2, this._refresh2]) .withItems([this._copy2, this._refresh2])
.component() .component()
} }
] ]];
];
await this._authKeyTable.setDataValues(data); await this._authKeyTable.setDataValues(data);
} }
} catch (e) { } catch (e) {
logError(TelemetryViews.IntegrationRuntimePage, 'ErrorLoadingStatus', e); logError(TelemetryViews.IntegrationRuntimePage, 'ErrorLoadingStatus', e);
} finally {
this._statusLoadingComponent.loading = false;
} }
} }
} }
export function createAuthenticationKeyTable(view: azdata.ModelView,): azdata.DeclarativeTableComponent { export function createAuthenticationKeyTable(view: azdata.ModelView,): azdata.DeclarativeTableComponent {
const authKeyTable = view.modelBuilder.declarativeTable().withProps({ const authKeyTable = view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.DATABASE_MIGRATION_SERVICE_AUTHENTICATION_KEYS, ariaLabel: constants.DATABASE_MIGRATION_SERVICE_AUTHENTICATION_KEYS,
columns: [ columns: [
{ {
@@ -484,45 +492,27 @@ export function createAuthenticationKeyTable(view: azdata.ModelView,): azdata.De
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
width: '50px', width: '50px',
isReadOnly: true, isReadOnly: true,
rowCssStyles: { rowCssStyles: { ...styles.BODY_CSS },
...styles.BODY_CSS headerCssStyles: { ...styles.BODY_CSS, 'font-weight': '600' }
},
headerCssStyles: {
...styles.BODY_CSS,
'font-weight': '600'
}
}, },
{ {
displayName: constants.AUTH_KEY_COLUMN_HEADER, displayName: constants.AUTH_KEY_COLUMN_HEADER,
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
width: '500px', width: '500px',
isReadOnly: true, isReadOnly: true,
rowCssStyles: { rowCssStyles: { ...styles.BODY_CSS },
...styles.BODY_CSS, headerCssStyles: { ...styles.BODY_CSS, 'font-weight': '600' }
},
headerCssStyles: {
...styles.BODY_CSS,
'font-weight': '600'
}
}, },
{ {
displayName: '', displayName: '',
valueType: azdata.DeclarativeDataType.component, valueType: azdata.DeclarativeDataType.component,
width: '30px', width: '30px',
isReadOnly: true, isReadOnly: true,
rowCssStyles: { rowCssStyles: { ...styles.BODY_CSS },
...styles.BODY_CSS headerCssStyles: { ...styles.BODY_CSS }
},
headerCssStyles: {
...styles.BODY_CSS
}
} }
], ],
CSSStyles: { CSSStyles: { 'margin-top': '5px', 'width': WIZARD_INPUT_COMPONENT_WIDTH }
'margin-top': '5px',
'width': WIZARD_INPUT_COMPONENT_WIDTH
}
}).component(); }).component();
return authKeyTable; return authKeyTable;
} }

View File

@@ -6,17 +6,22 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationMode, MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import { MigrationMode, MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings'; import * as constants from '../constants/strings';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
export class MigrationModePage extends MigrationWizardPage { export class MigrationModePage extends MigrationWizardPage {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
private originalMigrationMode!: MigrationMode; private _onlineButton!: azdata.RadioButtonComponent;
private _offlineButton!: azdata.RadioButtonComponent;
private _originalMigrationMode!: MigrationMode;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL, 'MigrationModePage'), migrationStateModel); super(
wizard,
azdata.window.createWizardPage(constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL, 'MigrationModePage'),
migrationStateModel);
this.migrationStateModel._databaseBackup.migrationMode = this.migrationStateModel._databaseBackup.migrationMode || MigrationMode.ONLINE; this.migrationStateModel._databaseBackup.migrationMode = this.migrationStateModel._databaseBackup.migrationMode || MigrationMode.ONLINE;
} }
@@ -25,115 +30,103 @@ export class MigrationModePage extends MigrationWizardPage {
const pageDescription = { const pageDescription = {
title: '', title: '',
component: view.modelBuilder.text().withProps({ component: view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION, value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
...styles.BODY_CSS,
'margin': '0'
}
}).component() }).component()
}; };
const form = view.modelBuilder.formContainer() const form = view.modelBuilder.formContainer()
.withFormItems( .withFormItems([
[
pageDescription, pageDescription,
this.migrationModeContainer(), this.migrationModeContainer()])
] .withProps({ CSSStyles: { 'padding-top': '0' } })
).withProps({ .component();
CSSStyles: {
'padding-top': '0' this._disposables.push(
} this._view.onClosed(
}).component(); e => this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
this._disposables.push(this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await view.initializeModel(form); await view.initializeModel(form);
} }
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> { public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.originalMigrationMode = this.migrationStateModel._databaseBackup.migrationMode; if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
this.wizard.registerNavigationValidator((e) => { return;
return true;
});
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (this.originalMigrationMode !== this.migrationStateModel._databaseBackup.migrationMode) {
this.migrationStateModel.refreshDatabaseBackupPage = true;
} }
this.wizard.registerNavigationValidator((e) => { const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
return true; this._onlineButton.enabled = !isSqlDbTarget;
}); if (isSqlDbTarget) {
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE;
this._offlineButton.checked = true;
await this._offlineButton.focus();
} }
this._originalMigrationMode = this.migrationStateModel._databaseBackup.migrationMode;
this.wizard.registerNavigationValidator((e) => true);
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (this._originalMigrationMode !== this.migrationStateModel._databaseBackup.migrationMode) {
this.migrationStateModel.refreshDatabaseBackupPage = true;
}
this.wizard.registerNavigationValidator((e) => true);
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> { protected async handleStateChange(e: StateChangeEvent): Promise<void> {
} }
private migrationModeContainer(): azdata.FormComponent { private migrationModeContainer(): azdata.FormComponent {
const buttonGroup = 'migrationMode'; const buttonGroup = 'migrationMode';
this._onlineButton = this._view.modelBuilder.radioButton()
const onlineButton = this._view.modelBuilder.radioButton().withProps({ .withProps({
label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL, label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL,
name: buttonGroup, name: buttonGroup,
checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE, checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, },
...styles.LABEL_CSS,
},
}).component(); }).component();
const onlineDescription = this._view.modelBuilder.text()
const onlineDescription = this._view.modelBuilder.text().withProps({ .withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION, value: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION,
CSSStyles: { CSSStyles: { ...styles.NOTE_CSS, 'margin-left': '20px' }
...styles.NOTE_CSS,
'margin-left': '20px'
}
}).component(); }).component();
this._disposables.push(
this._disposables.push(onlineButton.onDidChangeCheckedState((e) => { this._onlineButton.onDidChangeCheckedState(checked => {
if (e) { if (checked) {
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE; this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE;
} }
})); }));
const offlineButton = this._view.modelBuilder.radioButton().withProps({ this._offlineButton = this._view.modelBuilder.radioButton()
.withProps({
label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL, label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL,
name: buttonGroup, name: buttonGroup,
checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE, checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '12px' },
...styles.LABEL_CSS,
'margin-top': '12px'
},
}).component(); }).component();
const offlineDescription = this._view.modelBuilder.text()
const offlineDescription = this._view.modelBuilder.text().withProps({ .withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION, value: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION,
CSSStyles: { CSSStyles: { ...styles.NOTE_CSS, 'margin-left': '20px' }
...styles.NOTE_CSS,
'margin-left': '20px'
}
}).component(); }).component();
this._disposables.push(
this._disposables.push(offlineButton.onDidChangeCheckedState((e) => { this._offlineButton.onDidChangeCheckedState(checked => {
if (e) { if (checked) {
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE; this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE;
} }
})); }));
const flexContainer = this._view.modelBuilder.flexContainer().withItems( const flexContainer = this._view.modelBuilder.flexContainer()
[ .withItems([
onlineButton, this._onlineButton,
onlineDescription, onlineDescription,
offlineButton, this._offlineButton,
offlineDescription offlineDescription]
] ).withLayout({ flexFlow: 'column' })
).withLayout({ .component();
flexFlow: 'column'
}).component();
return { return { component: flexContainer };
component: flexContainer
};
} }
} }

View File

@@ -5,6 +5,7 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as utils from '../api/utils';
import * as mssql from 'mssql'; import * as mssql from 'mssql';
import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, PerformanceDataSourceOptions, StateChangeEvent } from '../models/stateMachine'; import { MigrationStateModel, MigrationTargetType, PerformanceDataSourceOptions, StateChangeEvent } from '../models/stateMachine';
@@ -83,11 +84,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
name: constants.SKU_RECOMMENDATION_VM_CARD_TEXT, name: constants.SKU_RECOMMENDATION_VM_CARD_TEXT,
icon: IconPathHelper.sqlVmLogo icon: IconPathHelper.sqlVmLogo
}, },
// { {
// type: MigrationTargetType.SQLDB, type: MigrationTargetType.SQLDB,
// name: constants.SKU_RECOMMENDATION_DB_CARD_TEXT, name: constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT,
// icon: IconPathHelper.sqlDatabaseLogo icon: IconPathHelper.sqlDatabaseLogo
// } }
]; ];
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
@@ -97,27 +98,23 @@ export class SKURecommendationPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView) { protected async registerContent(view: azdata.ModelView) {
this._view = view; this._view = view;
this._igComponent = this.createStatusComponent(view); // The first component giving basic information this._igComponent = this.createStatusComponent(view); // The first component giving basic information
this._assessmentStatusIcon = this._view.modelBuilder.image().withProps({ this._assessmentStatusIcon = this._view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.completedMigration, iconPath: IconPathHelper.completedMigration,
iconHeight: 17, iconHeight: 17,
iconWidth: 17, iconWidth: 17,
width: 20, width: 20,
height: 20 height: 20
}).component(); }).component();
const igContainer = this._view.modelBuilder.flexContainer().withProps({ const igContainer = this._view.modelBuilder.flexContainer()
CSSStyles: { .withProps({ CSSStyles: { 'align-items': 'center' } })
'align-items': 'center' .component();
} igContainer.addItem(this._assessmentStatusIcon, { flex: '0 0 auto' });
}).component(); igContainer.addItem(this._igComponent, { flex: '0 0 auto' });
igContainer.addItem(this._assessmentStatusIcon, {
flex: '0 0 auto'
});
igContainer.addItem(this._igComponent, {
flex: '0 0 auto'
});
this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved
this._skipAssessmentCheckbox = view.modelBuilder.checkBox().withProps({ this._skipAssessmentCheckbox = view.modelBuilder.checkBox()
.withProps({
label: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_BYPASS, label: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_BYPASS,
checked: false, checked: false,
CSSStyles: { CSSStyles: {
@@ -126,7 +123,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'display': 'none' 'display': 'none'
}, },
}).component(); }).component();
this._skipAssessmentSubText = view.modelBuilder.text().withProps({ this._skipAssessmentSubText = view.modelBuilder.text()
.withProps({
value: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL, value: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL,
CSSStyles: { CSSStyles: {
'margin': '0 0 0 15px', 'margin': '0 0 0 15px',
@@ -137,11 +135,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
}, },
}).component(); }).component();
this._disposables.push(this._skipAssessmentCheckbox.onChanged(async (value) => { this._disposables.push(
await this._setAssessmentState(false, true); this._skipAssessmentCheckbox.onChanged(
})); async (value) => await this._setAssessmentState(false, true)));
const refreshAssessmentButton = this._view.modelBuilder.button().withProps({ const refreshAssessmentButton = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.refresh, iconPath: IconPathHelper.refresh,
label: constants.REFRESH_ASSESSMENT_BUTTON_LABEL, label: constants.REFRESH_ASSESSMENT_BUTTON_LABEL,
width: 160, width: 160,
@@ -158,78 +157,57 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this.constructDetails(); await this.constructDetails();
})); }));
const statusContainer = this._view.modelBuilder.flexContainer().withLayout({ const statusContainer = this._view.modelBuilder.flexContainer()
flexFlow: 'column', .withLayout({ flexFlow: 'column' })
}).withItems( .withItems([
[
igContainer, igContainer,
this._detailsComponent, this._detailsComponent,
refreshAssessmentButton, refreshAssessmentButton,
this._skipAssessmentCheckbox, this._skipAssessmentCheckbox,
this._skipAssessmentSubText, this._skipAssessmentSubText])
] .withProps({ CSSStyles: { 'margin': '0' } })
).withProps({ .component();
CSSStyles: {
'margin': '0'
}
}).component();
this._chooseTargetComponent = await this.createChooseTargetComponent(view); this._chooseTargetComponent = await this.createChooseTargetComponent(view);
const _azureRecommendationsContainer = this.createAzureRecommendationContainer(view); const _azureRecommendationsContainer = this.createAzureRecommendationContainer(view);
this.assessmentGroupContainer = await this.createViewAssessmentsContainer(); this.assessmentGroupContainer = await this.createViewAssessmentsContainer();
this._formContainer = view.modelBuilder.formContainer().withFormItems( this._formContainer = view.modelBuilder.formContainer()
[ .withFormItems([
{ { component: statusContainer, title: '' },
title: '', { component: this._chooseTargetComponent },
component: statusContainer { component: _azureRecommendationsContainer },
}, { component: this.assessmentGroupContainer }])
{ .withProps({
component: this._chooseTargetComponent
},
{
component: _azureRecommendationsContainer
},
{
component: this.assessmentGroupContainer
},
]
).withProps({
CSSStyles: { CSSStyles: {
'display': 'none', 'display': 'none',
'padding-top': '0', 'padding-top': '0',
} }
}); });
this._assessmentComponent = this._view.modelBuilder.flexContainer().withLayout({ this._assessmentComponent = this._view.modelBuilder.flexContainer()
height: '100%', .withLayout({ height: '100%', flexFlow: 'column' })
flexFlow: 'column' .withProps({ CSSStyles: { 'margin-left': '30px' } })
}).withProps({ .component();
CSSStyles: {
'margin-left': '30px'
}
}).component();
this._assessmentComponent.addItem(this.createAssessmentProgress(), { flex: '0 0 auto' }); this._assessmentComponent.addItem(this.createAssessmentProgress(), { flex: '0 0 auto' });
this._assessmentComponent.addItem(await this.createAssessmentInfo(), { flex: '0 0 auto' }); this._assessmentComponent.addItem(await this.createAssessmentInfo(), { flex: '0 0 auto' });
this._rootContainer = this._view.modelBuilder.flexContainer().withLayout({ this._rootContainer = this._view.modelBuilder.flexContainer()
height: '100%', .withLayout({ height: '100%', flexFlow: 'column' })
flexFlow: 'column' .withProps({ ariaLive: 'polite' })
}).withProps({ .component();
ariaLive: 'polite',
}).component();
this._rootContainer.addItem(this._assessmentComponent, { flex: '0 0 auto' }); this._rootContainer.addItem(this._assessmentComponent, { flex: '0 0 auto' });
this._rootContainer.addItem(this._formContainer.component(), { flex: '0 0 auto' }); this._rootContainer.addItem(this._formContainer.component(), { flex: '0 0 auto' });
this._disposables.push(this._view.onClosed(e => { this._disposables.push(this._view.onClosed(
this._disposables.forEach( e => this._disposables.forEach(
d => { try { d.dispose(); } catch { } }); d => { try { d.dispose(); } catch { } })));
}));
await this._view.initializeModel(this._rootContainer); await this._view.initializeModel(this._rootContainer);
} }
private createStatusComponent(view: azdata.ModelView): azdata.TextComponent { private createStatusComponent(view: azdata.ModelView): azdata.TextComponent {
const component = view.modelBuilder.text().withProps({ const component = view.modelBuilder.text()
.withProps({
CSSStyles: { CSSStyles: {
...styles.SECTION_HEADER_CSS, ...styles.SECTION_HEADER_CSS,
'margin-left': '8px' 'margin-left': '8px'
@@ -239,16 +217,13 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
private createDetailsComponent(view: azdata.ModelView): azdata.TextComponent { private createDetailsComponent(view: azdata.ModelView): azdata.TextComponent {
const component = view.modelBuilder.text().withProps({ const component = view.modelBuilder.text()
CSSStyles: { .withProps({ CSSStyles: { ...styles.BODY_CSS } })
...styles.BODY_CSS .component();
}
}).component();
return component; return component;
} }
private async createChooseTargetComponent(view: azdata.ModelView): Promise<azdata.DivContainer> { private async createChooseTargetComponent(view: azdata.ModelView): Promise<azdata.DivContainer> {
const chooseYourTargetText = this._view.modelBuilder.text().withProps({ const chooseYourTargetText = this._view.modelBuilder.text().withProps({
value: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET, value: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
CSSStyles: { CSSStyles: {
@@ -339,11 +314,14 @@ export class SKURecommendationPage extends MigrationWizardPage {
] ]
}); });
this._disposables.push(this._rbg.onLinkClick(async (e: azdata.RadioCardLinkClickEvent) => { this._disposables.push(
this._rbg.onLinkClick(async (e: azdata.RadioCardLinkClickEvent) => {
if (this.hasRecommendations()) { if (this.hasRecommendations()) {
if (e.cardId === product.type) {
const skuRecommendationResultsDialog = new SkuRecommendationResultsDialog(this.migrationStateModel, product.type); const skuRecommendationResultsDialog = new SkuRecommendationResultsDialog(this.migrationStateModel, product.type);
if (e.cardId === skuRecommendationResultsDialog._targetType) { await skuRecommendationResultsDialog.openDialog(
await skuRecommendationResultsDialog.openDialog(e.cardId, this.migrationStateModel._skuRecommendationResults.recommendations); e.cardId,
this.migrationStateModel._skuRecommendationResults.recommendations);
} }
} }
})); }));
@@ -352,63 +330,61 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._disposables.push(this._rbg.onSelectionChanged(async (value) => { this._disposables.push(this._rbg.onSelectionChanged(async (value) => {
if (value) { if (value) {
this.assessmentGroupContainer.display = 'inline'; this.assessmentGroupContainer.display = 'inline';
await this.changeTargetType(value.cardId); this.changeTargetType(value.cardId);
} }
})); }));
this._rbgLoader = this._view.modelBuilder.loadingComponent().withItem( this._rbgLoader = this._view.modelBuilder.loadingComponent()
this._rbg .withItem(this._rbg)
).component(); .component();
const component = this._view.modelBuilder.divContainer().withItems( const component = this._view.modelBuilder.divContainer()
[ .withItems([chooseYourTargetText, this._rbgLoader])
chooseYourTargetText, .component();
this._rbgLoader
]
).component();
return component; return component;
} }
private async createViewAssessmentsContainer(): Promise<azdata.FlexContainer> { private async createViewAssessmentsContainer(): Promise<azdata.FlexContainer> {
this._viewAssessmentsHelperText = this._view.modelBuilder.text().withProps({ this._viewAssessmentsHelperText = this._view.modelBuilder.text().withProps({
value: constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI, value: constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI,
CSSStyles: { CSSStyles: { ...styles.SECTION_HEADER_CSS },
...styles.SECTION_HEADER_CSS
},
width: WIZARD_INPUT_COMPONENT_WIDTH width: WIZARD_INPUT_COMPONENT_WIDTH
}).component(); }).component();
const button = this._view.modelBuilder.button().withProps({ const button = this._view.modelBuilder.button().withProps({
label: constants.VIEW_SELECT_BUTTON_LABEL, label: constants.VIEW_SELECT_BUTTON_LABEL,
width: 100, width: 100,
CSSStyles: { CSSStyles: { 'margin': '12px 0' }
'margin': '12px 0'
}
}).component(); }).component();
let serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName; const serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI); const miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI);
let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM); const vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM);
const dbDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLDB);
this._disposables.push(button.onDidClick(async (e) => { this._disposables.push(button.onDidClick(async (e) => {
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) { switch (this._rbg.selectedCardId) {
case MigrationTargetType.SQLVM:
this._rbg.selectedCardId = MigrationTargetType.SQLVM; this._rbg.selectedCardId = MigrationTargetType.SQLVM;
await vmDialog.openDialog(); return await vmDialog.openDialog();
} else if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) { case MigrationTargetType.SQLMI:
this._rbg.selectedCardId = MigrationTargetType.SQLMI; this._rbg.selectedCardId = MigrationTargetType.SQLMI;
await miDialog.openDialog(); return await miDialog.openDialog();
case MigrationTargetType.SQLDB:
this._rbg.selectedCardId = MigrationTargetType.SQLDB;
return await dbDialog.openDialog();
} }
})); }));
this._databaseSelectedHelperText = this._view.modelBuilder.text().withProps({ this._databaseSelectedHelperText = this._view.modelBuilder.text()
CSSStyles: { .withProps({
...styles.BODY_CSS, CSSStyles: { ...styles.BODY_CSS },
}, ariaLive: 'polite',
ariaLive: 'polite'
}).component(); }).component();
const container = this._view.modelBuilder.flexContainer().withItems([ const container = this._view.modelBuilder.flexContainer()
.withItems([
this._viewAssessmentsHelperText, this._viewAssessmentsHelperText,
button, button,
this._databaseSelectedHelperText this._databaseSelectedHelperText
@@ -418,9 +394,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
return container; return container;
} }
private async changeTargetType(newTargetType: string) { private changeTargetType(newTargetType: string): void {
switch (newTargetType) { switch (newTargetType) {
case MigrationTargetType.SQLMI: { case MigrationTargetType.SQLMI:
const miDbs = this.migrationStateModel._miDbs.filter( const miDbs = this.migrationStateModel._miDbs.filter(
db => this.migrationStateModel._databasesForAssessment.findIndex( db => this.migrationStateModel._databasesForAssessment.findIndex(
dba => dba === db) >= 0); dba => dba === db) >= 0);
@@ -429,9 +405,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._targetType = MigrationTargetType.SQLMI; this.migrationStateModel._targetType = MigrationTargetType.SQLMI;
this.migrationStateModel._databasesForMigration = miDbs; this.migrationStateModel._databasesForMigration = miDbs;
break; break;
} case MigrationTargetType.SQLVM:
case MigrationTargetType.SQLVM: {
const vmDbs = this.migrationStateModel._vmDbs.filter( const vmDbs = this.migrationStateModel._vmDbs.filter(
db => this.migrationStateModel._databasesForAssessment.findIndex( db => this.migrationStateModel._databasesForAssessment.findIndex(
dba => dba === db) >= 0); dba => dba === db) >= 0);
@@ -440,10 +414,20 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._targetType = MigrationTargetType.SQLVM; this.migrationStateModel._targetType = MigrationTargetType.SQLVM;
this.migrationStateModel._databasesForMigration = vmDbs; this.migrationStateModel._databasesForMigration = vmDbs;
break; break;
} case MigrationTargetType.SQLDB:
const dbDbs = this.migrationStateModel._sqldbDbs.filter(
db => this.migrationStateModel._databasesForAssessment.findIndex(
dba => dba === db) >= 0);
this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_SQLDB;
this.migrationStateModel._targetType = MigrationTargetType.SQLDB;
this.migrationStateModel._databasesForMigration = dbDbs;
break;
} }
this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._databasesForMigration.length, this.migrationStateModel._databasesForAssessment.length); this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(
this.migrationStateModel._databasesForMigration.length,
this.migrationStateModel._databasesForAssessment.length);
this.migrationStateModel.refreshDatabaseBackupPage = true; this.migrationStateModel.refreshDatabaseBackupPage = true;
} }
@@ -459,7 +443,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
const errors: string[] = []; const errors: string[] = [];
await this._setAssessmentState(true, false); await this._setAssessmentState(true, false);
try { try {
await this.migrationStateModel.getDatabaseAssessments(MigrationTargetType.SQLMI); await this.migrationStateModel.getDatabaseAssessments([MigrationTargetType.SQLMI, MigrationTargetType.SQLDB]);
const assessmentError = this.migrationStateModel._assessmentResults?.assessmentError; const assessmentError = this.migrationStateModel._assessmentResults?.assessmentError;
if (assessmentError) { if (assessmentError) {
errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`); errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`);
@@ -485,14 +469,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
} else { } else {
this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration; this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration;
this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults?.databaseAssessments?.length); this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(
this.migrationStateModel._assessmentResults?.databaseAssessments?.length);
} }
} }
} else { } else {
// use prior assessment results // use prior assessment results
this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration; this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration;
this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults?.databaseAssessments?.length); this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(
this.migrationStateModel._assessmentResults?.databaseAssessments?.length);
} }
if (this.migrationStateModel.savedInfo?.migrationTargetType) { if (this.migrationStateModel.savedInfo?.migrationTargetType) {
@@ -543,46 +529,36 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
private async _setAssessmentState(assessing: boolean, failedAssessment: boolean): Promise<void> { private async _setAssessmentState(assessing: boolean, failedAssessment: boolean): Promise<void> {
let display: azdata.DisplayType = assessing ? 'block' : 'none'; await utils.updateControlDisplay(this._assessmentComponent, assessing);
await this._assessmentComponent.updateCssStyles({ 'display': display }); await utils.updateControlDisplay(this._skipAssessmentCheckbox, !assessing && failedAssessment);
this._assessmentComponent.display = display; await utils.updateControlDisplay(
this._skipAssessmentSubText,
!assessing && failedAssessment,
'block');
await utils.updateControlDisplay(this._formContainer.component(), !assessing);
await utils.updateControlDisplay(
this._chooseTargetComponent,
!failedAssessment || this._skipAssessmentCheckbox.checked === true);
display = !assessing && failedAssessment ? 'block' : 'none'; await utils.updateControlDisplay(
await this._skipAssessmentCheckbox.updateCssStyles({ 'display': display }); this.assessmentGroupContainer,
this._skipAssessmentCheckbox.display = display; this._rbg.selectedCardId !== undefined && (!failedAssessment || this._skipAssessmentCheckbox.checked === true));
await this._skipAssessmentSubText.updateCssStyles({ 'display': display });
this._skipAssessmentSubText.display = display;
await this._formContainer.component().updateCssStyles({ 'display': !assessing ? 'block' : 'none' });
display = failedAssessment && !this._skipAssessmentCheckbox.checked ? 'none' : 'block';
await this._chooseTargetComponent.updateCssStyles({ 'display': display });
this._chooseTargetComponent.display = display;
display = !this._rbg.selectedCardId || failedAssessment && !this._skipAssessmentCheckbox.checked ? 'none' : 'inline';
await this.assessmentGroupContainer.updateCssStyles({ 'display': display });
this.assessmentGroupContainer.display = display;
display = (this._rbg.selectedCardId
&& (!failedAssessment || this._skipAssessmentCheckbox.checked)
&& this.migrationStateModel._databasesForMigration?.length > 0)
? 'inline'
: 'none';
this._assessmentLoader.loading = assessing; this._assessmentLoader.loading = assessing;
} }
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> { public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
this.wizard.registerNavigationValidator((pageChangeInfo) => { this.wizard.registerNavigationValidator((pageChangeInfo) => {
const errors: string[] = []; this.wizard.message = { text: '' };
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true; return true;
} }
const errors: string[] = [];
if (this._rbg.selectedCardId === undefined || this._rbg.selectedCardId === '') { if (this._rbg.selectedCardId === undefined || this._rbg.selectedCardId === '') {
errors.push(constants.SELECT_TARGET_TO_CONTINUE); errors.push(constants.SELECT_TARGET_TO_CONTINUE);
} }
@@ -620,9 +596,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
public async refreshCardText(showLoadingIcon: boolean = true): Promise<void> { public async refreshCardText(showLoadingIcon: boolean = true): Promise<void> {
this._rbgLoader.loading = showLoadingIcon && true; this._rbgLoader.loading = showLoadingIcon && true;
if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) { switch (this._rbg.selectedCardId) {
case MigrationTargetType.SQLMI:
this.migrationStateModel._databasesForMigration = this.migrationStateModel._miDbs; this.migrationStateModel._databasesForMigration = this.migrationStateModel._miDbs;
} else { case MigrationTargetType.SQLDB:
this.migrationStateModel._databasesForMigration = this.migrationStateModel._sqldbDbs;
case MigrationTargetType.SQLVM:
this.migrationStateModel._databasesForMigration = this.migrationStateModel._vmDbs; this.migrationStateModel._databasesForMigration = this.migrationStateModel._vmDbs;
} }
@@ -650,21 +629,24 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.AZURE_RECOMMENDATION_CARD_IN_PROGRESS; constants.AZURE_RECOMMENDATION_CARD_IN_PROGRESS;
} else { } else {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.AZURE_RECOMMENDATION_CARD_NOT_ENABLED; this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.AZURE_RECOMMENDATION_CARD_NOT_ENABLED;
} }
} }
let recommendation; let recommendation;
switch (product.type) { switch (product.type) {
case MigrationTargetType.SQLMI: case MigrationTargetType.SQLMI:
this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue =
constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount);
if (this.hasRecommendations()) { if (this.hasRecommendations()) {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlMiRecommendationResults[0]; recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlMiRecommendationResults[0];
// result returned but no SKU recommended // result returned but no SKU recommended
if (!recommendation.targetSku) { if (!recommendation?.targetSku) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.SKU_RECOMMENDATION_NO_RECOMMENDATION; this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.SKU_RECOMMENDATION_NO_RECOMMENDATION;
} }
else { else {
const serviceTier = recommendation.targetSku.category?.sqlServiceTier === mssql.AzureSqlPaaSServiceTier.GeneralPurpose const serviceTier = recommendation.targetSku.category?.sqlServiceTier === mssql.AzureSqlPaaSServiceTier.GeneralPurpose
@@ -675,41 +657,62 @@ export class SKURecommendationPage extends MigrationWizardPage {
: recommendation.targetSku.category?.hardwareType === mssql.AzureSqlPaaSHardwareType.PremiumSeries : recommendation.targetSku.category?.hardwareType === mssql.AzureSqlPaaSHardwareType.PremiumSeries
? constants.PREMIUM_SERIES ? constants.PREMIUM_SERIES
: constants.PREMIUM_SERIES_MEMORY_OPTIMIZED; : constants.PREMIUM_SERIES_MEMORY_OPTIMIZED;
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.MI_CONFIGURATION_PREVIEW(hardwareType, serviceTier, recommendation.targetSku.computeSize!, recommendation.targetSku.storageMaxSizeInMb! / 1024); this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.MI_CONFIGURATION_PREVIEW(
hardwareType,
serviceTier,
recommendation.targetSku.computeSize!,
recommendation.targetSku.storageMaxSizeInMb! / 1024);
} }
} }
break; break;
case MigrationTargetType.SQLVM: case MigrationTargetType.SQLVM:
this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = constants.CAN_BE_MIGRATED(dbCount, dbCount); this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue =
constants.CAN_BE_MIGRATED(dbCount, dbCount);
if (this.hasRecommendations()) { if (this.hasRecommendations()) {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlVmRecommendationResults[0]; recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlVmRecommendationResults[0];
// result returned but no SKU recommended // result returned but no SKU recommended
if (!recommendation.targetSku) { if (!recommendation?.targetSku) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.SKU_RECOMMENDATION_NO_RECOMMENDATION; this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.SKU_RECOMMENDATION_NO_RECOMMENDATION;
this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = ''; this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = '';
} }
else { else {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.VM_CONFIGURATION(recommendation.targetSku.virtualMachineSize!.sizeName, recommendation.targetSku.virtualMachineSize!.vCPUsAvailable); this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.VM_CONFIGURATION(
recommendation.targetSku.virtualMachineSize!.sizeName,
recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
const dataDisk = constants.STORAGE_CONFIGURATION(recommendation.targetSku.dataDiskSizes![0].size, recommendation.targetSku.dataDiskSizes!.length); const dataDisk = constants.STORAGE_CONFIGURATION(
const storageDisk = constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length); recommendation.targetSku.dataDiskSizes![0].size,
recommendation.targetSku.dataDiskSizes!.length);
const storageDisk = constants.STORAGE_CONFIGURATION(
recommendation.targetSku.logDiskSizes![0].size,
recommendation.targetSku.logDiskSizes!.length);
const tempDb = recommendation.targetSku.tempDbDiskSizes!.length > 0 const tempDb = recommendation.targetSku.tempDbDiskSizes!.length > 0
? constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length) ? constants.STORAGE_CONFIGURATION(
recommendation.targetSku.logDiskSizes![0].size,
recommendation.targetSku.logDiskSizes!.length)
: constants.LOCAL_SSD; : constants.LOCAL_SSD;
this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = constants.VM_CONFIGURATION_PREVIEW(dataDisk, storageDisk, tempDb); this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue =
constants.VM_CONFIGURATION_PREVIEW(dataDisk, storageDisk, tempDb);
} }
} }
break; break;
case MigrationTargetType.SQLDB: case MigrationTargetType.SQLDB:
this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue =
constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount);
if (this.hasRecommendations()) { if (this.hasRecommendations()) {
const successfulRecommendationsCount = this.migrationStateModel._skuRecommendationResults.recommendations.sqlDbRecommendationResults.filter(r => r.targetSku !== null).length; const successfulRecommendationsCount =
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.RECOMMENDATIONS_AVAILABLE(successfulRecommendationsCount); this.migrationStateModel._skuRecommendationResults.recommendations.sqlDbRecommendationResults
.filter(r => r.targetSku !== null).length;
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.RECOMMENDATIONS_AVAILABLE(successfulRecommendationsCount);
} }
break; break;
} }
@@ -719,10 +722,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this._rbg.updateProperties({ cards: this._rbg.cards }); await this._rbg.updateProperties({ cards: this._rbg.cards });
if (this._rbg.selectedCardId) { if (this._rbg.selectedCardId) {
await this.changeTargetType(this._rbg.selectedCardId); this.changeTargetType(this._rbg.selectedCardId);
} }
this._rbgLoader.loading = showLoadingIcon && false; this._rbgLoader.loading = false;
} }
public async startCardLoading(): Promise<void> { public async startCardLoading(): Promise<void> {
@@ -730,9 +733,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
// but updating the card text will do for now // but updating the card text will do for now
this._supportedProducts.forEach((product, index) => { this._supportedProducts.forEach((product, index) => {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.LOADING_RECOMMENDATIONS; this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.LOADING_RECOMMENDATIONS;
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textStyles = { this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textStyles = { ...styles.BODY_CSS };
...styles.BODY_CSS,
};
this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = ''; this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = '';
}); });
@@ -740,10 +741,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
private createAssessmentProgress(): azdata.FlexContainer { private createAssessmentProgress(): azdata.FlexContainer {
this._assessmentLoader = this._view.modelBuilder.loadingComponent()
.component();
this._assessmentLoader = this._view.modelBuilder.loadingComponent().component(); this._assessmentProgress = this._view.modelBuilder.text()
.withProps({
this._assessmentProgress = this._view.modelBuilder.text().withProps({
value: constants.ASSESSMENT_IN_PROGRESS, value: constants.ASSESSMENT_IN_PROGRESS,
CSSStyles: { CSSStyles: {
...styles.PAGE_TITLE_CSS, ...styles.PAGE_TITLE_CSS,
@@ -751,7 +753,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
}).component(); }).component();
this._progressContainer = this._view.modelBuilder.flexContainer().withLayout({ this._progressContainer = this._view.modelBuilder.flexContainer()
.withLayout({
height: '100%', height: '100%',
flexFlow: 'row', flexFlow: 'row',
alignItems: 'center' alignItems: 'center'
@@ -763,7 +766,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
private async createAssessmentInfo(): Promise<azdata.TextComponent> { private async createAssessmentInfo(): Promise<azdata.TextComponent> {
this._assessmentInfo = this._view.modelBuilder.text().withProps({ this._assessmentInfo = this._view.modelBuilder.text()
.withProps({
value: constants.ASSESSMENT_IN_PROGRESS_CONTENT((await this.migrationStateModel.getSourceConnectionProfile()).serverName), value: constants.ASSESSMENT_IN_PROGRESS_CONTENT((await this.migrationStateModel.getSourceConnectionProfile()).serverName),
CSSStyles: { CSSStyles: {
...styles.BODY_CSS, ...styles.BODY_CSS,
@@ -774,14 +778,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
private createAzureRecommendationContainer(_view: azdata.ModelView): azdata.FlexContainer { private createAzureRecommendationContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({ const container = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: { CSSStyles: {
'flex-direction': 'column', 'flex-direction': 'column',
'max-width': '700px', 'max-width': '700px',
'margin-bottom': '1em', 'margin-bottom': '1em',
} }
}).component(); }).component();
this._azureRecommendationSectionText = _view.modelBuilder.text().withProps({ this._azureRecommendationSectionText = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION, value: constants.AZURE_RECOMMENDATION,
description: '', description: '',
CSSStyles: { CSSStyles: {
@@ -789,14 +795,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin': '12px 0 8px', 'margin': '12px 0 8px',
} }
}).component(); }).component();
this._azureRecommendationInfoText = _view.modelBuilder.text().withProps({ this._azureRecommendationInfoText = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_STATUS_NOT_ENABLED, value: constants.AZURE_RECOMMENDATION_STATUS_NOT_ENABLED,
CSSStyles: { CSSStyles: {
...styles.BODY_CSS, ...styles.BODY_CSS,
'margin': '0', 'margin': '0',
} }
}).component(); }).component();
const learnMoreLink = _view.modelBuilder.hyperlink().withProps({ const learnMoreLink = _view.modelBuilder.hyperlink()
.withProps({
label: constants.LEARN_MORE, label: constants.LEARN_MORE,
ariaLabel: constants.LEARN_MORE, ariaLabel: constants.LEARN_MORE,
url: 'https://aka.ms/ads-sql-sku-recommend', url: 'https://aka.ms/ads-sql-sku-recommend',
@@ -822,19 +830,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
}).component(); }).component();
const getAzureRecommendationDialog = new GetAzureRecommendationDialog(this, this.wizard, this.migrationStateModel); const getAzureRecommendationDialog = new GetAzureRecommendationDialog(this, this.wizard, this.migrationStateModel);
this._disposables.push(this._getAzureRecommendationButton.onDidClick(async (e) => { this._disposables.push(this._getAzureRecommendationButton.onDidClick(
await getAzureRecommendationDialog.openDialog(); async (e) => await getAzureRecommendationDialog.openDialog()));
}));
this._skuGetRecommendationContainer = _view.modelBuilder.flexContainer()
.withProps({ CSSStyles: { 'flex-direction': 'column', } })
.component();
this._skuGetRecommendationContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'column',
}
}).component();
this._skuGetRecommendationContainer.addItems([ this._skuGetRecommendationContainer.addItems([
azureRecommendationsInfoContainer, azureRecommendationsInfoContainer,
this._getAzureRecommendationButton, this._getAzureRecommendationButton]);
]);
this._skuDataCollectionStatusContainer = this.createPerformanceCollectionStatusContainer(_view); this._skuDataCollectionStatusContainer = this.createPerformanceCollectionStatusContainer(_view);
this._skuEditParametersContainer = this.createSkuEditParameters(_view); this._skuEditParametersContainer = this.createSkuEditParameters(_view);
@@ -848,29 +853,27 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
private createPerformanceCollectionStatusContainer(_view: azdata.ModelView): azdata.FlexContainer { private createPerformanceCollectionStatusContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({ const container = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: { CSSStyles: {
'flex-direction': 'column', 'flex-direction': 'column',
'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block', 'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block',
} }
}).component(); }).component();
this._skuDataCollectionStatusIcon = _view.modelBuilder.image().withProps({ this._skuDataCollectionStatusIcon = _view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.inProgressMigration, iconPath: IconPathHelper.inProgressMigration,
iconHeight: 16, iconHeight: 16,
iconWidth: 16, iconWidth: 16,
width: 16, width: 16,
height: 16, height: 16,
CSSStyles: { CSSStyles: { 'margin-right': '4px' }
'margin-right': '4px',
}
}).component(); }).component();
this._skuDataCollectionStatusText = _view.modelBuilder.text().withProps({ this._skuDataCollectionStatusText = _view.modelBuilder.text()
.withProps({
value: '', value: '',
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
...styles.BODY_CSS,
'margin': '0'
}
}).component(); }).component();
const statusIconTextContainer = _view.modelBuilder.flexContainer() const statusIconTextContainer = _view.modelBuilder.flexContainer()
@@ -887,7 +890,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
}).component(); }).component();
this._skuDataCollectionTimerText = _view.modelBuilder.text().withProps({ this._skuDataCollectionTimerText = _view.modelBuilder.text()
.withProps({
value: '', value: '',
CSSStyles: { CSSStyles: {
...styles.LIGHT_LABEL_CSS, ...styles.LIGHT_LABEL_CSS,
@@ -895,7 +899,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
}).component(); }).component();
this._skuStopDataCollectionButton = this._view.modelBuilder.button().withProps({ this._skuStopDataCollectionButton = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.cancel, iconPath: IconPathHelper.cancel,
label: constants.STOP_PERFORMANCE_COLLECTION, label: constants.STOP_PERFORMANCE_COLLECTION,
width: 150, width: 150,
@@ -922,14 +927,14 @@ export class SKURecommendationPage extends MigrationWizardPage {
'display': this.migrationStateModel.performanceCollectionStopped() ? 'block' : 'none', 'display': this.migrationStateModel.performanceCollectionStopped() ? 'block' : 'none',
} }
}).component(); }).component();
this._disposables.push(this._skuRestartDataCollectionButton.onDidClick(async (e) => { this._disposables.push(
this._skuRestartDataCollectionButton.onDidClick(async (e) => {
await this.migrationStateModel.startPerfDataCollection( await this.migrationStateModel.startPerfDataCollection(
this.migrationStateModel._skuRecommendationPerformanceLocation, this.migrationStateModel._skuRecommendationPerformanceLocation,
this.migrationStateModel._performanceDataQueryIntervalInSeconds, this.migrationStateModel._performanceDataQueryIntervalInSeconds,
this.migrationStateModel._staticDataQueryIntervalInSeconds, this.migrationStateModel._staticDataQueryIntervalInSeconds,
this.migrationStateModel._numberOfPerformanceDataQueryIterations, this.migrationStateModel._numberOfPerformanceDataQueryIterations,
this this);
);
await this.refreshSkuRecommendationComponents(); await this.refreshSkuRecommendationComponents();
})); }));
@@ -943,10 +948,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin': '0 0 0 12px', 'margin': '0 0 0 12px',
} }
}).component(); }).component();
this._disposables.push(this._refreshAzureRecommendationButton.onDidClick(async (e) => { this._disposables.push(
await this.refreshAzureRecommendation(); this._refreshAzureRecommendationButton.onDidClick(
})); async (e) => await this.refreshAzureRecommendation()));
this._skuLastRefreshTimeText = this._view.modelBuilder.text().withProps({
this._skuLastRefreshTimeText = this._view.modelBuilder.text()
.withProps({
value: constants.LAST_REFRESHED_TIME(), value: constants.LAST_REFRESHED_TIME(),
CSSStyles: { CSSStyles: {
...styles.SMALL_NOTE_CSS, ...styles.SMALL_NOTE_CSS,
@@ -966,14 +973,13 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._skuStopDataCollectionButton, this._skuStopDataCollectionButton,
this._skuRestartDataCollectionButton, this._skuRestartDataCollectionButton,
this._refreshAzureRecommendationButton, this._refreshAzureRecommendationButton,
this._skuLastRefreshTimeText, this._skuLastRefreshTimeText]);
]);
container.addItems([ container.addItems([
this._skuControlButtonsContainer, this._skuControlButtonsContainer,
statusIconTextContainer, statusIconTextContainer,
this._skuDataCollectionTimerText, this._skuDataCollectionTimerText]);
]);
return container; return container;
} }
@@ -984,7 +990,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block', 'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block',
} }
}).component(); }).component();
const recommendationParametersSection = _view.modelBuilder.text().withProps({ const recommendationParametersSection = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDATION_PARAMETERS, value: constants.RECOMMENDATION_PARAMETERS,
CSSStyles: { CSSStyles: {
...styles.BODY_CSS, ...styles.BODY_CSS,
@@ -992,7 +999,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
}).component(); }).component();
const editParametersButton = this._view.modelBuilder.button().withProps({ const editParametersButton = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.edit, iconPath: IconPathHelper.edit,
label: constants.EDIT_PARAMETERS, label: constants.EDIT_PARAMETERS,
width: 130, width: 130,
@@ -1004,15 +1012,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
}).component(); }).component();
let skuEditParametersDialog = new SkuEditParametersDialog(this, this.migrationStateModel); let skuEditParametersDialog = new SkuEditParametersDialog(this, this.migrationStateModel);
this._disposables.push(editParametersButton.onDidClick(async () => { this._disposables.push(
await skuEditParametersDialog.openDialog(); editParametersButton.onDidClick(
})); async () => await skuEditParametersDialog.openDialog()));
const createParameterGroup = (label: string, value: string): { const createParameterGroup = (label: string, value: string): {
flexContainer: azdata.FlexContainer, flexContainer: azdata.FlexContainer,
text: azdata.TextComponent, text: azdata.TextComponent,
} => { } => {
const parameterGroup = this._view.modelBuilder.flexContainer().withProps({ const parameterGroup = this._view.modelBuilder.flexContainer()
.withProps({
CSSStyles: { CSSStyles: {
'flex-direction': 'row', 'flex-direction': 'row',
'align-content': 'left', 'align-content': 'left',
@@ -1020,7 +1029,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin-right': '24px', 'margin-right': '24px',
} }
}).component(); }).component();
const labelText = this._view.modelBuilder.text().withProps({ const labelText = this._view.modelBuilder.text()
.withProps({
value: label + ':', value: label + ':',
CSSStyles: { CSSStyles: {
...styles.LIGHT_LABEL_CSS, ...styles.LIGHT_LABEL_CSS,
@@ -1028,7 +1038,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin-right': '4px', 'margin-right': '4px',
} }
}).component(); }).component();
const valueText = this._view.modelBuilder.text().withProps({ const valueText = this._view.modelBuilder.text()
.withProps({
value: value, value: value,
CSSStyles: { CSSStyles: {
...styles.BODY_CSS, ...styles.BODY_CSS,
@@ -1037,24 +1048,30 @@ export class SKURecommendationPage extends MigrationWizardPage {
}).component(); }).component();
parameterGroup.addItems([ parameterGroup.addItems([
labelText, labelText,
valueText, valueText]);
]);
return { return {
flexContainer: parameterGroup, flexContainer: parameterGroup,
text: valueText, text: valueText,
}; };
}; };
const scaleFactorParameterGroup = createParameterGroup(constants.SCALE_FACTOR, this.migrationStateModel._skuScalingFactor.toString()); const scaleFactorParameterGroup = createParameterGroup(
constants.SCALE_FACTOR,
this.migrationStateModel._skuScalingFactor.toString());
this._skuScaleFactorText = scaleFactorParameterGroup.text; this._skuScaleFactorText = scaleFactorParameterGroup.text;
const skuTargetPercentileParameterGroup = createParameterGroup(constants.PERCENTAGE_UTILIZATION, constants.PERCENTAGE(this.migrationStateModel._skuTargetPercentile)); const skuTargetPercentileParameterGroup = createParameterGroup(
constants.PERCENTAGE_UTILIZATION,
constants.PERCENTAGE(this.migrationStateModel._skuTargetPercentile));
this._skuTargetPercentileText = skuTargetPercentileParameterGroup.text; this._skuTargetPercentileText = skuTargetPercentileParameterGroup.text;
const skuEnablePreviewParameterGroup = createParameterGroup(constants.ENABLE_PREVIEW_SKU, this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO); const skuEnablePreviewParameterGroup = createParameterGroup(
constants.ENABLE_PREVIEW_SKU,
this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO);
this._skuEnablePreviewSkuText = skuEnablePreviewParameterGroup.text; this._skuEnablePreviewSkuText = skuEnablePreviewParameterGroup.text;
const parametersContainer = _view.modelBuilder.flexContainer().withProps({ const parametersContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: { CSSStyles: {
'margin': '8px 0', 'margin': '8px 0',
'flex-direction': 'row', 'flex-direction': 'row',
@@ -1172,7 +1189,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
} }
private hasRecommendations(): boolean { private hasRecommendations(): boolean {
return this.migrationStateModel._skuRecommendationResults?.recommendations && !this.migrationStateModel._skuRecommendationResults?.recommendationError ? true : false; return this.migrationStateModel._skuRecommendationResults?.recommendations
&& !this.migrationStateModel._skuRecommendationResults?.recommendationError
? true
: false;
} }
} }

View File

@@ -9,6 +9,7 @@ import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationSourceAuthenticationType, MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import { MigrationSourceAuthenticationType, MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings'; import * as constants from '../constants/strings';
import { createLabelTextComponent, createHeadingTextComponent, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import { createLabelTextComponent, createHeadingTextComponent, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import { AuthenticationType } from '../api/sqlUtils';
export class SqlSourceConfigurationPage extends MigrationWizardPage { export class SqlSourceConfigurationPage extends MigrationWizardPage {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
@@ -59,10 +60,13 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
const query = 'select SUSER_NAME()'; const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query); const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query);
const username = results.rows[0][0].displayValue; const username = results.rows[0][0].displayValue;
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!; this.migrationStateModel._authenticationType = connectionProfile.authenticationType === AuthenticationType.SqlLogin
? MigrationSourceAuthenticationType.Sql
: connectionProfile.authenticationType === AuthenticationType.Integrated
? MigrationSourceAuthenticationType.Integrated
: undefined!;
const sourceCredText = await createHeadingTextComponent(this._view, constants.SOURCE_CREDENTIALS); const sourceCredText = await createHeadingTextComponent(this._view, constants.SOURCE_CREDENTIALS);
const enterYourCredText = createLabelTextComponent( const enterYourCredText = createLabelTextComponent(
this._view, this._view,
constants.ENTER_YOUR_SQL_CREDS, constants.ENTER_YOUR_SQL_CREDS,

View File

@@ -24,134 +24,204 @@ export class SummaryPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView): Promise<void> { protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view; this._view = view;
this._flexContainer = view.modelBuilder.flexContainer().withLayout({ this._flexContainer = view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).component(); .component();
const form = view.modelBuilder.formContainer() const form = view.modelBuilder.formContainer()
.withFormItems( .withFormItems([{ component: this._flexContainer }])
[ .component();
{
component: this._flexContainer
}
]
);
this._disposables.push(this._view.onClosed(e => { this._disposables.push(
this._view.onClosed(e =>
this._disposables.forEach( this._disposables.forEach(
d => { try { d.dispose(); } catch { } }); d => { try { d.dispose(); } catch { } })));
}));
await view.initializeModel(form.component()); await view.initializeModel(form);
} }
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> { public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
const targetDatabaseSummary = new TargetDatabaseSummaryDialog(this.migrationStateModel); const targetDatabaseSummary = new TargetDatabaseSummaryDialog(this.migrationStateModel);
const targetDatabaseHyperlink = this._view.modelBuilder.hyperlink().withProps({ const isSqlVmTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLVM;
const isSqlMiTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLMI;
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
const isNetworkShare = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE;
const targetDatabaseHyperlink = this._view.modelBuilder.hyperlink()
.withProps({
url: '', url: '',
label: this.migrationStateModel._databasesForMigration?.length.toString(), label: (this.migrationStateModel._databasesForMigration?.length ?? 0).toString(),
CSSStyles: { CSSStyles: { ...styles.BODY_CSS, 'margin': '0px', 'width': '300px', }
...styles.BODY_CSS,
'margin': '0px',
'width': '300px',
}
}).component(); }).component();
this._disposables.push(targetDatabaseHyperlink.onDidClick(async e => { this._disposables.push(
await targetDatabaseSummary.initialize(); targetDatabaseHyperlink.onDidClick(
})); async e => await targetDatabaseSummary.initialize()));
const targetDatabaseRow = this._view.modelBuilder.flexContainer() const targetDatabaseRow = this._view.modelBuilder.flexContainer()
.withLayout( .withLayout({ flexFlow: 'row', alignItems: 'center', })
{ .withItems([
flexFlow: 'row', createLabelTextComponent(
alignItems: 'center', this._view,
}) constants.SUMMARY_DATABASE_COUNT_LABEL,
.withItems( { ...styles.BODY_CSS, 'width': '300px' }),
[ targetDatabaseHyperlink],
createLabelTextComponent(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, { CSSStyles: { 'margin-right': '5px' } })
{
...styles.BODY_CSS,
'width': '300px',
}
),
targetDatabaseHyperlink
],
{
CSSStyles: {
'margin-right': '5px'
}
})
.component(); .component();
this._flexContainer.addItems( this._flexContainer
[ .addItems([
await createHeadingTextComponent(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, true), await createHeadingTextComponent(
createInformationRow(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, this.migrationStateModel._azureAccount.displayInfo.displayName), this._view,
constants.SOURCE_DATABASES),
await createHeadingTextComponent(this._view, constants.SOURCE_DATABASES),
targetDatabaseRow, targetDatabaseRow,
await createHeadingTextComponent(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE), await createHeadingTextComponent(
createInformationRow(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE), this._view,
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), constants.AZURE_SQL_TARGET_PAGE_TITLE),
createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)), createInformationRow(
createInformationRow(this._view, constants.RESOURCE_GROUP, getResourceGroupFromId(this.migrationStateModel._targetServerInstance.id)), this._view,
createInformationRow(this._view, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)), constants.ACCOUNTS_SELECTION_PAGE_TITLE,
this.migrationStateModel._azureAccount.displayInfo.displayName),
createInformationRow(
this._view,
constants.AZURE_SQL_TARGET_PAGE_TITLE,
isSqlVmTarget
? constants.SUMMARY_VM_TYPE
: isSqlMiTarget
? constants.SUMMARY_MI_TYPE
: constants.SUMMARY_SQLDB_TYPE),
createInformationRow(
this._view,
constants.SUBSCRIPTION,
this.migrationStateModel._targetSubscription.name),
createInformationRow(
this._view,
constants.LOCATION,
await this.migrationStateModel.getLocationDisplayName(
this.migrationStateModel._targetServerInstance.location)),
createInformationRow(
this._view,
constants.RESOURCE_GROUP,
getResourceGroupFromId(
this.migrationStateModel._targetServerInstance.id)),
createInformationRow(
this._view,
(isSqlVmTarget)
? constants.SUMMARY_VM_TYPE
: (isSqlMiTarget)
? constants.SUMMARY_MI_TYPE
: constants.SUMMARY_SQLDB_TYPE,
await this.migrationStateModel.getLocationDisplayName(
this.migrationStateModel._targetServerInstance.name!)),
await createHeadingTextComponent(
this._view,
constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL),
createInformationRow(
this._view,
constants.MODE,
this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE
? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL
: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL),
]);
await createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL), if (this.migrationStateModel._targetType !== MigrationTargetType.SQLDB) {
createInformationRow(this._view, constants.MODE, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE ? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL : constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL), this._flexContainer.addItems([
await createHeadingTextComponent(
this._view,
constants.DATABASE_BACKUP_PAGE_TITLE),
await this.createNetworkContainerRows()]);
}
await createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE), this._flexContainer.addItems([
await this.createNetworkContainerRows(),
await createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._sqlMigrationService?.location!)),
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._sqlMigrationService?.properties?.resourceGroup!),
createInformationRow(this._view, constants.IR_PAGE_TITLE, this.migrationStateModel._sqlMigrationService?.name!)
]
);
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._nodeNames?.length > 0) { await createHeadingTextComponent(
this._flexContainer.addItem(createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames.join(', '))); this._view,
constants.IR_PAGE_TITLE),
createInformationRow(
this._view, constants.SUBSCRIPTION,
this.migrationStateModel._targetSubscription.name),
createInformationRow(
this._view,
constants.LOCATION,
await this.migrationStateModel.getLocationDisplayName(
this.migrationStateModel._sqlMigrationService?.location!)),
createInformationRow(
this._view,
constants.RESOURCE_GROUP,
this.migrationStateModel._sqlMigrationService?.properties?.resourceGroup!),
createInformationRow(
this._view,
constants.IR_PAGE_TITLE,
this.migrationStateModel._sqlMigrationService?.name!)]);
if (isSqlDbTarget ||
(isNetworkShare && this.migrationStateModel._nodeNames?.length > 0)) {
this._flexContainer.addItem(
createInformationRow(
this._view,
constants.SHIR,
this.migrationStateModel._nodeNames.join(', ')));
} }
} }
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> { public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this._flexContainer.clearItems(); this._flexContainer.clearItems();
this.wizard.registerNavigationValidator(async (pageChangeInfo) => { this.wizard.registerNavigationValidator(async (pageChangeInfo) => true);
return true;
});
} }
protected async handleStateChange(e: StateChangeEvent): Promise<void> { protected async handleStateChange(e: StateChangeEvent): Promise<void> {
} }
private async createNetworkContainerRows(): Promise<azdata.FlexContainer> { private async createNetworkContainerRows(): Promise<azdata.FlexContainer> {
const flexContainer = this._view.modelBuilder.flexContainer().withLayout({ const flexContainer = this._view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).component(); .component();
const networkShare = this.migrationStateModel._databaseBackup.networkShares[0];
switch (this.migrationStateModel._databaseBackup.networkContainerType) { switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.NETWORK_SHARE: case NetworkContainerType.NETWORK_SHARE:
flexContainer.addItems( flexContainer.addItems([
[ createInformationRow(
createInformationRow(this._view, constants.BACKUP_LOCATION, constants.NETWORK_SHARE), this._view,
createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.networkShares[0].windowsUser), constants.BACKUP_LOCATION,
await createHeadingTextComponent(this._view, constants.AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS), constants.NETWORK_SHARE),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name), createInformationRow(
createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.location), this._view,
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.resourceGroup!), constants.USER_ACCOUNT,
createInformationRow(this._view, constants.STORAGE_ACCOUNT, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.name!), networkShare.windowsUser),
] await createHeadingTextComponent(
); this._view,
constants.AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS),
createInformationRow(
this._view,
constants.SUBSCRIPTION,
this.migrationStateModel._databaseBackup.subscription.name),
createInformationRow(
this._view,
constants.LOCATION,
networkShare.storageAccount?.location),
createInformationRow(
this._view,
constants.RESOURCE_GROUP,
networkShare.storageAccount?.resourceGroup!),
createInformationRow(
this._view,
constants.STORAGE_ACCOUNT,
networkShare.storageAccount?.name!),
]);
break; break;
case NetworkContainerType.BLOB_CONTAINER: case NetworkContainerType.BLOB_CONTAINER:
flexContainer.addItems( flexContainer.addItems([
[ createInformationRow(
createInformationRow(this._view, constants.TYPE, constants.BLOB_CONTAINER), this._view,
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name) constants.TYPE,
] constants.BLOB_CONTAINER),
); createInformationRow(
this._view,
constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION,
this.migrationStateModel._databaseBackup.subscription.name)]);
} }
return flexContainer; return flexContainer;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage'; import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
import { azureResource } from 'azurecore'; import { azureResource } from 'azurecore';
import { ServiceContextChangeEvent } from '../dashboard/tabBase';
export const WIZARD_INPUT_COMPONENT_WIDTH = '600px'; export const WIZARD_INPUT_COMPONENT_WIDTH = '600px';
export class WizardController { export class WizardController {
@@ -27,7 +28,7 @@ export class WizardController {
constructor( constructor(
private readonly extensionContext: vscode.ExtensionContext, private readonly extensionContext: vscode.ExtensionContext,
private readonly _model: MigrationStateModel, private readonly _model: MigrationStateModel,
private readonly _onClosedCallback: () => Promise<void>) { private readonly _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
} }
public async openWizard(connectionId: string): Promise<void> { public async openWizard(connectionId: string): Promise<void> {
@@ -40,7 +41,11 @@ export class WizardController {
private async createWizard(stateModel: MigrationStateModel): Promise<void> { private async createWizard(stateModel: MigrationStateModel): Promise<void> {
const serverName = (await stateModel.getSourceConnectionProfile()).serverName; const serverName = (await stateModel.getSourceConnectionProfile()).serverName;
this._wizardObject = azdata.window.createWizard(loc.WIZARD_TITLE(serverName), 'MigrationWizard', 'wide'); this._wizardObject = azdata.window.createWizard(
loc.WIZARD_TITLE(serverName),
'MigrationWizard',
'wide');
this._wizardObject.generateScriptButton.enabled = false; this._wizardObject.generateScriptButton.enabled = false;
this._wizardObject.generateScriptButton.hidden = true; this._wizardObject.generateScriptButton.hidden = true;
const saveAndCloseButton = azdata.window.createButton(loc.SAVE_AND_CLOSE); const saveAndCloseButton = azdata.window.createButton(loc.SAVE_AND_CLOSE);
@@ -60,8 +65,7 @@ export class WizardController {
migrationModePage, migrationModePage,
databaseBackupPage, databaseBackupPage,
integrationRuntimePage, integrationRuntimePage,
summaryPage summaryPage];
];
this._wizardObject.pages = pages.map(p => p.getwizardPage()); this._wizardObject.pages = pages.map(p => p.getwizardPage());
@@ -82,17 +86,23 @@ export class WizardController {
// if the user selected network share and selected save & close afterwards, it should always return to the database backup page so that // if the user selected network share and selected save & close afterwards, it should always return to the database backup page so that
// the user can input their password again // the user can input their password again
if (this._model.savedInfo.closedPage >= Page.DatabaseBackup && this._model.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) { if (this._model.savedInfo.closedPage >= Page.DatabaseBackup &&
this._model.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
wizardSetupPromises.push(this._wizardObject.setCurrentPage(Page.DatabaseBackup)); wizardSetupPromises.push(this._wizardObject.setCurrentPage(Page.DatabaseBackup));
} else { } else {
wizardSetupPromises.push(this._wizardObject.setCurrentPage(this._model.savedInfo.closedPage)); wizardSetupPromises.push(this._wizardObject.setCurrentPage(this._model.savedInfo.closedPage));
} }
} }
this._model.extensionContext.subscriptions.push(this._wizardObject.onPageChanged(async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => { this._model.extensionContext.subscriptions.push(
this._wizardObject.onPageChanged(
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
const newPage = pageChangeInfo.newPage; const newPage = pageChangeInfo.newPage;
const lastPage = pageChangeInfo.lastPage; const lastPage = pageChangeInfo.lastPage;
this.sendPageButtonClickEvent(pageChangeInfo).catch(e => logError(TelemetryViews.MigrationWizardController, 'ErrorSendingPageButtonClick', e)); this.sendPageButtonClickEvent(pageChangeInfo)
.catch(e => logError(
TelemetryViews.MigrationWizardController,
'ErrorSendingPageButtonClick', e));
await pages[lastPage]?.onPageLeave(pageChangeInfo); await pages[lastPage]?.onPageLeave(pageChangeInfo);
await pages[newPage]?.onPageEnter(pageChangeInfo); await pages[newPage]?.onPageEnter(pageChangeInfo);
})); }));
@@ -110,11 +120,11 @@ export class WizardController {
await Promise.all(wizardSetupPromises); await Promise.all(wizardSetupPromises);
this._model.extensionContext.subscriptions.push( this._model.extensionContext.subscriptions.push(
this._wizardObject.onPageChanged( this._wizardObject.onPageChanged(
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => { async (pageChangeInfo: azdata.window.WizardPageChangeInfo) =>
await pages[0].onPageEnter(pageChangeInfo); await pages[0].onPageEnter(pageChangeInfo)));
}));
this._disposables.push(saveAndCloseButton.onClick(async () => { this._disposables.push(
saveAndCloseButton.onClick(async () => {
await stateModel.saveInfo(serverName, this._wizardObject.currentPage); await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
await this._wizardObject.close(); await this._wizardObject.close();
@@ -123,7 +133,8 @@ export class WizardController {
} }
})); }));
this._disposables.push(this._wizardObject.cancelButton.onClick(e => { this._disposables.push(
this._wizardObject.cancelButton.onClick(e => {
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard, TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick, TelemetryAction.PageButtonClick,
@@ -131,17 +142,20 @@ export class WizardController {
...this.getTelemetryProps(), ...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Cancel, 'buttonPressed': TelemetryAction.Cancel,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, {}); },
{});
})); }));
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT; this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
this._disposables.push( this._disposables.push(
this._wizardObject.doneButton.onClick(async (e) => { this._wizardObject.doneButton.onClick(async (e) => {
try {
await stateModel.startMigration(); await stateModel.startMigration();
await this.updateServiceContext(stateModel); await this.updateServiceContext(stateModel, this._serviceContextChangedEvent);
await this._onClosedCallback(); } catch (e) {
logError(TelemetryViews.MigrationWizardController, 'StartMigrationFailed', e);
} finally {
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard, TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick, TelemetryAction.PageButtonClick,
@@ -149,11 +163,16 @@ export class WizardController {
...this.getTelemetryProps(), ...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Done, 'buttonPressed': TelemetryAction.Done,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, {}); },
{});
}
})); }));
} }
private async updateServiceContext(stateModel: MigrationStateModel): Promise<void> { private async updateServiceContext(
stateModel: MigrationStateModel,
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {
const resourceGroup = this._getResourceGroupByName( const resourceGroup = this._getResourceGroupByName(
stateModel._resourceGroups, stateModel._resourceGroups,
stateModel._sqlMigrationService?.properties.resourceGroup); stateModel._sqlMigrationService?.properties.resourceGroup);
@@ -174,18 +193,28 @@ export class WizardController {
location: location, location: location,
resourceGroup: resourceGroup, resourceGroup: resourceGroup,
migrationService: stateModel._sqlMigrationService, migrationService: stateModel._sqlMigrationService,
}); },
serviceContextChangedEvent);
} }
private _getResourceGroupByName(resourceGroups: azureResource.AzureResourceResourceGroup[], displayName?: string): azureResource.AzureResourceResourceGroup | undefined { private _getResourceGroupByName(
resourceGroups: azureResource.AzureResourceResourceGroup[],
displayName?: string): azureResource.AzureResourceResourceGroup | undefined {
return resourceGroups.find(rg => rg.name === displayName); return resourceGroups.find(rg => rg.name === displayName);
} }
private _getLocationByValue(locations: azureResource.AzureLocation[], name?: string): azureResource.AzureLocation | undefined { private _getLocationByValue(
locations: azureResource.AzureLocation[],
name?: string): azureResource.AzureLocation | undefined {
return locations.find(loc => loc.name === name); return locations.find(loc => loc.name === name);
} }
private _getSubscriptionFromResourceId(subscriptions: azureResource.AzureResourceSubscription[], resourceId?: string): azureResource.AzureResourceSubscription | undefined { private _getSubscriptionFromResourceId(
subscriptions: azureResource.AzureResourceSubscription[],
resourceId?: string): azureResource.AzureResourceSubscription | undefined {
let parts = resourceId?.split('/subscriptions/'); let parts = resourceId?.split('/subscriptions/');
if (parts?.length && parts?.length > 1) { if (parts?.length && parts?.length > 1) {
parts = parts[1]?.split('/resourcegroups/'); parts = parts[1]?.split('/resourcegroups/');
@@ -198,7 +227,9 @@ export class WizardController {
} }
private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) { private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) {
const buttonPressed = pageChangeInfo.newPage > pageChangeInfo.lastPage ? TelemetryAction.Next : TelemetryAction.Prev; const buttonPressed = pageChangeInfo.newPage > pageChangeInfo.lastPage
? TelemetryAction.Next
: TelemetryAction.Prev;
const pageTitle = this._wizardObject.pages[pageChangeInfo.lastPage]?.title; const pageTitle = this._wizardObject.pages[pageChangeInfo.lastPage]?.title;
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard, TelemetryViews.SqlMigrationWizard,
@@ -207,7 +238,8 @@ export class WizardController {
...this.getTelemetryProps(), ...this.getTelemetryProps(),
'buttonPressed': buttonPressed, 'buttonPressed': buttonPressed,
'pageTitle': pageTitle 'pageTitle': pageTitle
}, {}); },
{});
} }
private getTelemetryProps() { private getTelemetryProps() {
@@ -221,33 +253,38 @@ export class WizardController {
} }
} }
export function createInformationRow(view: azdata.ModelView, label: string, value: string): azdata.FlexContainer { export function createInformationRow(
view: azdata.ModelView,
label: string,
value: string): azdata.FlexContainer {
return view.modelBuilder.flexContainer() return view.modelBuilder.flexContainer()
.withLayout( .withLayout({ flexFlow: 'row', alignItems: 'center', })
{ .withItems([
flexFlow: 'row', createLabelTextComponent(
alignItems: 'center', view,
}) label,
.withItems(
[
createLabelTextComponent(view, label,
{ {
...styles.BODY_CSS, ...styles.BODY_CSS,
'margin': '4px 0px', 'margin': '4px 0px',
'width': '300px', 'width': '300px',
} }),
), createTextComponent(
createTextComponent(view, value, view,
value,
{ {
...styles.BODY_CSS, ...styles.BODY_CSS,
'margin': '4px 0px', 'margin': '4px 0px',
'width': '300px', 'width': '300px',
} })])
) .component();
]).component();
} }
export async function createHeadingTextComponent(view: azdata.ModelView, value: string, firstElement: boolean = false): Promise<azdata.TextComponent> { export async function createHeadingTextComponent(
view: azdata.ModelView,
value: string,
firstElement: boolean = false): Promise<azdata.TextComponent> {
const component = createTextComponent(view, value); const component = createTextComponent(view, value);
await component.updateCssStyles({ await component.updateCssStyles({
...styles.LABEL_CSS, ...styles.LABEL_CSS,
@@ -256,14 +293,20 @@ export async function createHeadingTextComponent(view: azdata.ModelView, value:
return component; return component;
} }
export function createLabelTextComponent(view: azdata.ModelView, value: string, styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent { export function createLabelTextComponent(
const component = createTextComponent(view, value, styles); view: azdata.ModelView,
return component; value: string,
styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
return createTextComponent(view, value, styles);
} }
export function createTextComponent(view: azdata.ModelView, value: string, styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent { export function createTextComponent(
return view.modelBuilder.text().withProps({ view: azdata.ModelView,
value: value, value: string,
CSSStyles: styles styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
}).component();
return view.modelBuilder.text()
.withProps({ value: value, CSSStyles: styles })
.component();
} }