Add IR Migration configuration Validation to SQL Migration extension (#21386)

* re-factor and consolidate wizard pages

* validation WIP 11/10

* validate ir dialog

* navigation fixes

* bump version to 1.2.0

* add resource strings and fix navigatin issue

* map validation state to resource string clean up

* address review comments

* fix typos, address review comments

* address review feedback, readability

* fix res string, validation check, col width

* bug fixes, nav, sqldb migration

* fix nav/refresh/visibility issues

* fix nav issues, cancel pending validation items

* update error text / position

* fix localization bug
This commit is contained in:
brian-harris
2022-12-16 14:52:24 -08:00
committed by GitHub
parent 754d70d654
commit 2e240729af
29 changed files with 1993 additions and 692 deletions

View File

@@ -2,7 +2,7 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.1.3",
"version": "1.2.0",
"publisher": "Microsoft",
"preview": false,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",

View File

@@ -9,6 +9,7 @@ import * as azurecore from 'azurecore';
import * as constants from '../constants/strings';
import { getSessionIdHeader } from './utils';
import { URL } from 'url';
import { MigrationSourceAuthenticationType, MigrationStateModel, NetworkShare } from '../models/stateMachine';
const ARM_MGMT_API_VERSION = '2021-04-01';
const SQL_VM_API_VERSION = '2021-11-01-preview';
@@ -39,17 +40,18 @@ export async function getLocations(account: azdata.Account, subscription: Subscr
const path = `/subscriptions/${subscription.id}/providers/Microsoft.DataMigration?api-version=${ARM_MGMT_API_VERSION}`;
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const dataMigrationResourceProvider = (await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host)).response.data;
const sqlMigratonResource = dataMigrationResourceProvider.resourceTypes.find((r: any) => r.resourceType === 'SqlMigrationServices');
const sqlMigrationResourceLocations = sqlMigratonResource.locations;
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const dataMigrationResourceProvider = (await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host))?.response?.data;
const sqlMigratonResource = dataMigrationResourceProvider?.resourceTypes?.find((r: any) => r.resourceType === 'SqlMigrationServices');
const sqlMigrationResourceLocations = sqlMigratonResource?.locations ?? [];
if (response.errors?.length > 0) {
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
const filteredLocations = response.locations
.filter(loc => sqlMigrationResourceLocations.includes(loc.displayName));
const filteredLocations = response?.locations?.filter(
loc => sqlMigrationResourceLocations.includes(loc.displayName));
sortResourceArrayByName(filteredLocations);
@@ -209,7 +211,10 @@ export async function getAvailableSqlDatabaseServers(account: azdata.Account, su
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
@@ -221,7 +226,10 @@ export async function getAvailableSqlDatabases(account: azdata.Account, subscrip
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
@@ -234,7 +242,10 @@ export async function getAvailableSqlVMs(account: azdata.Account, subscription:
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
@@ -283,7 +294,10 @@ export async function getSqlMigrationServiceById(account: azdata.Account, subscr
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
response.response.data.properties.resourceGroup = getResourceGroupFromId(response.response.data.id);
return response.response.data;
@@ -295,7 +309,10 @@ export async function getSqlMigrationServicesByResourceGroup(account: azdata.Acc
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
response.response.data.value.forEach((sms: SqlMigrationService) => {
@@ -310,7 +327,10 @@ export async function getSqlMigrationServices(account: azdata.Account, subscript
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
response.response.data.value.forEach((sms: SqlMigrationService) => {
@@ -328,7 +348,10 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
};
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
const asyncUrl = response.response.headers['azure-asyncoperation'];
const asyncPath = asyncUrl.replace((new URL(asyncUrl)).origin + '/', ''); // path is everything after the hostname, e.g. the 'test' part of 'https://management.azure.com/test'
@@ -357,7 +380,10 @@ export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, su
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return {
authKey1: response?.response?.data?.authKey1 ?? '',
@@ -375,7 +401,10 @@ export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Accou
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return {
authKey1: response?.response?.data?.authKey1 ?? '',
@@ -387,7 +416,10 @@ export async function getStorageAccountAccessKeys(account: azdata.Account, subsc
const api = await getAzureCoreAPI();
const response = await api.getStorageAccountAccessKey(account, subscription, storageAccount, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return {
keyName1: response?.keyName1,
@@ -401,7 +433,10 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
}
@@ -420,7 +455,10 @@ export async function startDatabaseMigration(
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
const asyncUrl = response.response.headers['azure-asyncoperation'];
return {
@@ -440,7 +478,10 @@ export async function getMigrationDetails(account: azdata.Account, subscription:
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
@@ -452,7 +493,10 @@ export async function getServiceMigrations(account: azdata.Account, subscription
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data.value;
@@ -465,7 +509,10 @@ export async function getMigrationTargetInstance(account: azdata.Account, subscr
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
@@ -477,7 +524,10 @@ export async function getMigrationAsyncOperationDetails(account: azdata.Account,
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());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
}
@@ -489,7 +539,10 @@ export async function startMigrationCutover(account: azdata.Account, subscriptio
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data.value;
}
@@ -501,7 +554,10 @@ export async function stopMigration(account: azdata.Account, subscription: Subsc
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
}
@@ -510,16 +566,140 @@ export async function getLocationDisplayName(location: string): Promise<string>
return api.getRegionDisplayName(location);
}
export async function validateIrSqlDatabaseMigrationSettings(
migration: MigrationStateModel,
sourceServerName: string,
trustServerCertificate: boolean,
sourceDatabaseName: string,
targetDatabaseName: string,
testIrOnline: boolean = true,
testSourceConnectivity: boolean = true,
testTargetConnectivity: boolean = true): Promise<ValdiateIrDatabaseMigrationResponse> {
const api = await getAzureCoreAPI();
const account = migration._azureAccount;
const subscription = migration._targetSubscription;
const serviceId = migration._sqlMigrationService?.id;
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const path = encodeURI(`${serviceId}/validateIr?api-version=${DMSV2_API_VERSION}`);
const targetDatabaseServer = migration._targetServerInstance as AzureSqlDatabaseServer;
const requestBody: ValidateIrSqlDatabaseMigrationRequest = {
sourceDatabaseName: sourceDatabaseName,
targetDatabaseName: targetDatabaseName,
kind: AzureResourceKind.SQLDB,
validateIntegrationRuntimeOnline: testIrOnline,
sourceSqlConnection: {
testConnectivity: testSourceConnectivity,
dataSource: sourceServerName,
userName: migration._sqlServerUsername,
password: migration._sqlServerPassword,
authentication: migration._authenticationType,
trustServerCertificate: trustServerCertificate,
// encryptConnection: true,
},
targetSqlConnection: {
testConnectivity: testTargetConnectivity,
dataSource: targetDatabaseServer.properties.fullyQualifiedDomainName,
userName: migration._targetUserName,
password: migration._targetPassword,
encryptConnection: true,
trustServerCertificate: false,
authentication: MigrationSourceAuthenticationType.Sql,
}
};
const response = await api.makeAzureRestRequest(
account,
subscription,
path,
azurecore.HttpRequestMethod.POST,
requestBody,
true,
host);
if (response.errors.length > 0) {
throw new Error(response.errors.map(e => e.message).join(','));
}
return response.response.data;
}
export async function validateIrDatabaseMigrationSettings(
migration: MigrationStateModel,
sourceServerName: string,
trustServerCertificate: boolean,
sourceDatabaseName: string,
networkShare: NetworkShare,
testIrOnline: boolean = true,
testSourceLocationConnectivity: boolean = true,
testSourceConnectivity: boolean = true,
testBlobConnectivity: boolean = true): Promise<ValdiateIrDatabaseMigrationResponse> {
const api = await getAzureCoreAPI();
const account = migration._azureAccount;
const subscription = migration._targetSubscription;
const serviceId = migration._sqlMigrationService?.id;
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const path = encodeURI(`${serviceId}/validateIr?api-version=${DMSV2_API_VERSION}`);
const storage = await getStorageAccountAccessKeys(account, subscription, networkShare.storageAccount);
const requestBody: ValdiateIrDatabaseMigrationRequest = {
sourceDatabaseName: sourceDatabaseName ?? '',
kind: migration.isSqlMiTarget
? AzureResourceKind.SQLMI
: AzureResourceKind.SQLVM,
validateIntegrationRuntimeOnline: testIrOnline,
backupConfiguration: {
sourceLocation: {
testConnectivity: testSourceLocationConnectivity,
fileShare: {
path: networkShare.networkShareLocation,
username: networkShare.windowsUser,
password: networkShare.password,
},
},
targetLocation: {
testConnectivity: testBlobConnectivity,
accountKey: storage?.keyName1,
storageAccountResourceId: networkShare.storageAccount?.id
},
},
sourceSqlConnection: {
testConnectivity: testSourceConnectivity,
dataSource: sourceServerName,
userName: migration._sqlServerUsername,
password: migration._sqlServerPassword,
trustServerCertificate: trustServerCertificate,
encryptConnection: true,
authentication: migration._authenticationType,
}
};
const response = await api.makeAzureRestRequest(
account,
subscription,
path,
azurecore.HttpRequestMethod.POST,
requestBody,
true,
host);
if (response.errors.length > 0) {
throw new Error(response.errors.map(e => e.message).join(','));
}
return response.response.data;
}
type SortableAzureResources = AzureProduct | azurecore.azureResource.FileShare | azurecore.azureResource.BlobContainer | azurecore.azureResource.Blob | azurecore.azureResource.AzureResourceSubscription | SqlMigrationService;
export function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void {
if (!resourceArray) {
return;
}
resourceArray.sort((a: SortableAzureResources, b: SortableAzureResources) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
if (a?.name?.toLowerCase() < b?.name?.toLowerCase()) {
return -1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
if (a?.name?.toLowerCase() > b?.name?.toLowerCase()) {
return 1;
}
return 0;
@@ -640,8 +820,100 @@ export interface StartDatabaseMigrationRequest {
export interface StartDatabaseMigrationResponse {
status: number,
databaseMigration: DatabaseMigration
asyncUrl: string
databaseMigration: DatabaseMigration,
asyncUrl: string,
}
export enum AzureResourceKind {
SQLDB = 'SqlDb',
SQLMI = 'SqlMi',
SQLVM = 'SqlVm',
}
export interface ValidateIrSqlDatabaseMigrationRequest {
sourceDatabaseName: string,
targetDatabaseName: string,
kind: string,
validateIntegrationRuntimeOnline?: boolean,
sourceSqlConnection: {
testConnectivity?: boolean,
dataSource: string,
userName: string,
password: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
authentication: string,
},
targetSqlConnection: {
testConnectivity?: boolean,
dataSource: string,
userName: string,
password: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
authentication: string,
},
}
export interface ValdiateIrDatabaseMigrationRequest {
sourceDatabaseName: string,
kind: string,
validateIntegrationRuntimeOnline?: boolean,
backupConfiguration: {
targetLocation: {
testConnectivity?: boolean,
storageAccountResourceId?: string,
accountKey?: string,
},
sourceLocation: {
testConnectivity?: boolean,
fileShare: {
path: string,
username: string,
password: string,
}
}
},
sourceSqlConnection: {
testConnectivity?: boolean,
dataSource?: string,
userName?: string,
password?: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
authentication?: string,
},
}
export interface ValidationError {
code: string,
message: string,
}
export interface ValdiateIrDatabaseMigrationResponse {
kind: string,
sourceDatabaseName: string,
sourceSqlConnection: {
testConnectivity: boolean,
encryptConnection: true,
trustServerCertificate: false,
dataSource: string,
},
backupConfiguration: {
sourceLocation: {
testConnectivity: boolean,
fileShare: {
path: string, //?
},
},
targetLocation: {
testConnectivity: boolean,
storageAccountResourceId: string,
},
},
succeeded: boolean,
errors: ValidationError[],
validateIntegrationRuntimeOnline: boolean,
}
export interface DatabaseMigration {

View File

@@ -99,14 +99,14 @@ function getSqlDbConnectionProfile(
databaseName: databaseName,
userName: userName,
password: password,
authenticationType: 'SqlLogin',
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
savePassword: false,
saveProfile: false,
options: {
conectionName: '',
server: serverName,
database: databaseName,
authenticationType: 'SqlLogin',
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
user: userName,
password: password,
connectionTimeout: 60,
@@ -137,7 +137,7 @@ function getConnectionProfile(
azureResourceId: azureResourceId,
userName: userName,
password: password,
authenticationType: 'SqlLogin', // TODO: use azdata.connection.AuthenticationType.SqlLogin after next ADS release
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
savePassword: false,
groupFullName: connectId,
groupId: connectId,
@@ -146,7 +146,7 @@ function getConnectionProfile(
options: {
conectionName: connectId,
server: serverName,
authenticationType: 'SqlLogin',
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
user: userName,
password: password,
connectionTimeout: 60,

View File

@@ -204,10 +204,11 @@ export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?:
if (dropDown.values && dropDown.values.length > 0) {
let selectedIndex;
if (value) {
const searchValue = value.toLowerCase();
if (useDisplayName) {
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.displayName?.toLowerCase() === value.toLowerCase());
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.displayName?.toLowerCase() === searchValue);
} else {
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.name?.toLowerCase() === value.toLowerCase());
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.name?.toLowerCase() === searchValue);
}
} else {
selectedIndex = -1;
@@ -220,7 +221,7 @@ export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?:
export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void {
if (dropDown.values && dropDown.values.length > 0) {
if (index >= 0 && index <= dropDown.values.length - 1) {
if (index >= 0 && index < dropDown.values.length) {
dropDown.value = dropDown.values[index] as CategoryValue;
return;
}

View File

@@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
import { EOL } from 'os';
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
import { formatNumber, ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
import { ValidationError } from '../api/azure';
const localize = nls.loadMessageBundle();
// mirrors MigrationState as defined in RP
@@ -55,6 +56,7 @@ export const RESUME_TITLE = localize('sql.migration.resume.title', "Run migratio
export const START_NEW_SESSION = localize('sql.migration.start.session', "Start a new session");
export const RESUME_SESSION = localize('sql.migration.resume.session', "Resume previously saved session");
export const OPEN_SAVED_INFO_ERROR = localize("sql.migration.invalid.savedInfo", 'Cannot retrieve saved session. Try again by selecting new session.');
export const RUN_VALIDATION = localize('sql.migration.run.validation', "Run validation");
// Databases for assessment
export const DATABASE_FOR_ASSESSMENT_PAGE_TITLE = localize('sql.migration.database.assessment.title', "Databases for assessment");
@@ -300,7 +302,7 @@ 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 const AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE = localize('sql.migration.wizard.connection.error.title', "An error occurred while connecting 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);
}
@@ -534,14 +536,24 @@ export function TABLE_SELECTED_COUNT(selectedCount: number, rowCount: number): s
export function MISSING_TARGET_TABLES_COUNT(tables: number): string {
return localize('sql.migration.table.missing.count', "Missing target tables excluded from list: {0}", tables);
}
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 DATABASE_MISSING_TABLES = localize('sql.migration.database.missing.tables', "0 tables found.");
export const DATABASE_LOADING_TABLES = localize('sql.migration.database.loading.tables', "Loading tables list...");
export const TABLE_SELECTION_FILTER = localize('sql.migration.table.selection.filter', "Filter tables");
export const TABLE_SELECTION_UPDATE_BUTTON = localize('sql.migration.table.selection.update.button', "Update");
export const TABLE_SELECTION_CANCEL_BUTTON = localize('sql.migration.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");
export const TABLE_SELECTION_TABLENAME_COLUMN = localize('sql.migration.table.selection.tablename.column', "Table name");
export const TABLE_SELECTION_HASROWS_COLUMN = localize('sql.migration.table.selection.status.column', "Has rows");
export const VALIDATION_DIALOG_TITLE = localize('sql.migration.validation.dialog.title', "Running validation");
export const VALIDATION_MESSAGE_SUCCESS = localize('sql.migration.validation.success', "Validation completed successfully. Please click Next to proceed with the migration.");
export function VALIDATION_MESSAGE_CANCELED_ERRORS(msg: string): string {
return localize(
'sql.migration.validation.canceled.errors',
"Validation was canceled with the following error(s):{0}{1}", EOL, msg);
}
export const VALIDATION_MESSAGE_CANCELED = localize('sql.migration.validation.canceled', "Validation was canceled. Please run and validate the migration settings to continue.");
export const VALIDATION_MESSAGE_NOT_RUN = localize('sql.migration.validation.not.run', "Validation has not been run for the current configuration. Please run and validate the migration settings to continue.");
// integration runtime page
export const SELECT_RESOURCE_GROUP = localize('sql.migration.blob.resourceGroup.select', "Select a resource group.");
@@ -610,6 +622,121 @@ export const RESOURCE_GROUP_DESCRIPTION = localize('sql.migration.resource.group
export const NAME_OF_NEW_RESOURCE_GROUP = localize('sql.migration.name.of.new.rg', "Name of new resource group");
export const DATA_UPLOADED_INFO = localize('sql.migration.data.uploaded.info', "Comparison of the actual amount of data read from the source and the actual amount of data uploaded to the target.");
export const COPY_THROUGHPUT_INFO = localize('sql.migration.copy.throughput.info', "Data movement throughput achieved during the migration of your database backups to Azure. This is the rate of data transfer, calculated by data read divided by duration of backups migration to Azure.");
export const SERVICE_SELECTION_LOCATION_MESSAGE = localize('sql.migration.service.selection.location.msg', "Please select the location of your database backup files before continuing.");
// Validate IR dialog
export const VALIDATION_STATE_CANCELED = localize('sql.migration.validation.state.canceled', "Canceled");
export const VALIDATION_STATE_PENDING = localize('sql.migration.validation.state.pending', "Pending");
export const VALIDATION_STATE_RUNNING = localize('sql.migration.validation.state.running', "Running");
export const VALIDATION_STATE_SUCCEEDED = localize('sql.migration.validation.state.succeeded', "Succeeded");
export const VALIDATION_STATE_FAILED = localize('sql.migration.validation.state.failed', "Failed");
export const VALIDATE_IR_DONE_BUTTON = localize('sql.migration.validate.ir.done.button', "Done");
export const VALIDATE_IR_HEADING = localize('sql.migration.validate.ir.heading', "We are validating the following:");
export const VALIDATE_IR_START_VALIDATION = localize('sql.migration.validate.ir.start.validation', "Start validation");
export const VALIDATE_IR_STOP_VALIDATION = localize('sql.migration.validate.ir.stop.validation', "Stop validation");
export const VALIDATE_IR_COPY_RESULTS = localize('sql.migration.validate.ir.copy.results', "Copy validation results");
export const VALIDATE_IR_RESULTS_HEADING = localize('sql.migration.validate.ir.results.heading', "Validation step details");
export const VALIDATE_IR_VALIDATION_COMPLETED = localize('sql.migration.validate.ir.validation.completed', "Validation completed successfully.");
export const VALIDATE_IR_VALIDATION_CANCELED = localize('sql.migration.validate.ir.validation.camceled', "Validation check canceled");
export function VALIDATE_IR_VALIDATION_COMPLETED_ERRORS(msg: string): string {
return localize(
'sql.migration.validate.ir.completed.errors',
"Validation completed with the following error(s):{0}{1}", EOL, msg);
}
export function VALIDATE_IR_VALIDATION_STATUS(state: string | undefined, errors?: string[]): string {
const status = state ?? '';
if (errors && errors.length > 0) {
return localize(
'sql.migration.validate.ir.status.errors',
"Validation status: {0}{1}{2}", status, EOL, errors.join(EOL));
} else {
return localize(
'sql.migration.validate.ir.status',
"Validation status: {0}", status);
}
}
export function VALIDATE_IR_VALIDATION_STATUS_ERROR_COUNT(state: string | undefined, errorCount: number): string {
const status = state ?? '';
return errorCount > 1
? localize(
'sql.migration.validate.ir.status.error.count.many',
"{0} - {1} errors",
status,
errorCount)
: localize(
'sql.migration.validate.ir.status.error.count.one',
"{0} - 1 error",
status);
}
export function VALIDATE_IR_VALIDATION_STATUS_ERROR(state: string | undefined, errors: string[]): string {
const status = state ?? '';
return localize(
'sql.migration.validate.ir.status.error',
"{0}{1}{2}",
status,
EOL,
errors.join(EOL));
}
export const VALIDATE_IR_COLUMN_VALIDATION_STEPS = localize('sql.migration.validate.ir.column.validation.steps', "Validation steps");
export const VALIDATE_IR_COLUMN_STATUS = localize('sql.migration.validate.ir.column.status', "Status");
export const VALIDATE_IR_VALIDATION_RESULT_LABEL_SHIR = localize('sql.migration.validate.ir.validation.result.label.shir', "Integration runtime connectivity");
export const VALIDATE_IR_VALIDATION_RESULT_LABEL_STORAGE = localize('sql.migration.validate.ir.validation.result.label.storage', "Azure storage connectivity");
export function VALIDATE_IR_VALIDATION_RESULT_LABEL_SOURCE_DATABASE(databaseName: string): string {
return localize(
'sql.migration.validate.ir.validation.result.label.source.database',
"Source database connectivity: '{0}'", databaseName);
}
export function VALIDATE_IR_VALIDATION_RESULT_LABEL_NETWORK_SHARE(shareName: string): string {
return localize(
'sql.migration.validate.ir.validation.result.label.networkshare',
"Network share connectivity: '{0}' ", shareName);
}
export function VALIDATE_IR_VALIDATION_RESULT_LABEL_TARGET_DATABASE(databaseName: string): string {
return localize(
'sql.migration.validate.ir.validation.result.label.target.database',
"Target database connectivity: '{0}'", databaseName);
}
export function VALIDATE_IR_VALIDATION_RESULT_API_ERROR(databaseName: string, error: Error): string {
return localize(
'sql.migration.validate.ir.validation.result.api.error',
"Validation check error{0}Database:{1}{0}Error: {2} - {3}",
EOL,
databaseName,
error.name,
error.message);
}
export function VALIDATE_IR_VALIDATION_RESULT_ERROR(sourceDatabaseName: string, networkShareLocation: string, error: ValidationError): string {
return localize(
'sql.migration.validate.ir.validation.result.error',
"Validation check error{0}Source database: {1}{0}File share path: {2}{0}Error: {3} - {4}",
EOL,
sourceDatabaseName,
networkShareLocation,
error.code,
error.message);
}
export function VALIDATE_IR_SQLDB_VALIDATION_RESULT_ERROR(sourceDatabaseName: string, targetDatabaseName: string, error: ValidationError,): string {
return localize(
'sql.migration.validate.ir.sqldb.validation.result.error',
"Validation check error{0}Source database: {1}{0}Target database: {2}{0}Error: {3} - {4}",
EOL,
sourceDatabaseName,
targetDatabaseName,
error.code,
error.message);
}
// common strings
export const WARNING = localize('sql.migration.warning', "Warning");
export const ERROR = localize('sql.migration.error', "Error");

View File

@@ -104,7 +104,9 @@ export class DashboardStatusBar implements vscode.Disposable {
dialog.content = [tab];
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
dialog.okButton.focused = true;
dialog.okButton.position = 'left';
dialog.cancelButton.label = loc.CLOSE;
dialog.cancelButton.position = 'left';
this._context.subscriptions.push(
dialog.onClosed(async e => {
if (e === 'ok') {

View File

@@ -221,6 +221,7 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
dialog.okButton.hidden = true;
dialog.cancelButton.focused = true;
dialog.cancelButton.label = loc.CLOSE;
dialog.cancelButton.position = 'left';
azdata.window.openDialog(dialog);
}

View File

@@ -72,14 +72,16 @@ export class AssessmentResultsDialog {
this.dialog = azdata.window.createModelViewDialog(this.title, 'AssessmentResults', 'wide');
this.dialog.okButton.label = AssessmentResultsDialog.SelectButtonText;
this.dialog.okButton.position = 'left';
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
this.dialog.cancelButton.label = AssessmentResultsDialog.CancelButtonText;
this.dialog.cancelButton.position = 'left';
this._disposables.push(this.dialog.cancelButton.onClick(async () => await this.cancel()));
this._saveButton = azdata.window.createButton(
constants.SAVE_ASSESSMENT_REPORT,
'left');
'right');
this._disposables.push(
this._saveButton.onClick(async () => {
const folder = await utils.promptUserForFolder();

View File

@@ -74,7 +74,10 @@ export class SavedAssessmentDialog {
this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(constants.SAVED_ASSESSMENT_RESULT, constants.SAVED_ASSESSMENT_RESULT, '60%');
this.dialog.okButton.label = SavedAssessmentDialog.OkButtonText;
this.dialog.okButton.position = 'left';
this.dialog.cancelButton.label = SavedAssessmentDialog.CancelButtonText;
this.dialog.cancelButton.position = 'left';
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));
azdata.window.openDialog(this.dialog);

View File

@@ -187,6 +187,7 @@ export class CreateResourceGroupDialog {
});
});
this._dialogObject.okButton.label = constants.APPLY;
this._dialogObject.okButton.position = 'left';
this._dialogObject.content = [tab];
azdata.window.openDialog(this._dialogObject);

View File

@@ -58,6 +58,9 @@ export class CreateSqlMigrationServiceDialog {
this._model = migrationStateModel;
this._resourceGroupPreset = resourceGroupPreset;
this._dialogObject = azdata.window.createModelViewDialog(constants.CREATE_MIGRATION_SERVICE_TITLE, 'MigrationServiceDialog', 'medium');
this._dialogObject.okButton.position = 'left';
this._dialogObject.cancelButton.position = 'left';
let tab = azdata.window.createTab('');
this._dialogObject.registerCloseValidator(async () => {
return true;

View File

@@ -78,12 +78,12 @@ export class SelectMigrationServiceDialog {
});
this._dialog.okButton.label = constants.MIGRATION_SERVICE_SELECT_APPLY_LABEL;
this._dialog.okButton.position = 'right';
this._dialog.cancelButton.position = 'right';
this._dialog.okButton.position = 'left';
this._dialog.cancelButton.position = 'left';
this._deleteButton = azdata.window.createButton(
constants.MIGRATION_SERVICE_CLEAR,
'left');
'right');
this._disposables.push(
this._deleteButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext({}, this.onServiceContextChanged);

View File

@@ -290,10 +290,13 @@ export class GetAzureRecommendationDialog {
'narrow');
this.dialog.okButton.label = GetAzureRecommendationDialog.StartButtonText;
this.dialog.okButton.position = 'left';
this._disposables.push(
this.dialog.okButton.onClick(
async () => await this.execute()));
this.dialog.cancelButton.position = 'left';
this._disposables.push(
this.dialog.cancelButton.onClick(
() => this._isOpen = false));

View File

@@ -270,6 +270,8 @@ export class SkuEditParametersDialog {
'narrow');
this.dialog.okButton.label = SkuEditParametersDialog.UpdateButtonText;
this.dialog.okButton.position = 'left';
this.dialog.cancelButton.position = 'left';
this._disposables.push(
this.dialog.okButton.onClick(
async () => await this.execute()));

View File

@@ -492,6 +492,7 @@ export class SkuRecommendationResultsDialog {
this.dialog = azdata.window.createModelViewDialog(this.title!, 'SkuRecommendationResultsDialog', 'medium');
this.dialog.okButton.label = SkuRecommendationResultsDialog.OpenButtonText;
this.dialog.okButton.position = 'left';
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
this.dialog.cancelButton.hidden = true;
@@ -501,7 +502,7 @@ export class SkuRecommendationResultsDialog {
this._saveButton = azdata.window.createButton(
constants.SAVE_RECOMMENDATION_REPORT,
'left');
'right');
this._disposables.push(
this._saveButton.onClick(async () => {
const folder = await utils.promptUserForFolder();

View File

@@ -55,6 +55,8 @@ export class SqlMigrationServiceDetailsDialog {
this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL;
this._dialog.okButton.focused = true;
this._dialog.okButton.position = 'left';
this._dialog.cancelButton.hidden = true;
azdata.window.openDialog(this._dialog);
}

View File

@@ -185,11 +185,13 @@ export class TableMigrationSelectionDialog {
600);
this._dialog.okButton.label = constants.TABLE_SELECTION_UPDATE_BUTTON;
this._dialog.okButton.position = 'left';
this._disposables.push(
this._dialog.okButton.onClick(
async () => this._save()));
this._dialog.cancelButton.label = constants.TABLE_SELECTION_CANCEL_BUTTON;
this._dialog.cancelButton.position = 'left';
this._disposables.push(
this._dialog.cancelButton.onClick(
async () => this._isOpen = false));

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType } from '../../models/stateMachine';
import { MigrationMode, MigrationStateModel, NetworkContainerType } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import * as styles from '../../constants/styles';
@@ -33,7 +33,7 @@ export class TargetDatabaseSummaryDialog {
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
const isSqlDbMigration = this._model._targetType === MigrationTargetType.SQLDB;
const isSqlDbMigration = this._model.isSqlDbTarget;
const databaseCount = this._view.modelBuilder.text()
.withProps({
value: constants.COUNT_DATABASES(this._model._databasesForMigration.length),

View File

@@ -0,0 +1,664 @@
/*---------------------------------------------------------------------------------------------
* 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 { validateIrDatabaseMigrationSettings, validateIrSqlDatabaseMigrationSettings } from '../../api/azure';
import { MigrationStateModel, MigrationTargetType, NetworkShare, ValidateIrState, ValidationResult } from '../../models/stateMachine';
import { EOL } from 'os';
import { IconPathHelper } from '../../constants/iconPathHelper';
const DialogName = 'ValidateIrDialog';
enum ValidationResultIndex {
message = 0,
icon = 1,
status = 2,
errors = 3,
state = 4,
}
export const ValidationStatusLookup: constants.LookupTable<string | undefined> = {
[ValidateIrState.Canceled]: constants.VALIDATION_STATE_CANCELED,
[ValidateIrState.Failed]: constants.VALIDATION_STATE_FAILED,
[ValidateIrState.Pending]: constants.VALIDATION_STATE_PENDING,
[ValidateIrState.Running]: constants.VALIDATION_STATE_RUNNING,
[ValidateIrState.Succeeded]: constants.VALIDATION_STATE_SUCCEEDED,
default: undefined
};
export class ValidateIrDialog {
private _canceled: boolean = true;
private _dialog: azdata.window.Dialog | undefined;
private _disposables: vscode.Disposable[] = [];
private _isOpen: boolean = false;
private _model!: MigrationStateModel;
private _resultsTable!: azdata.TableComponent;
private _startLoader!: azdata.LoadingComponent;
private _startButton!: azdata.ButtonComponent;
private _cancelButton!: azdata.ButtonComponent;
private _copyButton!: azdata.ButtonComponent;
private _validationResult: any[][] = [];
private _valdiationErrors: string[] = [];
private _onClosed: () => void;
constructor(
model: MigrationStateModel,
onClosed: () => void) {
this._model = model;
this._onClosed = onClosed;
}
public async openDialog(dialogTitle: string, results?: ValidationResult[]): Promise<void> {
if (!this._isOpen) {
this._isOpen = true;
this._dialog = azdata.window.createModelViewDialog(
dialogTitle,
DialogName,
600);
const promise = this._initializeDialog(this._dialog);
azdata.window.openDialog(this._dialog);
await promise;
return this._runValidation(results);
}
}
private async _initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
return new Promise<void>((resolve, reject) => {
dialog.registerContent(async (view) => {
try {
dialog.okButton.label = constants.VALIDATE_IR_DONE_BUTTON;
dialog.okButton.position = 'left';
dialog.okButton.enabled = false;
dialog.cancelButton.position = 'left';
this._disposables.push(
dialog.cancelButton.onClick(
e => {
this._canceled = true;
this._saveResults();
this._onClosed();
}));
this._disposables.push(
dialog.okButton.onClick(
e => this._onClosed()));
const headingText = view.modelBuilder.text()
.withProps({
value: constants.VALIDATE_IR_HEADING,
CSSStyles: {
'font-size': '13px',
'font-weight': '400',
'margin-bottom': '10px',
},
})
.component();
this._startLoader = view.modelBuilder.loadingComponent()
.withProps({
loading: false,
CSSStyles: { 'margin': '5px 0 0 10px' }
})
.component();
const headingContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
justifyContent: 'flex-start',
})
.withItems([headingText, this._startLoader], { flex: '0 0 auto' })
.component();
this._resultsTable = await this._createResultsTable(view);
this._startButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.restartDataCollection,
iconHeight: 18,
iconWidth: 18,
width: 100,
label: constants.VALIDATE_IR_START_VALIDATION,
}).component();
this._cancelButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.stop,
iconHeight: 18,
iconWidth: 18,
width: 100,
label: constants.VALIDATE_IR_STOP_VALIDATION,
enabled: false,
}).component();
this._copyButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.copy,
iconHeight: 18,
iconWidth: 18,
width: 150,
label: constants.VALIDATE_IR_COPY_RESULTS,
enabled: false,
}).component();
this._disposables.push(
this._startButton.onDidClick(
async (e) => await this._runValidation()));
this._disposables.push(
this._cancelButton.onDidClick(
e => {
this._cancelButton.enabled = false;
this._canceled = true;
}));
this._disposables.push(
this._copyButton.onDidClick(
async (e) => this._copyValidationResults()));
const toolbar = view.modelBuilder.toolbarContainer()
.withToolbarItems([
{ component: this._startButton },
{ component: this._cancelButton },
{ component: this._copyButton }])
.component();
const resultsHeading = view.modelBuilder.text()
.withProps({
value: constants.VALIDATE_IR_RESULTS_HEADING,
CSSStyles: {
'font-size': '16px',
'font-weight': '600',
'margin-bottom': '10px'
},
})
.component();
const resultsText = view.modelBuilder.inputBox()
.withProps({
inputType: 'text',
height: 200,
multiline: true,
CSSStyles: { 'overflow': 'none auto' }
})
.component();
this._disposables.push(
this._resultsTable.onRowSelected(
async (e) => await this._updateResultsInfoBox(resultsText)));
const flex = view.modelBuilder.flexContainer()
.withItems([
headingContainer,
toolbar,
this._resultsTable,
resultsHeading,
resultsText],
{ 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);
resolve();
} catch (ex) {
reject(ex);
}
});
});
}
private _saveResults(): void {
const results = this._validationResults();
switch (this._model._targetType) {
case MigrationTargetType.SQLDB:
this._model._validateIrSqlDb = results;
break;
case MigrationTargetType.SQLMI:
this._model._validateIrSqlMi = results;
break;
case MigrationTargetType.SQLVM:
this._model._validateIrSqlVm = results;
break;
}
}
private _validationResults(): ValidationResult[] {
return this._validationResult.map(result => {
const state = result[ValidationResultIndex.state];
const finalState = this._canceled
? state === ValidateIrState.Running || state === ValidateIrState.Pending
? ValidateIrState.Canceled
: state
: state;
const errors = result[ValidationResultIndex.errors] ?? [];
return {
errors: errors,
state: finalState,
};
});
}
private async _runValidation(results?: ValidationResult[]): Promise<void> {
try {
this._startLoader.loading = true;
this._startButton.enabled = false;
this._cancelButton.enabled = true;
this._copyButton.enabled = false;
this._dialog!.okButton.enabled = false;
this._dialog!.cancelButton.enabled = true;
if (this._model.isIrTargetValidated && results) {
await this._initializeResults(results);
} else {
await this._validate();
}
} finally {
this._startLoader.loading = false;
this._startButton.enabled = true;
this._cancelButton.enabled = false;
this._copyButton.enabled = true;
this._dialog!.okButton.enabled = this._model.isIrTargetValidated;
this._dialog!.cancelButton.enabled = !this._model.isIrTargetValidated;
}
}
private async _copyValidationResults(): Promise<void> {
const errorsText = this._valdiationErrors.join(EOL);
const msg = errorsText.length === 0
? constants.VALIDATE_IR_VALIDATION_COMPLETED
: constants.VALIDATE_IR_VALIDATION_COMPLETED_ERRORS(errorsText);
return vscode.env.clipboard.writeText(msg);
}
private async _updateResultsInfoBox(text: azdata.InputBoxComponent): Promise<void> {
const selectedRows: number[] = this._resultsTable.selectedRows ?? [];
const statusMessages: string[] = [];
if (selectedRows.length > 0) {
for (let i = 0; i < selectedRows.length; i++) {
const row = selectedRows[i];
const results: any[] = this._validationResult[row];
const status = results[ValidationResultIndex.status];
const errors = results[ValidationResultIndex.errors];
statusMessages.push(
constants.VALIDATE_IR_VALIDATION_STATUS(ValidationStatusLookup[status], errors));
}
}
const msg = statusMessages.length > 0
? statusMessages.join(EOL)
: '';
text.value = msg;
}
private async _createResultsTable(view: azdata.ModelView): Promise<azdata.TableComponent> {
return view.modelBuilder.table()
.withProps({
columns: [
{
value: 'test',
name: constants.VALIDATE_IR_COLUMN_VALIDATION_STEPS,
type: azdata.ColumnType.text,
width: 380,
headerCssClass: 'no-borders',
cssClass: 'no-borders align-with-header',
},
{
value: 'image',
name: '',
type: azdata.ColumnType.icon,
width: 20,
headerCssClass: 'no-borders display-none',
cssClass: 'no-borders align-with-header',
},
{
value: 'message',
name: constants.VALIDATE_IR_COLUMN_STATUS,
type: azdata.ColumnType.text,
width: 150,
headerCssClass: 'no-borders',
cssClass: 'no-borders align-with-header',
},
],
data: [],
width: 580,
height: 300,
CSSStyles: {
'margin-top': '10px',
'margin-bottom': '10px',
},
})
.component();
}
private async _initializeResults(results?: ValidationResult[]): Promise<void> {
this._valdiationErrors = [];
if (this._model.isSqlDbTarget) {
// initialize validation results view for sqldb target
await this._initSqlDbIrResults(results);
} else {
// initialize validation results view for sqlmi, sqlvm targets
await this._initTestIrResults(results);
}
}
private async _validate(): Promise<void> {
this._canceled = false;
await this._initializeResults();
if (this._model.isSqlDbTarget) {
await this._validateSqlDbMigration();
} else {
await this._validateDatabaseMigration();
}
this._saveResults();
}
private async _validateDatabaseMigration(): Promise<void> {
const sqlConnections = await azdata.connection.getConnections();
const currentConnection = sqlConnections.find(
value => value.connectionId === this._model.sourceConnectionId);
const sourceServerName = currentConnection?.serverName!;
const trustServerCertificate = currentConnection?.options?.trustServerCertificate === true;
const databaseCount = this._model._databasesForMigration.length;
const sourceDatabaseName = this._model._databasesForMigration[0];
const networkShare = this._model._databaseBackup.networkShares[0];
let testNumber: number = 0;
const validate = async (
sourceDatabase: string,
network: NetworkShare,
testIrOnline: boolean,
testSourceLocationConnectivity: boolean,
testSourceConnectivity: boolean,
testBlobConnectivity: boolean): Promise<boolean> => {
try {
await this._updateValidateIrResults(testNumber, ValidateIrState.Running);
const response = await validateIrDatabaseMigrationSettings(
this._model,
sourceServerName,
trustServerCertificate,
sourceDatabase,
network,
testIrOnline,
testSourceLocationConnectivity,
testSourceConnectivity,
testBlobConnectivity);
if (response?.errors?.length > 0) {
const errors = response.errors.map(
error => constants.VALIDATE_IR_VALIDATION_RESULT_ERROR(
sourceDatabase,
network.networkShareLocation,
error));
await this._updateValidateIrResults(testNumber, ValidateIrState.Failed, errors);
} else {
await this._updateValidateIrResults(testNumber, ValidateIrState.Succeeded);
return true;
}
} catch (error) {
await this._updateValidateIrResults(
testNumber,
ValidateIrState.Failed,
[constants.VALIDATE_IR_VALIDATION_RESULT_API_ERROR(sourceDatabase, error)]);
}
return false;
};
// validate integration runtime (IR) is online
if (!await validate(sourceDatabaseName, networkShare, true, false, false, false)) {
this._canceled = true;
await this._updateValidateIrResults(testNumber + 1, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
return;
}
testNumber++;
// validate blob container connectivity
if (!await validate(sourceDatabaseName, networkShare, false, false, false, true)) {
await this._updateValidateIrResults(testNumber + 1, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
return;
}
for (let i = 0; i < databaseCount; i++) {
const sourceDatabaseName = this._model._databasesForMigration[i];
const networkShare = this._model._databaseBackup.networkShares[i];
testNumber++;
if (this._canceled) {
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
break;
}
// validate source connectivity
await validate(sourceDatabaseName, networkShare, false, false, true, false);
testNumber++;
if (this._canceled) {
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
break;
}
// valdiate source location / network share connectivity
await validate(sourceDatabaseName, networkShare, false, true, false, false);
}
}
private async _validateSqlDbMigration(): Promise<void> {
const sqlConnections = await azdata.connection.getConnections();
const currentConnection = sqlConnections.find(
value => value.connectionId === this._model.sourceConnectionId);
const sourceServerName = currentConnection?.serverName!;
const trustServerCertificate = currentConnection?.options['trustServerCertificate'] === true;
const databaseCount = this._model._databasesForMigration.length;
const sourceDatabaseName = this._model._databasesForMigration[0];
const targetDatabaseName = this._model._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? '';
let testNumber: number = 0;
const validate = async (
sourceDatabase: string,
targetDatabase: string,
testIrOnline: boolean,
testSourceConnectivity: boolean,
testTargetConnectivity: boolean): Promise<boolean> => {
await this._updateValidateIrResults(testNumber, ValidateIrState.Running);
try {
const response = await validateIrSqlDatabaseMigrationSettings(
this._model,
sourceServerName,
trustServerCertificate,
sourceDatabase,
targetDatabase,
testIrOnline,
testSourceConnectivity,
testTargetConnectivity);
if (response?.errors?.length > 0) {
const errors = response.errors.map(
error => constants.VALIDATE_IR_SQLDB_VALIDATION_RESULT_ERROR(
sourceDatabase,
targetDatabase,
error));
await this._updateValidateIrResults(testNumber, ValidateIrState.Failed, errors);
} else {
await this._updateValidateIrResults(testNumber, ValidateIrState.Succeeded);
return true;
}
} catch (error) {
await this._updateValidateIrResults(
testNumber,
ValidateIrState.Failed,
[constants.VALIDATE_IR_VALIDATION_RESULT_API_ERROR(sourceDatabase, error)]);
}
return false;
};
// validate IR is online
if (!await validate(sourceDatabaseName, targetDatabaseName, true, false, false)) {
this._canceled = true;
await this._updateValidateIrResults(testNumber + 1, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED]);
return;
}
for (let i = 0; i < databaseCount; i++) {
const sourceDatabaseName = this._model._databasesForMigration[i];
const targetDatabaseName = this._model._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? '';
testNumber++;
if (this._canceled) {
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED]);
break;
}
// validate source connectivity
await validate(sourceDatabaseName, targetDatabaseName, false, true, false);
testNumber++;
if (this._canceled) {
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED]);
break;
}
// validate target connectivity
await validate(sourceDatabaseName, targetDatabaseName, false, false, true);
}
}
private async _initTestIrResults(results?: ValidationResult[]): Promise<void> {
this._validationResult = [];
this._addValidationResult(constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SHIR);
this._addValidationResult(constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_STORAGE);
for (let i = 0; i < this._model._databasesForMigration.length; i++) {
const sourceDatabaseName = this._model._databasesForMigration[i];
const networkShare = this._model._databaseBackup.networkShares[i];
this._addValidationResult(
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SOURCE_DATABASE(
sourceDatabaseName));
this._addValidationResult(
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_NETWORK_SHARE(
networkShare.networkShareLocation));
}
if (results && results.length > 0) {
for (let row = 0; row < results.length; row++) {
await this._updateValidateIrResults(
row,
results[row].state,
results[row].errors,
false);
}
}
const data = this._validationResult.map(row => [
row[ValidationResultIndex.message],
row[ValidationResultIndex.icon],
row[ValidationResultIndex.status]]);
await this._resultsTable.updateProperty('data', data);
}
private async _initSqlDbIrResults(results?: ValidationResult[]): Promise<void> {
this._validationResult = [];
this._addValidationResult(constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SHIR);
this._model._databasesForMigration
.forEach(sourceDatabaseName => {
this._addValidationResult(
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SOURCE_DATABASE(
sourceDatabaseName));
const targetDatabaseName = this._model._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? '';
this._addValidationResult(
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_TARGET_DATABASE(
targetDatabaseName));
});
if (results && results.length > 0) {
for (let row = 0; row < results.length; row++) {
await this._updateValidateIrResults(
row,
results[row].state,
results[row].errors,
false);
}
}
const data = this._validationResult.map(row => [
row[ValidationResultIndex.message],
row[ValidationResultIndex.icon],
row[ValidationResultIndex.status]]);
await this._resultsTable.updateProperty('data', data);
}
private _addValidationResult(message: string): void {
this._validationResult.push([
message,
<azdata.IconColumnCellValue>{
icon: IconPathHelper.notStartedMigration,
title: ValidationStatusLookup[ValidateIrState.Pending],
},
ValidationStatusLookup[ValidateIrState.Pending],
[],
ValidateIrState.Pending]);
}
private async _updateValidateIrResults(row: number, state: ValidateIrState, errors: string[] = [], updateTable: boolean = true): Promise<void> {
if (state === ValidateIrState.Canceled) {
for (let cancelRow = row; cancelRow < this._validationResult.length; cancelRow++) {
await this._updateResults(cancelRow, state, errors);
}
} else {
await this._updateResults(row, state, errors);
}
if (updateTable) {
const data = this._validationResult.map(row => [
row[ValidationResultIndex.message],
row[ValidationResultIndex.icon],
row[ValidationResultIndex.status]]);
await this._resultsTable.updateProperty('data', data);
}
this._valdiationErrors.push(...errors);
}
private async _updateResults(row: number, state: ValidateIrState, errors: string[] = []): Promise<void> {
const result = this._validationResult[row];
const status = ValidationStatusLookup[state];
const statusMsg = state === ValidateIrState.Failed && errors.length > 0
? constants.VALIDATE_IR_VALIDATION_STATUS_ERROR_COUNT(status, errors.length)
: status;
const statusMessage = errors.length > 0
? constants.VALIDATE_IR_VALIDATION_STATUS_ERROR(status, errors)
: statusMsg;
this._validationResult[row] = [
result[ValidationResultIndex.message],
<azdata.IconColumnCellValue>{
icon: this._getValidationStateImage(state),
title: statusMessage,
},
statusMsg,
errors,
state];
}
private _getValidationStateImage(state: ValidateIrState): azdata.IconPath {
switch (state) {
case ValidateIrState.Canceled:
return IconPathHelper.cancel;
case ValidateIrState.Failed:
return IconPathHelper.error;
case ValidateIrState.Running:
return IconPathHelper.inProgressMigration;
case ValidateIrState.Succeeded:
return IconPathHelper.completedMigration;
case ValidateIrState.Pending:
default:
return IconPathHelper.notStartedMigration;
}
}
}

View File

@@ -15,8 +15,22 @@ import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError
import { hashString, deepClone } from '../api/utils';
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
import { excludeDatabases, TargetDatabaseInfo } from '../api/sqlUtils';
const localize = nls.loadMessageBundle();
export enum ValidateIrState {
Pending = 'Pending',
Running = 'Running',
Succeeded = 'Succeeded',
Failed = 'Failed',
Canceled = 'Canceled',
}
export interface ValidationResult {
errors: string[];
state: ValidateIrState;
}
export enum State {
INIT,
COLLECTING_SOURCE_INFO,
@@ -72,9 +86,8 @@ export enum Page {
DatabaseSelector,
SKURecommendation,
TargetSelection,
MigrationMode,
DatabaseBackup,
IntegrationRuntime,
DatabaseBackup,
Summary
}
@@ -212,6 +225,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _sqldbDbs: string[] = [];
public _targetType!: MigrationTargetType;
public _validateIrSqlDb: ValidationResult[] = [];
public _validateIrSqlMi: ValidationResult[] = [];
public _validateIrSqlVm: ValidationResult[] = [];
public _skuRecommendationResults!: SkuRecommendation;
public _skuRecommendationPerformanceDataSource!: PerformanceDataSourceOptions;
private _skuRecommendationApiResponse!: mssql.SkuRecommendationResult;
@@ -268,6 +285,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._databaseBackup = {} as DatabaseBackupModel;
this._databaseBackup.networkShares = [];
this._databaseBackup.blobs = [];
this._databaseBackup.networkContainerType = NetworkContainerType.BLOB_CONTAINER;
this._targetDatabaseNames = [];
this._assessmentReportFilePath = '';
this._skuRecommendationReportFilePaths = [];
@@ -279,6 +297,60 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._skuEnableElastic = false;
}
public get validationTargetResults(): ValidationResult[] {
switch (this._targetType) {
case MigrationTargetType.SQLDB:
return this._validateIrSqlDb;
case MigrationTargetType.SQLMI:
return this._validateIrSqlMi;
case MigrationTargetType.SQLVM:
return this._validateIrSqlVm;
default:
return [];
}
}
public resetIrValidationResults(): void {
if (this.isIrMigration) {
this._validateIrSqlDb = [];
this._validateIrSqlMi = [];
this._validateIrSqlVm = [];
}
}
public get isSqlVmTarget(): boolean {
return this._targetType === MigrationTargetType.SQLVM;
}
public get isSqlMiTarget(): boolean {
return this._targetType === MigrationTargetType.SQLMI;
}
public get isSqlDbTarget(): boolean {
return this._targetType === MigrationTargetType.SQLDB;
}
public get isIrTargetValidated(): boolean {
const results = this.validationTargetResults ?? [];
return results.length > 1
&& results.every(r =>
r.errors.length === 0 &&
r.state === ValidateIrState.Succeeded)
}
public get isBackupContainerNetworkShare(): boolean {
return this._databaseBackup?.networkContainerType === NetworkContainerType.NETWORK_SHARE;
}
public get isBackupContainerBlobContainer(): boolean {
return this._databaseBackup?.networkContainerType === NetworkContainerType.BLOB_CONTAINER;
}
public get isIrMigration(): boolean {
return this.isSqlDbTarget
|| this.isBackupContainerNetworkShare;
}
public get sourceConnectionId(): string {
return this._sourceConnectionId;
}
@@ -877,7 +949,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
value => value.connectionId === this.sourceConnectionId);
const isOfflineMigration = this._databaseBackup.migrationMode === MigrationMode.OFFLINE;
const isSqlDbTarget = this._targetType === MigrationTargetType.SQLDB;
const isSqlDbTarget = this.isSqlDbTarget;
const requestBody: StartDatabaseMigrationRequest = {
location: this._sqlMigrationService?.location!,
@@ -901,93 +973,93 @@ export class MigrationStateModel implements Model, vscode.Disposable {
for (let i = 0; i < this._databasesForMigration.length; i++) {
try {
switch (this._databaseBackup.networkContainerType) {
case NetworkContainerType.BLOB_CONTAINER:
requestBody.properties.backupConfiguration = {
targetLocation: undefined!,
sourceLocation: {
fileStorageType: 'AzureBlob',
azureBlob: {
storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id,
accountKey: this._databaseBackup.blobs[i].storageKey,
blobContainerName: this._databaseBackup.blobs[i].blobContainer.name
}
}
};
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;
}
if (isOfflineMigration) {
requestBody.properties.offlineConfiguration = {
offline: isOfflineMigration,
lastBackupName: this._databaseBackup.blobs[i]?.lastBackupFile
};
const sourceTables: string[] = [];
let selectedTables = 0;
targetDatabaseInfo?.sourceTables.forEach(sourceTableInfo => {
if (sourceTableInfo.selectedForMigration) {
selectedTables++;
sourceTables.push(sourceTableInfo.tableName);
}
break;
case NetworkContainerType.NETWORK_SHARE:
requestBody.properties.backupConfiguration = {
targetLocation: {
storageAccountResourceId: this._databaseBackup.networkShares[i].storageAccount.id,
accountKey: this._databaseBackup.networkShares[i].storageKey,
},
sourceLocation: {
fileStorageType: 'FileShare',
fileShare: {
path: this._databaseBackup.networkShares[i].networkShareLocation,
username: this._databaseBackup.networkShares[i].windowsUser,
password: this._databaseBackup.networkShares[i].password,
});
// 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: currentConnection?.options.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;
} else {
switch (this._databaseBackup.networkContainerType) {
case NetworkContainerType.BLOB_CONTAINER:
requestBody.properties.backupConfiguration = {
targetLocation: undefined!,
sourceLocation: {
fileStorageType: FileStorageType.AzureBlob,
azureBlob: {
storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id,
accountKey: this._databaseBackup.blobs[i].storageKey,
blobContainerName: this._databaseBackup.blobs[i].blobContainer.name
}
}
}
};
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: currentConnection?.options.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;
if (isOfflineMigration) {
requestBody.properties.offlineConfiguration = {
offline: isOfflineMigration,
lastBackupName: this._databaseBackup.blobs[i]?.lastBackupFile
};
}
break;
case NetworkContainerType.NETWORK_SHARE:
requestBody.properties.backupConfiguration = {
targetLocation: {
storageAccountResourceId: this._databaseBackup.networkShares[i].storageAccount.id,
accountKey: this._databaseBackup.networkShares[i].storageKey,
},
sourceLocation: {
fileStorageType: FileStorageType.FileShare,
fileShare: {
path: this._databaseBackup.networkShares[i].networkShareLocation,
username: this._databaseBackup.networkShares[i].windowsUser,
password: this._databaseBackup.networkShares[i].password,
}
}
};
break;
}
}
requestBody.properties.sourceDatabaseName = this._databasesForMigration[i];
const response = await startDatabaseMigration(
this._azureAccount,
@@ -1086,16 +1158,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
case Page.IntegrationRuntime:
saveInfo.sqlMigrationService = this._sqlMigrationService;
saveInfo.migrationMode = this._databaseBackup.migrationMode;
saveInfo.networkContainerType = this._databaseBackup.networkContainerType;
case Page.DatabaseBackup:
saveInfo.networkContainerType = this._databaseBackup.networkContainerType;
saveInfo.networkShares = this._databaseBackup.networkShares;
saveInfo.blobs = this._databaseBackup.blobs;
saveInfo.targetDatabaseNames = this._targetDatabaseNames;
case Page.MigrationMode:
saveInfo.migrationMode = this._databaseBackup.migrationMode;
case Page.TargetSelection:
saveInfo.azureAccount = deepClone(this._azureAccount);
saveInfo.azureTenant = deepClone(this._azureTenant);
@@ -1162,7 +1232,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._sourceDatabaseNames = this._databasesForMigration;
this._targetDatabaseNames = this.savedInfo.targetDatabaseNames;
this._databaseBackup.networkContainerType = this.savedInfo.networkContainerType || undefined!;
this._databaseBackup.networkContainerType = this.savedInfo.networkContainerType ?? NetworkContainerType.BLOB_CONTAINER;
this._databaseBackup.networkShares = this.savedInfo.networkShares;
this._databaseBackup.blobs = this.savedInfo.blobs;
this._databaseBackup.subscription = this.savedInfo.subscription || undefined!;

View File

@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import { EOL } from 'os';
import { getStorageAccountAccessKeys } from '../api/azure';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, NetworkShare, StateChangeEvent } from '../models/stateMachine';
import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, NetworkShare, StateChangeEvent, ValidateIrState, ValidationResult } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { IconPathHelper } from '../constants/iconPathHelper';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
@@ -16,9 +16,11 @@ import * as utils from '../api/utils';
import { logError, TelemetryViews } from '../telemtery';
import * as styles from '../constants/styles';
import { TableMigrationSelectionDialog } from '../dialog/tableMigrationSelection/tableMigrationSelectionDialog';
import { ValidateIrDialog } from '../dialog/validationResults/validateIrDialog';
const WIZARD_TABLE_COLUMN_WIDTH = '200px';
const WIZARD_TABLE_COLUMN_WIDTH_SMALL = '170px';
const VALIDATE_IR_CUSTOM_BUTTON_INDEX = 0;
const blobResourceGroupErrorStrings = [constants.RESOURCE_GROUP_NOT_FOUND];
const blobStorageAccountErrorStrings = [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT];
@@ -28,9 +30,6 @@ const blobFileErrorStrings = [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLO
export class DatabaseBackupPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _networkShareButton!: azdata.RadioButtonComponent;
private _blobContainerButton!: azdata.RadioButtonComponent;
private _sourceConnectionContainer!: azdata.FlexContainer;
private _networkShareContainer!: azdata.FlexContainer;
private _windowsUserAccountText!: azdata.InputBoxComponent;
@@ -62,7 +61,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private _networkShareTargetDatabaseNames: azdata.InputBoxComponent[] = [];
private _blobContainerTargetDatabaseNames: azdata.InputBoxComponent[] = [];
private _networkShareLocations: azdata.InputBoxComponent[] = [];
private _radioButtonContainer!: azdata.FlexContainer;
private _networkDetailsContainer!: azdata.FlexContainer;
private _existingDatabases: string[] = [];
@@ -81,21 +79,24 @@ export class DatabaseBackupPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
this._radioButtonContainer = this.createBackupLocationComponent();
this._sourceConnectionContainer = this.createSourceCredentialsContainer();
this._networkDetailsContainer = this.createNetworkDetailsContainer();
this._targetDatabaseContainer = this.createTargetDatabaseContainer();
this._networkShareStorageAccountDetails = this.createNetworkShareStorageAccountDetailsContainer();
this._migrationTableSection = this._migrationTableSelectionContainer();
this._disposables.push(
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].onClick(
async e => await this._validateIr()));
const form = this._view.modelBuilder.formContainer()
.withFormItems([
{ title: '', component: this._radioButtonContainer },
{ title: '', component: this._sourceConnectionContainer },
{ title: '', component: this._networkDetailsContainer },
{ title: '', component: this._migrationTableSection },
{ title: '', component: this._networkShareStorageAccountDetails },
{ title: '', component: this._targetDatabaseContainer },
{ title: '', component: this._networkShareStorageAccountDetails }])
{ title: '', component: this._migrationTableSection },
])
.withProps({ CSSStyles: { 'padding-top': '0' } })
.component();
@@ -108,56 +109,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
await view.initializeModel(form);
}
private createBackupLocationComponent(): azdata.FlexContainer {
const buttonGroup = 'networkContainer';
const selectLocationText = this._view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_PAGE_DESCRIPTION,
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._networkShareButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL,
checked: this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
this._disposables.push(
this._networkShareButton.onDidChangeCheckedState(async checked => {
if (checked) {
await this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE);
}
}));
this._blobContainerButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL,
checked: this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
this._disposables.push(
this._blobContainerButton.onDidChangeCheckedState(async checked => {
if (checked) {
await this.switchNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
}
}));
const flexContainer = this._view.modelBuilder.flexContainer()
.withItems([
selectLocationText,
this._networkShareButton,
this._blobContainerButton])
.withLayout({ flexFlow: 'column' })
.component();
return flexContainer;
}
private createNetworkDetailsContainer(): azdata.FlexContainer {
this._networkShareContainer = this.createNetworkShareContainer();
this._blobContainer = this.createBlobContainer();
@@ -200,7 +151,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).component();
this._disposables.push(
this._sqlSourceUsernameInput.onTextChanged(
value => this.migrationStateModel._sqlServerUsername = value));
value => {
this.migrationStateModel._sqlServerUsername = value;
this._resetValidationUI();
}));
const sqlPasswordLabel = this._view.modelBuilder.text()
.withProps({
@@ -218,7 +172,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).component();
this._disposables.push(
this._sqlSourcePassword.onTextChanged(
value => this.migrationStateModel._sqlServerPassword = value));
value => {
this.migrationStateModel._sqlServerPassword = value;
this._resetValidationUI();
}));
return this._view.modelBuilder.flexContainer()
.withItems([
@@ -273,7 +230,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' }
})
.withValidation((component) => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (this.migrationStateModel.isBackupContainerNetworkShare) {
if (component.value) {
if (!/^[A-Za-z0-9\\\._-]{7,}$/.test(component.value)) {
return false;
@@ -287,6 +244,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].windowsUser = value;
}
this._resetValidationUI();
}));
const passwordLabel = this._view.modelBuilder.text()
@@ -309,6 +267,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].password = value;
}
this._resetValidationUI();
}));
return this._view.modelBuilder.flexContainer()
@@ -325,6 +284,13 @@ export class DatabaseBackupPage extends MigrationWizardPage {
.component();
}
private _resetValidationUI(): void {
if (this.wizard.message.level === azdata.window.MessageLevel.Information) {
this.wizard.message = { text: '' };
}
this.migrationStateModel.resetIrValidationResults();
}
private createBlobContainer(): azdata.FlexContainer {
const blobHeading = this._view.modelBuilder.text()
.withProps({
@@ -612,6 +578,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = selectedStorageAccount;
}
this.migrationStateModel.resetIrValidationResults();
}
}
}));
@@ -658,40 +625,130 @@ export class DatabaseBackupPage extends MigrationWizardPage {
.component();
}
private async _updatePageControlsVisibility(containerType: NetworkContainerType): Promise<void> {
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
const isNetworkShare = containerType === NetworkContainerType.NETWORK_SHARE;
const isBlobContainer = containerType === NetworkContainerType.BLOB_CONTAINER;
private async _updatePageControlsVisibility(): Promise<void> {
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
const isNetworkShare = this.migrationStateModel.isBackupContainerNetworkShare;
const isBlobContainer = this.migrationStateModel.isBackupContainerBlobContainer;
await utils.updateControlDisplay(this._sourceConnectionContainer, isSqlDbTarget || isNetworkShare);
await utils.updateControlDisplay(this._migrationTableSection, isSqlDbTarget);
await utils.updateControlDisplay(this._radioButtonContainer, !isSqlDbTarget);
await utils.updateControlDisplay(this._networkDetailsContainer, !isSqlDbTarget);
await utils.updateControlDisplay(this._targetDatabaseContainer, !isSqlDbTarget);
await utils.updateControlDisplay(this._networkShareStorageAccountDetails, !isSqlDbTarget);
await utils.updateControlDisplay(this._networkShareContainer, isNetworkShare);
await utils.updateControlDisplay(this._networkShareStorageAccountDetails, isNetworkShare);
await utils.updateControlDisplay(this._networkTableContainer, isNetworkShare);
await utils.updateControlDisplay(this._blobContainer, isBlobContainer);
await utils.updateControlDisplay(this._blobTableContainer, isBlobContainer);
await utils.updateControlDisplay(this._networkShareContainer, isNetworkShare && !isSqlDbTarget);
await utils.updateControlDisplay(this._networkShareStorageAccountDetails, isNetworkShare && !isSqlDbTarget);
await utils.updateControlDisplay(this._networkTableContainer, isNetworkShare && !isSqlDbTarget);
await utils.updateControlDisplay(this._blobContainer, isBlobContainer && !isSqlDbTarget);
await utils.updateControlDisplay(this._blobTableContainer, isBlobContainer && !isSqlDbTarget);
await this._windowsUserAccountText.updateProperties({ required: isNetworkShare });
await this._passwordText.updateProperties({ required: isNetworkShare });
await this._windowsUserAccountText.updateProperties({ required: isNetworkShare && !isSqlDbTarget });
await this._passwordText.updateProperties({ required: isNetworkShare && !isSqlDbTarget });
await this._sqlSourceUsernameInput.updateProperties({ required: isNetworkShare || isSqlDbTarget });
await this._sqlSourcePassword.updateProperties({ required: isNetworkShare || isSqlDbTarget });
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
if (this.migrationStateModel.refreshDatabaseBackupPage) {
this._networkShareButton.checked = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE;
this._blobContainerButton.checked = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER;
await this._updatePageControlsVisibility(this.migrationStateModel._databaseBackup.networkContainerType);
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
}
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
this.wizard.message = { text: '' };
const errors: string[] = [];
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
if (isSqlDbTarget) {
if (!this._validateTableSelection()) {
errors.push(constants.DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE);
}
} else {
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.NETWORK_SHARE:
if ((<azdata.CategoryValue>this._networkShareStorageAccountResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
if ((<azdata.CategoryValue>this._networkShareContainerStorageAccountDropdown.value)?.displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
break;
case NetworkContainerType.BLOB_CONTAINER:
this._blobContainerResourceGroupDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.RESOURCE_GROUP_NOT_FOUND])) {
errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerStorageAccountDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT])) {
errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT])) {
errors.push(constants.INVALID_BLOB_CONTAINER_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
this._blobContainerLastBackupFileDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER])) {
errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
}
if (errors.length > 0) {
const duplicates: Map<string, number[]> = new Map();
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer?.id;
if (duplicates.has(blobContainerId)) {
duplicates.get(blobContainerId)?.push(i);
} else {
duplicates.set(blobContainerId, [i]);
}
}
duplicates.forEach((d) => {
if (d.length > 1) {
const dupString = `${d.map(index => this.migrationStateModel._databasesForMigration[index]).join(', ')}`;
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
}
});
}
break;
}
}
if (this.migrationStateModel.isSqlMiTarget) {
this.migrationStateModel._targetDatabaseNames.forEach(t => {
// Making sure if database with same name is not present on the target Azure SQL
if (this._existingDatabases.includes(t)) {
errors.push(constants.DATABASE_ALREADY_EXISTS_MI(t, this.migrationStateModel._targetServerInstance.name));
}
});
}
this.wizard.message = {
text: errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
if (errors.length > 0) {
return false;
}
if (this.migrationStateModel.isIrMigration) {
this.wizard.nextButton.enabled = this.migrationStateModel.isIrTargetValidated;
this.updateValidationResultUI();
return this.migrationStateModel.isIrTargetValidated;
} else {
return true;
}
});
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].hidden = !this.migrationStateModel.isIrMigration;
await this._updatePageControlsVisibility();
if (this.migrationStateModel.refreshDatabaseBackupPage) {
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
if (isSqlDbTarget) {
this.wizardPage.title = constants.DATABASE_TABLE_SELECTION_LABEL;
this.wizardPage.description = constants.DATABASE_TABLE_SELECTION_LABEL;
@@ -700,7 +757,15 @@ export class DatabaseBackupPage extends MigrationWizardPage {
try {
const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE;
const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1;
this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration;
const oldHidden = this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden;
const newHidden = !isOfflineMigration;
if (oldHidden !== newHidden) {
// clear values prior to hiding columns if changing column visibility
// to prevent null DeclarativeTableComponent - exception / _view null
await this._blobContainerTargetDatabaseNamesTable.setDataValues([]);
}
this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = newHidden;
this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => {
column.width = isOfflineMigration
? WIZARD_TABLE_COLUMN_WIDTH_SMALL
@@ -712,26 +777,37 @@ export class DatabaseBackupPage extends MigrationWizardPage {
(await this.migrationStateModel.getSourceConnectionProfile()).providerId,
azdata.DataProviderType.QueryProvider);
const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(
await (azdata.connection.getUriForConnection(
this.migrationStateModel.sourceConnectionId)), query);
let username = '';
try {
const query = 'select SUSER_NAME()';
const ownerUri = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
const results = await queryProvider.runQueryAndReturn(ownerUri, query);
username = results.rows[0][0]?.displayValue;
} catch (e) {
username = connectionProfile.userName;
}
const username = results.rows[0][0].displayValue;
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin'
? MigrationSourceAuthenticationType.Sql
: connectionProfile.authenticationType === 'Integrated' // TODO: use azdata.connection.AuthenticationType.Integrated after next ADS release
? MigrationSourceAuthenticationType.Integrated
: undefined!;
this.migrationStateModel._authenticationType =
connectionProfile.authenticationType === azdata.connection.AuthenticationType.SqlLogin
? MigrationSourceAuthenticationType.Sql
: connectionProfile.authenticationType === azdata.connection.AuthenticationType.Integrated
? MigrationSourceAuthenticationType.Integrated
: undefined!;
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(
this.migrationStateModel._authenticationType,
connectionProfile.serverName);
this._sqlSourceUsernameInput.value = username;
this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password;
this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares
? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser
: '';
this._windowsUserAccountText.value =
this.migrationStateModel._databaseBackup.networkShares[0]?.windowsUser
?? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser
?? '';
this._passwordText.value =
this.migrationStateModel._databaseBackup.networkShares[0]?.password
?? this.migrationStateModel.savedInfo?.networkShares[0]?.password
?? '';
this._networkShareTargetDatabaseNames = [];
this._networkShareLocations = [];
@@ -741,7 +817,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._blobContainerDropdowns = [];
this._blobContainerLastBackupFileDropdowns = [];
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) {
if (this.migrationStateModel.isSqlMiTarget) {
this._existingDatabases = await this.migrationStateModel.getManagedDatabases();
}
@@ -803,7 +879,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return false;
}
// Making sure if database with same name is not present on the target Azure SQL
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) {
if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) {
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
@@ -816,7 +892,8 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._disposables.push(
targetDatabaseInput.onTextChanged(async (value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
await this.validateFields();
this._resetValidationUI();
await this.validateFields(targetDatabaseInput);
}));
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
this._networkShareTargetDatabaseNames.push(targetDatabaseInput);
@@ -828,7 +905,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION,
width: '300px'
}).withValidation(c => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (this.migrationStateModel.isBackupContainerNetworkShare) {
if (c.value) {
if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(c.value)) {
return false;
@@ -840,7 +917,8 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._disposables.push(
networkShareLocationInput.onTextChanged(async (value) => {
this.migrationStateModel._databaseBackup.networkShares[index].networkShareLocation = value.trim();
await this.validateFields();
this._resetValidationUI();
await this.validateFields(networkShareLocationInput);
}));
networkShareLocationInput.value = this.migrationStateModel._databaseBackup.networkShares[index]?.networkShareLocation;
this._networkShareLocations.push(networkShareLocationInput);
@@ -856,7 +934,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return false;
}
// Making sure if database with same name is not present on the target Azure SQL
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) {
if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) {
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
@@ -868,7 +946,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).component();
this._disposables.push(
blobTargetDatabaseInput.onTextChanged(
(value) => { this.migrationStateModel._targetDatabaseNames[index] = value.trim(); }));
(value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
this._resetValidationUI();
}));
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput);
@@ -987,13 +1068,17 @@ export class DatabaseBackupPage extends MigrationWizardPage {
{ value: this._blobContainerStorageAccountDropdowns[index] },
{ value: this._blobContainerDropdowns[index] },
{ value: this._blobContainerLastBackupFileDropdowns[index] }]);
await this._blobContainerTargetDatabaseNamesTable.setDataValues([]);
await this._blobContainerTargetDatabaseNamesTable.setDataValues(blobContainerTargetData);
await this.getSubscriptionValues();
// clear change tracking flags
this.migrationStateModel.refreshDatabaseBackupPage = false;
this.migrationStateModel._didUpdateDatabasesForMigration = false;
this.migrationStateModel._didDatabaseMappingChange = false;
this.migrationStateModel._validateIrSqlDb = [];
this.migrationStateModel._validateIrSqlMi = [];
this.migrationStateModel._validateIrSqlVm = [];
} catch (error) {
console.log(error);
let errorText = error?.message;
@@ -1006,101 +1091,76 @@ export class DatabaseBackupPage extends MigrationWizardPage {
level: azdata.window.MessageLevel.Error
};
}
await this.validateFields();
this.updateValidationResultUI(true);
}
}
private async _validateIr(): Promise<void> {
this.wizard.message = { text: '' };
const dialog = new ValidateIrDialog(
this.migrationStateModel,
() => this.updateValidationResultUI());
let results: ValidationResult[] = [];
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLDB:
results = this.migrationStateModel._validateIrSqlDb;
break;
case MigrationTargetType.SQLMI:
results = this.migrationStateModel._validateIrSqlMi;
break;
case MigrationTargetType.SQLVM:
results = this.migrationStateModel._validateIrSqlVm;
break;
}
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return true;
await dialog.openDialog(constants.VALIDATION_DIALOG_TITLE, results);
}
public updateValidationResultUI(initializing?: boolean): void {
if (this.migrationStateModel.isIrMigration) {
const succeeded = this.migrationStateModel.isIrTargetValidated;
if (succeeded) {
this.wizard.message = {
level: azdata.window.MessageLevel.Information,
text: constants.VALIDATION_MESSAGE_SUCCESS,
};
} else {
const results = this.migrationStateModel.validationTargetResults;
const hasResults = results.length > 0;
if (initializing && !hasResults) {
return;
}
const canceled = results.some(result => result.state === ValidateIrState.Canceled);
const errors: string[] = results.flatMap(result => result.errors) ?? [];
const errorsMessage: string = errors.join(EOL);
const hasErrors = errors.length > 0;
const msg = hasResults
? hasErrors
? canceled
? constants.VALIDATION_MESSAGE_CANCELED_ERRORS(errorsMessage)
: constants.VALIDATE_IR_VALIDATION_COMPLETED_ERRORS(errorsMessage)
: constants.VALIDATION_MESSAGE_CANCELED
: constants.VALIDATION_MESSAGE_NOT_RUN;
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: msg,
};
}
this.wizard.message = { text: '' };
const errors: string[] = [];
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.NETWORK_SHARE:
if ((<azdata.CategoryValue>this._networkShareStorageAccountResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
if ((<azdata.CategoryValue>this._networkShareContainerStorageAccountDropdown.value)?.displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
break;
case NetworkContainerType.BLOB_CONTAINER:
this._blobContainerResourceGroupDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.RESOURCE_GROUP_NOT_FOUND])) {
errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerStorageAccountDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT])) {
errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
this._blobContainerDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT])) {
errors.push(constants.INVALID_BLOB_CONTAINER_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
this._blobContainerLastBackupFileDropdowns.forEach((v, index) => {
if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER])) {
errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index]));
}
});
}
if (errors.length > 0) {
const duplicates: Map<string, number[]> = new Map();
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer?.id;
if (duplicates.has(blobContainerId)) {
duplicates.get(blobContainerId)?.push(i);
} else {
duplicates.set(blobContainerId, [i]);
}
}
duplicates.forEach((d) => {
if (d.length > 1) {
const dupString = `${d.map(index => this.migrationStateModel._databasesForMigration[index]).join(', ')}`;
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
}
});
}
break;
default:
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
if (isSqlDbTarget) {
if (!this._validateTableSelection()) {
errors.push(constants.DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE);
}
break;
}
return false;
}
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) {
this.migrationStateModel._targetDatabaseNames.forEach(t => {
// Making sure if database with same name is not present on the target Azure SQL
if (this._existingDatabases.includes(t)) {
errors.push(constants.DATABASE_ALREADY_EXISTS_MI(t, this.migrationStateModel._targetServerInstance.name));
}
});
}
this.wizard.message = {
text: errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
if (errors.length > 0) {
return false;
}
return true;
});
}
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
try {
if (pageChangeInfo.newPage > pageChangeInfo.lastPage) {
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].hidden = true;
if (pageChangeInfo.newPage > pageChangeInfo.lastPage) {
if (!this.migrationStateModel.isSqlDbTarget) {
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.BLOB_CONTAINER:
for (let i = 0; i < this.migrationStateModel._databaseBackup.blobs.length; i++) {
@@ -1124,26 +1184,12 @@ export class DatabaseBackupPage extends MigrationWizardPage {
break;
}
}
} finally {
this.wizard.registerNavigationValidator((pageChangeInfo) => true);
}
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private async switchNetworkContainerFields(containerType: NetworkContainerType): Promise<void> {
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
this.wizard.nextButton.enabled = true;
this.migrationStateModel._databaseBackup.networkContainerType = containerType;
await this._updatePageControlsVisibility(containerType);
await this.validateFields();
}
private _validateTableSelection(): boolean {
for (const targetDatabaseInfo of this.migrationStateModel._sourceTargetMapping) {
const databaseInfo = targetDatabaseInfo[1];
@@ -1159,27 +1205,36 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return false;
}
private async validateFields(): Promise<void> {
await this._sqlSourceUsernameInput.validate();
await this._sqlSourcePassword.validate();
await this._windowsUserAccountText.validate();
await this._passwordText.validate();
await this._networkShareContainerSubscription.validate();
await this._networkShareStorageAccountResourceGroupDropdown.validate();
await this._networkShareContainerStorageAccountDropdown.validate();
await this._blobContainerSubscription.validate();
private async validateFields(component?: azdata.Component): Promise<void> {
await this._sqlSourceUsernameInput?.validate();
await this._sqlSourcePassword?.validate();
await this._windowsUserAccountText?.validate();
await this._passwordText?.validate();
await this._networkShareContainerSubscription?.validate();
await this._networkShareStorageAccountResourceGroupDropdown?.validate();
await this._networkShareContainerStorageAccountDropdown?.validate();
await this._blobContainerSubscription?.validate();
for (let i = 0; i < this._networkShareTargetDatabaseNames.length; i++) {
await this._networkShareTargetDatabaseNames[i].validate();
await this._networkShareLocations[i].validate();
await this._blobContainerTargetDatabaseNames[i].validate();
await this._blobContainerResourceGroupDropdowns[i].validate();
await this._blobContainerStorageAccountDropdowns[i].validate();
await this._blobContainerDropdowns[i].validate();
await this._networkShareTargetDatabaseNames[i]?.validate();
await this._networkShareLocations[i]?.validate();
await this._blobContainerTargetDatabaseNames[i]?.validate();
await this._blobContainerResourceGroupDropdowns[i]?.validate();
await this._blobContainerStorageAccountDropdowns[i]?.validate();
await this._blobContainerDropdowns[i]?.validate();
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
await this._blobContainerLastBackupFileDropdowns[i]?.validate();
}
}
if (this.migrationStateModel.isIrMigration) {
if (this.migrationStateModel.isSqlDbTarget) {
await this._databaseTable?.validate();
}
}
if (this.migrationStateModel.isBackupContainerNetworkShare) {
await this._networkShareTargetDatabaseNamesTable.validate();
}
await component?.validate();
}
private async getSubscriptionValues(): Promise<void> {
@@ -1265,66 +1320,75 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
private loadBlobStorageDropdown(index: number): void {
try {
this._blobContainerStorageAccountDropdowns[index].loading = true;
this._blobContainerStorageAccountDropdowns[index].values = utils.getAzureResourceDropdownValues(
this.migrationStateModel._storageAccounts,
this.migrationStateModel._location,
this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup?.name,
constants.NO_STORAGE_ACCOUNT_FOUND);
const dropDown = this._blobContainerStorageAccountDropdowns[index];
if (dropDown) {
try {
dropDown.loading = true;
dropDown.values = utils.getAzureResourceDropdownValues(
this.migrationStateModel._storageAccounts,
this.migrationStateModel._location,
this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup?.name,
constants.NO_STORAGE_ACCOUNT_FOUND);
utils.selectDefaultDropdownValue(
this._blobContainerStorageAccountDropdowns[index],
this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobStorageDropdown', error);
} finally {
this._blobContainerStorageAccountDropdowns[index].loading = false;
utils.selectDefaultDropdownValue(
dropDown,
this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobStorageDropdown', error);
} finally {
dropDown.loading = false;
}
}
}
private async loadBlobContainerDropdown(index: number): Promise<void> {
try {
this._blobContainerDropdowns[index].loading = true;
this.migrationStateModel._blobContainers = await utils.getBlobContainer(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount);
const dropDown = this._blobContainerDropdowns[index];
if (dropDown) {
try {
dropDown.loading = true;
this.migrationStateModel._blobContainers = await utils.getBlobContainer(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount);
this._blobContainerDropdowns[index].values = utils.getResourceDropdownValues(
this.migrationStateModel._blobContainers,
constants.NO_BLOBCONTAINERS_FOUND);
dropDown.values = utils.getResourceDropdownValues(
this.migrationStateModel._blobContainers,
constants.NO_BLOBCONTAINERS_FOUND);
utils.selectDefaultDropdownValue(
this._blobContainerDropdowns[index],
this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobContainers', error);
} finally {
this._blobContainerDropdowns[index].loading = false;
utils.selectDefaultDropdownValue(
dropDown,
this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobContainers', error);
} finally {
dropDown.loading = false;
}
}
}
private async loadBlobLastBackupFileDropdown(index: number): Promise<void> {
try {
this._blobContainerLastBackupFileDropdowns[index].loading = true;
this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount,
this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
this._blobContainerLastBackupFileDropdowns[index].values = await utils.getBlobLastBackupFileNamesValues(
this.migrationStateModel._lastFileNames);
utils.selectDefaultDropdownValue(
this._blobContainerLastBackupFileDropdowns[index],
this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobLastBackupFiles', error);
} finally {
this._blobContainerLastBackupFileDropdowns[index].loading = false;
const dropDown = this._blobContainerLastBackupFileDropdowns[index];
if (dropDown) {
try {
dropDown.loading = true;
this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount,
this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
dropDown.values = await utils.getBlobLastBackupFileNamesValues(
this.migrationStateModel._lastFileNames);
utils.selectDefaultDropdownValue(
dropDown,
this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile,
false);
} catch (error) {
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobLastBackupFiles', error);
} finally {
dropDown.loading = false;
}
}
}
@@ -1409,6 +1473,12 @@ export class DatabaseBackupPage extends MigrationWizardPage {
},
],
})
.withValidation(table => {
if (this.migrationStateModel.isSqlDbTarget) {
return this._validateTableSelection();
}
return true;
})
.component();
this._disposables.push(
@@ -1479,7 +1549,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
});
await this._databaseTable.updateProperty('data', data);
this._refreshLoading.loading = false;
}
}

View File

@@ -64,6 +64,9 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
}
public async onPageLeave(): Promise<void> {
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
const assessedDatabases = this.migrationStateModel._assessedDatabaseList ?? [];
const selectedDatabases = this.migrationStateModel._databasesForAssessment;
// run assessment if
@@ -75,15 +78,6 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|| assessedDatabases.length === 0
|| assessedDatabases.length !== selectedDatabases.length
|| assessedDatabases.some(db => selectedDatabases.indexOf(db) < 0);
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
});
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {

View File

@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { MigrationMode, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog';
import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
@@ -32,10 +32,22 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
private _copy2!: azdata.ButtonComponent;
private _refresh1!: azdata.ButtonComponent;
private _refresh2!: azdata.ButtonComponent;
private _onlineButton!: azdata.RadioButtonComponent;
private _offlineButton!: azdata.RadioButtonComponent;
private _modeContainer!: azdata.FlexContainer;
private _radioButtonContainer!: azdata.FlexContainer;
private _networkShareButton!: azdata.RadioButtonComponent;
private _blobContainerButton!: azdata.RadioButtonComponent;
private _originalMigrationMode!: MigrationMode;
private _disposables: vscode.Disposable[] = [];
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.IR_PAGE_TITLE), migrationStateModel);
this.migrationStateModel._databaseBackup.migrationMode =
this.migrationStateModel._databaseBackup.migrationMode ||
this.migrationStateModel.isSqlDbTarget
? MigrationMode.OFFLINE
: MigrationMode.ONLINE;
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
@@ -49,8 +61,13 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
.withItems([this._statusLoadingComponent])
.component();
this._radioButtonContainer = this.createBackupLocationComponent();
this._modeContainer = this.migrationModeContainer();
const form = view.modelBuilder.formContainer()
.withFormItems([
{ component: this._modeContainer },
{ component: this._radioButtonContainer },
{ component: this.migrationServiceDropdownContainer() },
{ component: this._dmsInfoContainer }])
.withProps({ CSSStyles: { 'padding-top': '0' } })
@@ -64,26 +81,132 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
await view.initializeModel(form);
}
private migrationModeContainer(): azdata.FlexContainer {
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;
this.migrationStateModel.refreshDatabaseBackupPage = true;
}
}));
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;
this.migrationStateModel.refreshDatabaseBackupPage = true;
}
}));
const flexContainer = this._view.modelBuilder.flexContainer()
.withItems([
this._onlineButton,
onlineDescription,
this._offlineButton,
offlineDescription]
).withLayout({ flexFlow: 'column' })
.component();
return flexContainer;
}
private createBackupLocationComponent(): azdata.FlexContainer {
const buttonGroup = 'networkContainer';
const selectLocationText = this._view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_PAGE_DESCRIPTION,
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._networkShareButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL,
checked: this.migrationStateModel.isBackupContainerNetworkShare,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
this._disposables.push(
this._networkShareButton.onDidChangeCheckedState(async checked => {
if (checked) {
this.migrationStateModel._databaseBackup.networkContainerType = NetworkContainerType.NETWORK_SHARE;
await utils.updateControlDisplay(this._dmsInfoContainer, true);
this.migrationStateModel.refreshDatabaseBackupPage = true;
}
}));
this._blobContainerButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL,
checked: this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
}).component();
this._disposables.push(
this._blobContainerButton.onDidChangeCheckedState(async checked => {
if (checked) {
this.migrationStateModel._databaseBackup.networkContainerType = NetworkContainerType.BLOB_CONTAINER;
await utils.updateControlDisplay(this._dmsInfoContainer, false);
this.migrationStateModel.refreshDatabaseBackupPage = true;
}
}));
const flexContainer = this._view.modelBuilder.flexContainer()
.withItems([
selectLocationText,
this._blobContainerButton,
this._networkShareButton,
])
.withLayout({ flexFlow: 'column' })
.component();
return flexContainer;
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
const isNetworkShare = this.migrationStateModel.isBackupContainerNetworkShare;
this._subscription.value = this.migrationStateModel._targetSubscription.name;
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) {
return true;
}
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
if (!isSqlDbTarget && !this._networkShareButton.checked && !this._blobContainerButton.checked) {
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.SERVICE_SELECTION_LOCATION_MESSAGE,
};
return false;
}
const state = this.migrationStateModel._sqlMigrationService?.properties?.integrationRuntimeState;
if (!this.migrationStateModel._sqlMigrationService) {
@@ -93,10 +216,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
};
return false;
}
if ((this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE)
&& state !== 'Online') {
if ((isSqlDbTarget || isNetworkShare) && state !== 'Online') {
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.SERVICE_OFFLINE_ERROR
@@ -105,10 +225,43 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
}
return true;
});
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
await utils.updateControlDisplay(this._modeContainer, !isSqlDbTarget);
this._onlineButton.enabled = !isSqlDbTarget;
if (isSqlDbTarget) {
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE;
this._offlineButton.checked = true;
}
this._originalMigrationMode = this.migrationStateModel._databaseBackup.migrationMode;
this._networkShareButton.checked = this.migrationStateModel.isBackupContainerNetworkShare;
this._blobContainerButton.checked = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER;
await utils.updateControlDisplay(
this._radioButtonContainer,
!isSqlDbTarget);
this._subscription.value = this.migrationStateModel._targetSubscription.name;
this._location.value = await getLocationDisplayName(
this.migrationStateModel._targetServerInstance.location);
await utils.updateControlDisplay(
this._dmsInfoContainer,
isSqlDbTarget || isNetworkShare);
await this.loadResourceGroupDropdown();
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator((pageChangeInfo) => true);
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
if (this._originalMigrationMode !== this.migrationStateModel._databaseBackup.migrationMode) {
this.migrationStateModel.refreshDatabaseBackupPage = true;
}
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
@@ -195,18 +348,20 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
async (value) => {
if (value && value !== 'undefined' && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) {
this.wizard.message = { text: '' };
await utils.updateControlDisplay(
this._dmsInfoContainer,
this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE);
const resourceGroupName = this.migrationStateModel._sqlMigrationServiceResourceGroup.name.toLowerCase();
const selectedDms = this.migrationStateModel._sqlMigrationServices.find(
dms => dms.name === value && dms.properties.resourceGroup.toLowerCase() === this.migrationStateModel._sqlMigrationServiceResourceGroup.name.toLowerCase());
dms => dms.name === value
&& dms.properties.resourceGroup.toLowerCase() === resourceGroupName);
if (selectedDms) {
this.migrationStateModel._sqlMigrationService = selectedDms;
await this.loadStatus();
}
await utils.updateControlDisplay(
this._dmsInfoContainer,
this.migrationStateModel.isSqlDbTarget ||
this.migrationStateModel.isBackupContainerNetworkShare);
} else {
this.migrationStateModel._sqlMigrationService = undefined;
await utils.updateControlDisplay(this._dmsInfoContainer, false);
@@ -276,7 +431,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this._disposables.push(
this._refreshButton.onDidClick(
async (e) => this.loadStatus()));
async (e) => await this.loadStatus()));
const connectionLabelContainer = this._view.modelBuilder.flexContainer()
.component();
@@ -398,7 +553,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this._dmsDropdown.values = utils.getAzureResourceDropdownValues(
this.migrationStateModel._sqlMigrationServices,
this.migrationStateModel._location,
this.migrationStateModel._sqlMigrationServiceResourceGroup.name,
this.migrationStateModel._sqlMigrationServiceResourceGroup?.name,
constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR);
utils.selectDefaultDropdownValue(

View File

@@ -1,132 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
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 _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);
this.migrationStateModel._databaseBackup.migrationMode = this.migrationStateModel._databaseBackup.migrationMode || MigrationMode.ONLINE;
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
const pageDescription = {
title: '',
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();
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;
}
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;
}
}));
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 flexContainer = this._view.modelBuilder.flexContainer()
.withItems([
this._onlineButton,
onlineDescription,
this._offlineButton,
offlineDescription]
).withLayout({ flexFlow: 'column' })
.component();
return { component: flexContainer };
}
}

View File

@@ -589,8 +589,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
this.wizard.registerNavigationValidator((pageChangeInfo) => true);
this.eventListener?.dispose();
}

View File

@@ -39,14 +39,11 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
});
this.wizard.registerNavigationValidator(pageChangeInfo => true);
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
});
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
@@ -59,9 +56,9 @@ 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'
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === azdata.connection.AuthenticationType.SqlLogin
? MigrationSourceAuthenticationType.Sql
: connectionProfile.authenticationType === 'Integrated'
: connectionProfile.authenticationType === azdata.connection.AuthenticationType.Integrated
? MigrationSourceAuthenticationType.Integrated
: undefined!;

View File

@@ -40,11 +40,13 @@ export class SummaryPage extends MigrationWizardPage {
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator(pageChangeInfo => true);
const targetDatabaseSummary = new TargetDatabaseSummaryDialog(this.migrationStateModel);
const isSqlVmTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLVM;
const isSqlMiTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLMI;
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
const isNetworkShare = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE;
const isSqlVmTarget = this.migrationStateModel.isSqlVmTarget;
const isSqlMiTarget = this.migrationStateModel.isSqlMiTarget;
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
const isNetworkShare = this.migrationStateModel.isBackupContainerNetworkShare;
const targetDatabaseHyperlink = this._view.modelBuilder.hyperlink()
.withProps({
@@ -132,8 +134,6 @@ export class SummaryPage extends MigrationWizardPage {
}
this._flexContainer.addItems([
await createHeadingTextComponent(
this._view,
constants.IR_PAGE_TITLE),
@@ -166,8 +166,9 @@ export class SummaryPage extends MigrationWizardPage {
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
this._flexContainer.clearItems();
this.wizard.registerNavigationValidator(async (pageChangeInfo) => true);
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {

View File

@@ -15,6 +15,7 @@ import * as utils from '../api/utils';
import { azureResource } from 'azurecore';
import { AzureSqlDatabaseServer, SqlVMServer } from '../api/azure';
import { collectTargetDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
export class TargetSelectionPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
@@ -38,6 +39,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
private _testConectionButton!: azdata.ButtonComponent;
private _connectionResultsInfoBox!: azdata.InfoBoxComponent;
private _migrationTargetPlatform!: MigrationTargetType;
private _serviceContext!: MigrationServiceContext;
constructor(
wizard: azdata.window.Wizard,
@@ -51,6 +53,8 @@ export class TargetSelectionPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
this._serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
this._pageDescription = this._view.modelBuilder.text()
.withProps({
value: constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(),
@@ -76,51 +80,6 @@ export class TargetSelectionPage extends MigrationWizardPage {
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_MI_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
break;
case MigrationTargetType.SQLVM:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_VM_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
break;
case MigrationTargetType.SQLDB:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE;
this._updateConnectionButtonState();
if (this.migrationStateModel._didUpdateDatabasesForMigration) {
await this._resetTargetMapping();
this.migrationStateModel._didUpdateDatabasesForMigration = false;
}
break;
}
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
await this._targetUserNameInputBox.updateProperty('required', isSqlDbTarget);
await this._targetPasswordInputBox.updateProperty('required', isSqlDbTarget);
await utils.updateControlDisplay(this._resourceAuthenticationContainer, isSqlDbTarget);
await this.populateAzureAccountsDropdown();
if (this._migrationTargetPlatform !== this.migrationStateModel._targetType) {
// if the user had previously selected values on this page, then went back to change the migration target platform
// and came back, forcibly reload the location/resource group/resource values since they will now be different
this._migrationTargetPlatform = this.migrationStateModel._targetType;
this._targetPasswordInputBox.value = '';
this.migrationStateModel._sqlMigrationServices = undefined!;
this.migrationStateModel._targetServerInstance = undefined!;
this.migrationStateModel._resourceGroup = undefined!;
this.migrationStateModel._location = undefined!;
await this.populateLocationDropdown();
}
this.wizard.registerNavigationValidator((pageChangeInfo) => {
this.wizard.message = { text: '' };
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
@@ -152,8 +111,8 @@ export class TargetSelectionPage extends MigrationWizardPage {
if (!targetMi || resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) {
errors.push(constants.INVALID_MANAGED_INSTANCE_ERROR);
}
if (targetMi?.properties?.state !== 'Ready') {
errors.push(constants.MI_NOT_READY_ERROR(targetMi.name, targetMi.properties.state));
if (targetMi && targetMi.properties?.state !== 'Ready') {
errors.push(constants.MI_NOT_READY_ERROR(targetMi.name, targetMi.properties?.state));
}
break;
case MigrationTargetType.SQLVM:
@@ -168,7 +127,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
errors.push(constants.INVALID_SQL_DATABASE_ERROR);
}
// TODO: verify what state check is needed/possible?
if (targetSqlDB?.properties?.state !== 'Ready') {
if (targetSqlDB && targetSqlDB.properties?.state !== 'Ready') {
errors.push(constants.SQLDB_NOT_READY_ERROR(targetSqlDB.name, targetSqlDB.properties.state));
}
@@ -201,10 +160,70 @@ export class TargetSelectionPage extends MigrationWizardPage {
}
return true;
});
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
return;
}
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_MI_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
break;
case MigrationTargetType.SQLVM:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_VM_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
break;
case MigrationTargetType.SQLDB:
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT);
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE;
this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE;
this._updateConnectionButtonState();
if (this.migrationStateModel._didUpdateDatabasesForMigration) {
await this._resetTargetMapping();
this.migrationStateModel._didUpdateDatabasesForMigration = false;
}
break;
}
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
await this._targetUserNameInputBox.updateProperties({ required: isSqlDbTarget });
await this._targetPasswordInputBox.updateProperties({ required: isSqlDbTarget });
await utils.updateControlDisplay(this._resourceAuthenticationContainer, isSqlDbTarget);
if (this._migrationTargetPlatform !== this.migrationStateModel._targetType) {
// if the user had previously selected values on this page, then went back to change the migration target platform
// and came back, forcibly reload the location/resource group/resource values since they will now be different
this._migrationTargetPlatform = this.migrationStateModel._targetType;
this._targetPasswordInputBox.value = '';
this.migrationStateModel._sqlMigrationServices = undefined!;
this.migrationStateModel._azureAccount = undefined!;
this.migrationStateModel._azureTenant = undefined!;
this.migrationStateModel._targetSubscription = undefined!;
this.migrationStateModel._location = undefined!;
this.migrationStateModel._resourceGroup = undefined!;
this.migrationStateModel._targetServerInstance = undefined!;
const clearDropDown = async (dropDown: azdata.DropDownComponent): Promise<void> => {
dropDown.values = [];
dropDown.value = undefined;
};
await clearDropDown(this._azureAccountsDropdown);
await clearDropDown(this._accountTenantDropdown);
await clearDropDown(this._azureSubscriptionDropdown);
await clearDropDown(this._azureLocationDropdown);
await clearDropDown(this._azureResourceGroupDropdown);
await clearDropDown(this._azureResourceDropdown);
}
await this.populateAzureAccountsDropdown();
}
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator(async (pageChangeInfo) => true);
this.wizard.registerNavigationValidator(pageChangeInfo => true);
this.wizard.message = { text: '' };
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
@@ -231,7 +250,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
this._disposables.push(
this._azureAccountsDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined') {
const selectedAccount = this.migrationStateModel._azureAccounts.find(account => account.displayInfo.displayName === value);
const selectedAccount = this.migrationStateModel._azureAccounts?.find(account => account.displayInfo.displayName === value);
this.migrationStateModel._azureAccount = (selectedAccount)
? utils.deepClone(selectedAccount)!
: undefined!;
@@ -287,7 +306,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
* Replacing all the tenants in azure account with the tenant user has selected.
* All azure requests will only run on this tenant from now on
*/
const selectedTenant = this.migrationStateModel._accountTenants.find(tenant => tenant.displayName === value);
const selectedTenant = this.migrationStateModel._accountTenants?.find(tenant => tenant.displayName === value);
if (selectedTenant) {
this.migrationStateModel._azureTenant = utils.deepClone(selectedTenant)!;
this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel._azureTenant];
@@ -328,7 +347,8 @@ export class TargetSelectionPage extends MigrationWizardPage {
this._disposables.push(
this._azureSubscriptionDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined' && value !== constants.NO_SUBSCRIPTIONS_FOUND) {
const selectedSubscription = this.migrationStateModel._subscriptions.find(subscription => `${subscription.name} - ${subscription.id}` === value);
const selectedSubscription = this.migrationStateModel._subscriptions?.find(
subscription => `${subscription.name} - ${subscription.id}` === value);
this.migrationStateModel._targetSubscription = (selectedSubscription)
? utils.deepClone(selectedSubscription)!
: undefined!;
@@ -358,7 +378,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
this._disposables.push(
this._azureLocationDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined' && value !== constants.NO_LOCATION_FOUND) {
const selectedLocation = this.migrationStateModel._locations.find(location => location.displayName === value);
const selectedLocation = this.migrationStateModel._locations?.find(location => location.displayName === value);
this.migrationStateModel._location = (selectedLocation)
? utils.deepClone(selectedLocation)!
: undefined!;
@@ -585,7 +605,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
this._disposables.push(
this._azureResourceGroupDropdown.onValueChanged(async (value) => {
if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) {
const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value);
const selectedResourceGroup = this.migrationStateModel._resourceGroups?.find(rg => rg.name === value);
this.migrationStateModel._resourceGroup = (selectedResourceGroup)
? utils.deepClone(selectedResourceGroup)!
: undefined!;
@@ -622,15 +642,15 @@ export class TargetSelectionPage extends MigrationWizardPage {
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLVM:
const selectedVm = this.migrationStateModel._targetSqlVirtualMachines.find(vm => vm.name === value);
const selectedVm = this.migrationStateModel._targetSqlVirtualMachines?.find(vm => vm.name === value);
if (selectedVm) {
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer;
}
break;
case MigrationTargetType.SQLMI:
const selectedMi = this.migrationStateModel._targetManagedInstances
.find(mi => mi.name === value
|| constants.UNAVAILABLE_TARGET_PREFIX(mi.name) === value);
const selectedMi = this.migrationStateModel._targetManagedInstances?.find(
mi => mi.name === value ||
constants.UNAVAILABLE_TARGET_PREFIX(mi.name) === value);
if (selectedMi) {
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance;
@@ -647,7 +667,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
}
break;
case MigrationTargetType.SQLDB:
const sqlDatabaseServer = this.migrationStateModel._targetSqlDatabaseServers.find(
const sqlDatabaseServer = this.migrationStateModel._targetSqlDatabaseServers?.find(
sqldb => sqldb.name === value || constants.UNAVAILABLE_TARGET_PREFIX(sqldb.name) === value);
if (sqlDatabaseServer) {
@@ -703,8 +723,8 @@ export class TargetSelectionPage extends MigrationWizardPage {
private _updateConnectionButtonState(): void {
const targetDatabaseServer = (this._azureResourceDropdown.value as azdata.CategoryValue)?.name ?? '';
const userName = this._targetUserNameInputBox.value ?? '';
const password = this._targetPasswordInputBox.value ?? '';
const userName = this.migrationStateModel._targetUserName ?? '';
const password = this.migrationStateModel._targetPassword ?? '';
this._testConectionButton.enabled = targetDatabaseServer.length > 0
&& userName.length > 0
&& password.length > 0;
@@ -764,12 +784,17 @@ export class TargetSelectionPage extends MigrationWizardPage {
try {
this._azureAccountsDropdown.loading = true;
this.migrationStateModel._azureAccounts = await utils.getAzureAccounts();
this._azureAccountsDropdown.values = await utils.getAzureAccountsDropdownValues(this.migrationStateModel._azureAccounts);
} finally {
this._azureAccountsDropdown.loading = false;
const accountId =
this.migrationStateModel._azureAccount?.displayInfo?.userId ??
this._serviceContext?.azureAccount?.displayInfo?.userId;
utils.selectDefaultDropdownValue(
this._azureAccountsDropdown,
this.migrationStateModel._azureAccount?.displayInfo?.userId,
accountId,
false);
}
}
@@ -777,17 +802,24 @@ export class TargetSelectionPage extends MigrationWizardPage {
private async populateTenantsDropdown(): Promise<void> {
try {
this._accountTenantDropdown.loading = true;
if (this.migrationStateModel._azureAccount && this.migrationStateModel._azureAccount.isStale === false && this.migrationStateModel._azureAccount.properties.tenants.length > 0) {
if (this.migrationStateModel._azureAccount?.isStale === false &&
this.migrationStateModel._azureAccount?.properties?.tenants?.length > 0) {
this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount);
this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants);
}
const tenantId =
this.migrationStateModel._azureTenant?.id ??
this._serviceContext?.tenant?.id;
utils.selectDefaultDropdownValue(
this._accountTenantDropdown,
this.migrationStateModel._azureTenant?.id,
tenantId,
true);
await this._accountTenantFlexContainer.updateCssStyles(this.migrationStateModel._azureAccount.properties.tenants.length > 1
? { 'display': 'inline' }
: { 'display': 'none' }
await this._accountTenantFlexContainer.updateCssStyles(
this.migrationStateModel._azureAccount?.properties?.tenants?.length > 1
? { 'display': 'inline' }
: { 'display': 'none' }
);
await this._azureAccountsDropdown.validate();
} finally {
@@ -804,9 +836,13 @@ export class TargetSelectionPage extends MigrationWizardPage {
console.log(e);
} finally {
this._azureSubscriptionDropdown.loading = false;
const subscriptionId =
this.migrationStateModel._targetSubscription?.id ??
this._serviceContext?.subscription?.id;
utils.selectDefaultDropdownValue(
this._azureSubscriptionDropdown,
this.migrationStateModel._targetSubscription?.id,
subscriptionId,
false);
}
}
@@ -848,9 +884,13 @@ export class TargetSelectionPage extends MigrationWizardPage {
console.log(e);
} finally {
this._azureLocationDropdown.loading = false;
const location =
this.migrationStateModel._location?.displayName ??
this._serviceContext?.location?.displayName;
utils.selectDefaultDropdownValue(
this._azureLocationDropdown,
this.migrationStateModel._location?.displayName,
location,
true);
}
}
@@ -882,6 +922,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
console.log(e);
} finally {
this._azureResourceGroupDropdown.loading = false;
utils.selectDefaultDropdownValue(
this._azureResourceGroupDropdown,
this.migrationStateModel._resourceGroup?.id,
@@ -916,9 +957,22 @@ export class TargetSelectionPage extends MigrationWizardPage {
}
} finally {
this._azureResourceDropdown.loading = false;
let targetName = '';
switch (this.migrationStateModel._targetType) {
case MigrationTargetType.SQLMI:
targetName = (this.migrationStateModel._targetServerInstance as azureResource.AzureSqlManagedInstance)?.name;
break;
case MigrationTargetType.SQLVM:
targetName = (this.migrationStateModel._targetServerInstance as SqlVMServer)?.name;
break;
case MigrationTargetType.SQLDB:
targetName = (this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer)?.name;
break;
}
utils.selectDefaultDropdownValue(
this._azureResourceDropdown,
this.migrationStateModel._targetServerInstance?.name,
targetName,
true);
}
}
@@ -944,7 +998,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
.component();
this._disposables.push(
targetDatabaseDropDown.onValueChanged((targetDatabaseName: string) => {
const targetDatabaseInfo = targetDatabases.find(
const targetDatabaseInfo = targetDatabases?.find(
targetDb => targetDb.databaseName === targetDatabaseName);
this.migrationStateModel._sourceTargetMapping.set(
sourceDatabase,

View File

@@ -13,7 +13,6 @@ import { DatabaseBackupPage } from './databaseBackupPage';
import { TargetSelectionPage } from './targetSelectionPage';
import { IntergrationRuntimePage } from './integrationRuntimePage';
import { SummaryPage } from './summaryPage';
import { MigrationModePage } from './migrationModePage';
import { DatabaseSelectorPage } from './databaseSelectorPage';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
import * as styles from '../constants/styles';
@@ -48,23 +47,41 @@ export class WizardController {
this._wizardObject.generateScriptButton.enabled = false;
this._wizardObject.generateScriptButton.hidden = true;
const saveAndCloseButton = azdata.window.createButton(loc.SAVE_AND_CLOSE);
this._wizardObject.customButtons = [saveAndCloseButton];
this._wizardObject.nextButton.position = 'left';
this._wizardObject.nextButton.secondary = false;
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
this._wizardObject.doneButton.position = 'left';
this._wizardObject.doneButton.secondary = false;
this._wizardObject.backButton.position = 'left';
this._wizardObject.backButton.secondary = true;
this._wizardObject.cancelButton.position = 'left';
this._wizardObject.cancelButton.secondary = true;
const saveAndCloseButton = azdata.window.createButton(
loc.SAVE_AND_CLOSE,
'right');
saveAndCloseButton.secondary = true;
const validateButton = azdata.window.createButton(
loc.RUN_VALIDATION,
'left');
validateButton.secondary = false;
validateButton.hidden = true;
this._wizardObject.customButtons = [validateButton, saveAndCloseButton];
const databaseSelectorPage = new DatabaseSelectorPage(this._wizardObject, stateModel);
const skuRecommendationPage = new SKURecommendationPage(this._wizardObject, stateModel);
const targetSelectionPage = new TargetSelectionPage(this._wizardObject, stateModel);
const migrationModePage = new MigrationModePage(this._wizardObject, stateModel);
const databaseBackupPage = new DatabaseBackupPage(this._wizardObject, stateModel);
const integrationRuntimePage = new IntergrationRuntimePage(this._wizardObject, stateModel);
const databaseBackupPage = new DatabaseBackupPage(this._wizardObject, stateModel);
const summaryPage = new SummaryPage(this._wizardObject, stateModel);
const pages: MigrationWizardPage[] = [
databaseSelectorPage,
skuRecommendationPage,
targetSelectionPage,
migrationModePage,
databaseBackupPage,
integrationRuntimePage,
databaseBackupPage,
summaryPage];
this._wizardObject.pages = pages.map(p => p.getwizardPage());
@@ -80,15 +97,15 @@ export class WizardController {
wizardSetupPromises.push(...pages.map(p => p.registerWizardContent()));
wizardSetupPromises.push(this._wizardObject.open());
if (this._model.retryMigration || this._model.resumeAssessment) {
if (this._model.savedInfo.closedPage >= Page.MigrationMode) {
if (this._model.savedInfo.closedPage >= Page.IntegrationRuntime) {
this._model.refreshDatabaseBackupPage = true;
}
// 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 &&
if (this._model.savedInfo.closedPage >= Page.IntegrationRuntime &&
this._model.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
wizardSetupPromises.push(this._wizardObject.setCurrentPage(Page.DatabaseBackup));
wizardSetupPromises.push(this._wizardObject.setCurrentPage(Page.IntegrationRuntime));
} else {
wizardSetupPromises.push(this._wizardObject.setCurrentPage(this._model.savedInfo.closedPage));
}
@@ -107,15 +124,7 @@ export class WizardController {
await pages[newPage]?.onPageEnter(pageChangeInfo);
}));
this._wizardObject.registerNavigationValidator(async validator => {
// const lastPage = validator.lastPage;
// const canLeave = await pages[lastPage]?.canLeave() ?? true;
// const canEnter = await pages[lastPage]?.canEnter() ?? true;
// return canEnter && canLeave;
return true;
});
this._wizardObject.registerNavigationValidator(async validator => true);
await Promise.all(wizardSetupPromises);
this._model.extensionContext.subscriptions.push(
@@ -146,8 +155,6 @@ export class WizardController {
{});
}));
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
this._disposables.push(
this._wizardObject.doneButton.onClick(async (e) => {
try {