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 constants from '../constants/strings';
import { getSessionIdHeader } from './utils';
import { ProvisioningState } from '../models/migrationLocalStorage';
import { URL } from 'url';
const ARM_MGMT_API_VERSION = '2021-04-01';
const SQL_VM_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';
async function getAzureCoreAPI(): Promise<azurecore.IExtension> {
@@ -93,6 +93,100 @@ export async function getAvailableSqlServers(account: azdata.Account, subscripti
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 = {
properties: {
virtualMachineResourceId: string,
@@ -108,6 +202,31 @@ export type SqlVMServer = {
tenantId: 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[]> {
const api = await getAzureCoreAPI();
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> {
const api = await getAzureCoreAPI();
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 = {
'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));
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
@@ -219,9 +338,9 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
for (i = 0; i < maxRetry; i++) {
const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncPath, azurecore.HttpRequestMethod.GET, undefined, true, host);
const creationStatus = asyncResponse.response.data.status;
if (creationStatus === ProvisioningState.Succeeded) {
if (creationStatus === constants.ProvisioningState.Succeeded) {
break;
} else if (creationStatus === ProvisioningState.Failed) {
} else if (creationStatus === constants.ProvisioningState.Failed) {
throw new Error(asyncResponse.errors.toString());
}
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;
}
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 path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=${DMSV2_API_VERSION}`);
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
@@ -438,10 +565,10 @@ export interface SqlMigrationServiceProperties {
}
export interface SqlMigrationService {
properties: SqlMigrationServiceProperties;
location: string;
id: string;
name: string;
location: string;
properties: SqlMigrationServiceProperties;
error: {
code: string,
message: string
@@ -485,14 +612,29 @@ export interface StartDatabaseMigrationRequest {
},
sourceLocation?: SourceLocation
},
sourceSqlConnection: {
authentication: string,
targetSqlConnection?: {
dataSource: string,
username: string,
password: string
authentication: 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,
offlineConfiguration: OfflineConfiguration,
offlineConfiguration?: OfflineConfiguration,
}
}
@@ -503,10 +645,10 @@ export interface StartDatabaseMigrationResponse {
}
export interface DatabaseMigration {
properties: DatabaseMigrationProperties;
id: string;
name: string;
type: string;
properties: DatabaseMigrationProperties;
}
export interface DatabaseMigrationProperties {
@@ -525,6 +667,7 @@ export interface DatabaseMigrationProperties {
backupConfiguration: BackupConfiguration;
offlineConfiguration: OfflineConfiguration;
migrationFailureError: ErrorInfo;
tableList: string[];
}
export interface MigrationStatusDetails {
@@ -543,6 +686,7 @@ export interface MigrationStatusDetails {
pendingLogBackupsCount: number;
invalidFiles: string[];
listOfCopyProgressDetails: CopyProgressDetail[];
sqlDataCopyErrors: string[];
}
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.
*--------------------------------------------------------------------------------------------*/
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 { IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
import * as crypto from 'crypto';
import * as azure from './azure';
import { azureResource, Tenant } from 'azurecore';
@@ -15,8 +14,26 @@ import { logError, TelemetryViews } from '../telemtery';
import { AdsMigrationStatus } from '../dashboard/tabBase';
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 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 {
if (!obj || typeof obj !== 'object') {
return obj;
@@ -145,19 +162,19 @@ export function filterMigrations(databaseMigrations: azure.DatabaseMigration[],
return filteredMigration.filter(
value => {
const status = getMigrationStatus(value);
return status === MigrationStatus.InProgress
|| status === MigrationStatus.Retriable
|| status === MigrationStatus.Creating;
return status === constants.MigrationStatus.InProgress
|| status === constants.MigrationStatus.Retriable
|| status === constants.MigrationStatus.Creating;
});
case AdsMigrationStatus.SUCCEEDED:
return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Succeeded);
value => getMigrationStatus(value) === constants.MigrationStatus.Succeeded);
case AdsMigrationStatus.FAILED:
return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Failed);
value => getMigrationStatus(value) === constants.MigrationStatus.Failed);
case AdsMigrationStatus.COMPLETING:
return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Completing);
value => getMigrationStatus(value) === constants.MigrationStatus.Completing);
}
return filteredMigration;
}
@@ -192,6 +209,8 @@ export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?:
selectedIndex = -1;
}
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 {
const properties = migration.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
let warningCount = 0;
const migrationStatus = getMigrationStatus(migration) ?? '';
if (properties.migrationFailureError?.message) {
warningCount++;
}
if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
const blockingErrors = properties.migrationStatusDetails?.fileUploadBlockingErrors.length ?? 0;
warningCount += blockingErrors;
}
if (properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount++;
}
// provisioning error
let warningCount = properties.provisioningError?.length > 0 ? 1 : 0;
// migration failure error
warningCount += properties.migrationFailureError?.message?.length > 0 ? 1 : 0;
// file upload blocking errors
warningCount += properties.migrationStatusDetails?.fileUploadBlockingErrors?.length ?? 0;
// 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)
+ (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
@@ -302,20 +324,20 @@ export function getPipelineStatusImage(status: string | undefined): IconPath {
export function getMigrationStatusImage(migration: azure.DatabaseMigration): IconPath {
const status = getMigrationStatus(migration);
switch (status) {
case MigrationStatus.InProgress:
case constants.MigrationStatus.InProgress:
return IconPathHelper.inProgressMigration;
case MigrationStatus.Succeeded:
case constants.MigrationStatus.Succeeded:
return IconPathHelper.completedMigration;
case MigrationStatus.Creating:
case constants.MigrationStatus.Creating:
return IconPathHelper.notStartedMigration;
case MigrationStatus.Completing:
case constants.MigrationStatus.Completing:
return IconPathHelper.completingCutover;
case MigrationStatus.Retriable:
case constants.MigrationStatus.Retriable:
return IconPathHelper.retry;
case MigrationStatus.Canceling:
case MigrationStatus.Canceled:
case constants.MigrationStatus.Canceling:
case constants.MigrationStatus.Canceled:
return IconPathHelper.cancel;
case MigrationStatus.Failed:
case constants.MigrationStatus.Failed:
default:
return IconPathHelper.error;
}
@@ -379,34 +401,7 @@ export async function getAzureAccountsDropdownValues(accounts: Account[]): Promi
}
export function getAzureTenants(account?: Account): Tenant[] {
let tenants: Tenant[] = [];
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;
return account?.properties.tenants || [];
}
export async function getAzureSubscriptions(account?: Account): Promise<azureResource.AzureResourceSubscription[]> {
@@ -441,172 +436,58 @@ export async function getAzureSubscriptionsDropdownValues(subscriptions: azureRe
return subscriptionsValues;
}
export async function getSqlManagedInstanceLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, managedInstances?: azureResource.AzureSqlManagedInstance[]): Promise<azureResource.AzureLocation[]> {
let locations: azureResource.AzureLocation[] = [];
export async function getResourceLocations(
account?: Account,
subscription?: azureResource.AzureResourceSubscription,
resources?: { location: string }[]): Promise<azureResource.AzureLocation[]> {
try {
if (account && subscription && managedInstances) {
locations = await azure.getLocations(account, subscription);
locations = locations.filter((loc, i) => managedInstances.some(mi => mi.location.toLowerCase() === loc.name.toLowerCase()));
if (account && subscription && resources) {
const locations = await azure.getLocations(account, subscription);
return locations
.filter((loc, i) => resources.some(resource => resource.location.toLowerCase() === loc.name.toLowerCase()))
.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceLocations', e);
logError(TelemetryViews.Utils, 'utils.getResourceLocations', e);
}
locations.sort((a, b) => a.displayName.localeCompare(b.displayName));
return locations;
return [];
}
export async function getSqlVirtualMachineLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, virtualMachines?: azure.SqlVMServer[]): Promise<azureResource.AzureLocation[]> {
let locations: azureResource.AzureLocation[] = [];
try {
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 function getServiceResourceGroupsByLocation(
resources: { location: string, id: string, tenantId?: string }[],
location: azureResource.AzureLocation): azureResource.AzureResourceResourceGroup[] {
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[] = [];
try {
if (managedInstances && location) {
resourceGroups = managedInstances
.filter((mi) => mi.location.toLowerCase() === location.name.toLowerCase())
.map((mi) => {
if (resources && location) {
const locationName = location.name.toLowerCase();
resourceGroups = resources
.filter(resource => resource.location.toLowerCase() === locationName)
.map(resource => {
return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(mi.id),
name: azure.getResourceGroupFromId(mi.id),
subscription: {
id: mi.subscriptionId
},
tenant: mi.tenantId
id: azure.getFullResourceGroupFromId(resource.id),
name: azure.getResourceGroupFromId(resource.id),
subscription: { id: getSubscriptionIdFromResourceId(resource.id) },
tenant: resource.tenantId
};
});
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceResourceGroups', 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;
return resourceGroups
.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i)
.sort((a, b) => a.name.localeCompare(b.name));
}
export async function getSqlVirtualMachineResourceGroups(virtualMachines?: azure.SqlVMServer[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
try {
if (virtualMachines && location) {
resourceGroups = virtualMachines
.filter((vm) => vm.location.toLowerCase() === location.name.toLowerCase())
.map((vm) => {
return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(vm.id),
name: azure.getResourceGroupFromId(vm.id),
subscription: {
id: vm.subscriptionId
},
tenant: vm.tenantId
};
});
export function getSubscriptionIdFromResourceId(resourceId: string): string | undefined {
let parts = resourceId?.split('/subscriptions/');
if (parts?.length > 1) {
parts = parts[1]?.split('/resourcegroups/');
if (parts?.length > 0) {
return parts[0];
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlVirtualMachineResourceGroups', 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 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;
return undefined;
}
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;
}
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[]> {
let managedInstances: azureResource.AzureSqlManagedInstance[] = [];
try {
@@ -687,6 +549,31 @@ export async function getManagedInstancesDropdownValues(managedInstances: azureR
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[]> {
let virtualMachines: azure.SqlVMServer[] = [];
try {
@@ -705,30 +592,6 @@ export async function getVirtualMachines(account?: Account, subscription?: azure
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[]> {
let storageAccounts: azure.StorageAccount[] = [];
try {
@@ -742,65 +605,18 @@ export async function getStorageAccounts(account?: Account, subscription?: azure
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[]> {
let sqlMigrationServices: azure.SqlMigrationService[] = [];
try {
if (account && subscription) {
sqlMigrationServices = (await azure.getSqlMigrationServices(account, subscription)).filter(dms => {
return dms.properties.provisioningState === ProvisioningState.Succeeded;
});
const services = await azure.getSqlMigrationServices(account, subscription);
return services
.filter(dms => dms.properties.provisioningState === constants.ProvisioningState.Succeeded)
.sort((a, b) => a.name.localeCompare(b.name));
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getAzureSqlMigrationServices', e);
}
sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name));
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;
return [];
}
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;
}
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[]> {
let lastFileNames: azureResource.Blob[] = [];
try {
@@ -848,39 +645,91 @@ export async function getBlobLastBackupFileNames(account?: Account, subscription
return lastFileNames;
}
export async function getBlobLastBackupFileNamesValues(lastFileNames: azureResource.Blob[]): Promise<CategoryValue[]> {
let lastFileNamesValues: CategoryValue[] = [];
lastFileNames.forEach((lastFileName) => {
lastFileNamesValues.push({
name: lastFileName.name,
displayName: lastFileName.name
export function getAzureResourceDropdownValues(
azureResources: { location: string, id: string, name: string }[],
location: azureResource.AzureLocation | undefined,
resourceGroup: string | undefined,
resourceNotFoundMessage: string): CategoryValue[] {
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 }];
}
return lastFileNamesValues;
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];
}
// '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> {
let path = '';
let options: vscode.OpenDialogOptions = {
const options: vscode.OpenDialogOptions = {
defaultUri: vscode.Uri.file(getUserHome()!),
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
};
let fileUris = await vscode.window.showOpenDialog(options);
if (fileUris && fileUris?.length > 0 && fileUris[0]) {
path = fileUris[0].fsPath;
const fileUris = await vscode.window.showOpenDialog(options);
if (fileUris && fileUris.length > 0 && fileUris[0]) {
return fileUris[0].fsPath;
}
return path;
return '';
}

View File

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

View File

@@ -6,11 +6,36 @@
import { AzureAccount } from 'azurecore';
import * as nls from 'vscode-nls';
import { EOL } from 'os';
import { MigrationStatus } from '../models/migrationLocalStorage';
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
import { ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
import { formatNumber, ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
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
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_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 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 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_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 function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string {
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);
}
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 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 {
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 {
return localize('sql.migration.sku.azureConfiguration.db', "{0} - {1} vCore", computeTier, vCore);
export function SQLDB_CONFIGURATION(computeTier: string, vCore: number): string {
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 {
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);
}
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
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.");
@@ -261,7 +328,6 @@ export function UNAVAILABLE_TARGET_PREFIX(targetName: string): string {
return localize('sql.migration.unavailable.target', "(Unavailable) {0}", targetName);
}
// 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_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.");
// 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
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);
}
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_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");
@@ -331,6 +404,7 @@ export function ACCOUNT_CREDENTIALS_REFRESH(accountName: string): string {
"{0} (requires credentials refresh)",
accountName);
}
export const SELECT_SERVICE_PLACEHOLDER = localize('sql.migration.select.service.select.migration.target', "Select a target server.");
// database backup page
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_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_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_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");
@@ -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_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 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 {
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_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
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");
@@ -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_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_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_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");
@@ -521,6 +644,9 @@ export const DATABASE_TO_BE_MIGRATED = localize('sql.migration.database.to.be.mi
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);
}
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
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_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_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_VERSION = localize('sql.migration.target.version', "Target version");
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 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_ARIA_CLICK_VIEW_ERROR_DETAILS = localize('sql.migration.error.aria.view.details', 'Click to view error details');
export interface LookupTable<T> {
[key: string]: T;
}
export const StatusLookup: LookupTable<string | undefined> = {
[MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'),
[MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'),
@@ -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 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_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 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 * as styles from '../constants/styles';
import * as loc from '../constants/strings';
import { filterMigrations } from '../api/utils';
import { filterMigrations, MenuCommands } from '../api/utils';
import { DatabaseMigration } from '../api/azure';
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
import { logError, TelemetryViews } from '../telemtery';
import { AdsMigrationStatus, MenuCommands, TabBase } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
import { AdsMigrationStatus, ServiceContextChangeEvent, TabBase } from './tabBase';
import { DashboardStatusBar } from './DashboardStatusBar';
interface IActionMetadata {
title?: string,
@@ -62,16 +62,15 @@ export class DashboardTab extends TabBase<DashboardTab> {
this.icon = IconPathHelper.sqlMigrationLogo;
}
public onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async create(
view: azdata.ModelView,
openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>,
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
statusBar: DashboardStatusBar): Promise<DashboardTab> {
this.view = view;
this.openMigrationFcn = openMigrationsFcn;
this.openMigrationsFcn = openMigrationsFcn;
this.serviceContextChangedEvent = serviceContextChangedEvent;
this.statusBar = statusBar;
await this.initialize(this.view);
@@ -80,23 +79,16 @@ export class DashboardTab extends TabBase<DashboardTab> {
}
public async refresh(): Promise<void> {
if (this.isRefreshing) {
if (this.isRefreshing || this._migrationStatusCardLoadingContainer === undefined) {
return;
}
this.isRefreshing = true;
this._migrationStatusCardLoadingContainer.loading = true;
let migrations: DatabaseMigration[] = [];
try {
this.isRefreshing = true;
this._refreshButton.enabled = false;
this._migrationStatusCardLoadingContainer.loading = true;
await this.statusBar.clearError();
migrations = await getCurrentMigrations();
} catch (e) {
await this.statusBar.showError(
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
e.message);
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
}
const migrations = await getCurrentMigrations();
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
let warningCount = 0;
@@ -125,8 +117,17 @@ export class DashboardTab extends TabBase<DashboardTab> {
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
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._refreshButton.enabled = true;
this.isRefreshing = false;
}
}
protected async initialize(view: azdata.ModelView): Promise<void> {
@@ -616,11 +617,8 @@ export class DashboardTab extends TabBase<DashboardTab> {
}).component();
this.disposables.push(
this._refreshButton.onDidClick(async (e) => {
this._refreshButton.enabled = false;
await this.refresh();
this._refreshButton.enabled = true;
}));
this._refreshButton.onDidClick(
async (e) => await this.refresh()));
const buttonContainer = view.modelBuilder.flexContainer()
.withProps({
@@ -668,7 +666,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_IN_PROGRESS);
this.disposables.push(
this._inProgressMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ONGOING)));
this._migrationStatusCardsContainer.addItem(
this._inProgressMigrationButton.container,
{ flex: '0 0 auto' });
@@ -681,7 +679,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
true);
this.disposables.push(
this._inProgressWarningMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ONGOING)));
this._migrationStatusCardsContainer.addItem(
this._inProgressWarningMigrationButton.container,
{ flex: '0 0 auto' });
@@ -693,7 +691,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_COMPLETED);
this.disposables.push(
this._successfulMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.SUCCEEDED)));
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.SUCCEEDED)));
this._migrationStatusCardsContainer.addItem(
this._successfulMigrationButton.container,
{ flex: '0 0 auto' });
@@ -705,7 +703,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_CUTOVER_CARD);
this.disposables.push(
this._completingMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.COMPLETING)));
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.COMPLETING)));
this._migrationStatusCardsContainer.addItem(
this._completingMigrationButton.container,
{ flex: '0 0 auto' });
@@ -717,7 +715,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.MIGRATION_FAILED);
this.disposables.push(
this._failedMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.FAILED)));
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.FAILED)));
this._migrationStatusCardsContainer.addItem(
this._failedMigrationButton.container,
{ flex: '0 0 auto' });
@@ -729,7 +727,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
loc.VIEW_ALL);
this.disposables.push(
this._allMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ALL)));
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ALL)));
this._migrationStatusCardsContainer.addItem(
this._allMigrationButton.container,
{ flex: '0 0 auto' });
@@ -759,9 +757,21 @@ export class DashboardTab extends TabBase<DashboardTab> {
})
.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._serviceContextButton.onDidClick(async () => {
const dialog = new SelectMigrationServiceDialog(async () => await this.onDialogClosed());
const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
await dialog.initialize();
}));

View File

@@ -8,11 +8,11 @@ import * as vscode from 'vscode';
import * as loc from '../constants/strings';
import { getSqlServerName, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemtery';
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { getResourceName } from '../api/azure';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { EmptySettingValue } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
import { DashboardStatusBar } from './DashboardStatusBar';
const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab';
@@ -36,13 +36,13 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
onClosedCallback: () => Promise<void>,
openMigrationsListFcn: () => Promise<void>,
statusBar: DashboardStatusBar,
): Promise<MigrationDetailsBlobContainerTab> {
this.view = view;
this.context = context;
this.onClosedCallback = onClosedCallback;
this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar;
await this.initialize(this.view);
@@ -51,12 +51,14 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
}
public async refresh(): Promise<void> {
if (this.isRefreshing || this.model?.migration === undefined) {
if (this.isRefreshing ||
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return;
}
this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
@@ -95,7 +97,7 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
@@ -114,9 +116,8 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
this.isRefreshing = false;
this.refreshLoader.loading = false;
this.refreshButton.enabled = true;
this.isRefreshing = false;
}
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 { logError, TelemetryViews } from '../telemtery';
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 { EmptySettingValue } from './tabBase';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
import { DashboardStatusBar } from './DashboardStatusBar';
const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab';
@@ -43,7 +43,6 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.TableComponent;
private _emptyTableFill!: azdata.FlexContainer;
@@ -56,12 +55,12 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
onClosedCallback: () => Promise<void>,
openMigrationsListFcn: () => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
this.view = view;
this.context = context;
this.onClosedCallback = onClosedCallback;
this.openMigrationsListFcn = openMigrationsListFcn;
this.statusBar = statusBar;
await this.initialize(this.view);
@@ -70,24 +69,20 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
}
public async refresh(): Promise<void> {
if (this.isRefreshing || this.model?.migration === undefined) {
if (this.isRefreshing ||
this.refreshLoader === undefined ||
this.model?.migration === undefined) {
return;
}
try {
this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
await this._fileTable.updateProperty('data', []);
try {
await this.model.fetchStatus();
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
}
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
@@ -142,7 +137,7 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
this._targetServerInfoField.text.value = targetServerName;
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._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.cancelButton.enabled = canCancelMigration(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.refreshButton.enabled = true;
this.isRefreshing = false;
}
}
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 { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { MigrationTargetType } from '../models/stateMachine';
import { DashboardStatusBar } from './sqlServerDashboard';
import { DashboardStatusBar } from './DashboardStatusBar';
export const infoFieldLgWidth: string = '330px';
export const infoFieldWidth: string = '250px';
@@ -38,8 +38,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
protected model!: MigrationCutoverDialogModel;
protected databaseLabel!: azdata.TextComponent;
protected serviceContext!: MigrationServiceContext;
protected onClosedCallback!: () => Promise<void>;
protected openMigrationsListFcn!: () => Promise<void>;
protected cutoverButton!: azdata.ButtonComponent;
protected refreshButton!: azdata.ButtonComponent;
protected cancelButton!: azdata.ButtonComponent;
@@ -49,7 +48,11 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
protected retryButton!: azdata.ButtonComponent;
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>;
@@ -80,7 +83,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
.component();
this.disposables.push(
migrationsTabLink.onDidClick(
async (e) => await this.onClosedCallback()));
async (e) => await this.openMigrationsListFcn()));
const breadCrumbImage = this.view.modelBuilder.image()
.withProps({
@@ -202,7 +205,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
this.context,
this.serviceContext,
this.model.migration,
this.onClosedCallback);
this.serviceContextChangedEvent);
await retryMigrationDialog.openDialog();
}
));
@@ -254,12 +257,10 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
async (e) => await this.refresh()));
this.refreshLoader = this.view.modelBuilder.loadingComponent()
.withItem(this.refreshButton)
.withProps({
loading: false,
CSSStyles: {
'height': '8px',
'margin-top': '4px'
}
CSSStyles: { 'height': '8px', 'margin-top': '4px' }
}).component();
toolbarContainer.addToolbarItems([
@@ -268,7 +269,6 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
<azdata.ToolbarComponent>{ component: this.retryButton },
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.refreshButton },
<azdata.ToolbarComponent>{ component: this.refreshLoader },
]);

View File

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

View File

@@ -6,20 +6,16 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
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 { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
import { getMigrationTargetType, getMigrationMode, canRetryMigration, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime, MenuCommands } from '../api/utils';
import { getMigrationTargetType, getMigrationMode, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
import { DatabaseMigration, getResourceName } from '../api/azure';
import { logError, TelemetryViews } from '../telemtery';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
import { AdsMigrationStatus, EmptySettingValue, ServiceContextChangeEvent, TabBase } from './tabBase';
import { MigrationMode } from '../models/stateMachine';
import { DashboardStatusBar } from './DashboardStatusBar';
export const MigrationsListTabId = 'MigrationsListTab';
@@ -58,12 +54,14 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
context: vscode.ExtensionContext,
view: azdata.ModelView,
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
statusBar: DashboardStatusBar,
): Promise<MigrationsListTab> {
this.view = view;
this.context = context;
this._openMigrationDetails = openMigrationDetails;
this.serviceContextChangedEvent = serviceContextChangedEvent;
this.statusBar = statusBar;
await this.initialize();
@@ -71,29 +69,28 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
return this;
}
public onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
.find(value => value.name === filter.toString());
this._statusDropdown.value = statusFilter;
await this._statusDropdown.updateProperties({ 'value': statusFilter });
}
}
public async refresh(): Promise<void> {
if (this.isRefreshing) {
if (this.isRefreshing ||
this._refreshLoader === undefined) {
return;
}
try {
this.isRefreshing = true;
this._refresh.enabled = false;
this._refreshLoader.loading = true;
await this.statusBar.clearError();
try {
await this._statusTable.updateProperty('data', []);
this._migrations = await getCurrentMigrations();
await this._populateMigrationTable();
@@ -105,26 +102,22 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
} finally {
this._refreshLoader.loading = false;
this._refresh.enabled = true;
this.isRefreshing = false;
}
}
protected async initialize(): Promise<void> {
this._registerCommands();
this._createStatusTable();
this.content = this.view.modelBuilder.flexContainer()
.withItems(
[
this._createToolbar(),
await this._createSearchAndSortContainer(),
this._createStatusTable()
this._statusTable,
],
{ CSSStyles: { 'width': '100%' } }
).withLayout({
width: '100%',
flexFlow: 'column',
}).withProps({ CSSStyles: { 'padding': '0px' } })
).withLayout({ width: '100%', flexFlow: 'column' })
.withProps({ CSSStyles: { 'padding': '0px' } })
.component();
}
@@ -144,20 +137,16 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
async (e) => await this.refresh()));
this._refreshLoader = this.view.modelBuilder.loadingComponent()
.withItem(this._refresh)
.withProps({
loading: false,
CSSStyles: {
'height': '8px',
'margin-top': '6px'
}
})
.component();
CSSStyles: { 'height': '8px', 'margin-top': '6px' }
}).component();
toolbar.addToolbarItems([
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this._refresh },
<azdata.ToolbarComponent>{ component: this._refreshLoader },
]);
@@ -178,16 +167,25 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
width: 230,
}).component();
const onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
this.disposables.push(
this._serviceContextButton.onDidClick(
async () => {
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
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()
.withProps({
stopEnterPropagation: true,
@@ -212,7 +210,9 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
.withProps({
ariaLabel: loc.MIGRATION_STATUS_FILTER,
values: this._statusDropdownValues,
width: '150px'
width: '150px',
fireOnTextChange: true,
value: this._statusDropdownValues[0],
}).component();
this.disposables.push(
this._statusDropdown.onValueChanged(
@@ -311,173 +311,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
return container;
}
private _registerCommands(): void {
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.Cutover,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(
migration => migration.id === migrationId);
if (canRetryMigration(migration)) {
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
await dialog.initialize();
if (cutoverDialogModel.CutoverError) {
void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
}
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
}
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_CUTOVER_ERROR,
loc.MIGRATION_CUTOVER_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewDatabase,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(m => m.id === migrationId);
await this._openMigrationDetails(migration!);
} catch (e) {
await this.statusBar.showError(
loc.OPEN_MIGRATION_DETAILS_ERROR,
loc.OPEN_MIGRATION_DETAILS_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewTarget,
async (migrationId: string) => {
try {
const migration = this._migrations.find(migration => migration.id === migrationId);
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
await vscode.env.openExternal(vscode.Uri.parse(url));
} catch (e) {
await this.statusBar.showError(
loc.OPEN_MIGRATION_TARGET_ERROR,
loc.OPEN_MIGRATION_TARGET_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewService,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
const dialog = new SqlMigrationServiceDetailsDialog(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await dialog.initialize();
} catch (e) {
await this.statusBar.showError(
loc.OPEN_MIGRATION_SERVICE_ERROR,
loc.OPEN_MIGRATION_SERVICE_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.CopyMigration,
async (migrationId: string) => {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
try {
await cutoverDialogModel.fetchStatus();
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
}
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.CancelMigration,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
if (canCancelMigration(migration)) {
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
await cutoverDialogModel.cancelMigration();
if (cutoverDialogModel.CancelMigrationError) {
void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
}
}
});
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
}
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_CANCELLATION_ERROR,
loc.MIGRATION_CANCELLATION_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.RetryMigration,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
if (canRetryMigration(migration)) {
let retryMigrationDialog = new RetryMigrationDialog(
this.context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration!,
async () => await this.onDialogClosed());
await retryMigrationDialog.openDialog();
}
else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
}
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_RETRY_ERROR,
loc.MIGRATION_RETRY_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
}
}));
}
private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
const sortDir = ascending ? -1 : 1;
switch (columnName) {
@@ -575,6 +408,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true);
const connectionProfile = await azdata.connection.getCurrentConnection();
const data: any[] = this._filteredMigrations.map((migration, index) => {
return [
<azdata.HyperlinkColumnCellValue>{
@@ -597,7 +431,11 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
getMigrationTime(migration.properties.endedOn), // finishTime
<azdata.ContextMenuColumnCellValue>{
title: '',
context: migration.id,
context: {
connectionId: connectionProfile.connectionId,
migrationId: migration.id,
migrationOperationId: migration.properties.migrationOperationId,
},
commands: this._getMenuCommands(migration), // context menu
},
];
@@ -632,7 +470,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
value: 'sourceDatabase',
width: 190,
type: azdata.ColumnType.hyperlink,
showText: true,
},
{
cssClass: rowCssStyles,
@@ -717,7 +554,8 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
]
}).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 migration = this._filteredMigrations[rowState.row];
switch (buttonState?.column) {

View File

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

View File

@@ -5,82 +5,121 @@
import * as azdata from 'azdata';
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 { 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 { MigrationsTab, MigrationsTabId } from './migrationsTab';
import { AdsMigrationStatus } from './tabBase';
import { AdsMigrationStatus, MigrationDetailsEvent, ServiceContextChangeEvent } from './tabBase';
export interface DashboardStatusBar {
showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise<void>;
clearError: () => Promise<void>;
errorTitle: string;
errorLabel: string;
errorDescription: string;
export interface MenuCommandArgs {
connectionId: string,
migrationId: string,
migrationOperationId: string,
}
export class DashboardWidget implements DashboardStatusBar {
private _context: vscode.ExtensionContext;
private _view!: azdata.ModelView;
private _tabs!: azdata.TabbedPanelComponent;
private _statusInfoBox!: azdata.InfoBoxComponent;
private _dashboardTab!: DashboardTab;
private _migrationsTab!: MigrationsTab;
private _disposables: vscode.Disposable[] = [];
export class DashboardWidget {
public stateModel!: MigrationStateModel;
private readonly _context: vscode.ExtensionContext;
private readonly _onServiceContextChanged: vscode.EventEmitter<ServiceContextChangeEvent>;
private readonly _migrationDetailsEvent: vscode.EventEmitter<MigrationDetailsEvent>;
private readonly _errorEvent: vscode.EventEmitter<ErrorEvent>;
constructor(context: vscode.ExtensionContext) {
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 errorLabel: string = '';
public errorDescription: string = '';
public async register(): Promise<void> {
await this._registerCommands();
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) => {
this._view = view;
this._disposables.push(
this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
const disposables: vscode.Disposable[] = [];
const _view = view;
const statusInfoBox = view.modelBuilder.infoBox()
.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> => {
this._tabs.selectTab(MigrationsTabId);
await this._migrationsTab.setMigrationFilter(filter);
if (!migrationsTabInitialized) {
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,
async (filter: AdsMigrationStatus) => await openMigrationFcn(filter),
this);
this._disposables.push(this._dashboardTab);
this._onServiceContextChanged,
statusBar);
disposables.push(dashboardTab);
this._migrationsTab = await new MigrationsTab().create(
const migrationsTab = await new MigrationsTab().create(
this._context,
view,
this);
this._disposables.push(this._migrationsTab);
this._onServiceContextChanged,
this._migrationDetailsEvent,
statusBar);
disposables.push(migrationsTab);
this._tabs = view.modelBuilder.tabbedPanel()
.withTabs([this._dashboardTab, this._migrationsTab])
const tabs = view.modelBuilder.tabbedPanel()
.withTabs([dashboardTab, migrationsTab])
.withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal })
.withProps({
CSSStyles: {
@@ -91,107 +130,338 @@ export class DashboardWidget implements DashboardStatusBar {
})
.component();
this._disposables.push(
this._tabs.onTabChanged(
async id => {
await this.clearError();
await this.onDialogClosed();
let migrationsTabInitialized = false;
disposables.push(
tabs.onTabChanged(async tabId => {
const connectionProfile = await azdata.connection.getCurrentConnection();
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()
.withLayout({ flexFlow: 'column' })
.withItems([this._statusInfoBox, this._tabs])
.withItems([statusInfoBox, tabs])
.component();
await view.initializeModel(flexContainer);
await this.refresh();
await dashboardTab.refresh();
});
}
public async refresh(): Promise<void> {
void this._migrationsTab.refresh();
await this._dashboardTab.refresh();
}
public async onDialogClosed(): Promise<void> {
await this._dashboardTab.onDialogClosed();
await this._migrationsTab.onDialogClosed();
}
private _errorDialogIsOpen: boolean = false;
protected async openErrorDialog(): Promise<void> {
if (this._errorDialogIsOpen) {
return;
}
private async _registerCommands(): Promise<void> {
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.Cutover,
async (args: MenuCommandArgs) => {
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,
inputType: 'text',
rows: 20,
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
})
.component()
])
.withLayout({
flexFlow: 'column',
width: 420,
})
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
.component();
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();
await this.clearError(args.connectionId);
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
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.showError(
args.connectionId,
loc.MIGRATION_CUTOVER_ERROR,
loc.MIGRATION_CUTOVER_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
}
this._errorDialogIsOpen = false;
}));
azdata.window.openDialog(dialog);
} catch (error) {
this._errorDialogIsOpen = false;
this._context.subscriptions.push(
vscode.commands.registerCommand(
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> {
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
private checkSavedInfo(serverName: string): SavedInfo | undefined {
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 { EOL } from 'os';
import { DatabaseMigration } from '../api/azure';
import { DashboardStatusBar } from './sqlServerDashboard';
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 enum AdsMigrationStatus {
@@ -23,17 +23,15 @@ export enum AdsMigrationStatus {
COMPLETING = 'completing'
}
export const MenuCommands = {
Cutover: 'sqlmigration.cutover',
ViewDatabase: 'sqlmigration.view.database',
ViewTarget: 'sqlmigration.view.target',
ViewService: 'sqlmigration.view.service',
CopyMigration: 'sqlmigration.copy.migration',
CancelMigration: 'sqlmigration.cancel.migration',
RetryMigration: 'sqlmigration.retry.migration',
StartMigration: 'sqlmigration.start',
IssueReporter: 'workbench.action.openIssueReporter',
};
export interface ServiceContextChangeEvent {
connectionId: string;
}
export interface MigrationDetailsEvent {
connectionId: string,
migrationId: string,
migrationOperationId: string,
}
export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
public content!: azdata.Component;
@@ -45,7 +43,8 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
protected view!: azdata.ModelView;
protected disposables: vscode.Disposable[] = [];
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 abstract initialize(view: azdata.ModelView): Promise<void>;
@@ -165,8 +164,9 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
const errors = [];
errors.push(migration.properties.provisioningError);
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?.sqlDataCopyErrors);
// remove undefined and duplicate error entries
return errors

View File

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

View File

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

View File

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

View File

@@ -156,9 +156,10 @@ export class ConfirmCutoverDialog {
height: 20,
label: constants.REFRESH,
}).component();
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
this._disposables.push(
refreshButton.onDidClick(async e => {
try {
refreshLoader.loading = true;
await this.migrationCutoverModel.fetchStatus();
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
} catch (e) {
@@ -232,9 +233,10 @@ export class ConfirmCutoverDialog {
headingRow.addItem(containerHeading, { flex: '0' });
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
this._disposables.push(
refreshButton.onDidClick(async e => {
try {
refreshLoader.loading = true;
await this.migrationCutoverModel.fetchStatus();
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
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 { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
import * as constants from '../../constants/strings';
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
@@ -110,7 +110,7 @@ export class MigrationCutoverDialogModel {
const files: BackupFileInfo[] = [];
this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
abs.listOfBackupFiles.forEach(f => {
if (f.status !== BackupFileInfoStatus.Restored) {
if (f.status !== constants.BackupFileInfoStatus.Restored) {
files.push(f);
}
});

View File

@@ -13,6 +13,7 @@ import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { WizardController } from '../../wizard/wizardController';
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
import * as constants from '../../constants/strings';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
export class RetryMigrationDialog {
@@ -20,15 +21,20 @@ export class RetryMigrationDialog {
private readonly _context: vscode.ExtensionContext,
private readonly _serviceContext: MigrationServiceContext,
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> {
let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
private async createMigrationStateModel(
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;
let savedInfo: SavedInfo;
savedInfo = {
const savedInfo: SavedInfo = {
closedPage: 0,
// DatabaseSelector
@@ -142,7 +148,7 @@ export class RetryMigrationDialog {
}
});
let activeConnection = await azdata.connection.getCurrentConnection();
const activeConnection = await azdata.connection.getCurrentConnection();
let connectionId: string = '';
let serverName: string = '';
if (!activeConnection) {
@@ -163,7 +169,7 @@ export class RetryMigrationDialog {
const wizardController = new WizardController(
this._context,
stateModel,
this._onClosedCallback);
this._serviceContextChangedEvent);
await wizardController.openWizard(stateModel.sourceConnectionId);
} else {
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 { SqlMigrationService } from '../../api/azure';
import { logError, TelemetryViews } from '../../telemtery';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
const CONTROL_MARGIN = '20px';
const INPUT_COMPONENT_WIDTH = '100%';
@@ -56,7 +57,7 @@ export class SelectMigrationServiceDialog {
private _deleteButton!: azdata.window.Button;
constructor(
private readonly _onClosedCallback: () => Promise<void>) {
private readonly onServiceContextChanged: vscode.EventEmitter<ServiceContextChangeEvent>) {
this._dialog = azdata.window.createModelViewDialog(
constants.MIGRATION_SERVICE_SELECT_TITLE,
'SelectMigraitonServiceDialog',
@@ -85,10 +86,10 @@ export class SelectMigrationServiceDialog {
'left');
this._disposables.push(
this._deleteButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext({});
await this._onClosedCallback();
await MigrationLocalStorage.saveMigrationServiceContext({}, this.onServiceContextChanged);
azdata.window.closeDialog(this._dialog);
}));
this._dialog.customButtons = [this._deleteButton];
azdata.window.openDialog(this._dialog);
@@ -262,7 +263,7 @@ export class SelectMigrationServiceDialog {
? utils.deepClone(selectedLocation)
: undefined!;
await this._populateResourceGroupDropdown();
await this._populateMigrationServiceDropdown();
this._populateMigrationServiceDropdown();
}
}));
@@ -290,7 +291,7 @@ export class SelectMigrationServiceDialog {
this._serviceContext.resourceGroup = (selectedResourceGroup)
? utils.deepClone(selectedResourceGroup)
: undefined!;
await this._populateMigrationServiceDropdown();
this._populateMigrationServiceDropdown();
}
}));
@@ -323,10 +324,10 @@ export class SelectMigrationServiceDialog {
}));
this._disposables.push(
this._dialog.okButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext);
await this._onClosedCallback();
}));
this._dialog.okButton.onClick(async (value) =>
await MigrationLocalStorage.saveMigrationServiceContext(
this._serviceContext,
this.onServiceContextChanged)));
return this._view.modelBuilder.flexContainer()
.withItems([
@@ -417,8 +418,14 @@ export class SelectMigrationServiceDialog {
private async _populateLocationDropdown(): Promise<void> {
try {
this._azureLocationDropdown.loading = true;
this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this._serviceContext.azureAccount, this._serviceContext.subscription);
this._locations = await utils.getSqlMigrationServiceLocations(this._serviceContext.azureAccount, this._serviceContext.subscription, this._sqlMigrationServices);
this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(
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);
if (this._azureLocationDropdown.values.length > 0) {
utils.selectDefaultDropdownValue(
@@ -439,8 +446,13 @@ export class SelectMigrationServiceDialog {
private async _populateResourceGroupDropdown(): Promise<void> {
try {
this._azureResourceGroupDropdown.loading = true;
this._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this._sqlMigrationServices, this._serviceContext.location!);
this._azureResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups);
this._resourceGroups = utils.getServiceResourceGroupsByLocation(
this._sqlMigrationServices,
this._serviceContext.location!);
this._azureResourceGroupDropdown.values = utils.getResourceDropdownValues(
this._resourceGroups,
constants.RESOURCE_GROUP_NOT_FOUND);
if (this._azureResourceGroupDropdown.values.length > 0) {
utils.selectDefaultDropdownValue(
this._azureResourceGroupDropdown,
@@ -457,10 +469,15 @@ export class SelectMigrationServiceDialog {
}
}
private async _populateMigrationServiceDropdown(): Promise<void> {
private _populateMigrationServiceDropdown(): void {
try {
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) {
utils.selectDefaultDropdownValue(
this._azureServiceDropdown,

View File

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

View File

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

View File

@@ -34,15 +34,13 @@ export class SkuRecommendationResultsDialog {
constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) {
switch (this._targetType) {
case MigrationTargetType.SQLMI:
this.targetName = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
this.targetName = constants.SKU_RECOMMENDATION_MI_CARD_TEXT;
break;
case MigrationTargetType.SQLVM:
this.targetName = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
this.targetName = constants.SKU_RECOMMENDATION_VM_CARD_TEXT;
break;
case MigrationTargetType.SQLDB:
this.targetName = constants.AZURE_SQL_DATABASE;
this.targetName = constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT;
break;
}
@@ -79,7 +77,9 @@ export class SkuRecommendationResultsDialog {
this.targetRecommendations?.forEach((recommendation, index) => {
if (index > 0) {
const separator = _view.modelBuilder.separator().withProps({ width: 750 }).component();
const separator = _view.modelBuilder.separator()
.withProps({ width: 750 })
.component();
container.addItem(separator);
}
@@ -101,7 +101,9 @@ export class SkuRecommendationResultsDialog {
recommendation = <mssql.IaaSSkuRecommendationResultItem>recommendationItem;
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);
}
@@ -123,31 +125,31 @@ export class SkuRecommendationResultsDialog {
: constants.PREMIUM_SERIES_MEMORY_OPTIMIZED;
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!);
const storageLabel = _view.modelBuilder.text().withProps({
const storageLabel = _view.modelBuilder.text()
.withProps({
value: constants.STORAGE_HEADER,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
}).component();
const storageValue = _view.modelBuilder.text().withProps({
const storageValue = _view.modelBuilder.text()
.withProps({
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
CSSStyles: {
...styles.BODY_CSS,
}
CSSStyles: { ...styles.BODY_CSS, }
}).component();
storageSection.addItems([
storageLabel,
storageValue,
]);
storageValue]);
}
break;
}
const recommendationContainer = _view.modelBuilder.flexContainer().withProps({
const recommendationContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'margin-bottom': '20px',
'flex-direction': 'column',
@@ -155,52 +157,41 @@ export class SkuRecommendationResultsDialog {
}).component();
if (this._targetType === MigrationTargetType.SQLDB) {
const databaseNameLabel = _view.modelBuilder.text().withProps({
const databaseNameLabel = _view.modelBuilder.text()
.withProps({
value: recommendation.databaseName!,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
}
CSSStyles: { ...styles.SECTION_HEADER_CSS, }
}).component();
recommendationContainer.addItem(databaseNameLabel);
}
const targetDeploymentTypeLabel = _view.modelBuilder.text().withProps({
const targetDeploymentTypeLabel = _view.modelBuilder.text()
.withProps({
value: constants.TARGET_DEPLOYMENT_TYPE,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '0',
}
CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', }
}).component();
const targetDeploymentTypeValue = _view.modelBuilder.text().withProps({
const targetDeploymentTypeValue = _view.modelBuilder.text()
.withProps({
value: this.targetName,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0',
}
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
}).component();
const azureConfigurationLabel = _view.modelBuilder.text().withProps({
const azureConfigurationLabel = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_CONFIGURATION,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0', }
}).component();
const azureConfigurationValue = _view.modelBuilder.text().withProps({
const azureConfigurationValue = _view.modelBuilder.text()
.withProps({
value: configuration,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0',
}
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
}).component();
recommendationContainer.addItems([
targetDeploymentTypeLabel,
targetDeploymentTypeValue,
targetDeploymentTypeLabel,
targetDeploymentTypeValue,
azureConfigurationLabel,
azureConfigurationValue,
@@ -209,23 +200,21 @@ export class SkuRecommendationResultsDialog {
const recommendationsReasonSection = _view.modelBuilder.text().withProps({
value: constants.RECOMMENDATION_REASON,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin': '12px 0 0'
}
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin': '12px 0 0' }
}).component();
const reasonsContainer = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications) || [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
const reasonsContainer = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications)
|| [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
justifications?.forEach(text => {
reasonsContainer.addItem(
_view.modelBuilder.text().withProps({
value: text,
CSSStyles: {
...styles.BODY_CSS,
}
CSSStyles: { ...styles.BODY_CSS, }
}).component()
);
});
@@ -235,25 +224,22 @@ export class SkuRecommendationResultsDialog {
recommendationContainer.addItems([
recommendationsReasonSection,
reasonsContainer,
storagePropertiesContainer,
]);
storagePropertiesContainer]);
return recommendationContainer;
}
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,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
}).component();
const recommendedTargetStorageInfo = _view.modelBuilder.text().withProps({
const recommendedTargetStorageInfo = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
CSSStyles: {
...styles.BODY_CSS,
}
CSSStyles: { ...styles.BODY_CSS, }
}).component();
const headerCssStyle = {
@@ -333,20 +319,21 @@ export class SkuRecommendationResultsDialog {
logDiskTableRow,
];
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storageConfigurationTableRows,
width: 700
}).component();
const container = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
const container = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
recommendedTargetStorageSection,
recommendedTargetStorageInfo,
storageConfigurationTable,
]).component();
storageConfigurationTable])
.component();
return container;
}
@@ -375,18 +362,15 @@ export class SkuRecommendationResultsDialog {
break;
case MigrationTargetType.SQLDB:
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements.filter(d => {
return databaseName === d.databaseName;
})[0]!;
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements
.filter((d) => databaseName === d.databaseName)[0]!;
break;
}
const storagePropertiesSection = _view.modelBuilder.text().withProps({
const storagePropertiesSection = _view.modelBuilder.text()
.withProps({
value: constants.SOURCE_PROPERTIES,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
}).component();
const headerCssStyle = {
@@ -407,7 +391,7 @@ export class SkuRecommendationResultsDialog {
};
const columnWidth = 80;
let columns: azdata.DeclarativeTableColumn[] = [
const columns: azdata.DeclarativeTableColumn[] = [
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.DIMENSION,
@@ -450,19 +434,18 @@ export class SkuRecommendationResultsDialog {
ioLatencyRow,
];
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storagePropertiesTableRows,
width: 300
}).component();
const container = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
storagePropertiesSection,
storagePropertiesTable,
]).component();
const container = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([storagePropertiesSection, storagePropertiesTable])
.component();
return container;
}
@@ -537,10 +520,9 @@ export class SkuRecommendationResultsDialog {
}));
this.dialog.customButtons = [this._saveButton];
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));
const promise = this.initializeDialog(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 { MigrationMode, MigrationStateModel, NetworkContainerType } from '../../models/stateMachine';
import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import * as styles from '../../constants/styles';
@@ -25,21 +25,19 @@ export class TargetDatabaseSummaryDialog {
this._dialogObject = azdata.window.createModelViewDialog(
constants.DATABASE_TO_BE_MIGRATED,
'TargetDatabaseSummaryDialog',
dialogWidth
);
dialogWidth);
}
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) => {
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),
CSSStyles: {
...styles.BODY_CSS,
'margin-bottom': '20px'
}
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '20px' }
}).component();
const headerCssStyle = {
@@ -61,7 +59,7 @@ export class TargetDatabaseSummaryDialog {
const columnWidth = 150;
let columns: azdata.DeclarativeTableColumn[] = [
const columns: azdata.DeclarativeTableColumn[] = [
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.SOURCE_DATABASE,
@@ -70,7 +68,6 @@ export class TargetDatabaseSummaryDialog {
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.TARGET_DATABASE_NAME,
@@ -78,39 +75,43 @@ export class TargetDatabaseSummaryDialog {
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}
];
}];
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
columns.push({
columns.push(
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.LOCATION,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.RESOURCE_GROUP,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.SUMMARY_AZURE_STORAGE,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
isReadOnly: true,
@@ -119,6 +120,15 @@ export class TargetDatabaseSummaryDialog {
headerCssStyles: headerCssStyle,
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 {
columns.push({
valueType: azdata.DeclarativeDataType.string,
@@ -134,59 +144,54 @@ export class TargetDatabaseSummaryDialog {
this._model._databasesForMigration.forEach((db, index) => {
const tableRow: azdata.DeclarativeTableCellValue[] = [];
tableRow.push({
value: db
}, {
value: this._model._targetDatabaseNames[index]
});
tableRow.push(
{ value: db },
{ value: this._model._targetDatabaseNames[index] });
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
tableRow.push({
value: this._model._databaseBackup.blobs[index].storageAccount.location
}, {
value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup!
}, {
value: this._model._databaseBackup.blobs[index].storageAccount.name
}, {
value: this._model._databaseBackup.blobs[index].blobContainer.name
});
tableRow.push(
{ value: this._model._databaseBackup.blobs[index].storageAccount.location },
{ value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup! },
{ value: this._model._databaseBackup.blobs[index].storageAccount.name },
{ value: this._model._databaseBackup.blobs[index].blobContainer.name });
if (this._model._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
tableRow.push({
value: this._model._databaseBackup.blobs[index].lastBackupFile!
});
tableRow.push(
{ 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 {
tableRow.push({
value: this._model._databaseBackup.networkShares[index].networkShareLocation
});
tableRow.push(
{ value: this._model._databaseBackup.networkShares[index].networkShareLocation });
}
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,
columns: columns,
dataValues: tableRows,
width: this._tableLength
}).component();
const container = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
}).withItems([
databaseCount,
databaseTable
]).component();
const formBuilder = this._view.modelBuilder.formContainer().withFormItems(
[
{
component: container
}
],
{
horizontal: false
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
const container = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([databaseCount, databaseTable])
.component();
const form = this._view.modelBuilder.formContainer()
.withFormItems(
[{ component: container }],
{ horizontal: false })
.withLayout({ width: '100%' })
.component();
return view.initializeModel(form);
});
this._dialogObject.content = [tab];

View File

@@ -4,154 +4,14 @@
*--------------------------------------------------------------------------------------------*/
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 { 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;
export async function activate(context: vscode.ExtensionContext) {
sqlMigration = new SQLMigration(context);
await sqlMigration.registerCommands();
export async function activate(context: vscode.ExtensionContext): Promise<DashboardWidget> {
widget = new DashboardWidget(context);
widget.register();
await widget.register();
return widget;
}
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 { deepClone } from '../api/utils';
import * as loc from '../constants/strings';
import { ServiceContextChangeEvent } from '../dashboard/tabBase';
export class MigrationLocalStorage {
private static context: vscode.ExtensionContext;
@@ -26,15 +27,16 @@ export class MigrationLocalStorage {
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();
if (connectionProfile) {
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) {
const accounts = await azdata.accounts.getAllAccounts();
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);
if (subscription) {
serviceContext.azureAccount = account;
await this.saveMigrationServiceContext(serviceContext);
await this.saveMigrationServiceContext(serviceContext, serviceContextChangedEvent);
}
}
}
@@ -87,30 +89,3 @@ export interface MigrationServiceContext {
resourceGroup?: azurecore.azureResource.AzureResourceResourceGroup,
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;
await this.wizard.setCurrentPage(current + 1);
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as utils from '../api/utils';
import * as mssql from 'mssql';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, PerformanceDataSourceOptions, StateChangeEvent } from '../models/stateMachine';
@@ -83,11 +84,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
name: constants.SKU_RECOMMENDATION_VM_CARD_TEXT,
icon: IconPathHelper.sqlVmLogo
},
// {
// type: MigrationTargetType.SQLDB,
// name: constants.SKU_RECOMMENDATION_DB_CARD_TEXT,
// icon: IconPathHelper.sqlDatabaseLogo
// }
{
type: MigrationTargetType.SQLDB,
name: constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT,
icon: IconPathHelper.sqlDatabaseLogo
}
];
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
@@ -97,27 +98,23 @@ export class SKURecommendationPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView) {
this._view = view;
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,
iconHeight: 17,
iconWidth: 17,
width: 20,
height: 20
}).component();
const igContainer = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'align-items': 'center'
}
}).component();
igContainer.addItem(this._assessmentStatusIcon, {
flex: '0 0 auto'
});
igContainer.addItem(this._igComponent, {
flex: '0 0 auto'
});
const igContainer = this._view.modelBuilder.flexContainer()
.withProps({ CSSStyles: { 'align-items': 'center' } })
.component();
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._skipAssessmentCheckbox = view.modelBuilder.checkBox().withProps({
this._skipAssessmentCheckbox = view.modelBuilder.checkBox()
.withProps({
label: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_BYPASS,
checked: false,
CSSStyles: {
@@ -126,7 +123,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'display': 'none'
},
}).component();
this._skipAssessmentSubText = view.modelBuilder.text().withProps({
this._skipAssessmentSubText = view.modelBuilder.text()
.withProps({
value: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL,
CSSStyles: {
'margin': '0 0 0 15px',
@@ -137,11 +135,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
},
}).component();
this._disposables.push(this._skipAssessmentCheckbox.onChanged(async (value) => {
await this._setAssessmentState(false, true);
}));
this._disposables.push(
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,
label: constants.REFRESH_ASSESSMENT_BUTTON_LABEL,
width: 160,
@@ -158,78 +157,57 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this.constructDetails();
}));
const statusContainer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
}).withItems(
[
const statusContainer = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
igContainer,
this._detailsComponent,
refreshAssessmentButton,
this._skipAssessmentCheckbox,
this._skipAssessmentSubText,
]
).withProps({
CSSStyles: {
'margin': '0'
}
}).component();
this._skipAssessmentSubText])
.withProps({ CSSStyles: { 'margin': '0' } })
.component();
this._chooseTargetComponent = await this.createChooseTargetComponent(view);
const _azureRecommendationsContainer = this.createAzureRecommendationContainer(view);
this.assessmentGroupContainer = await this.createViewAssessmentsContainer();
this._formContainer = view.modelBuilder.formContainer().withFormItems(
[
{
title: '',
component: statusContainer
},
{
component: this._chooseTargetComponent
},
{
component: _azureRecommendationsContainer
},
{
component: this.assessmentGroupContainer
},
]
).withProps({
this._formContainer = view.modelBuilder.formContainer()
.withFormItems([
{ component: statusContainer, title: '' },
{ component: this._chooseTargetComponent },
{ component: _azureRecommendationsContainer },
{ component: this.assessmentGroupContainer }])
.withProps({
CSSStyles: {
'display': 'none',
'padding-top': '0',
}
});
this._assessmentComponent = this._view.modelBuilder.flexContainer().withLayout({
height: '100%',
flexFlow: 'column'
}).withProps({
CSSStyles: {
'margin-left': '30px'
}
}).component();
this._assessmentComponent = this._view.modelBuilder.flexContainer()
.withLayout({ height: '100%', flexFlow: 'column' })
.withProps({ CSSStyles: { 'margin-left': '30px' } })
.component();
this._assessmentComponent.addItem(this.createAssessmentProgress(), { flex: '0 0 auto' });
this._assessmentComponent.addItem(await this.createAssessmentInfo(), { flex: '0 0 auto' });
this._rootContainer = this._view.modelBuilder.flexContainer().withLayout({
height: '100%',
flexFlow: 'column'
}).withProps({
ariaLive: 'polite',
}).component();
this._rootContainer = this._view.modelBuilder.flexContainer()
.withLayout({ height: '100%', flexFlow: 'column' })
.withProps({ ariaLive: 'polite' })
.component();
this._rootContainer.addItem(this._assessmentComponent, { flex: '0 0 auto' });
this._rootContainer.addItem(this._formContainer.component(), { flex: '0 0 auto' });
this._disposables.push(this._view.onClosed(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 this._view.initializeModel(this._rootContainer);
}
private createStatusComponent(view: azdata.ModelView): azdata.TextComponent {
const component = view.modelBuilder.text().withProps({
const component = view.modelBuilder.text()
.withProps({
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-left': '8px'
@@ -239,16 +217,13 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private createDetailsComponent(view: azdata.ModelView): azdata.TextComponent {
const component = view.modelBuilder.text().withProps({
CSSStyles: {
...styles.BODY_CSS
}
}).component();
const component = view.modelBuilder.text()
.withProps({ CSSStyles: { ...styles.BODY_CSS } })
.component();
return component;
}
private async createChooseTargetComponent(view: azdata.ModelView): Promise<azdata.DivContainer> {
const chooseYourTargetText = this._view.modelBuilder.text().withProps({
value: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
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 (e.cardId === product.type) {
const skuRecommendationResultsDialog = new SkuRecommendationResultsDialog(this.migrationStateModel, product.type);
if (e.cardId === skuRecommendationResultsDialog._targetType) {
await skuRecommendationResultsDialog.openDialog(e.cardId, this.migrationStateModel._skuRecommendationResults.recommendations);
await skuRecommendationResultsDialog.openDialog(
e.cardId,
this.migrationStateModel._skuRecommendationResults.recommendations);
}
}
}));
@@ -352,63 +330,61 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._disposables.push(this._rbg.onSelectionChanged(async (value) => {
if (value) {
this.assessmentGroupContainer.display = 'inline';
await this.changeTargetType(value.cardId);
this.changeTargetType(value.cardId);
}
}));
this._rbgLoader = this._view.modelBuilder.loadingComponent().withItem(
this._rbg
).component();
this._rbgLoader = this._view.modelBuilder.loadingComponent()
.withItem(this._rbg)
.component();
const component = this._view.modelBuilder.divContainer().withItems(
[
chooseYourTargetText,
this._rbgLoader
]
).component();
const component = this._view.modelBuilder.divContainer()
.withItems([chooseYourTargetText, this._rbgLoader])
.component();
return component;
}
private async createViewAssessmentsContainer(): Promise<azdata.FlexContainer> {
this._viewAssessmentsHelperText = this._view.modelBuilder.text().withProps({
value: constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI,
CSSStyles: {
...styles.SECTION_HEADER_CSS
},
CSSStyles: { ...styles.SECTION_HEADER_CSS },
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
const button = this._view.modelBuilder.button().withProps({
label: constants.VIEW_SELECT_BUTTON_LABEL,
width: 100,
CSSStyles: {
'margin': '12px 0'
}
CSSStyles: { 'margin': '12px 0' }
}).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);
let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM);
const miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI);
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) => {
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) {
switch (this._rbg.selectedCardId) {
case MigrationTargetType.SQLVM:
this._rbg.selectedCardId = MigrationTargetType.SQLVM;
await vmDialog.openDialog();
} else if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) {
return await vmDialog.openDialog();
case 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({
CSSStyles: {
...styles.BODY_CSS,
},
ariaLive: 'polite'
this._databaseSelectedHelperText = this._view.modelBuilder.text()
.withProps({
CSSStyles: { ...styles.BODY_CSS },
ariaLive: 'polite',
}).component();
const container = this._view.modelBuilder.flexContainer().withItems([
const container = this._view.modelBuilder.flexContainer()
.withItems([
this._viewAssessmentsHelperText,
button,
this._databaseSelectedHelperText
@@ -418,9 +394,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
return container;
}
private async changeTargetType(newTargetType: string) {
private changeTargetType(newTargetType: string): void {
switch (newTargetType) {
case MigrationTargetType.SQLMI: {
case MigrationTargetType.SQLMI:
const miDbs = this.migrationStateModel._miDbs.filter(
db => this.migrationStateModel._databasesForAssessment.findIndex(
dba => dba === db) >= 0);
@@ -429,9 +405,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._targetType = MigrationTargetType.SQLMI;
this.migrationStateModel._databasesForMigration = miDbs;
break;
}
case MigrationTargetType.SQLVM: {
case MigrationTargetType.SQLVM:
const vmDbs = this.migrationStateModel._vmDbs.filter(
db => this.migrationStateModel._databasesForAssessment.findIndex(
dba => dba === db) >= 0);
@@ -440,10 +414,20 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._targetType = MigrationTargetType.SQLVM;
this.migrationStateModel._databasesForMigration = vmDbs;
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;
}
@@ -459,7 +443,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
const errors: string[] = [];
await this._setAssessmentState(true, false);
try {
await this.migrationStateModel.getDatabaseAssessments(MigrationTargetType.SQLMI);
await this.migrationStateModel.getDatabaseAssessments([MigrationTargetType.SQLMI, MigrationTargetType.SQLDB]);
const assessmentError = this.migrationStateModel._assessmentResults?.assessmentError;
if (assessmentError) {
errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`);
@@ -485,14 +469,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
} else {
this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration;
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 {
// use prior assessment results
this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration;
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) {
@@ -543,46 +529,36 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private async _setAssessmentState(assessing: boolean, failedAssessment: boolean): Promise<void> {
let display: azdata.DisplayType = assessing ? 'block' : 'none';
await this._assessmentComponent.updateCssStyles({ 'display': display });
this._assessmentComponent.display = display;
await utils.updateControlDisplay(this._assessmentComponent, assessing);
await utils.updateControlDisplay(this._skipAssessmentCheckbox, !assessing && failedAssessment);
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 this._skipAssessmentCheckbox.updateCssStyles({ 'display': display });
this._skipAssessmentCheckbox.display = display;
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';
await utils.updateControlDisplay(
this.assessmentGroupContainer,
this._rbg.selectedCardId !== undefined && (!failedAssessment || this._skipAssessmentCheckbox.checked === true));
this._assessmentLoader.loading = assessing;
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
this.wizard.registerNavigationValidator((pageChangeInfo) => {
const errors: string[] = [];
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
this.wizard.message = { text: '' };
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
const errors: string[] = [];
if (this._rbg.selectedCardId === undefined || this._rbg.selectedCardId === '') {
errors.push(constants.SELECT_TARGET_TO_CONTINUE);
}
@@ -620,9 +596,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
public async refreshCardText(showLoadingIcon: boolean = true): Promise<void> {
this._rbgLoader.loading = showLoadingIcon && true;
if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) {
switch (this._rbg.selectedCardId) {
case MigrationTargetType.SQLMI:
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;
}
@@ -650,21 +629,24 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.AZURE_RECOMMENDATION_CARD_IN_PROGRESS;
} 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;
switch (product.type) {
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()) {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlMiRecommendationResults[0];
// result returned but no SKU recommended
if (!recommendation.targetSku) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.SKU_RECOMMENDATION_NO_RECOMMENDATION;
if (!recommendation?.targetSku) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.SKU_RECOMMENDATION_NO_RECOMMENDATION;
}
else {
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
? constants.PREMIUM_SERIES
: 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;
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()) {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlVmRecommendationResults[0];
// result returned but no SKU recommended
if (!recommendation.targetSku) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.SKU_RECOMMENDATION_NO_RECOMMENDATION;
if (!recommendation?.targetSku) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.SKU_RECOMMENDATION_NO_RECOMMENDATION;
this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = '';
}
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 storageDisk = constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length);
const dataDisk = constants.STORAGE_CONFIGURATION(
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
? 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;
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;
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()) {
const 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);
const 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;
}
@@ -719,10 +722,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this._rbg.updateProperties({ cards: this._rbg.cards });
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> {
@@ -730,9 +733,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
// but updating the card text will do for now
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].textStyles = {
...styles.BODY_CSS,
};
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textStyles = { ...styles.BODY_CSS };
this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = '';
});
@@ -740,10 +741,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
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,
CSSStyles: {
...styles.PAGE_TITLE_CSS,
@@ -751,7 +753,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}).component();
this._progressContainer = this._view.modelBuilder.flexContainer().withLayout({
this._progressContainer = this._view.modelBuilder.flexContainer()
.withLayout({
height: '100%',
flexFlow: 'row',
alignItems: 'center'
@@ -763,7 +766,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
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),
CSSStyles: {
...styles.BODY_CSS,
@@ -774,14 +778,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private createAzureRecommendationContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
const container = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'column',
'max-width': '700px',
'margin-bottom': '1em',
}
}).component();
this._azureRecommendationSectionText = _view.modelBuilder.text().withProps({
this._azureRecommendationSectionText = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION,
description: '',
CSSStyles: {
@@ -789,14 +795,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin': '12px 0 8px',
}
}).component();
this._azureRecommendationInfoText = _view.modelBuilder.text().withProps({
this._azureRecommendationInfoText = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_STATUS_NOT_ENABLED,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0',
}
}).component();
const learnMoreLink = _view.modelBuilder.hyperlink().withProps({
const learnMoreLink = _view.modelBuilder.hyperlink()
.withProps({
label: constants.LEARN_MORE,
ariaLabel: constants.LEARN_MORE,
url: 'https://aka.ms/ads-sql-sku-recommend',
@@ -822,19 +830,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}).component();
const getAzureRecommendationDialog = new GetAzureRecommendationDialog(this, this.wizard, this.migrationStateModel);
this._disposables.push(this._getAzureRecommendationButton.onDidClick(async (e) => {
await getAzureRecommendationDialog.openDialog();
}));
this._disposables.push(this._getAzureRecommendationButton.onDidClick(
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([
azureRecommendationsInfoContainer,
this._getAzureRecommendationButton,
]);
this._getAzureRecommendationButton]);
this._skuDataCollectionStatusContainer = this.createPerformanceCollectionStatusContainer(_view);
this._skuEditParametersContainer = this.createSkuEditParameters(_view);
@@ -848,29 +853,27 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private createPerformanceCollectionStatusContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
const container = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'column',
'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block',
}
}).component();
this._skuDataCollectionStatusIcon = _view.modelBuilder.image().withProps({
this._skuDataCollectionStatusIcon = _view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.inProgressMigration,
iconHeight: 16,
iconWidth: 16,
width: 16,
height: 16,
CSSStyles: {
'margin-right': '4px',
}
CSSStyles: { 'margin-right': '4px' }
}).component();
this._skuDataCollectionStatusText = _view.modelBuilder.text().withProps({
this._skuDataCollectionStatusText = _view.modelBuilder.text()
.withProps({
value: '',
CSSStyles: {
...styles.BODY_CSS,
'margin': '0'
}
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
const statusIconTextContainer = _view.modelBuilder.flexContainer()
@@ -887,7 +890,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}).component();
this._skuDataCollectionTimerText = _view.modelBuilder.text().withProps({
this._skuDataCollectionTimerText = _view.modelBuilder.text()
.withProps({
value: '',
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
@@ -895,7 +899,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}).component();
this._skuStopDataCollectionButton = this._view.modelBuilder.button().withProps({
this._skuStopDataCollectionButton = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.cancel,
label: constants.STOP_PERFORMANCE_COLLECTION,
width: 150,
@@ -922,14 +927,14 @@ export class SKURecommendationPage extends MigrationWizardPage {
'display': this.migrationStateModel.performanceCollectionStopped() ? 'block' : 'none',
}
}).component();
this._disposables.push(this._skuRestartDataCollectionButton.onDidClick(async (e) => {
this._disposables.push(
this._skuRestartDataCollectionButton.onDidClick(async (e) => {
await this.migrationStateModel.startPerfDataCollection(
this.migrationStateModel._skuRecommendationPerformanceLocation,
this.migrationStateModel._performanceDataQueryIntervalInSeconds,
this.migrationStateModel._staticDataQueryIntervalInSeconds,
this.migrationStateModel._numberOfPerformanceDataQueryIterations,
this
);
this);
await this.refreshSkuRecommendationComponents();
}));
@@ -943,10 +948,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin': '0 0 0 12px',
}
}).component();
this._disposables.push(this._refreshAzureRecommendationButton.onDidClick(async (e) => {
await this.refreshAzureRecommendation();
}));
this._skuLastRefreshTimeText = this._view.modelBuilder.text().withProps({
this._disposables.push(
this._refreshAzureRecommendationButton.onDidClick(
async (e) => await this.refreshAzureRecommendation()));
this._skuLastRefreshTimeText = this._view.modelBuilder.text()
.withProps({
value: constants.LAST_REFRESHED_TIME(),
CSSStyles: {
...styles.SMALL_NOTE_CSS,
@@ -966,14 +973,13 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._skuStopDataCollectionButton,
this._skuRestartDataCollectionButton,
this._refreshAzureRecommendationButton,
this._skuLastRefreshTimeText,
]);
this._skuLastRefreshTimeText]);
container.addItems([
this._skuControlButtonsContainer,
statusIconTextContainer,
this._skuDataCollectionTimerText,
]);
this._skuDataCollectionTimerText]);
return container;
}
@@ -984,7 +990,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block',
}
}).component();
const recommendationParametersSection = _view.modelBuilder.text().withProps({
const recommendationParametersSection = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDATION_PARAMETERS,
CSSStyles: {
...styles.BODY_CSS,
@@ -992,7 +999,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}).component();
const editParametersButton = this._view.modelBuilder.button().withProps({
const editParametersButton = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.edit,
label: constants.EDIT_PARAMETERS,
width: 130,
@@ -1004,15 +1012,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}).component();
let skuEditParametersDialog = new SkuEditParametersDialog(this, this.migrationStateModel);
this._disposables.push(editParametersButton.onDidClick(async () => {
await skuEditParametersDialog.openDialog();
}));
this._disposables.push(
editParametersButton.onDidClick(
async () => await skuEditParametersDialog.openDialog()));
const createParameterGroup = (label: string, value: string): {
flexContainer: azdata.FlexContainer,
text: azdata.TextComponent,
} => {
const parameterGroup = this._view.modelBuilder.flexContainer().withProps({
const parameterGroup = this._view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'row',
'align-content': 'left',
@@ -1020,7 +1029,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin-right': '24px',
}
}).component();
const labelText = this._view.modelBuilder.text().withProps({
const labelText = this._view.modelBuilder.text()
.withProps({
value: label + ':',
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
@@ -1028,7 +1038,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
'margin-right': '4px',
}
}).component();
const valueText = this._view.modelBuilder.text().withProps({
const valueText = this._view.modelBuilder.text()
.withProps({
value: value,
CSSStyles: {
...styles.BODY_CSS,
@@ -1037,24 +1048,30 @@ export class SKURecommendationPage extends MigrationWizardPage {
}).component();
parameterGroup.addItems([
labelText,
valueText,
]);
valueText]);
return {
flexContainer: parameterGroup,
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;
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;
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;
const parametersContainer = _view.modelBuilder.flexContainer().withProps({
const parametersContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'margin': '8px 0',
'flex-direction': 'row',
@@ -1172,7 +1189,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
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 * as constants from '../constants/strings';
import { createLabelTextComponent, createHeadingTextComponent, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import { AuthenticationType } from '../api/sqlUtils';
export class SqlSourceConfigurationPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
@@ -59,10 +60,13 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query);
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 enterYourCredText = createLabelTextComponent(
this._view,
constants.ENTER_YOUR_SQL_CREDS,

View File

@@ -24,134 +24,204 @@ export class SummaryPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
this._flexContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._flexContainer = view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
const form = view.modelBuilder.formContainer()
.withFormItems(
[
{
component: this._flexContainer
}
]
);
.withFormItems([{ component: this._flexContainer }])
.component();
this._disposables.push(this._view.onClosed(e => {
this._disposables.push(
this._view.onClosed(e =>
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> {
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: '',
label: this.migrationStateModel._databasesForMigration?.length.toString(),
CSSStyles: {
...styles.BODY_CSS,
'margin': '0px',
'width': '300px',
}
label: (this.migrationStateModel._databasesForMigration?.length ?? 0).toString(),
CSSStyles: { ...styles.BODY_CSS, 'margin': '0px', 'width': '300px', }
}).component();
this._disposables.push(targetDatabaseHyperlink.onDidClick(async e => {
await targetDatabaseSummary.initialize();
}));
this._disposables.push(
targetDatabaseHyperlink.onDidClick(
async e => await targetDatabaseSummary.initialize()));
const targetDatabaseRow = this._view.modelBuilder.flexContainer()
.withLayout(
{
flexFlow: 'row',
alignItems: 'center',
})
.withItems(
[
createLabelTextComponent(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL,
{
...styles.BODY_CSS,
'width': '300px',
}
),
targetDatabaseHyperlink
],
{
CSSStyles: {
'margin-right': '5px'
}
})
.withLayout({ flexFlow: 'row', alignItems: 'center', })
.withItems([
createLabelTextComponent(
this._view,
constants.SUMMARY_DATABASE_COUNT_LABEL,
{ ...styles.BODY_CSS, 'width': '300px' }),
targetDatabaseHyperlink],
{ CSSStyles: { 'margin-right': '5px' } })
.component();
this._flexContainer.addItems(
[
await createHeadingTextComponent(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, true),
createInformationRow(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, this.migrationStateModel._azureAccount.displayInfo.displayName),
await createHeadingTextComponent(this._view, constants.SOURCE_DATABASES),
this._flexContainer
.addItems([
await createHeadingTextComponent(
this._view,
constants.SOURCE_DATABASES),
targetDatabaseRow,
await createHeadingTextComponent(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE),
createInformationRow(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_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, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)),
await createHeadingTextComponent(
this._view,
constants.AZURE_SQL_TARGET_PAGE_TITLE),
createInformationRow(
this._view,
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),
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),
if (this.migrationStateModel._targetType !== MigrationTargetType.SQLDB) {
this._flexContainer.addItems([
await createHeadingTextComponent(
this._view,
constants.DATABASE_BACKUP_PAGE_TITLE),
await this.createNetworkContainerRows()]);
}
await createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE),
await this.createNetworkContainerRows(),
this._flexContainer.addItems([
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) {
this._flexContainer.addItem(createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames.join(', ')));
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 (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> {
this._flexContainer.clearItems();
this.wizard.registerNavigationValidator(async (pageChangeInfo) => {
return true;
});
this.wizard.registerNavigationValidator(async (pageChangeInfo) => true);
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private async createNetworkContainerRows(): Promise<azdata.FlexContainer> {
const flexContainer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
const flexContainer = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
const networkShare = this.migrationStateModel._databaseBackup.networkShares[0];
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.NETWORK_SHARE:
flexContainer.addItems(
[
createInformationRow(this._view, constants.BACKUP_LOCATION, constants.NETWORK_SHARE),
createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.networkShares[0].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, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.location),
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.resourceGroup!),
createInformationRow(this._view, constants.STORAGE_ACCOUNT, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.name!),
]
);
flexContainer.addItems([
createInformationRow(
this._view,
constants.BACKUP_LOCATION,
constants.NETWORK_SHARE),
createInformationRow(
this._view,
constants.USER_ACCOUNT,
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;
case NetworkContainerType.BLOB_CONTAINER:
flexContainer.addItems(
[
createInformationRow(this._view, constants.TYPE, constants.BLOB_CONTAINER),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name)
]
);
flexContainer.addItems([
createInformationRow(
this._view,
constants.TYPE,
constants.BLOB_CONTAINER),
createInformationRow(
this._view,
constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION,
this.migrationStateModel._databaseBackup.subscription.name)]);
}
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 { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
import { azureResource } from 'azurecore';
import { ServiceContextChangeEvent } from '../dashboard/tabBase';
export const WIZARD_INPUT_COMPONENT_WIDTH = '600px';
export class WizardController {
@@ -27,7 +28,7 @@ export class WizardController {
constructor(
private readonly extensionContext: vscode.ExtensionContext,
private readonly _model: MigrationStateModel,
private readonly _onClosedCallback: () => Promise<void>) {
private readonly _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
}
public async openWizard(connectionId: string): Promise<void> {
@@ -40,7 +41,11 @@ export class WizardController {
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
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.hidden = true;
const saveAndCloseButton = azdata.window.createButton(loc.SAVE_AND_CLOSE);
@@ -60,8 +65,7 @@ export class WizardController {
migrationModePage,
databaseBackupPage,
integrationRuntimePage,
summaryPage
];
summaryPage];
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
// 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));
} else {
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 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[newPage]?.onPageEnter(pageChangeInfo);
}));
@@ -110,11 +120,11 @@ export class WizardController {
await Promise.all(wizardSetupPromises);
this._model.extensionContext.subscriptions.push(
this._wizardObject.onPageChanged(
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
await pages[0].onPageEnter(pageChangeInfo);
}));
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) =>
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 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(
TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick,
@@ -131,17 +142,20 @@ export class WizardController {
...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Cancel,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, {});
},
{});
}));
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
this._disposables.push(
this._wizardObject.doneButton.onClick(async (e) => {
try {
await stateModel.startMigration();
await this.updateServiceContext(stateModel);
await this._onClosedCallback();
await this.updateServiceContext(stateModel, this._serviceContextChangedEvent);
} catch (e) {
logError(TelemetryViews.MigrationWizardController, 'StartMigrationFailed', e);
} finally {
sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick,
@@ -149,11 +163,16 @@ export class WizardController {
...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Done,
'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(
stateModel._resourceGroups,
stateModel._sqlMigrationService?.properties.resourceGroup);
@@ -174,18 +193,28 @@ export class WizardController {
location: location,
resourceGroup: resourceGroup,
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);
}
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);
}
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/');
if (parts?.length && parts?.length > 1) {
parts = parts[1]?.split('/resourcegroups/');
@@ -198,7 +227,9 @@ export class WizardController {
}
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;
sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard,
@@ -207,7 +238,8 @@ export class WizardController {
...this.getTelemetryProps(),
'buttonPressed': buttonPressed,
'pageTitle': pageTitle
}, {});
},
{});
}
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()
.withLayout(
{
flexFlow: 'row',
alignItems: 'center',
})
.withItems(
[
createLabelTextComponent(view, label,
.withLayout({ flexFlow: 'row', alignItems: 'center', })
.withItems([
createLabelTextComponent(
view,
label,
{
...styles.BODY_CSS,
'margin': '4px 0px',
'width': '300px',
}
),
createTextComponent(view, value,
}),
createTextComponent(
view,
value,
{
...styles.BODY_CSS,
'margin': '4px 0px',
'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);
await component.updateCssStyles({
...styles.LABEL_CSS,
@@ -256,14 +293,20 @@ export async function createHeadingTextComponent(view: azdata.ModelView, value:
return component;
}
export function createLabelTextComponent(view: azdata.ModelView, value: string, styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
const component = createTextComponent(view, value, styles);
return component;
export function createLabelTextComponent(
view: azdata.ModelView,
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 {
return view.modelBuilder.text().withProps({
value: value,
CSSStyles: styles
}).component();
export function createTextComponent(
view: azdata.ModelView,
value: string,
styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
return view.modelBuilder.text()
.withProps({ value: value, CSSStyles: styles })
.component();
}