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) => {
return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(mi.id),
name: azure.getResourceGroupFromId(mi.id),
subscription: {
id: mi.subscriptionId
},
tenant: mi.tenantId
};
});
}
} catch (e) {
logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceResourceGroups', e);
if (resources && location) {
const locationName = location.name.toLowerCase();
resourceGroups = resources
.filter(resource => resource.location.toLowerCase() === locationName)
.map(resource => {
return <azureResource.AzureResourceResourceGroup>{
id: azure.getFullResourceGroupFromId(resource.id),
name: azure.getResourceGroupFromId(resource.id),
subscription: { id: getSubscriptionIdFromResourceId(resource.id) },
tenant: resource.tenantId
};
});
}
// 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
});
});
if (lastFileNamesValues.length === 0) {
lastFileNamesValues = [
{
displayName: constants.NO_BLOBFILES_FOUND,
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 };
});
}
return lastFileNamesValues;
return [{ name: '', displayName: resourceNotFoundMessage }];
}
export function getResourceDropdownValues(resources: { id: string, name: string }[], resourceNotFoundMessage: string): CategoryValue[] {
return resources?.map(resource => { return { name: resource.id, displayName: resource.name }; })
|| [{ name: '', displayName: resourceNotFoundMessage }];
}
export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise<CategoryValue[]> {
return tenants?.map(tenant => { return { name: tenant.id, displayName: tenant.displayName }; })
|| [{ name: '', displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR }];
}
export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise<CategoryValue[]> {
return locations?.map(location => { return { name: location.name, displayName: location.displayName }; })
|| [{ name: '', displayName: constants.NO_LOCATION_FOUND }];
}
export async function getBlobLastBackupFileNamesValues(blobs: azureResource.Blob[]): Promise<CategoryValue[]> {
return blobs?.map(blob => { return { name: blob.name, displayName: blob.name }; })
|| [{ name: '', displayName: constants.NO_BLOBFILES_FOUND }];
}
export async function updateControlDisplay(control: Component, visible: boolean, displayStyle: DisplayType = 'inline'): Promise<void> {
const display = visible ? displayStyle : 'none';
control.display = display;
await control.updateCssStyles({ 'display': display });
await control.updateProperties({ 'display': display });
}
export function generateGuid(): string {
const hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
let oct: string = '';
let tmp: number;
/* tslint:disable:no-bitwise */
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
}
// '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'),
@@ -794,7 +921,7 @@ export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration
export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source credentials");
export const ENTER_YOUR_SQL_CREDS = localize('sql.migration.enter.your.sql.cred', "Enter the credentials for the source SQL Server instance. These credentials will be used while migrating databases to Azure SQL.");
export const SERVER = localize('sql.migration.server', "Server");
export const USERNAME = localize('sql.migration.username', "Username");
export const USERNAME = localize('sql.migration.username', "User name");
export const SIZE = localize('sql.migration.size', "Size (MB)");
export const LAST_BACKUP = localize('sql.migration.last.backup', "Last backup");
export const DATABASE_MIGRATE_TEXT = localize('sql.migrate.text', "Select the databases that you want to migrate to Azure SQL.");
@@ -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,53 +79,55 @@ 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();
const migrations = await getCurrentMigrations();
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
let warningCount = 0;
for (let i = 0; i < inProgressMigrations.length; i++) {
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount += 1;
}
}
if (warningCount > 0) {
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
this._inProgressMigrationButton.container.display = 'none';
this._inProgressWarningMigrationButton.container.display = '';
} else {
this._inProgressMigrationButton.container.display = '';
this._inProgressWarningMigrationButton.container.display = 'none';
}
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
await this._updateSummaryStatus();
} 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;
}
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
let warningCount = 0;
for (let i = 0; i < inProgressMigrations.length; i++) {
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount += 1;
}
}
if (warningCount > 0) {
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
this._inProgressMigrationButton.container.display = 'none';
this._inProgressWarningMigrationButton.container.display = '';
} else {
this._inProgressMigrationButton.container.display = '';
this._inProgressWarningMigrationButton.container.display = 'none';
}
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
await this._updateSummaryStatus();
this.isRefreshing = false;
this._migrationStatusCardLoadingContainer.loading = false;
}
protected async initialize(view: azdata.ModelView): Promise<void> {
@@ -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,126 +69,128 @@ 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;
}
this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
await this._fileTable.updateProperty('data', []);
try {
this.isRefreshing = true;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
await this._fileTable.updateProperty('data', []);
await this.model.fetchStatus();
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
await this.showMigrationErrors(migration);
const sqlServerName = migration.properties.sourceServerName;
const sourceDatabaseName = migration.properties.sourceDatabaseName;
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration.name;
const targetServerName = getResourceName(migration.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
let lastAppliedSSN: string;
let lastAppliedBackupFileTakenOn: string;
const tableData: ActiveBackupFileSchema[] = [];
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
(activeBackupSet) => {
tableData.push(
...activeBackupSet.listOfBackupFiles.map(f => {
return {
fileName: f.fileName,
type: activeBackupSet.backupType,
status: f.status,
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
};
})
);
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
const backupLocation = fileShare?.path! ?? EmptySettingValue;
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
if (tableData.length === 0) {
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
this._fileTable.height = '50px';
await this._fileTable.updateProperty('data', []);
} else {
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
this._fileTable.height = '300px';
// Sorting files in descending order of backupStartTime
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
}
const data = tableData.map(row => [
row.fileName,
row.type,
row.status,
row.dataUploaded,
row.copyThroughput,
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
row.firstLSN,
row.lastLSN
]) || [];
await this._fileTable.updateProperty('data', data);
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
} finally {
this.refreshLoader.loading = false;
this.isRefreshing = false;
}
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
await this.showMigrationErrors(migration);
const sqlServerName = migration.properties.sourceServerName;
const sourceDatabaseName = migration.properties.sourceDatabaseName;
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration.name;
const targetServerName = getResourceName(migration.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
let lastAppliedSSN: string;
let lastAppliedBackupFileTakenOn: string;
const tableData: ActiveBackupFileSchema[] = [];
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
(activeBackupSet) => {
tableData.push(
...activeBackupSet.listOfBackupFiles.map(f => {
return {
fileName: f.fileName,
type: activeBackupSet.backupType,
status: f.status,
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
};
})
);
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
const backupLocation = fileShare?.path! ?? EmptySettingValue;
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
if (tableData.length === 0) {
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
this._fileTable.height = '50px';
await this._fileTable.updateProperty('data', []);
} else {
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
this._fileTable.height = '300px';
// Sorting files in descending order of backupStartTime
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
}
const data = tableData.map(row => [
row.fileName,
row.type,
row.status,
row.dataUploaded,
row.copyThroughput,
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
row.firstLSN,
row.lastLSN
]) || [];
await this._fileTable.updateProperty('data', data);
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
this.isRefreshing = false;
this.refreshLoader.loading = false;
this.refreshButton.enabled = true;
}
protected async initialize(view: azdata.ModelView): Promise<void> {

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;
}
this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
try {
this.isRefreshing = true;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
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);
} finally {
this.refreshLoader.loading = false;
this.isRefreshing = false;
}
this.isRefreshing = false;
this.refreshLoader.loading = false;
this.refreshButton.enabled = true;
}
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;
}
this.isRefreshing = true;
this._refresh.enabled = false;
this._refreshLoader.loading = true;
await this.statusBar.clearError();
try {
this.isRefreshing = true;
this._refreshLoader.loading = true;
await this.statusBar.clearError();
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,25 +554,26 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
]
}).component();
this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState;
const migration = this._filteredMigrations[rowState.row];
switch (buttonState?.column) {
case 2:
const status = getMigrationStatus(migration);
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
const errors = this.getMigrationErrors(migration!);
this.disposables.push(
this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState;
const migration = this._filteredMigrations[rowState.row];
switch (buttonState?.column) {
case 2:
const status = getMigrationStatus(migration);
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
const errors = this.getMigrationErrors(migration!);
this.showDialogMessage(
loc.DATABASE_MIGRATION_STATUS_TITLE,
statusMessage,
errors);
break;
case 0:
await this._openMigrationDetails(migration);
break;
}
}));
this.showDialogMessage(
loc.DATABASE_MIGRATION_STATUS_TITLE,
statusMessage,
errors);
break;
case 0:
await this._openMigrationDetails(migration);
break;
}
}));
return this._statusTable;
}

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();
}));
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()));
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();
}
}));
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();
}
private async _registerCommands(): Promise<void> {
this._context.subscriptions.push(
vscode.commands.registerCommand(
MenuCommands.Cutover,
async (args: MenuCommandArgs) => {
try {
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);
public async onDialogClosed(): Promise<void> {
await this._dashboardTab.onDialogClosed();
await this._migrationsTab.onDialogClosed();
}
private _errorDialogIsOpen: boolean = false;
protected async openErrorDialog(): Promise<void> {
if (this._errorDialogIsOpen) {
return;
}
try {
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();
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({
label: constants.START_NEW_SESSION,
name: buttonGroup,
CSSStyles: {
...styles.BODY_CSS,
'margin-bottom': '8px'
},
checked: true
}).component();
const radioStart = view.modelBuilder.radioButton()
.withProps({
label: constants.START_NEW_SESSION,
name: buttonGroup,
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '8px' },
checked: true
}).component();
this._disposables.push(radioStart.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = false;
}
}));
const radioContinue = view.modelBuilder.radioButton().withProps({
label: constants.RESUME_SESSION,
name: buttonGroup,
CSSStyles: {
...styles.BODY_CSS,
},
checked: false
}).component();
this._disposables.push(
radioStart.onDidChangeCheckedState(checked => {
if (checked) {
this.stateModel.resumeAssessment = false;
}
}));
const radioContinue = view.modelBuilder.radioButton()
.withProps({
label: constants.RESUME_SESSION,
name: buttonGroup,
CSSStyles: { ...styles.BODY_CSS },
checked: false
}).component();
this._disposables.push(radioContinue.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = true;
}
}));
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'
}
}).component();
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,36 +545,47 @@ export class SqlDatabaseTree {
this.refreshImpactedObject(impactedObject);
}));
const objectDetailsTitle = this._view.modelBuilder.text().withProps({
value: constants.OBJECT_DETAILS,
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin': '12px 0px 0px 0px',
}
}).component();
const objectDetailsTitle = this._view.modelBuilder.text()
.withProps({
value: constants.OBJECT_DETAILS,
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin': '12px 0px 0px 0px',
}
}).component();
const objectDescriptionStyle = {
...styles.BODY_CSS,
'margin': '5px 0px 0px 0px',
'word-wrap': 'break-word'
};
this._objectDetailsType = this._view.modelBuilder.text().withProps({
value: constants.TYPES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsType = this._view.modelBuilder.text()
.withProps({
value: constants.TYPES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsName = this._view.modelBuilder.text().withProps({
value: constants.NAMES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsName = this._view.modelBuilder.text()
.withProps({
value: constants.NAMES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsSample = this._view.modelBuilder.text().withProps({
value: '',
CSSStyles: objectDescriptionStyle
}).component();
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,76 +601,91 @@ export class SqlDatabaseTree {
'width': '200px',
'word-wrap': 'break-word'
};
const descriptionTitle = this._view.modelBuilder.text().withProps({
value: constants.DESCRIPTION,
CSSStyles: LABEL_CSS
}).component();
this._descriptionText = this._view.modelBuilder.text().withProps({
CSSStyles: textStyle
}).component();
const descriptionTitle = this._view.modelBuilder.text()
.withProps({
value: constants.DESCRIPTION,
CSSStyles: LABEL_CSS
}).component();
this._descriptionText = this._view.modelBuilder.text()
.withProps({
CSSStyles: textStyle
}).component();
const recommendationTitle = this._view.modelBuilder.text().withProps({
value: constants.RECOMMENDATION,
CSSStyles: LABEL_CSS
}).component();
this._recommendationText = this._view.modelBuilder.text().withProps({
CSSStyles: textStyle
}).component();
const moreInfo = this._view.modelBuilder.text().withProps({
value: constants.MORE_INFO,
CSSStyles: LABEL_CSS
}).component();
this._moreInfo = this._view.modelBuilder.hyperlink().withProps({
label: '',
url: '',
CSSStyles: textStyle,
ariaLabel: constants.MORE_INFO,
showLinkIcon: true
}).component();
const recommendationTitle = this._view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDATION,
CSSStyles: LABEL_CSS
}).component();
this._recommendationText = this._view.modelBuilder.text()
.withProps({
CSSStyles: textStyle
}).component();
const moreInfo = this._view.modelBuilder.text()
.withProps({
value: constants.MORE_INFO,
CSSStyles: LABEL_CSS
}).component();
this._moreInfo = this._view.modelBuilder.hyperlink()
.withProps({
label: '',
url: '',
CSSStyles: textStyle,
ariaLabel: constants.MORE_INFO,
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({
value: '',
CSSStyles: {
...styles.LABEL_CSS,
'margin-top': '12px',
'height': '48px',
'width': '540px',
'border-bottom': 'solid 1px'
}
}).component();
this._assessmentTitle = this._view.modelBuilder.text()
.withProps({
value: '',
CSSStyles: {
...styles.LABEL_CSS,
'margin-top': '12px',
'height': '48px',
'width': '540px',
'border-bottom': 'solid 1px'
}
}).component();
return this._assessmentTitle;
}
private createTitleComponent(): azdata.TextComponent {
const title = this._view.modelBuilder.text().withProps({
value: constants.TARGET_PLATFORM,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0 0 4px 0'
}
});
return title.component();
return this._view.modelBuilder.text()
.withProps({
value: constants.TARGET_PLATFORM,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0 0 4px 0'
}
}).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: '',
@@ -758,21 +764,21 @@ export class SqlDatabaseTree {
}
]
}
).component();
).component();
this._disposables.push(this._assessmentResultsTable.onRowSelected(async (e) => {
const selectedIssue = e.row > -1 ? this._activeIssues[e.row] : undefined;
await this.refreshAssessmentDetails(selectedIssue);
}));
const container = this._view.modelBuilder.flexContainer().withItems([this._assessmentResultsTable]).withLayout({
flexFlow: 'column',
height: '100%'
}).withProps({
CSSStyles: {
'border-right': 'solid 1px'
}
}).component();
const container = this._view.modelBuilder.flexContainer()
.withItems([this._assessmentResultsTable])
.withLayout({
flexFlow: 'column',
height: '100%'
})
.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,8 +855,9 @@ export class SqlDatabaseTree {
this._impactedObjects = selectedIssue?.impactedObjects || [];
this._recommendationText.value = selectedIssue?.message || constants.NA;
await this._impactedObjectsTable.setDataValues(this._impactedObjects.map(
(object) => [{ value: object.objectType }, { value: object.name }]));
await this._impactedObjectsTable.setDataValues(
this._impactedObjects.map(
(object) => [{ value: object.objectType }, { value: object.name }]));
this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1;
}
@@ -884,56 +872,55 @@ 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
},
{
value: '0',
style: styleRight
}
]];
this._dbNames.forEach((db) => {
this._databaseTableValues.push([
{
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
value: selectedDbs.includes(db),
style: styleLeft
},
{
value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, db),
style: styleLeft
},
{
value: '0',
style: styleRight
}
]
];
this._dbNames.forEach((db) => {
this._databaseTableValues.push(
[
{
value: selectedDbs.includes(db),
style: styleLeft
},
{
value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, db),
style: styleLeft
},
{
value: '0',
style: styleRight
}
]
);
]);
});
} else {
instanceTableValues = [
[
{
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
style: styleLeft
},
{
value: this._model._assessmentResults?.issues?.length,
style: styleRight
}
]
];
this._model._assessmentResults?.databaseAssessments.sort((db1, db2) => {
return db2.issues?.length - db1.issues?.length;
});
instanceTableValues = [[
{
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
style: styleLeft
},
{
value: this._model._assessmentResults?.issues?.length,
style: styleRight
}
]];
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,23 +928,21 @@ export class SqlDatabaseTree {
if (db.issues.find(item => item.databaseRestoreFails)) {
selectable = false;
}
this._databaseTableValues.push(
[
{
value: selectedDbs.includes(db.name),
style: styleLeft,
enabled: selectable
},
{
value: this.createIconTextCell((selectable) ? IconPathHelper.sqlDatabaseLogo : IconPathHelper.sqlDatabaseWarningLogo, db.name),
style: styleLeft
},
{
value: db.issues?.length,
style: styleRight
}
]
);
this._databaseTableValues.push([
{
value: selectedDbs.includes(db.name),
style: styleLeft,
enabled: selectable
},
{
value: this.createIconTextCell((selectable) ? IconPathHelper.sqlDatabaseLogo : IconPathHelper.sqlDatabaseWarningLogo, db.name),
style: styleLeft
},
{
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,20 +156,21 @@ export class ConfirmCutoverDialog {
height: 20,
label: constants.REFRESH,
}).component();
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
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) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
container.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
@@ -232,22 +233,23 @@ export class ConfirmCutoverDialog {
headingRow.addItem(containerHeading, { flex: '0' });
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date()));
this.refreshFileTable(fileTable);
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
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()));
this.refreshFileTable(fileTable);
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
headingRow.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({

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,93 +111,86 @@ 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);
}
}));
const openExistingButton = _view.modelBuilder.radioButton()
.withProps({
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([
chooseMethodText,
radioButtonContainer,
this._openExistingContainer,
this._collectDataContainer,
]).component();
const container = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
chooseMethodText,
radioButtonContainer,
this._openExistingContainer,
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({
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
CSSStyles: {
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component();
const instructions = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
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({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 320,
CSSStyles: {
'margin-right': '12px'
},
}).component();
this._disposables.push(this._collectDataFolderInput.onTextChanged(async (value) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
this._collectDataFolderInput = _view.modelBuilder.inputBox()
.withProps({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 320,
CSSStyles: { 'margin-right': '12px' },
}).component();
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({
label: constants.BROWSE,
width: 100,
CSSStyles: {
'margin': '0'
}
}).component();
const browseButton = _view.modelBuilder.button()
.withProps({
label: constants.BROWSE,
width: 100,
CSSStyles: { 'margin': '0' }
}).component();
this._disposables.push(browseButton.onDidClick(async (e) => {
let folder = await utils.promptUserForFolder();
this._collectDataFolderInput.value = folder;
@@ -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({
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
CSSStyles: {
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component();
const instructions = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
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) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
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({
label: constants.OPEN,
width: 100,
CSSStyles: {
'margin': '0'
}
}).component();
this._disposables.push(openButton.onDidClick(async (e) => {
let folder = await utils.promptUserForFolder();
this._openExistingFolderInput.value = folder;
}));
const openButton = _view.modelBuilder.button()
.withProps({
label: constants.OPEN,
width: 100,
CSSStyles: { 'margin': '0' }
}).component();
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,24 +261,22 @@ 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;
}
@@ -306,27 +284,32 @@ export class GetAzureRecommendationDialog {
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.forEach(
d => { try { d.dispose(); } catch { } });
}));
this._disposables.push(
view.onClosed(e =>
this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
await view.initializeModel(flex);
resolve();
@@ -50,56 +53,50 @@ 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({
value: constants.EDIT_PARAMETERS_TEXT,
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
const description = _view.modelBuilder.text()
.withProps({
value: constants.EDIT_PARAMETERS_TEXT,
CSSStyles: { ...styles.BODY_CSS }
})
.component();
const WIZARD_INPUT_COMPONENT_WIDTH = '300px';
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
}
}).component();
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',
},
}).withValidation(c => {
if (Number(c.value) && Number(c.value) > 0) {
return true;
}
return false;
}).component();
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 }
}).component();
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' },
}).withValidation(c => {
if (Number(c.value) && Number(c.value) > 0) {
return true;
}
return false;
}).component();
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,
}
}).component();
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 }
}).component();
const createPercentageValues = () => {
let values: azdata.CategoryValue[] = [];
const values: azdata.CategoryValue[] = [];
TARGET_PERCENTILE_VALUES.forEach(n => {
const val = n.toString();
values.push({
@@ -109,27 +106,27 @@ export class SkuEditParametersDialog {
});
return values;
};
this._targetPercentileDropdown = _view.modelBuilder.dropDown().withProps({
values: createPercentageValues(),
ariaLabel: constants.PERCENTAGE_UTILIZATION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: false,
required: true,
fireOnTextChange: true,
CSSStyles: {
'margin-top': '-1em',
'margin-bottom': '8px',
},
}).component();
this._targetPercentileDropdown = _view.modelBuilder.dropDown()
.withProps({
values: createPercentageValues(),
ariaLabel: constants.PERCENTAGE_UTILIZATION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: false,
required: true,
fireOnTextChange: true,
CSSStyles: {
'margin-top': '-1em',
'margin-bottom': '8px',
},
}).component();
const enablePreviewLabel = _view.modelBuilder.text().withProps({
value: constants.ENABLE_PREVIEW_SKU,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS,
}
}).component();
const enablePreviewLabel = _view.modelBuilder.text()
.withProps({
value: constants.ENABLE_PREVIEW_SKU,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS, }
}).component();
const buttonGroup = 'enablePreviewSKUs';
const enablePreviewRadioButtonContainer = _view.modelBuilder.flexContainer()
.withProps({
@@ -151,11 +148,12 @@ export class SkuEditParametersDialog {
'margin': '0'
},
}).component();
this._disposables.push(enablePreviewButton.onDidChangeCheckedState(async (e) => {
if (e) {
this._enablePreviewValue = true;
}
}));
this._disposables.push(
enablePreviewButton.onDidChangeCheckedState(async checked => {
if (checked) {
this._enablePreviewValue = true;
}
}));
const disablePreviewButton = _view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
@@ -167,23 +165,21 @@ export class SkuEditParametersDialog {
'margin': '0 12px',
}
}).component();
this._disposables.push(disablePreviewButton.onDidChangeCheckedState(async (e) => {
if (e) {
this._enablePreviewValue = false;
}
}));
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,84 +125,73 @@ 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({
value: constants.STORAGE_HEADER,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
}).component();
const storageValue = _view.modelBuilder.text().withProps({
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
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({
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
CSSStyles: { ...styles.BODY_CSS, }
}).component();
storageSection.addItems([
storageLabel,
storageValue,
]);
storageValue]);
}
break;
}
const recommendationContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'margin-bottom': '20px',
'flex-direction': 'column',
}
}).component();
if (this._targetType === MigrationTargetType.SQLDB) {
const databaseNameLabel = _view.modelBuilder.text().withProps({
value: recommendation.databaseName!,
const recommendationContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-bottom': '20px',
'flex-direction': 'column',
}
}).component();
if (this._targetType === MigrationTargetType.SQLDB) {
const databaseNameLabel = _view.modelBuilder.text()
.withProps({
value: recommendation.databaseName!,
CSSStyles: { ...styles.SECTION_HEADER_CSS, }
}).component();
recommendationContainer.addItem(databaseNameLabel);
}
const targetDeploymentTypeLabel = _view.modelBuilder.text().withProps({
value: constants.TARGET_DEPLOYMENT_TYPE,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '0',
}
}).component();
const targetDeploymentTypeValue = _view.modelBuilder.text().withProps({
value: this.targetName,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0',
}
}).component();
const targetDeploymentTypeLabel = _view.modelBuilder.text()
.withProps({
value: constants.TARGET_DEPLOYMENT_TYPE,
CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', }
}).component();
const targetDeploymentTypeValue = _view.modelBuilder.text()
.withProps({
value: this.targetName,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
}).component();
const azureConfigurationLabel = _view.modelBuilder.text().withProps({
value: constants.AZURE_CONFIGURATION,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
}).component();
const azureConfigurationValue = _view.modelBuilder.text().withProps({
value: configuration,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0',
}
}).component();
const azureConfigurationLabel = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_CONFIGURATION,
CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0', }
}).component();
const azureConfigurationValue = _view.modelBuilder.text()
.withProps({
value: configuration,
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,26 +224,23 @@ 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({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
}).component();
const recommendedTargetStorageInfo = _view.modelBuilder.text().withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
const recommendedTargetStorageSection = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
}).component();
const recommendedTargetStorageInfo = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
CSSStyles: { ...styles.BODY_CSS, }
}).component();
const headerCssStyle = {
'border': 'none',
@@ -333,20 +319,21 @@ export class SkuRecommendationResultsDialog {
logDiskTableRow,
];
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storageConfigurationTableRows,
width: 700
}).component();
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([
recommendedTargetStorageSection,
recommendedTargetStorageInfo,
storageConfigurationTable,
]).component();
const container = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
recommendedTargetStorageSection,
recommendedTargetStorageInfo,
storageConfigurationTable])
.component();
return container;
}
@@ -375,19 +362,16 @@ 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({
value: constants.SOURCE_PROPERTIES,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
}).component();
const storagePropertiesSection = _view.modelBuilder.text()
.withProps({
value: constants.SOURCE_PROPERTIES,
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
}).component();
const headerCssStyle = {
'border': 'none',
@@ -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({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storagePropertiesTableRows,
width: 300
}).component();
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,22 +25,20 @@ 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({
value: constants.COUNT_DATABASES(this._model._databasesForMigration.length),
CSSStyles: {
...styles.BODY_CSS,
'margin-bottom': '20px'
}
}).component();
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' }
}).component();
const headerCssStyle = {
'border': 'none',
@@ -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,46 +75,59 @@ export class TargetDatabaseSummaryDialog {
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}
];
}];
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
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,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle,
hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE
});
} else if (isSqlDbMigration) {
columns.push({
valueType: azdata.DeclarativeDataType.string,
displayName: constants.LOCATION,
displayName: constants.TARGET_TABLE_COUNT_NAME,
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,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle,
hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE
});
} else {
columns.push({
@@ -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({
ariaLabel: constants.DATABASE_TO_BE_MIGRATED,
columns: columns,
dataValues: tableRows,
width: this._tableLength
}).component();
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 form = this._view.modelBuilder.formContainer()
.withFormItems(
[{ component: container }],
{ horizontal: false })
.withLayout({ width: '100%' })
.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();
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 () {
await classVariable.refreshPerfDataCollection();
if (await classVariable.isWaitingForFirstTimeRefresh()) {
await page.refreshSkuRecommendationComponents(); // update timer
}
}, refreshIntervalInMs);
this._autoRefreshPerfDataCollectionHandle = setInterval(
async function () {
await classVariable.refreshPerfDataCollection();
if (await classVariable.isWaitingForFirstTimeRefresh()) {
await page.refreshSkuRecommendationComponents(); // update timer
}
},
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 () {
await page.refreshAzureRecommendation();
}, this.refreshGetSkuRecommendationFrequency);
this._autoRefreshGetSkuRecommendationHandle = setTimeout(
async function () {
await page.refreshAzureRecommendation();
},
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,
this._targetSubscription,
<SqlManagedInstance>this._targetServerInstance)).map(t => t.name);
return (
await getSqlManagedInstanceDatabases(this._azureAccount,
this._targetSubscription,
<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.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> {
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,327 +93,325 @@ 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({
value: constants.IR_PAGE_DESCRIPTION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
...styles.BODY_CSS,
'margin-bottom': '16px'
}
}).component();
const descriptionText = this._view.modelBuilder.text()
.withProps({
value: constants.IR_PAGE_DESCRIPTION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '16px' }
}).component();
const subscriptionLabel = this._view.modelBuilder.text().withProps({
value: constants.SUBSCRIPTION,
CSSStyles: {
...styles.LABEL_CSS
}
}).component();
this._subscription = this._view.modelBuilder.text().withProps({
enabled: false,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'margin': '0'
}
}).component();
const subscriptionLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SUBSCRIPTION,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
this._subscription = this._view.modelBuilder.text()
.withProps({
enabled: false,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { 'margin': '0' }
}).component();
const locationLabel = this._view.modelBuilder.text().withProps({
value: constants.LOCATION,
CSSStyles: {
...styles.LABEL_CSS,
'margin-top': '1em'
}
}).component();
this._location = this._view.modelBuilder.text().withProps({
enabled: false,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'margin': '0'
}
}).component();
const locationLabel = this._view.modelBuilder.text()
.withProps({
value: constants.LOCATION,
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '1em' }
}).component();
this._location = this._view.modelBuilder.text()
.withProps({
enabled: false,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { 'margin': '0' }
}).component();
const resourceGroupLabel = this._view.modelBuilder.text().withProps({
value: constants.RESOURCE_GROUP,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS
}
}).component();
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'
}
}).component();
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();
}
}));
const resourceGroupLabel = this._view.modelBuilder.text()
.withProps({
value: constants.RESOURCE_GROUP,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
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' }
}).component();
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!;
this.populateDms();
}
}));
const migrationServiceDropdownLabel = this._view.modelBuilder.text().withProps({
value: constants.IR_PAGE_TITLE,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS
}
}).component();
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'
}
}).component();
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());
if (selectedDms) {
this.migrationStateModel._sqlMigrationService = selectedDms;
await this.loadMigrationServiceStatus();
}
} else {
this.migrationStateModel._sqlMigrationService = undefined;
this._dmsInfoContainer.display = 'none';
}
}));
const migrationServiceDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.IR_PAGE_TITLE,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
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' }
}).component();
this._disposables.push(
this._dmsDropdown.onValueChanged(
async (value) => {
if (value && value !== 'undefined' && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) {
this.wizard.message = { text: '' };
const createNewMigrationService = this._view.modelBuilder.hyperlink().withProps({
label: constants.CREATE_NEW,
ariaLabel: constants.CREATE_NEW_MIGRATION_SERVICE,
url: '',
CSSStyles: {
...styles.BODY_CSS
}
}).component();
await utils.updateControlDisplay(
this._dmsInfoContainer,
this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE);
this._disposables.push(createNewMigrationService.onDidClick(async (e) => {
const dialog = new CreateSqlMigrationServiceDialog();
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();
}));
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.loadStatus();
}
} else {
this.migrationStateModel._sqlMigrationService = undefined;
await utils.updateControlDisplay(this._dmsInfoContainer, false);
}
}));
const flexContainer = this._view.modelBuilder.flexContainer().withItems([
descriptionText,
subscriptionLabel,
this._subscription,
locationLabel,
this._location,
resourceGroupLabel,
this._resourceGroupDropdown,
migrationServiceDropdownLabel,
this._dmsDropdown,
createNewMigrationService
]).withLayout({
flexFlow: 'column'
}).component();
return flexContainer;
const createNewMigrationService = this._view.modelBuilder.hyperlink()
.withProps({
label: constants.CREATE_NEW,
ariaLabel: constants.CREATE_NEW_MIGRATION_SERVICE,
url: '',
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._disposables.push(
createNewMigrationService.onDidClick(
async (e) => {
const dialog = new CreateSqlMigrationServiceDialog();
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();
this.populateDms();
}));
return this._view.modelBuilder.flexContainer()
.withItems([
descriptionText,
subscriptionLabel,
this._subscription,
locationLabel,
this._location,
resourceGroupLabel,
this._resourceGroupDropdown,
migrationServiceDropdownLabel,
this._dmsDropdown,
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({
value: constants.SERVICE_CONNECTION_STATUS,
CSSStyles: {
...styles.LABEL_CSS,
'width': '130px'
}
}).component();
const connectionStatusLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SERVICE_CONNECTION_STATUS,
CSSStyles: { ...styles.LABEL_CSS, 'width': '130px' }
}).component();
this._refreshButton = this._view.modelBuilder.button().withProps({
iconWidth: '18px',
iconHeight: '18px',
iconPath: IconPathHelper.refresh,
height: '18px',
width: '18px',
ariaLabel: constants.REFRESH,
}).component();
this._refreshButton = this._view.modelBuilder.button()
.withProps({
iconWidth: '18px',
iconHeight: '18px',
iconPath: IconPathHelper.refresh,
height: '18px',
width: '18px',
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({
width: WIZARD_INPUT_COMPONENT_WIDTH,
style: 'error',
text: '',
CSSStyles: {
...styles.BODY_CSS
}
}).component();
this._dmsStatusInfoBox = this._view.modelBuilder.infoBox()
.withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH,
style: 'error',
text: '',
CSSStyles: { ...styles.BODY_CSS }
}).component();
const authenticationKeysLabel = this._view.modelBuilder.text().withProps({
value: constants.AUTHENTICATION_KEYS,
CSSStyles: {
...styles.LABEL_CSS
}
}).component();
const authenticationKeysLabel = this._view.modelBuilder.text()
.withProps({
value: constants.AUTHENTICATION_KEYS,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
this._copy1 = this._view.modelBuilder.button().withProps({
title: constants.COPY_KEY1,
iconPath: IconPathHelper.copy,
ariaLabel: constants.COPY_KEY1,
}).component();
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) => {
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![0][1].value);
void vscode.window.showInformationMessage(constants.SERVICE_KEY1_COPIED_HELP);
}));
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({
title: constants.COPY_KEY2,
iconPath: IconPathHelper.copy,
ariaLabel: constants.COPY_KEY2,
}).component();
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) => {
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![1][1].value);
void vscode.window.showInformationMessage(constants.SERVICE_KEY2_COPIED_HELP);
}));
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({
title: constants.REFRESH_KEY1,
iconPath: IconPathHelper.refresh,
ariaLabel: constants.REFRESH_KEY1,
}).component();
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({
title: constants.REFRESH_KEY2,
iconPath: IconPathHelper.refresh,
ariaLabel: constants.REFRESH_KEY2,
}).component();
this._refresh2 = this._view.modelBuilder.button()
.withProps({
title: constants.REFRESH_KEY2,
iconPath: IconPathHelper.refresh,
ariaLabel: constants.REFRESH_KEY2,
}).component();
this._authKeyTable = createAuthenticationKeyTable(this._view);
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(
[
connectionLabelContainer,
this._connectionStatusLoader
]
);
container.addItems([
connectionLabelContainer,
this._connectionStatusLoader]);
return container;
}
public async loadResourceGroupDropdown(): Promise<void> {
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._resourceGroupDropdown.loading = true;
this._dmsDropdown.loading = true;
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> {
this._dmsDropdown.loading = true;
public populateDms(): void {
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.loading = true;
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,65 +470,49 @@ 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({
ariaLabel: constants.DATABASE_MIGRATION_SERVICE_AUTHENTICATION_KEYS,
columns: [
{
displayName: constants.NAME,
valueType: azdata.DeclarativeDataType.string,
width: '50px',
isReadOnly: true,
rowCssStyles: {
...styles.BODY_CSS
const authKeyTable = view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.DATABASE_MIGRATION_SERVICE_AUTHENTICATION_KEYS,
columns: [
{
displayName: constants.NAME,
valueType: azdata.DeclarativeDataType.string,
width: '50px',
isReadOnly: true,
rowCssStyles: { ...styles.BODY_CSS },
headerCssStyles: { ...styles.BODY_CSS, 'font-weight': '600' }
},
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,
{
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' }
},
headerCssStyles: {
...styles.BODY_CSS,
'font-weight': '600'
{
displayName: '',
valueType: azdata.DeclarativeDataType.component,
width: '30px',
isReadOnly: true,
rowCssStyles: { ...styles.BODY_CSS },
headerCssStyles: { ...styles.BODY_CSS }
}
},
{
displayName: '',
valueType: azdata.DeclarativeDataType.component,
width: '30px',
isReadOnly: true,
rowCssStyles: {
...styles.BODY_CSS
},
headerCssStyles: {
...styles.BODY_CSS
}
}
],
CSSStyles: {
'margin-top': '5px',
'width': WIZARD_INPUT_COMPONENT_WIDTH
}
}).component();
],
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({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0'
}
}).component()
component: view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component()
};
const form = view.modelBuilder.formContainer()
.withFormItems(
[
pageDescription,
this.migrationModeContainer(),
]
).withProps({
CSSStyles: {
'padding-top': '0'
}
}).component();
.withFormItems([
pageDescription,
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';
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, },
}).component();
const onlineDescription = this._view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION,
CSSStyles: { ...styles.NOTE_CSS, 'margin-left': '20px' }
}).component();
this._disposables.push(
this._onlineButton.onDidChangeCheckedState(checked => {
if (checked) {
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE;
}
}));
const 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,
},
}).component();
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' },
}).component();
const offlineDescription = this._view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION,
CSSStyles: { ...styles.NOTE_CSS, 'margin-left': '20px' }
}).component();
this._disposables.push(
this._offlineButton.onDidChangeCheckedState(checked => {
if (checked) {
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE;
}
}));
const onlineDescription = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION,
CSSStyles: {
...styles.NOTE_CSS,
'margin-left': '20px'
}
}).component();
this._disposables.push(onlineButton.onDidChangeCheckedState((e) => {
if (e) {
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE;
}
}));
const 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'
},
}).component();
const offlineDescription = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION,
CSSStyles: {
...styles.NOTE_CSS,
'margin-left': '20px'
}
}).component();
this._disposables.push(offlineButton.onDidChangeCheckedState((e) => {
if (e) {
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 };
}
}

File diff suppressed because it is too large Load Diff

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.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.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({
url: '',
label: this.migrationStateModel._databasesForMigration?.length.toString(),
CSSStyles: {
...styles.BODY_CSS,
'margin': '0px',
'width': '300px',
}
}).component();
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;
this._disposables.push(targetDatabaseHyperlink.onDidClick(async e => {
await targetDatabaseSummary.initialize();
}));
const targetDatabaseHyperlink = this._view.modelBuilder.hyperlink()
.withProps({
url: '',
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()));
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,20 +86,26 @@ 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) => {
const newPage = pageChangeInfo.newPage;
const lastPage = pageChangeInfo.lastPage;
this.sendPageButtonClickEvent(pageChangeInfo).catch(e => logError(TelemetryViews.MigrationWizardController, 'ErrorSendingPageButtonClick', e));
await pages[lastPage]?.onPageLeave(pageChangeInfo);
await pages[newPage]?.onPageEnter(pageChangeInfo);
}));
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));
await pages[lastPage]?.onPageLeave(pageChangeInfo);
await pages[newPage]?.onPageEnter(pageChangeInfo);
}));
this._wizardObject.registerNavigationValidator(async validator => {
// const lastPage = validator.lastPage;
@@ -110,50 +120,59 @@ 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);
}));
this._disposables.push(saveAndCloseButton.onClick(async () => {
await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
await this._wizardObject.close();
if (stateModel.performanceCollectionInProgress()) {
void vscode.window.showInformationMessage(loc.SAVE_AND_CLOSE_POPUP);
}
}));
this._disposables.push(this._wizardObject.cancelButton.onClick(e => {
sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick,
{
...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Cancel,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, {});
}));
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) =>
await pages[0].onPageEnter(pageChangeInfo)));
this._disposables.push(
this._wizardObject.doneButton.onClick(async (e) => {
await stateModel.startMigration();
await this.updateServiceContext(stateModel);
await this._onClosedCallback();
saveAndCloseButton.onClick(async () => {
await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
await this._wizardObject.close();
if (stateModel.performanceCollectionInProgress()) {
void vscode.window.showInformationMessage(loc.SAVE_AND_CLOSE_POPUP);
}
}));
this._disposables.push(
this._wizardObject.cancelButton.onClick(e => {
sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick,
{
...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Done,
'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, this._serviceContextChangedEvent);
} catch (e) {
logError(TelemetryViews.MigrationWizardController, 'StartMigrationFailed', e);
} finally {
sendSqlMigrationActionEvent(
TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick,
{
...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,
{
...styles.BODY_CSS,
'margin': '4px 0px',
'width': '300px',
}
),
createTextComponent(view, value,
{
...styles.BODY_CSS,
'margin': '4px 0px',
'width': '300px',
}
)
]).component();
.withLayout({ flexFlow: 'row', alignItems: 'center', })
.withItems([
createLabelTextComponent(
view,
label,
{
...styles.BODY_CSS,
'margin': '4px 0px',
'width': '300px',
}),
createTextComponent(
view,
value,
{
...styles.BODY_CSS,
'margin': '4px 0px',
'width': '300px',
})])
.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();
}