mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Add new wizard for login migrations experience (#21317)
This commit is contained in:
@@ -1151,6 +1151,36 @@ export namespace SqlMigrationRefreshPerfDataCollectionRequest {
|
||||
export const type = new RequestType<SqlMigrationRefreshPerfDataCollectionParams, mssql.RefreshPerfDataCollectionResult, void, void>('migration/refreshperfdatacollection');
|
||||
}
|
||||
|
||||
export interface StartLoginMigrationsParams {
|
||||
sourceConnectionString: string;
|
||||
targetConnectionString: string;
|
||||
loginList: string[];
|
||||
aadDomainName: string;
|
||||
}
|
||||
|
||||
export namespace StartLoginMigrationRequest {
|
||||
export const type = new RequestType<StartLoginMigrationsParams, mssql.StartLoginMigrationResult, void, void>('migration/startloginmigration');
|
||||
}
|
||||
|
||||
export namespace ValidateLoginMigrationRequest {
|
||||
export const type = new RequestType<StartLoginMigrationsParams, mssql.StartLoginMigrationResult, void, void>('migration/validateloginmigration');
|
||||
}
|
||||
|
||||
export namespace MigrateLoginsRequest {
|
||||
export const type = new RequestType<StartLoginMigrationsParams, mssql.StartLoginMigrationResult, void, void>('migration/migratelogins');
|
||||
}
|
||||
|
||||
export namespace EstablishUserMappingRequest {
|
||||
export const type = new RequestType<StartLoginMigrationsParams, mssql.StartLoginMigrationResult, void, void>('migration/establishusermapping');
|
||||
}
|
||||
|
||||
export namespace MigrateServerRolesAndSetPermissionsRequest {
|
||||
export const type = new RequestType<StartLoginMigrationsParams, mssql.StartLoginMigrationResult, void, void>('migration/migrateserverrolesandsetpermissions');
|
||||
}
|
||||
|
||||
export namespace LoginMigrationNotification {
|
||||
export const type = new NotificationType<mssql.StartLoginMigrationResult, void>('migration/loginmigrationnotification"');
|
||||
}
|
||||
// ------------------------------- <Sql Migration> -----------------------------
|
||||
|
||||
// ------------------------------- < Table Designer > ------------------------------------
|
||||
|
||||
21
extensions/mssql/src/mssql.d.ts
vendored
21
extensions/mssql/src/mssql.d.ts
vendored
@@ -706,6 +706,11 @@ declare module 'mssql' {
|
||||
startPerfDataCollection(ownerUri: string, dataFolder: string, perfQueryIntervalInSec: number, staticQueryIntervalInSec: number, numberOfIterations: number): Promise<StartPerfDataCollectionResult | undefined>;
|
||||
stopPerfDataCollection(): Promise<StopPerfDataCollectionResult | undefined>;
|
||||
refreshPerfDataCollection(lastRefreshedTime: Date): Promise<RefreshPerfDataCollectionResult | undefined>;
|
||||
startLoginMigration(sourceConnectionString: string, targetConnectionString: string, loginList: string[], aadDomainName: string): Promise<StartLoginMigrationResult | undefined>;
|
||||
validateLoginMigration(sourceConnectionString: string, targetConnectionString: string, loginList: string[], aadDomainName: string): Promise<StartLoginMigrationResult | undefined>;
|
||||
migrateLogins(sourceConnectionString: string, targetConnectionString: string, loginList: string[], aadDomainName: string): Promise<StartLoginMigrationResult | undefined>;
|
||||
establishUserMapping(sourceConnectionString: string, targetConnectionString: string, loginList: string[], aadDomainName: string): Promise<StartLoginMigrationResult | undefined>;
|
||||
migrateServerRolesAndSetPermissions(sourceConnectionString: string, targetConnectionString: string, loginList: string[], aadDomainName: string): Promise<StartLoginMigrationResult | undefined>;
|
||||
}
|
||||
|
||||
// SqlMigration interfaces -----------------------------------------------------------------------
|
||||
@@ -814,4 +819,20 @@ declare module 'mssql' {
|
||||
*/
|
||||
createSas(connectionUri: string, blobContainerUri: string, blobStorageKey: string, storageAccountName: string, expirationDate: string): Promise<CreateSasResponse>;
|
||||
}
|
||||
|
||||
export enum LoginMigrationStep {
|
||||
StartValidations = 0,
|
||||
MigrateLogins = 1,
|
||||
EstablishUserMapping = 2,
|
||||
MigrateServerRoles = 3,
|
||||
EstablishServerRoleMapping = 4,
|
||||
SetLoginPermissions = 5,
|
||||
SetServerRolePermissions = 6,
|
||||
}
|
||||
|
||||
export interface StartLoginMigrationResult {
|
||||
exceptionMap: { [login: string]: any };
|
||||
completedStep: LoginMigrationStep;
|
||||
elapsedTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,4 +126,115 @@ export class SqlMigrationService implements mssql.ISqlMigrationService {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async startLoginMigration(
|
||||
sourceConnectionString: string,
|
||||
targetConnectionString: string,
|
||||
loginList: string[],
|
||||
aadDomainName: string): Promise<mssql.StartLoginMigrationResult | undefined> {
|
||||
let params: contracts.StartLoginMigrationsParams = {
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
loginList,
|
||||
aadDomainName
|
||||
};
|
||||
|
||||
try {
|
||||
return this.client.sendRequest(contracts.StartLoginMigrationRequest.type, params);
|
||||
}
|
||||
catch (e) {
|
||||
this.client.logFailedRequest(contracts.StartLoginMigrationRequest.type, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async validateLoginMigration(
|
||||
sourceConnectionString: string,
|
||||
targetConnectionString: string,
|
||||
loginList: string[],
|
||||
aadDomainName: string): Promise<mssql.StartLoginMigrationResult | undefined> {
|
||||
let params: contracts.StartLoginMigrationsParams = {
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
loginList,
|
||||
aadDomainName
|
||||
};
|
||||
|
||||
try {
|
||||
return this.client.sendRequest(contracts.ValidateLoginMigrationRequest.type, params);
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
this.client.logFailedRequest(contracts.ValidateLoginMigrationRequest.type, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async migrateLogins(
|
||||
sourceConnectionString: string,
|
||||
targetConnectionString: string,
|
||||
loginList: string[],
|
||||
aadDomainName: string): Promise<mssql.StartLoginMigrationResult | undefined> {
|
||||
let params: contracts.StartLoginMigrationsParams = {
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
loginList,
|
||||
aadDomainName
|
||||
};
|
||||
|
||||
try {
|
||||
return this.client.sendRequest(contracts.MigrateLoginsRequest.type, params);
|
||||
}
|
||||
catch (e) {
|
||||
this.client.logFailedRequest(contracts.MigrateLoginsRequest.type, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async establishUserMapping(
|
||||
sourceConnectionString: string,
|
||||
targetConnectionString: string,
|
||||
loginList: string[],
|
||||
aadDomainName: string): Promise<mssql.StartLoginMigrationResult | undefined> {
|
||||
let params: contracts.StartLoginMigrationsParams = {
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
loginList,
|
||||
aadDomainName
|
||||
};
|
||||
|
||||
try {
|
||||
return this.client.sendRequest(contracts.EstablishUserMappingRequest.type, params);
|
||||
}
|
||||
catch (e) {
|
||||
this.client.logFailedRequest(contracts.EstablishUserMappingRequest.type, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async migrateServerRolesAndSetPermissions(
|
||||
sourceConnectionString: string,
|
||||
targetConnectionString: string,
|
||||
loginList: string[],
|
||||
aadDomainName: string): Promise<mssql.StartLoginMigrationResult | undefined> {
|
||||
let params: contracts.StartLoginMigrationsParams = {
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
loginList,
|
||||
aadDomainName
|
||||
};
|
||||
|
||||
try {
|
||||
return this.client.sendRequest(contracts.MigrateServerRolesAndSetPermissionsRequest.type, params);
|
||||
}
|
||||
catch (e) {
|
||||
this.client.logFailedRequest(contracts.MigrateServerRolesAndSetPermissionsRequest.type, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
3
extensions/sql-migration/images/notFound.svg
Normal file
3
extensions/sql-migration/images/notFound.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12Z" fill="#7A7A7A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -74,6 +74,10 @@ export async function createResourceGroup(account: azdata.Account, subscription:
|
||||
}
|
||||
|
||||
export type SqlManagedInstance = azurecore.azureResource.AzureSqlManagedInstance;
|
||||
export function isSqlManagedInstance(instance: any): instance is SqlManagedInstance {
|
||||
return (instance as SqlManagedInstance) !== undefined;
|
||||
}
|
||||
|
||||
export async function getAvailableManagedInstanceProducts(account: azdata.Account, subscription: Subscription): Promise<SqlManagedInstance[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const result = await api.getSqlManagedInstances(account, [subscription], false);
|
||||
@@ -165,6 +169,10 @@ export interface ServerPrivateEndpointConnection {
|
||||
readonly properties?: PrivateEndpointConnectionProperties;
|
||||
}
|
||||
|
||||
export function isAzureSqlDatabaseServer(instance: any): instance is AzureSqlDatabaseServer {
|
||||
return (instance as AzureSqlDatabaseServer) !== undefined;
|
||||
}
|
||||
|
||||
export interface AzureSqlDatabaseServer {
|
||||
id: string,
|
||||
name: string,
|
||||
|
||||
@@ -58,6 +58,22 @@ const query_databases_with_size = `
|
||||
WHERE sys.databases.state = 0
|
||||
`;
|
||||
|
||||
const query_login_tables_sql = `
|
||||
SELECT
|
||||
sp.name as login,
|
||||
sp.type_desc as login_type,
|
||||
sp.default_database_name,
|
||||
case when sp.is_disabled = 1 then 'Disabled' else 'Enabled' end as status
|
||||
FROM sys.server_principals sp
|
||||
LEFT JOIN sys.sql_logins sl ON sp.principal_id = sl.principal_id
|
||||
WHERE sp.type NOT IN ('G', 'R') AND sp.type_desc IN (
|
||||
'SQL_LOGIN'
|
||||
--, 'WINDOWS_LOGIN'
|
||||
) AND sp.name NOT LIKE '##%##'
|
||||
ORDER BY sp.name;`;
|
||||
|
||||
const query_is_sys_admin_sql = `SELECT IS_SRVROLEMEMBER('sysadmin');`;
|
||||
|
||||
export const excludeDatabases: string[] = [
|
||||
'master',
|
||||
'tempdb',
|
||||
@@ -85,6 +101,13 @@ export interface TargetDatabaseInfo {
|
||||
targetTables: Map<string, TableInfo>;
|
||||
}
|
||||
|
||||
export interface LoginTableInfo {
|
||||
loginName: string;
|
||||
loginType: string;
|
||||
defaultDatabaseName: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
function getSqlDbConnectionProfile(
|
||||
serverName: string,
|
||||
tenantId: string,
|
||||
@@ -123,7 +146,7 @@ function getSqlDbConnectionProfile(
|
||||
};
|
||||
}
|
||||
|
||||
function getConnectionProfile(
|
||||
export function getConnectionProfile(
|
||||
serverName: string,
|
||||
azureResourceId: string,
|
||||
userName: string,
|
||||
@@ -319,3 +342,64 @@ export async function getDatabasesList(connectionProfile: azdata.connection.Conn
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectSourceLogins(sourceConnectionId: string): Promise<LoginTableInfo[]> {
|
||||
const ownerUri = await azdata.connection.getUriForConnection(sourceConnectionId);
|
||||
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
|
||||
'MSSQL',
|
||||
azdata.DataProviderType.QueryProvider);
|
||||
|
||||
const results = await queryProvider.runQueryAndReturn(
|
||||
ownerUri,
|
||||
query_login_tables_sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
loginName: getSqlString(row[0]),
|
||||
loginType: getSqlString(row[1]),
|
||||
defaultDatabaseName: getSqlString(row[2]),
|
||||
status: getSqlString(row[3]),
|
||||
};
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
export async function collectTargetLogins(
|
||||
targetServer: AzureSqlDatabaseServer,
|
||||
userName: string,
|
||||
password: string): Promise<string[]> {
|
||||
|
||||
const connectionProfile = getConnectionProfile(
|
||||
targetServer.properties.fullyQualifiedDomainName,
|
||||
targetServer.id,
|
||||
userName,
|
||||
password);
|
||||
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (result.connected && result.connectionId) {
|
||||
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
|
||||
'MSSQL',
|
||||
azdata.DataProviderType.QueryProvider);
|
||||
|
||||
const ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
|
||||
const results = await queryProvider.runQueryAndReturn(
|
||||
ownerUri,
|
||||
query_login_tables_sql);
|
||||
|
||||
return results.rows.map(row => getSqlString(row[0])) ?? [];
|
||||
}
|
||||
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
|
||||
export async function isSysAdmin(sourceConnectionId: string): Promise<boolean> {
|
||||
const ownerUri = await azdata.connection.getUriForConnection(sourceConnectionId);
|
||||
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
|
||||
'MSSQL',
|
||||
azdata.DataProviderType.QueryProvider);
|
||||
|
||||
const results = await queryProvider.runQueryAndReturn(
|
||||
ownerUri,
|
||||
query_is_sys_admin_sql);
|
||||
|
||||
return getSqlBoolean(results.rows[0][0]);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export const MenuCommands = {
|
||||
CancelMigration: 'sqlmigration.cancel.migration',
|
||||
RetryMigration: 'sqlmigration.retry.migration',
|
||||
StartMigration: 'sqlmigration.start',
|
||||
StartLoginMigration: 'sqlmigration.login.start',
|
||||
IssueReporter: 'workbench.action.openIssueReporter',
|
||||
OpenNotebooks: 'sqlmigration.openNotebooks',
|
||||
NewSupportRequest: 'sqlmigration.newsupportrequest',
|
||||
@@ -295,6 +296,22 @@ export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration)
|
||||
return constants.STATUS_VALUE(migrationStatus) + (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
|
||||
}
|
||||
|
||||
export function getLoginStatusMessage(loginFound: boolean): string {
|
||||
if (loginFound) {
|
||||
return constants.LOGINS_FOUND;
|
||||
} else {
|
||||
return constants.LOGINS_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLoginStatusImage(loginFound: boolean): IconPath {
|
||||
if (loginFound) {
|
||||
return IconPathHelper.completedMigration;
|
||||
} else {
|
||||
return IconPathHelper.notFound;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPipelineStatusImage(status: string | undefined): IconPath {
|
||||
// status codes: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
|
||||
switch (status) {
|
||||
@@ -677,6 +694,10 @@ export function getAzureResourceDropdownValues(
|
||||
}
|
||||
|
||||
export function getResourceDropdownValues(resources: { id: string, name: string }[], resourceNotFoundMessage: string): CategoryValue[] {
|
||||
if (!resources || !resources.length) {
|
||||
return [{ name: '', displayName: resourceNotFoundMessage }];
|
||||
}
|
||||
|
||||
return resources?.map(resource => { return { name: resource.id, displayName: resource.name }; })
|
||||
|| [{ name: '', displayName: resourceNotFoundMessage }];
|
||||
}
|
||||
@@ -687,6 +708,10 @@ export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise<
|
||||
}
|
||||
|
||||
export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise<CategoryValue[]> {
|
||||
if (!locations || !locations.length) {
|
||||
return [{ name: '', displayName: constants.NO_LOCATION_FOUND }];
|
||||
}
|
||||
|
||||
return locations?.map(location => { return { name: location.name, displayName: location.displayName }; })
|
||||
|| [{ name: '', displayName: constants.NO_LOCATION_FOUND }];
|
||||
}
|
||||
|
||||
@@ -37,6 +37,13 @@ export const PipelineStatusCodes = {
|
||||
Cancelled: 'Cancelled',
|
||||
};
|
||||
|
||||
export const LoginMigrationStatusCodes = {
|
||||
// status codes: 'InProgress' | 'Failed' | 'Succeeded'
|
||||
InProgress: 'InProgress',
|
||||
Succeeded: 'Succeeded',
|
||||
Failed: 'Failed',
|
||||
};
|
||||
|
||||
const _dateFormatter = new Intl.DateTimeFormat(
|
||||
undefined, {
|
||||
year: 'numeric',
|
||||
|
||||
@@ -48,6 +48,7 @@ export class IconPathHelper {
|
||||
public static addNew: IconPath;
|
||||
public static breadCrumb: IconPath;
|
||||
public static allTables: IconPath;
|
||||
public static notFound: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
IconPathHelper.copy = {
|
||||
@@ -198,5 +199,9 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/allTables.svg'),
|
||||
dark: context.asAbsolutePath('images/allTables.svg'),
|
||||
};
|
||||
IconPathHelper.notFound = {
|
||||
light: context.asAbsolutePath('images/notFound.svg'),
|
||||
dark: context.asAbsolutePath('images/notFound.svg'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,75 @@ export function TIME_IN_MINUTES(val: number): number {
|
||||
return val * 60000;
|
||||
}
|
||||
|
||||
// Login Migrations
|
||||
export function LOGIN_WIZARD_TITLE(instanceName: string): string {
|
||||
return localize('sql-migration.login.wizard.title', "Migrate logins from '{0}' to Azure SQL", instanceName);
|
||||
}
|
||||
export const LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_DESCRIPTION = localize('sql.login.migration.wizard.target.description', "Select the target Azure SQL Managed Instance, Azure SQL VM, or Azure SQL database(s) where you want to migrate your logins.");
|
||||
export const LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_PREVIEW_WARNING = localize('sql.login.migration.wizard.target.data.migration.warning', "Please note that login migration feature is in private preview mode.");
|
||||
export const LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_DATA_MIGRATION_WARNING = localize('sql.login.migration.wizard.target.data.migration.warning', "You must successfully migrate all your database(s) to the target before starting the login migration else the migration will fail. Also if the source and target database names are not same then some permissions may not be applied properly. Learn more");
|
||||
export function LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_PERMISSIONS_WARNING(userName: string, instanceName: string): string {
|
||||
if (!userName || !userName.length) {
|
||||
return localize('sql.login.migration.wizard.target.permission.warning', "Please ensure that the current user has sysadmin permissions to get all login information for the current instance ({0}).", instanceName);
|
||||
}
|
||||
return localize('sql.login.migration.wizard.target.permission.warning', "Please ensure that the current user ({0}) has sysadmin permissions to get all login information for the current instance ({1}).", userName, instanceName);
|
||||
}
|
||||
export const LOGIN_MIGRATIONS_TARGET_TYPE_SELECTION_TITLE = localize('sql.login.migration.wizard.target.type.title', "Azure SQL target type");
|
||||
export const LOGIN_MIGRATIONS_MI_TEXT = localize('sql.login.migration.mi.title', "Azure SQL Managed Instance");
|
||||
export const LOGIN_MIGRATIONS_DB_TEXT = localize('sql.login.migration.db.title', "Azure SQL Database");
|
||||
export const LOGIN_MIGRATIONS_VM_TEXT = localize('sql.login.migration.vm.title', "SQL Server on Azure Virtual Machine");
|
||||
export const LOGIN_MIGRATIONS_AZURE_SQL_TARGET_PAGE_TITLE = localize('sql.login.migration.target.title', "Azure SQL target");
|
||||
export const LOGIN_MIGRATIONS_SELECT_LOGINS_PAGE_TITLE = localize('sql.login.migration.select.page.title', "Select login(s) to migrate");
|
||||
export const LOGIN_MIGRATIONS_SELECT_LOGINS_WINDOWS_AUTH_WARNING = localize('sql.login.migration.select.logins.windows.auth.warning', "Please note that this wizard does not display windows authentication login types because migrating that type is currently not supported. Capability for migrating windows authentication logins is coming soon.");
|
||||
export const LOGIN_MIGRATIONS_STATUS_PAGE_TITLE = localize('sql.login.migration.status.page.title', "Migration Status");
|
||||
export function LOGIN_MIGRATIONS_STATUS_PAGE_DESCRIPTION(numLogins: number, targetType: string, targetName: string): string {
|
||||
return localize('sql.login.migration.status.page.description', "Migrating {0} logins to target {1} '{2}'", numLogins, targetType, targetName);
|
||||
}
|
||||
export function LOGIN_MIGRATIONS_COMPLETED_STATUS_PAGE_DESCRIPTION(numLogins: number, targetType: string, targetName: string): string {
|
||||
return localize('sql.login.migration.status.page.description.completed', "Completed migrating {0} logins to {1} '{2}'", numLogins, targetType, targetName);
|
||||
}
|
||||
export function LOGIN_MIGRATIONS_FAILED_STATUS_PAGE_DESCRIPTION(numLogins: number, targetType: string, targetName: string): string {
|
||||
return localize('sql.login.migration.status.page.description.failed', "Failed migrating {0} logins to {1} '{2}'", numLogins, targetType, targetName);
|
||||
}
|
||||
export const LOGIN_MIGRATIONS_STATUS_PAGE_PREVIOUS_BUTTON_TITLE = localize('sql.login.migration.status.page.previous.button.title', "Previous (Disabled)");
|
||||
export const LOGIN_MIGRATIONS_STATUS_PAGE_PREVIOUS_BUTTON_ERROR = localize('sql.login.migration.status.page.previous.button.error', "Login migration has already been initiated and going back to prior page is disabled.");
|
||||
export const LOGIN_MIGRATIONS_GET_LOGINS_QUERY = localize('sql.login.migration.get.logins.query',
|
||||
"SELECT sp.name as login, sp.type_desc as login_type, sp.default_database_name, case when sp.is_disabled = 1 then 'Disabled' else 'Enabled' end as status FROM sys.server_principals sp LEFT JOIN sys.sql_logins sl ON sp.principal_id = sl.principal_id WHERE sp.type NOT IN ('G', 'R') AND sp.type_desc IN ('SQL_LOGIN', 'WINDOWS_LOGIN') ORDER BY sp.name;");
|
||||
export function LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE(targetType: string): string {
|
||||
return localize('sql.migration.wizard.login.error.title', "An error occurred while trying to get {0} login information.", targetType);
|
||||
}
|
||||
export function LOGIN_MIGRATIONS_GET_LOGINS_ERROR(message: string): string {
|
||||
return localize('sql.migration.wizard.target.login.error', "Error getting login information: {0}", message);
|
||||
}
|
||||
export const SELECT_LOGIN_TO_CONTINUE = localize('sql.migration.select.database.to.continue', "Please select 1 or more logins for migration");
|
||||
export const LOGIN_MIGRATE_BUTTON_TEXT = localize('sql.migration.start.login.migration.button', "Migrate");
|
||||
export function LOGIN_MIGRATIONS_GET_CONNECTION_STRING(dataSource: string, id: string, pass: string): string {
|
||||
return localize('sql.login.migration.get.connection.string', "data source={0};initial catalog=master;user id={1};password={2};TrustServerCertificate=True;Integrated Security=false;", dataSource, id, pass);
|
||||
}
|
||||
export const LOGIN_MIGRATION_IN_PROGRESS = localize('sql.login.migration.in.progress', "Login migration in progress");
|
||||
export const LOGIN_MIGRATION_REFRESHING_LOGIN_DATA = localize('sql.login.migration.select.in.progress', "Refreshing login list from source and target");
|
||||
export function LOGIN_MIGRATION_REFRESH_LOGIN_DATA_SUCCESSFUL(numSourceLogins: number, numTargetLogins: number): string {
|
||||
return localize('sql.login.migration.refresh.login.data.successful', "Refreshing login list was successful. Source logins found {0}, Target logins found {1}", numSourceLogins, numTargetLogins);
|
||||
}
|
||||
export const LOGIN_MIGRATION_REFRESH_SOURCE_LOGIN_DATA_FAILED = localize('sql.login.migration.refresh.source.login.data.failed', "Refreshing login list from source failed");
|
||||
export const LOGIN_MIGRATION_REFRESH_TARGET_LOGIN_DATA_FAILED = localize('sql.login.migration.refresh.target.login.data.failed', "Refreshing login list from target failed");
|
||||
export const STARTING_LOGIN_MIGRATION = localize('sql.migration.starting.login', "Validating and migrating logins are in progress");
|
||||
export const STARTING_LOGIN_MIGRATION_FAILED = localize('sql.migration.starting.login.failed', "Validating and migrating logins failed");
|
||||
export const ESTABLISHING_USER_MAPPINGS = localize('sql.login.migration.establish.user.mappings', "Validating and migrating logins completed.\n\nEstablishing user mappings.");
|
||||
export const ESTABLISHING_USER_MAPPINGS_FAILED = localize('sql.login.migration.establish.user.mappings.failed', "Establishing user mappings failed");
|
||||
export const MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS = localize('sql.login.migration.migrate.server.roles.and.set.permissions', "Establishing user mappings completed.\n\nCurrently, migrating server roles, establishing server mappings and setting permissions. This will take some time.");
|
||||
export const MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS_FAILED = localize('sql.login.migration.migrate.server.roles.and.set.permissions.failed', "Migrating server roles, establishing server mappings and setting permissions failed.");
|
||||
export const LOGIN_MIGRATIONS_COMPLETE = localize('sql.login.migration.complete', "Completed migrating logins");
|
||||
export const LOGIN_MIGRATIONS_FAILED = localize('sql.login.migration.failed', "Migrating logins failed");
|
||||
export function LOGIN_MIGRATIONS_ERROR(message: string): string {
|
||||
return localize('sql.login.migration..error', "Login migration error: {0}", message);
|
||||
}
|
||||
export const LOGINS_FOUND = localize('sql.login.migration.logins.found', "Login found");
|
||||
export const LOGINS_NOT_FOUND = localize('sql.login.migration.logins.not.found', "Login not found");
|
||||
export const LOGIN_MIGRATION_STATUS_SUCCEEDED = localize('sql.login.migration.status.succeeded', "Succeeded");
|
||||
export const LOGIN_MIGRATION_STATUS_FAILED = localize('sql.login.migration.status.failed', "Failed");
|
||||
export const LOGIN_MIGRATION_STATUS_IN_PROGRESS = localize('sql.login.migration.status.in.progress', "In progress");
|
||||
|
||||
// Azure SQL Target
|
||||
export const AZURE_SQL_TARGET_PAGE_TITLE = localize('sql.migration.wizard.target.title', "Azure SQL target");
|
||||
export function AZURE_SQL_TARGET_PAGE_DESCRIPTION(targetInstance: string = 'instance'): string {
|
||||
@@ -311,6 +380,11 @@ export function SQL_TARGET_CONNECTION_SUCCESS(databaseCount: string): string {
|
||||
}
|
||||
|
||||
export const SQL_TARGET_MISSING_SOURCE_DATABASES = localize('sql.migration.wizard.source.missing', 'Connection was successful but did not find any target databases.');
|
||||
|
||||
export function SQL_TARGET_CONNECTION_SUCCESS_LOGINS(databaseCount: string): string {
|
||||
return localize('sql.login.migration.wizard.target.connection.success', "Connection was successful.", databaseCount);
|
||||
}
|
||||
|
||||
export const SQL_TARGET_MAPPING_ERROR_MISSING_TARGET = localize(
|
||||
'sql.migration.wizard.target.missing',
|
||||
'Database mapping error. Missing target databases to migrate. Please configure the target server connection and click connect to collect the list of available database migration targets.');
|
||||
@@ -402,6 +476,7 @@ export function SQLDB_NOT_READY_ERROR(sqldbName: string, state: string): string
|
||||
return localize('sql.migration.sqldb.not.ready', "The SQL database server '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available SQL database server.", sqldbName, state);
|
||||
}
|
||||
|
||||
export const SELECT_AN_TARGET_TYPE = localize('sql.migration.select.service.select.target.type.', "Select target Azure SQL Type");
|
||||
export const SELECT_AN_ACCOUNT = localize('sql.migration.select.service.select.a.', "Sign into Azure and select an account");
|
||||
export const SELECT_A_TENANT = localize('sql.migration.select.service.select.a.tenant', "Select a tenant");
|
||||
export const SELECT_A_SUBSCRIPTION = localize('sql.migration.select.service.select.a.subscription', "Select a subscription");
|
||||
@@ -804,6 +879,8 @@ export const DASHBOARD_TITLE = localize('sql.migration.dashboard.title', "Azure
|
||||
export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance, SQL Server on Azure Virtual Machines or Azure SQL Database.");
|
||||
export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL");
|
||||
export const DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION = localize('sql.migration.dashboard.migrate.task.button.description', "Migrate a SQL Server instance to Azure SQL.");
|
||||
export const DASHBOARD_LOGIN_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.login.migrate.task.button', "Migrate logins to Azure SQL");
|
||||
export const DASHBOARD_LOGIN_MIGRATE_TASK_BUTTON_DESCRIPTION = localize('sql.migration.dashboard.login.migrate.task.button.description', "Migrate SQL Server logins to Azure SQL.");
|
||||
export const DATABASE_MIGRATION_STATUS = localize('sql.migration.database.migration.status', "Database migration status");
|
||||
export const HELP_TITLE = localize('sql.migration.dashboard.help.title', "Help articles and video links");
|
||||
export const PRE_REQ_TITLE = localize('sql.migration.pre.req.title', "Things you need before starting your Azure SQL migration:");
|
||||
@@ -924,6 +1001,12 @@ export const OFFLINE = localize('sql.migration.offline', "Offline");
|
||||
export const DATABASE = localize('sql.migration.database', "Database");
|
||||
export const SRC_DATABASE = localize('sql.migration.src.database', "Source database");
|
||||
export const SRC_SERVER = localize('sql.migration.src.server', "Source name");
|
||||
export const SOURCE_LOGIN = localize('sql.migration.source.login', "Source login");
|
||||
export const LOGIN_TYPE = localize('sql.login.migration.type', "Login type");
|
||||
export const DEFAULT_DATABASE = localize('sql.migration.default.database', "Default database");
|
||||
export const LOGIN_STATUS_COLUMN = localize('sql.login.migration.status.column', "Status");
|
||||
export const LOGIN_TARGET_STATUS_COLUMN = localize('sql.login.migration.target.status.column', "Target Status");
|
||||
export const LOGIN_MIGRATION_STATUS_COLUMN = localize('sql.login.migration.migration.status.column', "Migration Status");
|
||||
|
||||
export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Migration status");
|
||||
export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
|
||||
@@ -1112,6 +1195,12 @@ export function DATABASES(selectedCount: number, totalCount: number): string {
|
||||
export function DATABASES_SELECTED(selectedCount: number, totalCount: number): string {
|
||||
return localize('sql.migration.databases.selected', "{0}/{1} databases selected", selectedCount, totalCount);
|
||||
}
|
||||
export function LOGINS_SELECTED(selectedCount: number, totalCount: number): string {
|
||||
return localize('sql.login.migrations.selected', "{0}/{1} logins selected", selectedCount, totalCount);
|
||||
}
|
||||
export function NUMBER_LOGINS_MIGRATING(displayedMigratingCount: number, totalMigratingCount: number): string {
|
||||
return localize('sql.migration.number.logins.migrating', "{0}/{1} migrating logins displayed", displayedMigratingCount, totalMigratingCount);
|
||||
}
|
||||
export function ISSUES_COUNT(totalCount: number): string {
|
||||
return localize('sql.migration.issues.count', "Issues ({0})", totalCount);
|
||||
}
|
||||
@@ -1153,6 +1242,8 @@ export const MIGRATION_SERVICE_DESCRIPTION = localize('sql.migration.select.serv
|
||||
// Desktop tabs
|
||||
export const DESKTOP_MIGRATION_BUTTON_LABEL = localize('sql.migration.tab.button.migration.label', 'New migration');
|
||||
export const DESKTOP_MIGRATION_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.migration.description', 'Migrate to Azure SQL');
|
||||
export const DESKTOP_LOGIN_MIGRATION_BUTTON_LABEL = localize('sql.migration.tab.button.login.migration.label', 'New login migration (PREVIEW)');
|
||||
export const DESKTOP_LOGIN_MIGRATION_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.login.migration.description', 'Migrate logins to Azure SQL');
|
||||
export const DESKTOP_SUPPORT_BUTTON_LABEL = localize('sql.migration.tab.button.support.label', 'New support request');
|
||||
export const DESKTOP_SUPPORT_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.support.description', 'New support request');
|
||||
export const DESKTOP_FEEDBACK_BUTTON_LABEL = localize('sql.migration.tab.button.feedback.label', 'Feedback');
|
||||
@@ -1180,6 +1271,10 @@ export function DATABASE_MIGRATION_STATUS_LABEL(status?: string): string {
|
||||
return localize('sql.migration.database.migration.status.label', 'Database migration status: {0}', status ?? '');
|
||||
}
|
||||
|
||||
export function LOGIN_MIGRATION_STATUS_LABEL(status?: string): string {
|
||||
return localize('sql.migration.database.migration.status.label', 'Login migration status: {0}', status ?? '');
|
||||
}
|
||||
|
||||
export function TABLE_MIGRATION_STATUS_LABEL(status?: string): string {
|
||||
return localize('sql.migration.table.migration.status.label', 'Table migration status: {0}', status ?? '');
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
const toolbar = view.modelBuilder.toolbarContainer();
|
||||
toolbar.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton() },
|
||||
// <azdata.ToolbarComponent>{ component: this.createNewLoginMigrationButton() }, // TODO Enable when login migrations is ready for public consumption
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton() },
|
||||
]);
|
||||
|
||||
@@ -143,6 +143,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
}).component();
|
||||
|
||||
toolbar.addToolbarItems([
|
||||
// <azdata.ToolbarComponent>{ component: this.createNewLoginMigrationButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
|
||||
|
||||
@@ -337,6 +337,11 @@ export class DashboardWidget {
|
||||
MenuCommands.StartMigration,
|
||||
async () => await this.launchMigrationWizard()));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.StartLoginMigration,
|
||||
async () => await this.launchLoginMigrationWizard()));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.OpenNotebooks,
|
||||
@@ -369,6 +374,11 @@ export class DashboardWidget {
|
||||
MenuCommands.StartMigration,
|
||||
async () => await this.launchMigrationWizard()));
|
||||
|
||||
this._context.subscriptions.push(azdata.tasks.registerTask(
|
||||
MenuCommands.StartLoginMigration,
|
||||
async () => await this.launchLoginMigrationWizard()));
|
||||
|
||||
|
||||
this._context.subscriptions.push(
|
||||
azdata.tasks.registerTask(
|
||||
MenuCommands.NewSupportRequest,
|
||||
@@ -456,6 +466,34 @@ export class DashboardWidget {
|
||||
}
|
||||
}
|
||||
|
||||
public async launchLoginMigrationWizard(): Promise<void> {
|
||||
const activeConnection = await azdata.connection.getCurrentConnection();
|
||||
let connectionId: string = '';
|
||||
let serverName: string = '';
|
||||
if (!activeConnection) {
|
||||
const connection = await azdata.connection.openConnectionDialog();
|
||||
if (connection) {
|
||||
connectionId = connection.connectionId;
|
||||
serverName = connection.options.server;
|
||||
}
|
||||
} else {
|
||||
connectionId = activeConnection.connectionId;
|
||||
serverName = activeConnection.serverName;
|
||||
}
|
||||
if (serverName) {
|
||||
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
|
||||
if (api) {
|
||||
this.stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
|
||||
this._context.subscriptions.push(this.stateModel);
|
||||
const wizardController = new WizardController(
|
||||
this._context,
|
||||
this.stateModel,
|
||||
this._onServiceContextChanged);
|
||||
await wizardController.openLoginWizard(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkSavedInfo(serverName: string): SavedInfo | undefined {
|
||||
return this._context.globalState.get<SavedInfo>(`${this.stateModel.mementoString}.${serverName}`);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DatabaseMigration } from '../api/azure';
|
||||
import { getSelectedServiceStatus } from '../models/migrationLocalStorage';
|
||||
import { MenuCommands, SqlMigrationExtensionId } from '../api/utils';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
import { ShowStatusMessageDialog } from '../dialog/generic/genericDialogs';
|
||||
|
||||
export const EmptySettingValue = '-';
|
||||
|
||||
@@ -95,6 +96,29 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected createNewLoginMigrationButton(): azdata.ButtonComponent {
|
||||
const newLoginMigrationButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
label: loc.DESKTOP_LOGIN_MIGRATION_BUTTON_LABEL,
|
||||
description: loc.DESKTOP_LOGIN_MIGRATION_BUTTON_DESCRIPTION,
|
||||
height: 24,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
iconPath: IconPathHelper.addNew,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
newLoginMigrationButton.onDidClick(async () => {
|
||||
const actionId = MenuCommands.StartLoginMigration;
|
||||
const args = {
|
||||
extensionId: SqlMigrationExtensionId,
|
||||
issueTitle: loc.DASHBOARD_LOGIN_MIGRATE_TASK_BUTTON_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}));
|
||||
return newLoginMigrationButton;
|
||||
}
|
||||
|
||||
protected createNewMigrationButton(): azdata.ButtonComponent {
|
||||
const newMigrationButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
@@ -179,50 +203,6 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
|
||||
statusMessage: string,
|
||||
errorMessage: string,
|
||||
): void {
|
||||
const tab = azdata.window.createTab(title);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: statusMessage })
|
||||
.component(),
|
||||
])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: 420,
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '0 15px' } })
|
||||
.component();
|
||||
|
||||
if (errorMessage.length > 0) {
|
||||
flex.addItem(
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: errorMessage,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
inputType: 'text',
|
||||
height: 100,
|
||||
CSSStyles: { 'overflow': 'hidden auto' },
|
||||
})
|
||||
.component()
|
||||
);
|
||||
}
|
||||
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
title,
|
||||
'messageDialog',
|
||||
450,
|
||||
'normal');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.hidden = true;
|
||||
dialog.cancelButton.focused = true;
|
||||
dialog.cancelButton.label = loc.CLOSE;
|
||||
dialog.cancelButton.position = 'left';
|
||||
|
||||
azdata.window.openDialog(dialog);
|
||||
ShowStatusMessageDialog(title, statusMessage, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 constants from '../../constants/strings';
|
||||
|
||||
export function ShowStatusMessageDialog(
|
||||
title: string,
|
||||
statusMessage: string,
|
||||
errorMessage: string,
|
||||
): void {
|
||||
const tab = azdata.window.createTab(title);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: statusMessage })
|
||||
.component(),
|
||||
])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: 420,
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '0 15px' } })
|
||||
.component();
|
||||
|
||||
if (errorMessage.length > 0) {
|
||||
flex.addItem(
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: errorMessage,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
inputType: 'text',
|
||||
height: 100,
|
||||
CSSStyles: { 'overflow': 'hidden auto' },
|
||||
})
|
||||
.component()
|
||||
);
|
||||
}
|
||||
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
title,
|
||||
'messageDialog',
|
||||
450,
|
||||
'normal');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.hidden = true;
|
||||
dialog.cancelButton.focused = true;
|
||||
dialog.cancelButton.label = constants.CLOSE;
|
||||
dialog.cancelButton.position = 'left';
|
||||
|
||||
azdata.window.openDialog(dialog);
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationStateModel, StateChangeEvent } from './stateMachine';
|
||||
import { ShowStatusMessageDialog } from '../dialog/generic/genericDialogs';
|
||||
|
||||
export abstract class MigrationWizardPage {
|
||||
constructor(
|
||||
protected readonly wizard: azdata.window.Wizard,
|
||||
@@ -80,4 +82,12 @@ export abstract class MigrationWizardPage {
|
||||
const current = this.wizard.currentPage;
|
||||
await this.wizard.setCurrentPage(current + 1);
|
||||
}
|
||||
|
||||
protected showDialogMessage(
|
||||
title: string,
|
||||
statusMessage: string,
|
||||
errorMessage: string,
|
||||
): void {
|
||||
ShowStatusMessageDialog(title, statusMessage, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,14 @@ import * as azdata from 'azdata';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from 'mssql';
|
||||
import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer } from '../api/azure';
|
||||
import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer, isSqlManagedInstance, isAzureSqlDatabaseServer } from '../api/azure';
|
||||
import * as constants from '../constants/strings';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
|
||||
import { hashString, deepClone } from '../api/utils';
|
||||
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
|
||||
import { excludeDatabases, TargetDatabaseInfo } from '../api/sqlUtils';
|
||||
|
||||
import { excludeDatabases, getConnectionProfile, LoginTableInfo, TargetDatabaseInfo } from '../api/sqlUtils';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum ValidateIrState {
|
||||
@@ -200,6 +199,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _sourceDatabaseNames!: string[];
|
||||
public _targetDatabaseNames!: string[];
|
||||
|
||||
public _targetServerName!: string;
|
||||
public _targetUserName!: string;
|
||||
public _targetPassword!: string;
|
||||
public _sourceTargetMapping: Map<string, TargetDatabaseInfo | undefined> = new Map();
|
||||
@@ -242,6 +242,11 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _perfDataCollectionErrors!: string[];
|
||||
public _perfDataCollectionIsCollecting!: boolean;
|
||||
|
||||
public _loginsForMigration!: LoginTableInfo[];
|
||||
public _aadDomainName!: string;
|
||||
public _loginMigrationsResult!: mssql.StartLoginMigrationResult;
|
||||
public _loginMigrationsError: any;
|
||||
|
||||
public readonly _refreshGetSkuRecommendationIntervalInMinutes = 10;
|
||||
public readonly _performanceDataQueryIntervalInSeconds = 30;
|
||||
public readonly _staticDataQueryIntervalInSeconds = 60;
|
||||
@@ -506,6 +511,132 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return this._skuRecommendationResults;
|
||||
}
|
||||
|
||||
|
||||
public async getSourceConnectionString(): Promise<string> {
|
||||
return await azdata.connection.getConnectionString(this._sourceConnectionId, true);
|
||||
}
|
||||
|
||||
public async setTargetServerName(): Promise<void> {
|
||||
// If target server name has already been set, we can skip this part
|
||||
if (this._targetServerName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSqlManagedInstance(this._targetServerInstance) || isAzureSqlDatabaseServer(this._targetServerInstance)) {
|
||||
this._targetServerName = this._targetServerName ?? this._targetServerInstance.properties.fullyQualifiedDomainName;
|
||||
}
|
||||
}
|
||||
|
||||
public async getTargetConnectionString(): Promise<string> {
|
||||
await this.setTargetServerName();
|
||||
const connectionProfile = getConnectionProfile(
|
||||
this._targetServerName,
|
||||
this._targetServerInstance.id,
|
||||
this._targetUserName,
|
||||
this._targetPassword);
|
||||
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (result.connected && result.connectionId) {
|
||||
return azdata.connection.getConnectionString(result.connectionId, true);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private updateLoginMigrationResults(newResult: mssql.StartLoginMigrationResult): void {
|
||||
if (this._loginMigrationsResult && this._loginMigrationsResult.exceptionMap) {
|
||||
for (var key in newResult.exceptionMap) {
|
||||
this._loginMigrationsResult.exceptionMap[key] = [...this._loginMigrationsResult.exceptionMap[key] || [], newResult.exceptionMap[key]]
|
||||
}
|
||||
} else {
|
||||
this._loginMigrationsResult = newResult;
|
||||
}
|
||||
}
|
||||
|
||||
public async migrateLogins(): Promise<Boolean> {
|
||||
try {
|
||||
const sourceConnectionString = await this.getSourceConnectionString();
|
||||
const targetConnectionString = await this.getTargetConnectionString();
|
||||
console.log('Starting Login Migration at: ', new Date());
|
||||
|
||||
console.time("migrateLogins")
|
||||
var response = (await this.migrationService.migrateLogins(
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
this._loginsForMigration.map(row => row.loginName),
|
||||
this._aadDomainName
|
||||
))!;
|
||||
console.timeEnd("migrateLogins")
|
||||
|
||||
this.updateLoginMigrationResults(response)
|
||||
} catch (error) {
|
||||
console.log('Failed Login Migration at: ', new Date());
|
||||
logError(TelemetryViews.LoginMigrationWizard, 'StartLoginMigrationFailed', error);
|
||||
this._loginMigrationsError = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO AKMA : emit telemetry
|
||||
return true;
|
||||
}
|
||||
|
||||
public async establishUserMappings(): Promise<Boolean> {
|
||||
try {
|
||||
const sourceConnectionString = await this.getSourceConnectionString();
|
||||
const targetConnectionString = await this.getTargetConnectionString();
|
||||
|
||||
console.time("establishUserMapping")
|
||||
var response = (await this.migrationService.establishUserMapping(
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
this._loginsForMigration.map(row => row.loginName),
|
||||
this._aadDomainName
|
||||
))!;
|
||||
console.timeEnd("establishUserMapping")
|
||||
|
||||
this.updateLoginMigrationResults(response)
|
||||
} catch (error) {
|
||||
console.log('Failed Login Migration at: ', new Date());
|
||||
logError(TelemetryViews.LoginMigrationWizard, 'StartLoginMigrationFailed', error);
|
||||
this._loginMigrationsError = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO AKMA : emit telemetry
|
||||
return true;
|
||||
}
|
||||
|
||||
public async migrateServerRolesAndSetPermissions(): Promise<Boolean> {
|
||||
try {
|
||||
const sourceConnectionString = await this.getSourceConnectionString();
|
||||
const targetConnectionString = await this.getTargetConnectionString();
|
||||
|
||||
console.time("migrateServerRolesAndSetPermissions")
|
||||
var response = (await this.migrationService.migrateServerRolesAndSetPermissions(
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
this._loginsForMigration.map(row => row.loginName),
|
||||
this._aadDomainName
|
||||
))!;
|
||||
console.timeEnd("migrateServerRolesAndSetPermissions")
|
||||
|
||||
this.updateLoginMigrationResults(response)
|
||||
|
||||
console.log('Ending Login Migration at: ', new Date());
|
||||
console.log('Login migration response: ', response);
|
||||
|
||||
console.log('AKMA DEBUG response: ', response);
|
||||
} catch (error) {
|
||||
console.log('Failed Login Migration at: ', new Date());
|
||||
logError(TelemetryViews.LoginMigrationWizard, 'StartLoginMigrationFailed', error);
|
||||
this._loginMigrationsError = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO AKMA : emit telemetry
|
||||
return true;
|
||||
}
|
||||
|
||||
private async generateSkuRecommendationTelemetry(): Promise<void> {
|
||||
try {
|
||||
this._skuRecommendationResults?.recommendations?.sqlDbRecommendationResults
|
||||
@@ -1260,6 +1391,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public GetTargetType(): string {
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
return constants.LOGIN_MIGRATIONS_MI_TEXT;
|
||||
case MigrationTargetType.SQLVM:
|
||||
return constants.LOGIN_MIGRATIONS_VM_TEXT;
|
||||
case MigrationTargetType.SQLDB:
|
||||
return constants.LOGIN_MIGRATIONS_DB_TEXT;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerAssessment {
|
||||
|
||||
@@ -35,7 +35,9 @@ export enum TelemetryViews {
|
||||
SkuRecommendationWizard = 'SkuRecommendationWizard',
|
||||
DataCollectionWizard = 'GetAzureRecommendationDialog',
|
||||
SelectMigrationServiceDialog = 'SelectMigrationServiceDialog',
|
||||
Utils = 'Utils'
|
||||
Utils = 'Utils',
|
||||
LoginMigrationWizardController = 'LoginMigrationWizardController',
|
||||
LoginMigrationWizard = 'LoginMigrationWizard'
|
||||
}
|
||||
|
||||
export enum TelemetryAction {
|
||||
|
||||
439
extensions/sql-migration/src/wizard/loginMigrationStatusPage.ts
Normal file
439
extensions/sql-migration/src/wizard/loginMigrationStatusPage.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import { debounce, getPipelineStatusImage } from '../api/utils';
|
||||
import * as styles from '../constants/styles';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { EOL } from 'os';
|
||||
import { LoginMigrationStatusCodes } from '../constants/helper';
|
||||
|
||||
export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
private _migratingLoginsTable!: azdata.TableComponent;
|
||||
private _loginCount!: azdata.TextComponent;
|
||||
private _loginsTableValues!: any[];
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _progressLoaderContainer!: azdata.FlexContainer;
|
||||
private _migrationProgress!: azdata.InfoBoxComponent;
|
||||
private _progressLoader!: azdata.LoadingComponent;
|
||||
private _progressContainer!: azdata.FlexContainer;
|
||||
private _migrationProgressDetails!: azdata.TextComponent;
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.LOGIN_MIGRATIONS_STATUS_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
this._view = view;
|
||||
|
||||
const flex = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}).component();
|
||||
flex.addItem(await this.createRootContainer(view), { flex: '1 1 auto' });
|
||||
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(flex);
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
this.wizard.message = {
|
||||
text: constants.LOGIN_MIGRATIONS_STATUS_PAGE_PREVIOUS_BUTTON_ERROR,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this.wizard.backButton.enabled = false;
|
||||
this.wizard.backButton.hidden = true;
|
||||
this.wizard.backButton.label = constants.LOGIN_MIGRATIONS_STATUS_PAGE_PREVIOUS_BUTTON_TITLE;
|
||||
this.wizard.doneButton.enabled = false;
|
||||
|
||||
await this._loadMigratingLoginsList(this.migrationStateModel);
|
||||
|
||||
// load unfiltered table list
|
||||
await this._filterTableList('');
|
||||
|
||||
var result = await this._runLoginMigrations();
|
||||
|
||||
if (!result) {
|
||||
if (this.migrationStateModel._targetServerInstance) {
|
||||
await this._migrationProgress.updateProperties({
|
||||
'text': constants.LOGIN_MIGRATIONS_FAILED_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name),
|
||||
'style': 'error',
|
||||
});
|
||||
} else {
|
||||
await this._migrationProgress.updateProperties({
|
||||
'text': constants.LOGIN_MIGRATIONS_FAILED,
|
||||
'style': 'error',
|
||||
});
|
||||
}
|
||||
|
||||
this.wizard.message = {
|
||||
text: constants.LOGIN_MIGRATIONS_FAILED,
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
description: constants.LOGIN_MIGRATIONS_ERROR(this.migrationStateModel._loginMigrationsError.message),
|
||||
};
|
||||
|
||||
this._progressLoader.loading = false;
|
||||
}
|
||||
|
||||
await this._loadMigratingLoginsList(this.migrationStateModel);
|
||||
await this._filterTableList('');
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
this.wizard.message = {
|
||||
text: '',
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private createMigrationProgressLoader(): azdata.FlexContainer {
|
||||
this._progressLoader = this._view.modelBuilder.loadingComponent()
|
||||
.withProps({
|
||||
loadingText: constants.LOGIN_MIGRATION_IN_PROGRESS,
|
||||
loadingCompletedText: constants.LOGIN_MIGRATIONS_COMPLETE,
|
||||
loading: true,
|
||||
CSSStyles: { 'margin-right': '20px' }
|
||||
})
|
||||
.component();
|
||||
|
||||
this._migrationProgress = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'information',
|
||||
text: constants.LOGIN_MIGRATION_IN_PROGRESS,
|
||||
CSSStyles: {
|
||||
...styles.PAGE_TITLE_CSS,
|
||||
'margin-right': '20px',
|
||||
'font-size': '13px',
|
||||
'line-height': '126%'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._progressLoaderContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
height: '100%',
|
||||
flexFlow: 'row',
|
||||
alignItems: 'center'
|
||||
}).component();
|
||||
|
||||
this._progressLoaderContainer.addItem(this._migrationProgress, { flex: '0 0 auto' });
|
||||
this._progressLoaderContainer.addItem(this._progressLoader, { flex: '0 0 auto' });
|
||||
|
||||
return this._progressLoaderContainer;
|
||||
}
|
||||
|
||||
private async createMigrationProgressDetails(): Promise<azdata.TextComponent> {
|
||||
this._migrationProgressDetails = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.STARTING_LOGIN_MIGRATION,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'width': '660px'
|
||||
}
|
||||
}).component();
|
||||
return this._migrationProgressDetails;
|
||||
}
|
||||
|
||||
private createSearchComponent(): azdata.DivContainer {
|
||||
let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({
|
||||
stopEnterPropagation: true,
|
||||
placeHolder: constants.SEARCH,
|
||||
width: 200
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
resourceSearchBox.onTextChanged(value => this._filterTableList(value)));
|
||||
|
||||
const searchContainer = this._view.modelBuilder.divContainer().withItems([resourceSearchBox]).withProps({
|
||||
CSSStyles: {
|
||||
'width': '200px',
|
||||
'margin-top': '8px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
return searchContainer;
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private async _filterTableList(value: string): Promise<void> {
|
||||
let tableRows = this._loginsTableValues;
|
||||
if (this._loginsTableValues && value?.length > 0) {
|
||||
tableRows = this._loginsTableValues
|
||||
.filter(row => {
|
||||
const searchText = value?.toLowerCase();
|
||||
return row[0]?.toLowerCase()?.indexOf(searchText) > -1 // source login
|
||||
|| row[1]?.toLowerCase()?.indexOf(searchText) > -1 // login type
|
||||
|| row[2]?.toLowerCase()?.indexOf(searchText) > -1 // default database
|
||||
|| row[3]?.title.toLowerCase()?.indexOf(searchText) > -1; // migration status
|
||||
});
|
||||
}
|
||||
|
||||
await this._migratingLoginsTable.updateProperty('data', tableRows);
|
||||
await this.updateDisplayedLoginCount();
|
||||
}
|
||||
|
||||
public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
await this._loadMigratingLoginsList(this.migrationStateModel);
|
||||
|
||||
this._progressContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ height: '100%', flexFlow: 'column' })
|
||||
.withProps({ CSSStyles: { 'margin-bottom': '10px' } })
|
||||
.component();
|
||||
|
||||
this._progressContainer.addItem(this.createMigrationProgressLoader(), { flex: '0 0 auto' });
|
||||
this._progressContainer.addItem(await this.createMigrationProgressDetails(), { flex: '0 0 auto' });
|
||||
|
||||
this._loginCount = this._view.modelBuilder.text().withProps({
|
||||
value: constants.NUMBER_LOGINS_MIGRATING(this._loginsTableValues.length, this._loginsTableValues.length),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin-top': '8px'
|
||||
},
|
||||
ariaLive: 'polite'
|
||||
}).component();
|
||||
|
||||
const cssClass = 'no-borders';
|
||||
this._migratingLoginsTable = this._view.modelBuilder.table()
|
||||
.withProps({
|
||||
data: [],
|
||||
width: 650,
|
||||
height: '100%',
|
||||
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
|
||||
columns: [
|
||||
{
|
||||
name: constants.SOURCE_LOGIN,
|
||||
value: 'sourceLogin',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 250,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.LOGIN_TYPE,
|
||||
value: 'loginType',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 90,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.DEFAULT_DATABASE,
|
||||
value: 'defaultDatabase',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 100,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
<azdata.HyperlinkColumn>{
|
||||
name: constants.LOGIN_MIGRATION_STATUS_COLUMN,
|
||||
value: 'migrationStatus',
|
||||
width: 120,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
icon: IconPathHelper.inProgressMigration,
|
||||
showText: true,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
]
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._migratingLoginsTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
switch (buttonState?.column) {
|
||||
case 3:
|
||||
const loginName = this._migratingLoginsTable!.data[rowState.row][0];
|
||||
const status = this._migratingLoginsTable!.data[rowState.row][3].title;
|
||||
const statusMessage = constants.LOGIN_MIGRATION_STATUS_LABEL(status);
|
||||
var errors = [];
|
||||
|
||||
if (this.migrationStateModel._loginMigrationsResult?.exceptionMap) {
|
||||
const exception_key = Object.keys(this.migrationStateModel._loginMigrationsResult.exceptionMap).find(key => key.toLocaleLowerCase() === loginName.toLocaleLowerCase());
|
||||
if (exception_key) {
|
||||
for (var exception of this.migrationStateModel._loginMigrationsResult.exceptionMap[exception_key]) {
|
||||
if (Array.isArray(exception)) {
|
||||
for (var inner_exception of exception) {
|
||||
errors.push(inner_exception.Message);
|
||||
}
|
||||
} else {
|
||||
errors.push(exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unique_errors = new Set(errors);
|
||||
|
||||
// TODO AKMA: Make errors prettier (spacing between errors is weird)
|
||||
this.showDialogMessage(
|
||||
constants.DATABASE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
[...unique_errors].join(EOL));
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
// load unfiltered table list
|
||||
await this._filterTableList('');
|
||||
|
||||
const flex = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0px 28px 0px 28px'
|
||||
}
|
||||
}).component();
|
||||
flex.addItem(this._progressContainer, { flex: '0 0 auto' });
|
||||
flex.addItem(this.createSearchComponent(), { flex: '0 0 auto' });
|
||||
flex.addItem(this._loginCount, { flex: '0 0 auto' });
|
||||
flex.addItem(this._migratingLoginsTable);
|
||||
return flex;
|
||||
}
|
||||
|
||||
private async _loadMigratingLoginsList(stateMachine: MigrationStateModel): Promise<void> {
|
||||
const loginList = stateMachine._loginsForMigration || [];
|
||||
loginList.sort((a, b) => a.loginName.localeCompare(b.loginName));
|
||||
|
||||
this._loginsTableValues = loginList.map(login => {
|
||||
const loginName = login.loginName;
|
||||
|
||||
var status = LoginMigrationStatusCodes.InProgress;
|
||||
var title = constants.LOGIN_MIGRATION_STATUS_IN_PROGRESS;
|
||||
if (stateMachine._loginMigrationsError) {
|
||||
status = LoginMigrationStatusCodes.Failed;
|
||||
title = constants.LOGIN_MIGRATION_STATUS_FAILED;
|
||||
} else if (stateMachine._loginMigrationsResult) {
|
||||
status = LoginMigrationStatusCodes.Succeeded;
|
||||
title = constants.LOGIN_MIGRATION_STATUS_SUCCEEDED;
|
||||
var didLoginFail = Object.keys(stateMachine._loginMigrationsResult.exceptionMap).some(key => key.toLocaleLowerCase() === loginName.toLocaleLowerCase());
|
||||
if (didLoginFail) {
|
||||
status = LoginMigrationStatusCodes.Failed;
|
||||
title = constants.LOGIN_MIGRATION_STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
loginName,
|
||||
login.loginType,
|
||||
login.defaultDatabaseName,
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: getPipelineStatusImage(status),
|
||||
title: title,
|
||||
},
|
||||
];
|
||||
}) || [];
|
||||
}
|
||||
|
||||
private _getTotalNumberOfLogins(): number {
|
||||
return this._loginsTableValues?.length || 0;
|
||||
}
|
||||
|
||||
private async updateDisplayedLoginCount() {
|
||||
await this._loginCount.updateProperties({
|
||||
'value': constants.NUMBER_LOGINS_MIGRATING(this._getNumberOfDisplayedLogins(), this._getTotalNumberOfLogins())
|
||||
});
|
||||
}
|
||||
|
||||
private _getNumberOfDisplayedLogins(): number {
|
||||
return this._migratingLoginsTable?.data?.length || 0;
|
||||
}
|
||||
|
||||
private async _runLoginMigrations(): Promise<Boolean> {
|
||||
this._progressLoader.loading = true;
|
||||
|
||||
if (this.migrationStateModel._targetServerInstance) {
|
||||
await this._migrationProgress.updateProperties({
|
||||
'text': constants.LOGIN_MIGRATIONS_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name)
|
||||
});
|
||||
}
|
||||
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.STARTING_LOGIN_MIGRATION
|
||||
});
|
||||
|
||||
var result = await this.migrationStateModel.migrateLogins();
|
||||
|
||||
if (!result) {
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.STARTING_LOGIN_MIGRATION_FAILED
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.ESTABLISHING_USER_MAPPINGS
|
||||
});
|
||||
|
||||
result = await this.migrationStateModel.establishUserMappings();
|
||||
|
||||
if (!result) {
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.ESTABLISHING_USER_MAPPINGS_FAILED
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS
|
||||
});
|
||||
|
||||
result = await this.migrationStateModel.migrateServerRolesAndSetPermissions();
|
||||
|
||||
if (!result) {
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS_FAILED
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'CSSStyles': { 'display': 'none' },
|
||||
});
|
||||
|
||||
if (this.migrationStateModel._targetServerInstance) {
|
||||
await this._migrationProgress.updateProperties({
|
||||
'text': constants.LOGIN_MIGRATIONS_COMPLETED_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name),
|
||||
'style': 'success',
|
||||
});
|
||||
} else {
|
||||
await this._migrationProgress.updateProperties({
|
||||
'text': constants.LOGIN_MIGRATIONS_COMPLETE,
|
||||
'style': 'success',
|
||||
});
|
||||
}
|
||||
|
||||
this._progressLoader.loading = false;
|
||||
|
||||
this.wizard.doneButton.enabled = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,974 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { EOL } from 'os';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import * as styles from '../constants/styles';
|
||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
import * as utils from '../api/utils';
|
||||
import { azureResource } from 'azurecore';
|
||||
import { AzureSqlDatabaseServer, SqlVMServer } from '../api/azure';
|
||||
import { collectTargetDatabaseInfo, TargetDatabaseInfo, isSysAdmin } from '../api/sqlUtils';
|
||||
|
||||
export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private _pageDescription!: azdata.TextComponent;
|
||||
private _azureSqlTargetTypeDropdown!: azdata.DropDownComponent;
|
||||
private _azureAccountsDropdown!: azdata.DropDownComponent;
|
||||
private _accountTenantDropdown!: azdata.DropDownComponent;
|
||||
private _accountTenantFlexContainer!: azdata.FlexContainer;
|
||||
private _azureSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _azureLocationDropdown!: azdata.DropDownComponent;
|
||||
private _azureResourceGroupLabel!: azdata.TextComponent;
|
||||
private _azureResourceGroupDropdown!: azdata.DropDownComponent;
|
||||
private _azureResourceDropdownLabel!: azdata.TextComponent;
|
||||
private _azureResourceDropdown!: azdata.DropDownComponent;
|
||||
private _resourceSelectionContainer!: azdata.FlexContainer;
|
||||
private _resourceAuthenticationContainer!: azdata.FlexContainer;
|
||||
private _targetUserNameInputBox!: azdata.InputBoxComponent;
|
||||
private _targetPasswordInputBox!: azdata.InputBoxComponent;
|
||||
private _testConectionButton!: azdata.ButtonComponent;
|
||||
private _connectionResultsInfoBox!: azdata.InfoBoxComponent;
|
||||
private _migrationTargetPlatform!: MigrationTargetType;
|
||||
|
||||
constructor(
|
||||
wizard: azdata.window.Wizard,
|
||||
migrationStateModel: MigrationStateModel) {
|
||||
super(
|
||||
wizard,
|
||||
azdata.window.createWizardPage(constants.LOGIN_MIGRATIONS_AZURE_SQL_TARGET_PAGE_TITLE),
|
||||
migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
this._view = view;
|
||||
|
||||
const loginMigrationPreviewInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'information',
|
||||
text: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_PREVIEW_WARNING,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
const loginMigrationInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'information',
|
||||
text: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_DATA_MIGRATION_WARNING,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
const hasSysAdminPermissions: boolean = await isSysAdmin(this.migrationStateModel.sourceConnectionId);
|
||||
const connectionProfile: azdata.connection.ConnectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
|
||||
const permissionsInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'warning',
|
||||
text: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_PERMISSIONS_WARNING(connectionProfile.userName, connectionProfile.serverName),
|
||||
CSSStyles: { ...styles.BODY_CSS },
|
||||
}).component();
|
||||
|
||||
if (hasSysAdminPermissions) {
|
||||
await permissionsInfoBox.updateProperties({
|
||||
'CSSStyles': { 'display': 'none' },
|
||||
});
|
||||
}
|
||||
|
||||
this._pageDescription = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.LOGIN_MIGRATIONS_TARGET_SELECTION_PAGE_DESCRIPTION,
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
|
||||
}).component();
|
||||
|
||||
const form = this._view.modelBuilder.formContainer()
|
||||
.withFormItems([
|
||||
{ component: loginMigrationPreviewInfoBox },
|
||||
{ component: loginMigrationInfoBox },
|
||||
{ component: permissionsInfoBox },
|
||||
{ component: this._pageDescription },
|
||||
{ component: this.createAzureSqlTargetTypeDropdown() },
|
||||
{ component: this.createAzureAccountsDropdown() },
|
||||
{ component: this.createAzureTenantContainer() },
|
||||
{ component: this.createTargetDropdownContainer() }
|
||||
]).withProps({
|
||||
CSSStyles: { 'padding-top': '0' }
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
await this._view.initializeModel(form);
|
||||
}
|
||||
|
||||
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
this.wizard.nextButton.enabled = false;
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
this.wizard.message = {
|
||||
text: '',
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
return true;
|
||||
}
|
||||
if (!this.migrationStateModel._targetServerInstance || !this.migrationStateModel._targetUserName || !this.migrationStateModel._targetPassword) {
|
||||
this.wizard.message = {
|
||||
text: constants.SELECT_DATABASE_TO_CONTINUE,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
}; ``
|
||||
return false;
|
||||
}
|
||||
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();
|
||||
break;
|
||||
}
|
||||
|
||||
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
|
||||
console.log(isSqlDbTarget);
|
||||
|
||||
if (this._targetUserNameInputBox) {
|
||||
await this._targetUserNameInputBox.updateProperty('required', true);
|
||||
}
|
||||
|
||||
if (this._targetPasswordInputBox) {
|
||||
await this._targetPasswordInputBox.updateProperty('required', true);
|
||||
}
|
||||
|
||||
await utils.updateControlDisplay(this._resourceAuthenticationContainer, true);
|
||||
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();
|
||||
}
|
||||
|
||||
if (this.migrationStateModel._didUpdateDatabasesForMigration) {
|
||||
this._updateConnectionButtonState();
|
||||
}
|
||||
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
this.wizard.message = { text: '' };
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
if (!this.migrationStateModel._azureAccount) {
|
||||
errors.push(constants.INVALID_ACCOUNT_ERROR);
|
||||
}
|
||||
|
||||
if (!this.migrationStateModel._targetSubscription ||
|
||||
(<azdata.CategoryValue>this._azureSubscriptionDropdown.value)?.displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
}
|
||||
if (!this.migrationStateModel._location ||
|
||||
(<azdata.CategoryValue>this._azureLocationDropdown.value)?.displayName === constants.NO_LOCATION_FOUND) {
|
||||
errors.push(constants.INVALID_LOCATION_ERROR);
|
||||
}
|
||||
if (!this.migrationStateModel._resourceGroup ||
|
||||
(<azdata.CategoryValue>this._azureResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
|
||||
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
|
||||
}
|
||||
|
||||
const resourceDropdownValue = (<azdata.CategoryValue>this._azureResourceDropdown.value)?.displayName;
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
const targetMi = this.migrationStateModel._targetServerInstance as azureResource.AzureSqlManagedInstance;
|
||||
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));
|
||||
}
|
||||
break;
|
||||
case MigrationTargetType.SQLVM:
|
||||
const targetVm = this.migrationStateModel._targetServerInstance as SqlVMServer;
|
||||
if (!targetVm || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) {
|
||||
errors.push(constants.INVALID_VIRTUAL_MACHINE_ERROR);
|
||||
}
|
||||
break;
|
||||
case MigrationTargetType.SQLDB:
|
||||
const targetSqlDB = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer;
|
||||
if (!targetSqlDB || resourceDropdownValue === constants.NO_SQL_DATABASE_FOUND) {
|
||||
errors.push(constants.INVALID_SQL_DATABASE_ERROR);
|
||||
}
|
||||
// TODO: verify what state check is needed/possible?
|
||||
if (targetSqlDB.properties.state !== 'Ready') {
|
||||
errors.push(constants.SQLDB_NOT_READY_ERROR(targetSqlDB.name, targetSqlDB.properties.state));
|
||||
}
|
||||
|
||||
// validate target sqldb username exists
|
||||
const targetUsernameValue = this._targetUserNameInputBox.value ?? '';
|
||||
if (targetUsernameValue.length < 1) {
|
||||
errors.push(constants.MISSING_TARGET_USERNAME_ERROR);
|
||||
}
|
||||
|
||||
// validate target sqldb password exists
|
||||
const targetPasswordValue = this._targetPasswordInputBox.value ?? '';
|
||||
if (targetPasswordValue.length < 1) {
|
||||
errors.push(constants.MISSING_TARGET_PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
this.wizard.message = {
|
||||
text: errors.join(EOL),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
this.wizard.registerNavigationValidator(async (pageChangeInfo) => true);
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private createAzureSqlTargetTypeDropdown(): azdata.FlexContainer {
|
||||
const azureSqlTargetTypeLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.LOGIN_MIGRATIONS_TARGET_TYPE_SELECTION_TITLE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-top': '-1em'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._azureSqlTargetTypeDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.LOGIN_MIGRATIONS_TARGET_TYPE_SELECTION_TITLE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
placeholder: constants.SELECT_AN_TARGET_TYPE,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._azureSqlTargetTypeDropdown.values = [/* constants.LOGIN_MIGRATIONS_DB_TEXT, */ constants.LOGIN_MIGRATIONS_MI_TEXT, constants.LOGIN_MIGRATIONS_VM_TEXT];
|
||||
|
||||
this._disposables.push(this._azureSqlTargetTypeDropdown.onValueChanged(async (value) => {
|
||||
switch (value) {
|
||||
case constants.LOGIN_MIGRATIONS_DB_TEXT: {
|
||||
this.migrationStateModel._targetType = MigrationTargetType.SQLDB;
|
||||
break;
|
||||
}
|
||||
case constants.LOGIN_MIGRATIONS_MI_TEXT: {
|
||||
this.migrationStateModel._targetType = MigrationTargetType.SQLMI;
|
||||
break;
|
||||
}
|
||||
case constants.LOGIN_MIGRATIONS_VM_TEXT: {
|
||||
this.migrationStateModel._targetType = MigrationTargetType.SQLVM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
break;
|
||||
}
|
||||
await this.populateAzureAccountsDropdown();
|
||||
await this.populateSubscriptionDropdown();
|
||||
await this.populateLocationDropdown();
|
||||
console.log(this.migrationStateModel._targetType);
|
||||
}));
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
})
|
||||
.withItems([
|
||||
azureSqlTargetTypeLabel,
|
||||
this._azureSqlTargetTypeDropdown
|
||||
])
|
||||
.component();
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private createAzureAccountsDropdown(): azdata.FlexContainer {
|
||||
const azureAccountLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' }
|
||||
}).component();
|
||||
this._azureAccountsDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
placeholder: constants.SELECT_AN_ACCOUNT,
|
||||
CSSStyles: { 'margin-top': '-1em' },
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._azureAccountsDropdown.onValueChanged(async (value) => {
|
||||
if (value && value !== 'undefined') {
|
||||
const selectedAccount = this.migrationStateModel._azureAccounts.find(account => account.displayInfo.displayName === value);
|
||||
this.migrationStateModel._azureAccount = (selectedAccount)
|
||||
? utils.deepClone(selectedAccount)!
|
||||
: undefined!;
|
||||
}
|
||||
await this.populateTenantsDropdown();
|
||||
}));
|
||||
|
||||
const linkAccountButton = this._view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: constants.ACCOUNT_LINK_BUTTON_LABEL,
|
||||
url: '',
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
linkAccountButton.onDidClick(async (event) => {
|
||||
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
|
||||
await this.populateAzureAccountsDropdown();
|
||||
this.wizard.message = { text: '' };
|
||||
await this._azureAccountsDropdown.validate();
|
||||
}));
|
||||
|
||||
return this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([
|
||||
azureAccountLabel,
|
||||
this._azureAccountsDropdown,
|
||||
linkAccountButton])
|
||||
.component();
|
||||
}
|
||||
|
||||
private createAzureTenantContainer(): azdata.FlexContainer {
|
||||
const azureTenantDropdownLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.AZURE_TENANT,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
|
||||
this._accountTenantDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.AZURE_TENANT,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
fireOnTextChange: true,
|
||||
placeholder: constants.SELECT_A_TENANT
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._accountTenantDropdown.onValueChanged(async (value) => {
|
||||
if (value && value !== 'undefined') {
|
||||
/**
|
||||
* 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);
|
||||
if (selectedTenant) {
|
||||
this.migrationStateModel._azureTenant = utils.deepClone(selectedTenant)!;
|
||||
this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel._azureTenant];
|
||||
}
|
||||
}
|
||||
await this.populateSubscriptionDropdown();
|
||||
}));
|
||||
|
||||
this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([
|
||||
azureTenantDropdownLabel,
|
||||
this._accountTenantDropdown])
|
||||
.withProps({ CSSStyles: { 'display': 'none' } })
|
||||
.component();
|
||||
return this._accountTenantFlexContainer;
|
||||
}
|
||||
|
||||
private createTargetDropdownContainer(): azdata.FlexContainer {
|
||||
const subscriptionDropdownLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.SUBSCRIPTION,
|
||||
description: constants.TARGET_SUBSCRIPTION_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS, }
|
||||
}).component();
|
||||
this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.SUBSCRIPTION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
placeholder: constants.SELECT_A_SUBSCRIPTION,
|
||||
CSSStyles: { 'margin-top': '-1em' },
|
||||
}).component();
|
||||
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);
|
||||
this.migrationStateModel._targetSubscription = (selectedSubscription)
|
||||
? utils.deepClone(selectedSubscription)!
|
||||
: undefined!;
|
||||
this.migrationStateModel.refreshDatabaseBackupPage = true;
|
||||
}
|
||||
await this.populateLocationDropdown();
|
||||
}));
|
||||
|
||||
const azureLocationLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.LOCATION,
|
||||
description: constants.TARGET_LOCATION_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
this._azureLocationDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.LOCATION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
placeholder: constants.SELECT_A_LOCATION,
|
||||
CSSStyles: { 'margin-top': '-1em' },
|
||||
}).component();
|
||||
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);
|
||||
this.migrationStateModel._location = (selectedLocation)
|
||||
? utils.deepClone(selectedLocation)!
|
||||
: undefined!;
|
||||
}
|
||||
this.migrationStateModel.refreshDatabaseBackupPage = true;
|
||||
await this.populateResourceGroupDropdown();
|
||||
}));
|
||||
|
||||
this._resourceSelectionContainer = this._createResourceDropdowns();
|
||||
this._resourceAuthenticationContainer = this._createResourceAuthenticationContainer();
|
||||
|
||||
return this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
subscriptionDropdownLabel,
|
||||
this._azureSubscriptionDropdown,
|
||||
azureLocationLabel,
|
||||
this._azureLocationDropdown,
|
||||
this._resourceSelectionContainer,
|
||||
this._resourceAuthenticationContainer])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
}
|
||||
|
||||
private _createResourceAuthenticationContainer(): azdata.FlexContainer {
|
||||
// target user name
|
||||
const targetUserNameLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.TARGET_USERNAME_LAbEL,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' }
|
||||
}).component();
|
||||
this._targetUserNameInputBox = this._view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
width: '300px',
|
||||
inputType: 'text',
|
||||
placeHolder: constants.TARGET_USERNAME_PLACEHOLDER,
|
||||
required: false,
|
||||
CSSStyles: { 'margin-top': '-1em' },
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._targetUserNameInputBox.onTextChanged(
|
||||
(value: string) => {
|
||||
this.migrationStateModel._targetUserName = value ?? '';
|
||||
this._updateConnectionButtonState();
|
||||
}));
|
||||
|
||||
this._disposables.push(
|
||||
this._targetUserNameInputBox.onValidityChanged(
|
||||
valid => this._updateConnectionButtonState()));
|
||||
|
||||
// target password
|
||||
const targetPasswordLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.TARGET_PASSWORD_LAbEL,
|
||||
requiredIndicator: true,
|
||||
title: '',
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' }
|
||||
}).component();
|
||||
this._targetPasswordInputBox = this._view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
width: '300px',
|
||||
inputType: 'password',
|
||||
placeHolder: constants.TARGET_PASSWORD_PLACEHOLDER,
|
||||
required: false,
|
||||
CSSStyles: { 'margin-top': '-1em' },
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._targetPasswordInputBox.onTextChanged(
|
||||
(value: string) => {
|
||||
this.migrationStateModel._targetPassword = value ?? '';
|
||||
this._updateConnectionButtonState();
|
||||
}));
|
||||
|
||||
this._disposables.push(
|
||||
this._targetPasswordInputBox.onValidityChanged(
|
||||
valid => this._updateConnectionButtonState()));
|
||||
|
||||
// test connection button
|
||||
this._testConectionButton = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
enabled: false,
|
||||
label: constants.TARGET_CONNECTION_LABEL,
|
||||
width: '80px',
|
||||
}).component();
|
||||
|
||||
this._connectionResultsInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'success',
|
||||
text: '',
|
||||
announceText: true,
|
||||
CSSStyles: { 'display': 'none' },
|
||||
})
|
||||
.component();
|
||||
|
||||
const connectionButtonLoadingContainer = this._view.modelBuilder.loadingComponent()
|
||||
.withItem(this._testConectionButton)
|
||||
.withProps({ loading: false })
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._testConectionButton.onDidClick(async (value) => {
|
||||
this.wizard.message = { text: '' };
|
||||
|
||||
const targetDatabaseServer = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer;
|
||||
const userName = this.migrationStateModel._targetUserName;
|
||||
const password = this.migrationStateModel._targetPassword;
|
||||
const targetDatabases: TargetDatabaseInfo[] = [];
|
||||
if (targetDatabaseServer && userName && password) {
|
||||
try {
|
||||
connectionButtonLoadingContainer.loading = true;
|
||||
await utils.updateControlDisplay(this._connectionResultsInfoBox, false);
|
||||
this.wizard.nextButton.enabled = false;
|
||||
targetDatabases.push(
|
||||
...await collectTargetDatabaseInfo(
|
||||
targetDatabaseServer,
|
||||
userName,
|
||||
password));
|
||||
await this._showConnectionResults(targetDatabases);
|
||||
} catch (error) {
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE,
|
||||
description: constants.SQL_TARGET_CONNECTION_ERROR(error.message),
|
||||
};
|
||||
await this._showConnectionResults(
|
||||
targetDatabases,
|
||||
constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE);
|
||||
}
|
||||
finally {
|
||||
connectionButtonLoadingContainer.loading = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const connectionContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
connectionButtonLoadingContainer,
|
||||
this._connectionResultsInfoBox],
|
||||
{ flex: '0 0 auto' })
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '15px 0 0 0' } })
|
||||
.component();
|
||||
|
||||
return this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
targetUserNameLabel,
|
||||
this._targetUserNameInputBox,
|
||||
targetPasswordLabel,
|
||||
this._targetPasswordInputBox,
|
||||
connectionContainer])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withProps({ CSSStyles: { 'margin': '15px 0 0 0' } })
|
||||
.component();
|
||||
}
|
||||
|
||||
private async _showConnectionResults(
|
||||
databases: TargetDatabaseInfo[],
|
||||
errorMessage?: string): Promise<void> {
|
||||
|
||||
const hasError = errorMessage !== undefined;
|
||||
const hasDatabases = databases.length > 0;
|
||||
this._connectionResultsInfoBox.style = hasError
|
||||
? 'error'
|
||||
: hasDatabases
|
||||
? 'success'
|
||||
: 'warning';
|
||||
this._connectionResultsInfoBox.text = hasError
|
||||
? constants.SQL_TARGET_CONNECTION_ERROR(errorMessage)
|
||||
: constants.SQL_TARGET_CONNECTION_SUCCESS_LOGINS(databases.length.toLocaleString());
|
||||
await utils.updateControlDisplay(this._connectionResultsInfoBox, true);
|
||||
|
||||
if (!hasError) {
|
||||
this.wizard.nextButton.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _createResourceDropdowns(): azdata.FlexContainer {
|
||||
this._azureResourceGroupLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.RESOURCE_GROUP,
|
||||
description: constants.TARGET_RESOURCE_GROUP_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.RESOURCE_GROUP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
placeholder: constants.SELECT_A_RESOURCE_GROUP,
|
||||
CSSStyles: { 'margin-top': '-1em' },
|
||||
}).component();
|
||||
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);
|
||||
this.migrationStateModel._resourceGroup = (selectedResourceGroup)
|
||||
? utils.deepClone(selectedResourceGroup)!
|
||||
: undefined!;
|
||||
}
|
||||
await this.populateResourceInstanceDropdown();
|
||||
}));
|
||||
|
||||
this._azureResourceDropdownLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
|
||||
description: constants.TARGET_RESOURCE_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
this._azureResourceDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
placeholder: constants.SELECT_SERVICE_PLACEHOLDER,
|
||||
CSSStyles: { 'margin-top': '-1em' },
|
||||
loading: false,
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._azureResourceDropdown.onValueChanged(async (value) => {
|
||||
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
|
||||
if (value && value !== 'undefined' &&
|
||||
value !== constants.NO_MANAGED_INSTANCE_FOUND &&
|
||||
value !== constants.NO_SQL_DATABASE_SERVER_FOUND &&
|
||||
value !== constants.NO_VIRTUAL_MACHINE_FOUND) {
|
||||
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLVM:
|
||||
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);
|
||||
|
||||
if (selectedMi) {
|
||||
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance;
|
||||
|
||||
this.wizard.message = { text: '' };
|
||||
if (this.migrationStateModel._targetServerInstance.properties.state !== 'Ready') {
|
||||
this.wizard.message = {
|
||||
text: constants.MI_NOT_READY_ERROR(
|
||||
this.migrationStateModel._targetServerInstance.name,
|
||||
this.migrationStateModel._targetServerInstance.properties.state),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MigrationTargetType.SQLDB:
|
||||
const sqlDatabaseServer = this.migrationStateModel._targetSqlDatabaseServers.find(
|
||||
sqldb => sqldb.name === value || constants.UNAVAILABLE_TARGET_PREFIX(sqldb.name) === value);
|
||||
|
||||
if (sqlDatabaseServer) {
|
||||
this.migrationStateModel._targetServerInstance = utils.deepClone(sqlDatabaseServer)! as AzureSqlDatabaseServer;
|
||||
this.wizard.message = { text: '' };
|
||||
if (this.migrationStateModel._targetServerInstance.properties.state === 'Ready') {
|
||||
this._targetUserNameInputBox.value = this.migrationStateModel._targetServerInstance.properties.administratorLogin;
|
||||
} else {
|
||||
this.wizard.message = {
|
||||
text: constants.SQLDB_NOT_READY_ERROR(
|
||||
this.migrationStateModel._targetServerInstance.name,
|
||||
this.migrationStateModel._targetServerInstance.properties.state),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.migrationStateModel._targetServerInstance = undefined!;
|
||||
if (isSqlDbTarget) {
|
||||
this._targetUserNameInputBox.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
this.migrationStateModel._sqlMigrationServices = undefined!;
|
||||
if (isSqlDbTarget) {
|
||||
this._targetPasswordInputBox.value = '';
|
||||
this._updateConnectionButtonState();
|
||||
}
|
||||
}));
|
||||
|
||||
return this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
this._azureResourceGroupLabel,
|
||||
this._azureResourceGroupDropdown,
|
||||
this._azureResourceDropdownLabel,
|
||||
this._azureResourceDropdown])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
}
|
||||
|
||||
private _updateConnectionButtonState(): void {
|
||||
const targetDatabaseServer = (this._azureResourceDropdown.value as azdata.CategoryValue)?.name ?? '';
|
||||
const userName = this._targetUserNameInputBox.value ?? '';
|
||||
const password = this._targetPasswordInputBox.value ?? '';
|
||||
this._testConectionButton.enabled = targetDatabaseServer.length > 0
|
||||
&& userName.length > 0
|
||||
&& password.length > 0;
|
||||
}
|
||||
|
||||
private async populateAzureAccountsDropdown(): Promise<void> {
|
||||
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;
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._azureAccountsDropdown,
|
||||
this.migrationStateModel._azureAccount?.displayInfo?.userId,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount);
|
||||
this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants);
|
||||
}
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._accountTenantDropdown,
|
||||
this.migrationStateModel._azureTenant?.id,
|
||||
true);
|
||||
await this._accountTenantFlexContainer.updateCssStyles(this.migrationStateModel._azureAccount.properties.tenants.length > 1
|
||||
? { 'display': 'inline' }
|
||||
: { 'display': 'none' }
|
||||
);
|
||||
await this._azureAccountsDropdown.validate();
|
||||
} finally {
|
||||
this._accountTenantDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async populateSubscriptionDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureSubscriptionDropdown.loading = true;
|
||||
this.migrationStateModel._subscriptions = await utils.getAzureSubscriptions(this.migrationStateModel._azureAccount);
|
||||
this._azureSubscriptionDropdown.values = await utils.getAzureSubscriptionsDropdownValues(this.migrationStateModel._subscriptions);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureSubscriptionDropdown.loading = false;
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._azureSubscriptionDropdown,
|
||||
this.migrationStateModel._targetSubscription?.id,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
public async populateLocationDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureLocationDropdown.loading = true;
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.migrationStateModel._targetManagedInstances = await utils.getManagedInstances(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription);
|
||||
this.migrationStateModel._locations = await utils.getResourceLocations(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._targetManagedInstances);
|
||||
break;
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.migrationStateModel._targetSqlVirtualMachines = await utils.getVirtualMachines(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription);
|
||||
this.migrationStateModel._locations = await utils.getResourceLocations(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._targetSqlVirtualMachines);
|
||||
break;
|
||||
case MigrationTargetType.SQLDB:
|
||||
this.migrationStateModel._targetSqlDatabaseServers = await utils.getAzureSqlDatabaseServers(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription);
|
||||
this.migrationStateModel._locations = await utils.getResourceLocations(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._targetSqlDatabaseServers);
|
||||
break;
|
||||
}
|
||||
this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureLocationDropdown.loading = false;
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._azureLocationDropdown,
|
||||
this.migrationStateModel._location?.displayName,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
public async populateResourceGroupDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureResourceGroupDropdown.loading = true;
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
|
||||
this.migrationStateModel._targetManagedInstances,
|
||||
this.migrationStateModel._location);
|
||||
break;
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
|
||||
this.migrationStateModel._targetSqlVirtualMachines,
|
||||
this.migrationStateModel._location);
|
||||
break;
|
||||
case MigrationTargetType.SQLDB:
|
||||
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
|
||||
this.migrationStateModel._targetSqlDatabaseServers,
|
||||
this.migrationStateModel._location);
|
||||
break;
|
||||
}
|
||||
this._azureResourceGroupDropdown.values = utils.getResourceDropdownValues(
|
||||
this.migrationStateModel._resourceGroups,
|
||||
constants.RESOURCE_GROUP_NOT_FOUND);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureResourceGroupDropdown.loading = false;
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._azureResourceGroupDropdown,
|
||||
this.migrationStateModel._resourceGroup?.id,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
private async populateResourceInstanceDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureResourceDropdown.loading = true;
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this._azureResourceDropdown.values = await utils.getManagedInstancesDropdownValues(
|
||||
this.migrationStateModel._targetManagedInstances,
|
||||
this.migrationStateModel._location,
|
||||
this.migrationStateModel._resourceGroup);
|
||||
break;
|
||||
case MigrationTargetType.SQLVM:
|
||||
this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(
|
||||
this.migrationStateModel._targetSqlVirtualMachines,
|
||||
this.migrationStateModel._location,
|
||||
this.migrationStateModel._resourceGroup?.name,
|
||||
constants.NO_VIRTUAL_MACHINE_FOUND);
|
||||
break;
|
||||
case MigrationTargetType.SQLDB:
|
||||
this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(
|
||||
this.migrationStateModel._targetSqlDatabaseServers,
|
||||
this.migrationStateModel._location,
|
||||
this.migrationStateModel._resourceGroup?.name,
|
||||
constants.NO_SQL_DATABASE_SERVER_FOUND);
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
this._azureResourceDropdown.loading = false;
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._azureResourceDropdown,
|
||||
this.migrationStateModel._targetServerInstance?.name,
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
412
extensions/sql-migration/src/wizard/loginSelectorPage.ts
Normal file
412
extensions/sql-migration/src/wizard/loginSelectorPage.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import { debounce, getLoginStatusImage, getLoginStatusMessage } from '../api/utils';
|
||||
import * as styles from '../constants/styles';
|
||||
import { collectSourceLogins, collectTargetLogins, LoginTableInfo } from '../api/sqlUtils';
|
||||
import { AzureSqlDatabaseServer } from '../api/azure';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as utils from '../api/utils';
|
||||
|
||||
export class LoginSelectorPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
private _loginSelectorTable!: azdata.TableComponent;
|
||||
private _loginNames!: string[];
|
||||
private _loginCount!: azdata.TextComponent;
|
||||
private _loginTableValues!: any[];
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _isCurrentPage: boolean;
|
||||
private _refreshResultsInfoBox!: azdata.InfoBoxComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
private _refreshLoading!: azdata.LoadingComponent;
|
||||
private _filterTableValue!: string;
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.LOGIN_MIGRATIONS_SELECT_LOGINS_PAGE_TITLE), migrationStateModel);
|
||||
this._isCurrentPage = false;
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
this._view = view;
|
||||
|
||||
const flex = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}).component();
|
||||
flex.addItem(await this.createRootContainer(view), { flex: '1 1 auto' });
|
||||
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(flex);
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this._isCurrentPage = true;
|
||||
this.updateNextButton();
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
this.wizard.message = {
|
||||
text: '',
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
return true;
|
||||
}
|
||||
if (this.selectedLogins().length === 0) {
|
||||
this.wizard.message = {
|
||||
text: constants.SELECT_LOGIN_TO_CONTINUE,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
await this._loadLoginList();
|
||||
|
||||
// load unfiltered table list and pre-select list of logins saved in state
|
||||
await this._filterTableList('', this.migrationStateModel._loginsForMigration);
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
|
||||
this._isCurrentPage = false;
|
||||
this.resetNextButton();
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private createSearchComponent(): azdata.DivContainer {
|
||||
let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({
|
||||
stopEnterPropagation: true,
|
||||
placeHolder: constants.SEARCH,
|
||||
width: 200
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
resourceSearchBox.onTextChanged(value => this._filterTableList(value, this.migrationStateModel._loginsForMigration || [])));
|
||||
|
||||
const searchContainer = this._view.modelBuilder.divContainer().withItems([resourceSearchBox]).withProps({
|
||||
CSSStyles: {
|
||||
'width': '200px',
|
||||
'margin-top': '8px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
return searchContainer;
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private async _filterTableList(value: string, selectedList?: LoginTableInfo[]): Promise<void> {
|
||||
this._filterTableValue = value;
|
||||
const selectedRows: number[] = [];
|
||||
const selectedLogins = selectedList || this.selectedLogins();
|
||||
let tableRows = this._loginTableValues ?? [];
|
||||
if (this._loginTableValues && value?.length > 0) {
|
||||
tableRows = this._loginTableValues
|
||||
.filter(row => {
|
||||
const searchText = value?.toLowerCase();
|
||||
return row[1]?.toLowerCase()?.indexOf(searchText) > -1 // source login
|
||||
|| row[2]?.toLowerCase()?.indexOf(searchText) > -1 // login type
|
||||
|| row[3]?.toLowerCase()?.indexOf(searchText) > -1 // default database
|
||||
|| row[4]?.toLowerCase()?.indexOf(searchText) > -1 // status
|
||||
|| row[5]?.title?.toLowerCase()?.indexOf(searchText) > -1; // target status
|
||||
});
|
||||
}
|
||||
|
||||
for (let rowIdx = 0; rowIdx < tableRows.length; rowIdx++) {
|
||||
const login: string = tableRows[rowIdx][1];
|
||||
if (selectedLogins.some(selectedLogin => selectedLogin.loginName.toLowerCase() === login.toLowerCase())) {
|
||||
selectedRows.push(rowIdx);
|
||||
}
|
||||
}
|
||||
|
||||
await this._loginSelectorTable.updateProperty('data', tableRows);
|
||||
this._loginSelectorTable.selectedRows = selectedRows;
|
||||
await this.updateValuesOnSelection();
|
||||
}
|
||||
|
||||
|
||||
public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
|
||||
const windowsAuthInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'information',
|
||||
text: constants.LOGIN_MIGRATIONS_SELECT_LOGINS_WINDOWS_AUTH_WARNING,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
this._refreshButton = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
label: constants.DATABASE_TABLE_REFRESH_LABEL,
|
||||
width: 70,
|
||||
CSSStyles: { 'margin': '15px 0 0 0' },
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._refreshButton.onDidClick(
|
||||
async e => await this._loadLoginList()));
|
||||
|
||||
this._refreshLoading = this._view.modelBuilder.loadingComponent()
|
||||
.withItem(this._refreshButton)
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: { 'margin-right': '20px', 'margin-top': '15px' }
|
||||
})
|
||||
.component();
|
||||
|
||||
this._refreshResultsInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'success',
|
||||
text: '',
|
||||
announceText: true,
|
||||
CSSStyles: { 'display': 'none', 'margin-left': '5px' },
|
||||
})
|
||||
.component();
|
||||
|
||||
const refreshContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
this._refreshLoading,
|
||||
this._refreshResultsInfoBox],
|
||||
{ flex: '0 0 auto' })
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
})
|
||||
.component();
|
||||
|
||||
await this._loadLoginList();
|
||||
this._loginCount = this._view.modelBuilder.text().withProps({
|
||||
value: constants.LOGINS_SELECTED(
|
||||
this.selectedLogins().length,
|
||||
this._loginTableValues.length),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin-top': '8px'
|
||||
},
|
||||
ariaLive: 'polite'
|
||||
}).component();
|
||||
|
||||
const cssClass = 'no-borders';
|
||||
this._loginSelectorTable = this._view.modelBuilder.table()
|
||||
.withProps({
|
||||
data: [],
|
||||
width: 650,
|
||||
height: '100%',
|
||||
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
|
||||
columns: [
|
||||
<azdata.CheckboxColumn>{
|
||||
value: '',
|
||||
width: 10,
|
||||
type: azdata.ColumnType.checkBox,
|
||||
action: azdata.ActionOnCellCheckboxCheck.selectRow,
|
||||
resizable: false,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.SOURCE_LOGIN,
|
||||
value: 'sourceLogin',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 250,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.LOGIN_TYPE,
|
||||
value: 'loginType',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 90,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.DEFAULT_DATABASE,
|
||||
value: 'defaultDatabase',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 130,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.LOGIN_STATUS_COLUMN,
|
||||
value: 'status',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 90,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
<azdata.HyperlinkColumn>{
|
||||
name: constants.LOGIN_TARGET_STATUS_COLUMN,
|
||||
value: 'targetStatus',
|
||||
width: 150,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
icon: IconPathHelper.inProgressMigration,
|
||||
showText: true,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
]
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._loginSelectorTable.onRowSelected(async (e) => {
|
||||
await this.updateValuesOnSelection();
|
||||
}));
|
||||
|
||||
// load unfiltered table list and pre-select list of logins saved in state
|
||||
await this._filterTableList('', this.migrationStateModel._loginsForMigration);
|
||||
|
||||
const flex = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0px 28px 0px 28px'
|
||||
}
|
||||
}).component();
|
||||
flex.addItem(windowsAuthInfoBox, { flex: '0 0 auto' });
|
||||
flex.addItem(refreshContainer, { flex: '0 0 auto' });
|
||||
flex.addItem(this.createSearchComponent(), { flex: '0 0 auto' });
|
||||
flex.addItem(this._loginCount, { flex: '0 0 auto' });
|
||||
flex.addItem(this._loginSelectorTable);
|
||||
return flex;
|
||||
}
|
||||
|
||||
private async _loadLoginList(): Promise<void> {
|
||||
this._refreshLoading.loading = true;
|
||||
this.wizard.nextButton.enabled = false;
|
||||
await utils.updateControlDisplay(this._refreshResultsInfoBox, true);
|
||||
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESHING_LOGIN_DATA;
|
||||
this._refreshResultsInfoBox.style = 'information';
|
||||
|
||||
const stateMachine: MigrationStateModel = this.migrationStateModel;
|
||||
const selectedLogins: LoginTableInfo[] = stateMachine._loginsForMigration || [];
|
||||
const sourceLogins: LoginTableInfo[] = [];
|
||||
const targetLogins: string[] = [];
|
||||
|
||||
// execute a query against the source to get the logins
|
||||
try {
|
||||
sourceLogins.push(...await collectSourceLogins(stateMachine.sourceConnectionId));
|
||||
} catch (error) {
|
||||
this._refreshLoading.loading = false;
|
||||
this._refreshResultsInfoBox.style = 'error';
|
||||
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESH_SOURCE_LOGIN_DATA_FAILED;
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('source'),
|
||||
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
|
||||
};
|
||||
}
|
||||
|
||||
// execute a query against the target to get the logins
|
||||
try {
|
||||
if (this.isTargetInstanceSet()) {
|
||||
targetLogins.push(...await collectTargetLogins(stateMachine._targetServerInstance as AzureSqlDatabaseServer, stateMachine._targetUserName, stateMachine._targetPassword));
|
||||
}
|
||||
else {
|
||||
// TODO AKMA : Emit telemetry here saying target info is empty
|
||||
}
|
||||
} catch (error) {
|
||||
this._refreshLoading.loading = false;
|
||||
this._refreshResultsInfoBox.style = 'error';
|
||||
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESH_TARGET_LOGIN_DATA_FAILED;
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('target'),
|
||||
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
|
||||
};
|
||||
}
|
||||
|
||||
this._loginNames = [];
|
||||
|
||||
this._loginTableValues = sourceLogins.map(row => {
|
||||
const loginName = row.loginName;
|
||||
this._loginNames.push(loginName);
|
||||
const isLoginOnTarget = targetLogins.some(targetLogin => targetLogin.toLowerCase() === loginName.toLowerCase());
|
||||
return [
|
||||
selectedLogins?.some(selectedLogin => selectedLogin.loginName.toLowerCase() === loginName.toLowerCase()),
|
||||
loginName,
|
||||
row.loginType,
|
||||
row.defaultDatabaseName,
|
||||
row.status,
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: getLoginStatusImage(isLoginOnTarget),
|
||||
title: getLoginStatusMessage(isLoginOnTarget),
|
||||
},
|
||||
];
|
||||
}) || [];
|
||||
|
||||
await this._filterTableList(this._filterTableValue);
|
||||
this._refreshLoading.loading = false;
|
||||
this._refreshResultsInfoBox.text = constants.LOGIN_MIGRATION_REFRESH_LOGIN_DATA_SUCCESSFUL(sourceLogins.length, targetLogins.length);
|
||||
this._refreshResultsInfoBox.style = 'success';
|
||||
this.updateNextButton();
|
||||
}
|
||||
|
||||
public selectedLogins(): LoginTableInfo[] {
|
||||
const rows = this._loginSelectorTable?.data || [];
|
||||
const logins = this._loginSelectorTable?.selectedRows || [];
|
||||
return logins
|
||||
.filter(rowIdx => rowIdx < rows.length)
|
||||
.map(rowIdx => {
|
||||
return {
|
||||
loginName: rows[rowIdx][1],
|
||||
loginType: rows[rowIdx][2],
|
||||
defaultDatabaseName: rows[rowIdx][3],
|
||||
status: rows[rowIdx][4].title,
|
||||
};
|
||||
})
|
||||
|| [];
|
||||
}
|
||||
|
||||
private async updateValuesOnSelection() {
|
||||
const selectedLogins = this.selectedLogins() || [];
|
||||
await this._loginCount.updateProperties({
|
||||
'value': constants.LOGINS_SELECTED(
|
||||
selectedLogins.length,
|
||||
this._loginSelectorTable.data?.length || 0)
|
||||
});
|
||||
|
||||
this.migrationStateModel._loginsForMigration = selectedLogins;
|
||||
this.migrationStateModel._aadDomainName = "";
|
||||
this.updateNextButton();
|
||||
}
|
||||
|
||||
private updateNextButton() {
|
||||
// Only uppdate next label if we are currently on this page
|
||||
if (this._isCurrentPage) {
|
||||
this.wizard.nextButton.label = constants.LOGIN_MIGRATE_BUTTON_TEXT;
|
||||
this.wizard.nextButton.enabled = this.migrationStateModel?._loginsForMigration?.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private resetNextButton() {
|
||||
this.wizard.nextButton.label = constants.NEXT_LABEL;
|
||||
this.wizard.nextButton.enabled = true;
|
||||
}
|
||||
|
||||
private isTargetInstanceSet() {
|
||||
const stateMachine: MigrationStateModel = this.migrationStateModel;
|
||||
return stateMachine._targetServerInstance && stateMachine._targetUserName && stateMachine._targetPassword;
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,12 @@ import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { SKURecommendationPage } from './skuRecommendationPage';
|
||||
import { DatabaseBackupPage } from './databaseBackupPage';
|
||||
import { TargetSelectionPage } from './targetSelectionPage';
|
||||
import { LoginMigrationTargetSelectionPage } from './loginMigrationTargetSelectionPage';
|
||||
import { IntergrationRuntimePage } from './integrationRuntimePage';
|
||||
import { SummaryPage } from './summaryPage';
|
||||
import { LoginMigrationStatusPage } from './loginMigrationStatusPage';
|
||||
import { DatabaseSelectorPage } from './databaseSelectorPage';
|
||||
import { LoginSelectorPage } from './loginSelectorPage';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
|
||||
import * as styles from '../constants/styles';
|
||||
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
|
||||
@@ -38,6 +41,14 @@ export class WizardController {
|
||||
}
|
||||
}
|
||||
|
||||
public async openLoginWizard(connectionId: string): Promise<void> {
|
||||
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
|
||||
if (api) {
|
||||
this.extensionContext.subscriptions.push(this._model);
|
||||
await this.createLoginWizard(this._model);
|
||||
}
|
||||
}
|
||||
|
||||
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
|
||||
const serverName = (await stateModel.getSourceConnectionProfile()).serverName;
|
||||
this._wizardObject = azdata.window.createWizard(
|
||||
@@ -176,6 +187,66 @@ export class WizardController {
|
||||
}));
|
||||
}
|
||||
|
||||
private async createLoginWizard(stateModel: MigrationStateModel): Promise<void> {
|
||||
const serverName = (await stateModel.getSourceConnectionProfile()).serverName;
|
||||
this._wizardObject = azdata.window.createWizard(
|
||||
loc.LOGIN_WIZARD_TITLE(serverName),
|
||||
'LoginMigrationWizard',
|
||||
'wide');
|
||||
|
||||
this._wizardObject.generateScriptButton.enabled = false;
|
||||
this._wizardObject.generateScriptButton.hidden = true;
|
||||
const targetSelectionPage = new LoginMigrationTargetSelectionPage(this._wizardObject, stateModel);
|
||||
const loginSelectorPage = new LoginSelectorPage(this._wizardObject, stateModel);
|
||||
const migrationStatusPage = new LoginMigrationStatusPage(this._wizardObject, stateModel);
|
||||
|
||||
const pages: MigrationWizardPage[] = [
|
||||
targetSelectionPage,
|
||||
loginSelectorPage,
|
||||
migrationStatusPage
|
||||
];
|
||||
|
||||
this._wizardObject.pages = pages.map(p => p.getwizardPage());
|
||||
|
||||
const wizardSetupPromises: Thenable<void>[] = [];
|
||||
wizardSetupPromises.push(...pages.map(p => p.registerWizardContent()));
|
||||
wizardSetupPromises.push(this._wizardObject.open());
|
||||
|
||||
this._model.extensionContext.subscriptions.push(
|
||||
this._wizardObject.onPageChanged(
|
||||
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
|
||||
const newPage = pageChangeInfo.newPage;
|
||||
const lastPage = pageChangeInfo.lastPage;
|
||||
this.sendPageButtonClickEvent(pageChangeInfo)
|
||||
.catch(e => logError(
|
||||
TelemetryViews.LoginMigrationWizardController,
|
||||
'ErrorSendingPageButtonClick', e));
|
||||
await pages[lastPage]?.onPageLeave(pageChangeInfo);
|
||||
await pages[newPage]?.onPageEnter(pageChangeInfo);
|
||||
}));
|
||||
|
||||
this._wizardObject.registerNavigationValidator(async validator => {
|
||||
return true;
|
||||
});
|
||||
|
||||
await Promise.all(wizardSetupPromises);
|
||||
|
||||
this._disposables.push(
|
||||
this._wizardObject.cancelButton.onClick(e => {
|
||||
// TODO AKMA: add dialog prompting confirmation of cancel if migration is in progress
|
||||
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.LoginMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
'buttonPressed': TelemetryAction.Cancel,
|
||||
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
|
||||
},
|
||||
{});
|
||||
}));
|
||||
}
|
||||
|
||||
private async updateServiceContext(
|
||||
stateModel: MigrationStateModel,
|
||||
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user