Migration extension UX update and bug fixes. (#15384)

* Adding a null check to prevent infinite next button loading on account selection page.

* Remove a useless validation in migration cutover page

* Fixed some component formatting in source selection page

* Completely updated target selection page UX according to latest figma mockup

* Adding confirmation for migration cutover and cancel

* migration vbump

* azdata vbump in migration extension

* letting users do a cutover with unrestored files

* Fixing some localized strings

* Adding readme file for migration extension.

* Adding a static link for readme gif

* added sql mi typing, localized strings, some null checks

* casting target instance as sql mi
This commit is contained in:
Aasim Khan
2021-05-11 16:27:16 +00:00
committed by GitHub
parent fff2bd5089
commit 0b41baaa0c
15 changed files with 495 additions and 242 deletions

View File

@@ -91,6 +91,32 @@ declare module 'azureResource' {
}
export interface AzureSqlManagedInstance extends AzureGraphResource {
sku: {
capacity: number;
family: string;
name: string;
tier: 'GeneralPurpose' | 'BusinessCritical';
},
properties: {
provisioningState: string,
storageAccountType: string,
maintenanceConfigurationId: string,
state: string,
licenseType: string,
zoneRedundant: false,
fullyQualifiedDomainName: string,
collation: string,
administratorLogin: string,
minimalTlsVersion: string,
subnetId: string,
publicDataEndpointEnabled: boolean,
storageSizeInGB: number,
timezoneId: string,
proxyOverride: string,
vCores: number,
dnsZone: string,
}
}
export interface ManagedDatabase {

View File

@@ -0,0 +1,30 @@
# Azure SQL Migration
Azure SQL Migration extension can be used to determine 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 or SQL Server on Azure Virtual Machine.
## Installation
From Azure Data Studio extension gallery, install the latest version of “Azure SQL Migration” extension and launch the wizard as shown below.
![migration-animation](https://github.com/microsoft/azuredatastudio/blob/main/extensions/sql-migration/images/ADSMigration.gif)
## Things you need before starting Azure SQL migration
- Azure account details
- Azure SQL Managed Instance or SQL Server on Azure Virtual Machine
- Backup location details
## Getting started
Refer to getting started document (https://aka.ms/ads-sql-migration) for detailed documentation on capabilities and current limitations.
## Need assistance or have questions/feedback
Please reach out to DMSFeedback@microsoft.com
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Privacy Statement
The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software.
## License
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt).

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -2,7 +2,7 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.0.11",
"version": "0.0.12",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -10,7 +10,7 @@
"aiKey": "06ba2446-fa56-40aa-853a-26b73255b723",
"engines": {
"vscode": "*",
"azdata": ">=1.27.0"
"azdata": ">=1.29.0"
},
"activationEvents": [
"onDashboardOpen",

View File

@@ -291,7 +291,7 @@ function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void
});
}
function getResourceGroupFromId(id: string): string {
export function getResourceGroupFromId(id: string): string {
return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase();
}

View File

@@ -45,6 +45,12 @@ export const SUBSCRIPTION_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.
export const SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE = localize('sql.migration.wizard.subscription.azure.account.title', "Azure Account");
export const SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE = localize('sql.migration.wizard.subscription.azure.subscription.title', "Azure Subscription");
export const SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE = localize('sql.migration.wizard.subscription.azure.product.title', "Azure Product");
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI = localize('sql.migration.sku.recommendation.view.assessment.mi', "View assessment results and select one or more database(s) to migrate to Azure SQL Managed Instance (PaaS)");
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM = localize('sql.migration.sku.recommendation.view.assessment.vm', "View assessment results and select one or more database(s) to migrate to SQL Server on Azure Virtual Machine (IaaS)");
export const VIEW_SELECT_BUTTON_LABEL = localize('sql.migration.view.select.button.label', "View/Select");
export function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string {
return localize('total.databases.selected', "{0} of {1} Database(s) selected.", selectedDbCount, totalDbCount);
}
export const ASSESSMENT_COMPLETED = (serverName: string): string => {
return localize('sql.migration.generic.congratulations', "We have completed the assessment of your SQL Server Instance '{0}'.", serverName);
@@ -52,6 +58,9 @@ export const ASSESSMENT_COMPLETED = (serverName: string): string => {
export function ASSESSMENT_TILE(serverName: string): string {
return localize('sql.migration.assessment', "Assessment Dialog for '{0}'", serverName);
}
export function CAN_BE_MIGRATED(eligibleDbs: number, totalDbs: number): string {
return localize('sql.migration.can.be.migrated', "{0} out of {1} databases can be migrated", eligibleDbs, totalDbs);
}
// Accounts page
export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Azure Account");
@@ -270,6 +279,7 @@ export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP
//Migration cutover dialog
export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
export const COMPLETE_CUTOVER = localize('sql.migration.complete.cutover', "Complete cutover");
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database name");
export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
@@ -299,8 +309,23 @@ export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) {
}
export const COPY_MIGRATION_DETAILS = localize('sql.migration.copy.migration.details', "Copy Migration Details");
export const DETAILS_COPIED = localize('sql.migration.details.copied', "Details copied");
export const CANCEL_MIGRATION_CONFIRMATION = localize('sql.cancel.migration.confirmation', "Are you sure you want to cancel this migration?");
export const YES = localize('sql.migration.yes', "Yes");
export const NO = localize('sql.migration.no', "No");
//Migration confirm cutover dialog
export const BUSINESS_CRITICAL_INFO = localize('sql.migration.bc.info', "Managed Instance migration cutover for Business Critical service tier can take significantly longer than General Purpose as three secondary replicas have to be seeded for Always On High Availability group. This operation duration depends on the size of data. Seeding speed in 90% of cases is 220 GB/hour or higher.");
export const CUTOVER_HELP_MAIN = localize('sql.migration.cutover.help.main', "When you are ready to do the migration cutover, perform the following steps to complete the database migration. Please note that the database is ready for cutover only after a full backup has been restored on the target Azure SQL Database Managed Instance.");
export const CUTOVER_HELP_STEP1 = localize('sql.migration.cutover.step.1', "1. Stop all the incoming transactions coming to the source database.");
export const CUTOVER_HELP_STEP2 = localize('sql.migration.cutover.step.2', "2. Take the final transaction log backup and provide backup file in the SMB network share.");
export const CUTOVER_HELP_STEP3 = localize('sql.migration.cutover.step.3', "3. Make sure all the pending log backups are restored on the target. At that point, “Pending log backups” counter shows zero and then perform the cutover. Performing cutover operation without applying all the transaction log backup files may result in loss of data.");
export function PENDING_BACKUPS(count: number): string {
return localize('sql.migartion.cutover.pending.backup', "Pending log backups: {0}", count);
}
export const CONFIRM_CUTOVER_CHECKBOX = localize('sql.migration.confirm.checkbox.message', "Confirm all pending log backups are restored");
export function CUTOVER_IN_PROGRESS(dbName: string): string {
return localize('sql.migration.cutover.in.progress', "Cutover in progress for database '{0}'", dbName);
}
//Migration status dialog
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
export const ONLINE = localize('sql.migration.online', "Online");

View File

@@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* 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 { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as constants from '../../constants/strings';
import * as vscode from 'vscode';
import { SqlManagedInstance } from '../../api/azure';
export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
constructor(private migrationCutoverModel: MigrationCutoverDialogModel) {
this._dialogObject = azdata.window.createModelViewDialog('', 'ConfirmCutoverDialog', 500);
}
async initialize(): Promise<void> {
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
const completeCutoverText = view.modelBuilder.text().withProps({
value: constants.COMPLETE_CUTOVER,
CSSStyles: {
'font-size': '20px',
'font-weight': 'bold',
'margin-bottom': '0px'
}
}).component();
const sourceDatabaseText = view.modelBuilder.text().withProps({
value: this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName,
CSSStyles: {
'font-size': '10px',
'margin': '5px 0px 10px 0px'
}
}).component();
const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component();
let infoDisplay = 'none';
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
infoDisplay = 'inline';
}
const businessCriticalinfoBox = this._view.modelBuilder.infoBox().withProps({
text: constants.BUSINESS_CRITICAL_INFO,
style: 'information',
CSSStyles: {
'font-size': '13px',
'display': infoDisplay
}
}).component();
const helpMainText = this._view.modelBuilder.text().withProps({
value: constants.CUTOVER_HELP_MAIN,
CSSStyles: {
'font-size': '13px',
}
}).component();
const helpStepsText = this._view.modelBuilder.text().withProps({
value: `${constants.CUTOVER_HELP_STEP1}
${constants.CUTOVER_HELP_STEP2}
${constants.CUTOVER_HELP_STEP3}`,
CSSStyles: {
'font-size': '13px',
}
}).component();
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length;
const pendingText = this._view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold'
},
value: constants.PENDING_BACKUPS(pendingBackupCount!)
}).component();
const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({
CSSStyles: {
'font-size': '13px',
},
label: constants.CONFIRM_CUTOVER_CHECKBOX,
}).component();
confirmCheckbox.onChanged(e => {
this._dialogObject.okButton.enabled = e;
});
const container = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
completeCutoverText,
sourceDatabaseText,
separator,
businessCriticalinfoBox,
helpMainText,
helpStepsText,
pendingText,
confirmCheckbox
]).component();
this._dialogObject.okButton.enabled = false;
this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER;
this._dialogObject.okButton.onClick((e) => {
this.migrationCutoverModel.startCutover();
vscode.window.showInformationMessage(constants.CUTOVER_IN_PROGRESS(this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName));
});
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
component: container
}
],
{
horizontal: false
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
this._dialogObject.content = [tab];
azdata.window.openDialog(this._dialogObject);
}
}

View File

@@ -11,6 +11,7 @@ import * as loc from '../../constants/strings';
import { getSqlServerName } from '../../api/utils';
import { EOL } from 'os';
import * as vscode from 'vscode';
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
export class MigrationCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
@@ -41,8 +42,6 @@ export class MigrationCutoverDialog {
private fileTable!: azdata.TableComponent;
private _startCutover!: boolean;
constructor(migration: MigrationContext) {
this._model = new MigrationCutoverDialogModel(migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 1000);
@@ -333,22 +332,17 @@ export class MigrationCutoverDialog {
iconPath: IconPathHelper.cutover,
iconHeight: '14px',
iconWidth: '12px',
label: 'Start Cutover',
label: loc.COMPLETE_CUTOVER,
height: '20px',
width: '100px',
width: '130px',
enabled: false
}).component();
this._cutoverButton.onDidClick(async (e) => {
if (this._startCutover) {
await this._model.startCutover();
this.refreshStatus();
} else {
this._dialogObject.message = {
text: loc.CANNOT_START_CUTOVER_ERROR,
level: azdata.window.MessageLevel.Error
};
}
await this.refreshStatus();
const dialog = new ConfirmCutoverDialog(this._model);
await dialog.initialize();
await this.refreshStatus();
});
headerActions.addItem(this._cutoverButton, {
@@ -365,7 +359,12 @@ export class MigrationCutoverDialog {
}).component();
this._cancelButton.onDidClick((e) => {
this.cancelMigration();
vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
await this.cancelMigration();
await this.refreshStatus();
}
});
});
headerActions.addItem(this._cancelButton, {
@@ -537,14 +536,9 @@ export class MigrationCutoverDialog {
row.lastLSN
];
});
if (this._model.migrationStatus.properties.migrationStatusDetails?.isFullBackupRestored) {
this._startCutover = true;
}
if (migrationStatusTextValue === MigrationStatus.InProgress) {
const fileNotRestored = await tableData.some(file => file.status !== 'Restored' && file.status !== 'Ignored');
this._cutoverButton.enabled = !fileNotRestored;
this._cancelButton.enabled = true;
this._cutoverButton.enabled = tableData.length > 0;
} else {
this._cutoverButton.enabled = false;
this._cancelButton.enabled = false;

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { azureResource } from 'azureResource';
import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails } from '../api/azure';
import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails, SqlVMServer } from '../api/azure';
import * as azdata from 'azdata';
@@ -62,7 +62,7 @@ export class MigrationLocalStorage {
public static saveMigration(
connectionProfile: azdata.connection.ConnectionProfile,
migrationContext: DatabaseMigration,
targetMI: SqlManagedInstance,
targetMI: SqlManagedInstance | SqlVMServer,
azureAccount: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
controller: SqlMigrationService,
@@ -93,7 +93,7 @@ export class MigrationLocalStorage {
export interface MigrationContext {
sourceConnectionProfile: azdata.connection.ConnectionProfile,
migrationContext: DatabaseMigration,
targetManagedInstance: SqlManagedInstance,
targetManagedInstance: SqlManagedInstance | SqlVMServer,
azureAccount: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
controller: SqlMigrationService,

View File

@@ -108,7 +108,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _resourceGroup!: azureResource.AzureResourceResourceGroup;
public _targetManagedInstances!: SqlManagedInstance[];
public _targetSqlVirtualMachines!: SqlVMServer[];
public _targetServerInstance!: SqlManagedInstance;
public _targetServerInstance!: SqlManagedInstance | SqlVMServer;
public _databaseBackup!: DatabaseBackupModel;
public _migrationDbs: string[] = [];
public _storageAccounts!: StorageAccount[];
@@ -444,7 +444,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getManagedDatabases(): Promise<string[]> {
return (await getSqlManagedInstanceDatabases(this._azureAccount,
this._targetSubscription,
this._targetServerInstance)).map(t => t.name);
<SqlManagedInstance>this._targetServerInstance)).map(t => t.name);
}
public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {

View File

@@ -37,9 +37,9 @@ export class AccountsSelectionPage extends MigrationWizardPage {
const azureAccountLabel = view.modelBuilder.text().withProps({
value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
requiredIndicator: true,
CSSStyles: {
'margin': '0px'
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
@@ -96,7 +96,10 @@ export class AccountsSelectionPage extends MigrationWizardPage {
const linkAccountButton = view.modelBuilder.hyperlink()
.withProps({
label: constants.ACCOUNT_LINK_BUTTON_LABEL,
url: ''
url: '',
CSSStyles: {
'font-size': '13px',
}
})
.component();
@@ -130,9 +133,9 @@ export class AccountsSelectionPage extends MigrationWizardPage {
const azureTenantDropdownLabel = view.modelBuilder.text().withProps({
value: constants.AZURE_TENANT,
requiredIndicator: true,
CSSStyles: {
'margin': '0px'
'font-size': '13px',
'font-weight': 'bold'
}
}).component();
@@ -185,7 +188,7 @@ export class AccountsSelectionPage extends MigrationWizardPage {
public async onPageEnter(): Promise<void> {
this.wizard.registerNavigationValidator(pageChangeInfo => {
if (this.migrationStateModel._azureAccount.isStale === true) {
if (this.migrationStateModel._azureAccount?.isStale === true) {
this.wizard.message = {
text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount)
};

View File

@@ -10,7 +10,7 @@ import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog';
import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import { getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../api/azure';
import { getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlManagedInstance, SqlMigrationService } from '../api/azure';
import { IconPathHelper } from '../constants/iconPathHelper';
export class IntergrationRuntimePage extends MigrationWizardPage {
@@ -400,7 +400,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
}
this._dmsDropdown.loading = true;
try {
this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetServerInstance, resourceGroupName);
this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, <SqlManagedInstance>this.migrationStateModel._targetServerInstance, resourceGroupName);
let index = -1;
if (this.migrationStateModel._sqlMigrationService) {
index = (<azdata.CategoryValue[]>this._dmsDropdown.values).findIndex(v => v.displayName.toLowerCase() === this.migrationStateModel._sqlMigrationService.name.toLowerCase());

View File

@@ -20,7 +20,6 @@ export interface Product {
}
export class SKURecommendationPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _igComponent!: azdata.TextComponent;
private _assessmentStatusIcon!: azdata.ImageComponent;
@@ -42,12 +41,17 @@ export class SKURecommendationPage extends MigrationWizardPage {
private _formContainer!: azdata.ComponentBuilder<azdata.FormContainer, azdata.ComponentProperties>;
private _assessmentLoader!: azdata.LoadingComponent;
private _rootContainer!: azdata.FlexContainer;
private _viewAssessmentsHelperText!: azdata.TextComponent;
private _databaseSelectedHelperText!: azdata.TextComponent;
private assessmentGroupContainer!: azdata.FlexContainer;
private _targetContainer!: azdata.FlexContainer;
private _supportedProducts: Product[] = [
{
type: MigrationTargetType.SQLMI,
name: constants.SKU_RECOMMENDATION_MI_CARD_TEXT,
icon: IconPathHelper.sqlMiLogo
icon: IconPathHelper.sqlMiLogo,
},
{
type: MigrationTargetType.SQLVM,
@@ -81,112 +85,27 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved
const chooseYourTargetText = this._view.modelBuilder.text().withProps({
value: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
'margin-top': '16px'
}
}).component();
const statusContainer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems(
[
igContainer,
this._detailsComponent
this._detailsComponent,
chooseYourTargetText
]
).component();
this._chooseTargetComponent = await this.createChooseTargetComponent(view);
this._azureSubscriptionText = this.createAzureSubscriptionText(view);
const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text().withProps({
value: constants.SUBSCRIPTION,
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true
}).component();
this._managedInstanceSubscriptionDropdown.onValueChanged((e) => {
if (e) {
const selectedIndex = (<azdata.CategoryValue[]>this._managedInstanceSubscriptionDropdown.values)?.findIndex(v => v.displayName === e);
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(selectedIndex);
this.migrationStateModel._targetServerInstance = undefined!;
this.migrationStateModel._sqlMigrationService = undefined!;
this.populateLocationAndResourceGroupDropdown();
}
});
this._resourceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE,
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
const azureLocationLabel = view.modelBuilder.text().withProps({
value: constants.LOCATION,
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._azureLocationDropdown = view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._azureLocationDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._location = this.migrationStateModel.getLocation(e.index);
this.populateResourceInstanceDropdown();
}
});
this._resourceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE,
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
const azureResourceGroupLabel = view.modelBuilder.text().withProps({
value: constants.RESOURCE_GROUP,
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._azureResourceGroupDropdown = view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._azureResourceGroupDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index);
this.populateResourceInstanceDropdown();
}
});
this._resourceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE,
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._resourceDropdown = view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._resourceDropdown.onValueChanged((e) => {
if (e.selected &&
e.selected !== constants.NO_MANAGED_INSTANCE_FOUND &&
e.selected !== constants.NO_VIRTUAL_MACHINE_FOUND) {
this.migrationStateModel._sqlMigrationServices = undefined!;
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) {
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(e.index);
} else {
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index);
}
}
});
const targetContainer = view.modelBuilder.flexContainer().withItems(
[
managedInstanceSubscriptionDropdownLabel,
this._managedInstanceSubscriptionDropdown,
azureLocationLabel,
this._azureLocationDropdown,
azureResourceGroupLabel,
this._azureResourceGroupDropdown,
this._resourceDropdownLabel,
this._resourceDropdown
]
).withLayout({
flexFlow: 'column'
}).component();
this.assessmentGroupContainer = await this.createViewAssessmentsContainer();
this._targetContainer = this.createTargetDropdownContainer();
this._formContainer = view.modelBuilder.formContainer().withFormItems(
[
{
@@ -194,14 +113,13 @@ export class SKURecommendationPage extends MigrationWizardPage {
component: statusContainer
},
{
title: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
component: this._chooseTargetComponent
},
{
component: this._azureSubscriptionText
component: this.assessmentGroupContainer
},
{
component: targetContainer
component: this._targetContainer
}
]
).withProps({
@@ -237,7 +155,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
CSSStyles: {
'font-size': '14px',
'margin': '0 0 0 8px',
'line-height': '20px'
'line-height': '20px',
'font-weight': 'bold'
}
}).component();
return component;
@@ -256,87 +175,46 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._rbg = this._view!.modelBuilder.radioCardGroup().withProps({
cards: [],
cardWidth: '600px',
cardHeight: '40px',
orientation: azdata.Orientation.Vertical,
iconHeight: '30px',
iconWidth: '30px'
iconHeight: '35px',
iconWidth: '35px',
cardWidth: '250px',
cardHeight: '130px',
iconPosition: 'left',
CSSStyles: {
'margin-top': '0px'
}
}).component();
this._supportedProducts.forEach((product) => {
const descriptions: azdata.RadioCardDescription[] = [
{
textValue: product.name,
textStyles: {
'font-size': '14px',
'font-weight': 'bold',
'line-height': '20px'
},
linkDisplayValue: 'Learn more',
linkStyles: {
'font-size': '14px',
'line-height': '20px'
},
displayLinkCodicon: true,
linkCodiconStyles: {
'font-size': '14px',
'line-height': '20px'
},
},
{
textValue: '0 selected',
textStyles: {
'font-size': '13px',
'line-height': '18px'
},
linkStyles: {
'font-size': '14px',
'line-height': '20px'
},
linkDisplayValue: 'View/Change',
displayLinkCodicon: true,
linkCodiconStyles: {
'font-size': '13px',
'line-height': '18px'
}
}
];
this._rbg.cards.push({
id: product.type,
icon: product.icon,
descriptions
descriptions: [
{
textValue: product.name,
textStyles: {
'font-size': '14px',
'font-weight': 'bold'
}
},
{
textValue: '',
textStyles: {
'font-size': '13px',
}
}
]
});
});
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI);
let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM);
this._rbg.onLinkClick(async (value) => {
if (value.cardId === MigrationTargetType.SQLVM) {
this._rbg.selectedCardId = MigrationTargetType.SQLVM;
if (value.description.linkDisplayValue === 'View/Change') {
await vmDialog.openDialog();
} else if (value.description.linkDisplayValue === 'Learn more') {
vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/azure/azure-sql/virtual-machines/windows/sql-server-on-azure-vm-iaas-what-is-overview'));
}
} else if (value.cardId === MigrationTargetType.SQLMI) {
this._rbg.selectedCardId = MigrationTargetType.SQLMI;
if (value.description.linkDisplayValue === 'View/Change') {
await miDialog.openDialog();
} else if (value.description.linkDisplayValue === 'Learn more') {
vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/azure/azure-sql/managed-instance/sql-managed-instance-paas-overview '));
}
this._rbg.onSelectionChanged((value) => {
if (value) {
this.assessmentGroupContainer.display = 'inline';
this.changeTargetType(value.cardId);
}
});
this._rbg.onSelectionChanged((value) => {
this.changeTargetType(value.cardId);
});
this._rbg.selectedCardId = MigrationTargetType.SQLMI;
this._rbgLoader = this._view.modelBuilder.loadingComponent().withItem(
this._rbg
).component();
@@ -349,17 +227,180 @@ export class SKURecommendationPage extends MigrationWizardPage {
return component;
}
private async createViewAssessmentsContainer(): Promise<azdata.FlexContainer> {
this._viewAssessmentsHelperText = this._view.modelBuilder.text().withProps({
value: constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
},
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
const button = this._view.modelBuilder.button().withProps({
label: constants.VIEW_SELECT_BUTTON_LABEL,
width: 100
}).component();
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI);
let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM);
button.onDidClick(async (e) => {
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) {
this._rbg.selectedCardId = MigrationTargetType.SQLVM;
await vmDialog.openDialog();
} else if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) {
this._rbg.selectedCardId = MigrationTargetType.SQLMI;
await miDialog.openDialog();
}
});
this._databaseSelectedHelperText = this._view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '13px',
}
}).component();
const container = this._view.modelBuilder.flexContainer().withItems([
this._viewAssessmentsHelperText,
button,
this._databaseSelectedHelperText
]).withProps({
'display': 'none'
}).component();
return container;
}
private createTargetDropdownContainer(): azdata.FlexContainer {
this._azureSubscriptionText = this._view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '13px',
'line-height': '18px'
}
}).component();
const managedInstanceSubscriptionDropdownLabel = this._view.modelBuilder.text().withProps({
value: constants.SUBSCRIPTION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._managedInstanceSubscriptionDropdown = this._view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: true
}).component();
this._managedInstanceSubscriptionDropdown.onValueChanged((e) => {
if (e) {
const selectedIndex = (<azdata.CategoryValue[]>this._managedInstanceSubscriptionDropdown.values)?.findIndex(v => v.displayName === e);
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(selectedIndex);
this.migrationStateModel._targetServerInstance = undefined!;
this.migrationStateModel._sqlMigrationService = undefined!;
this.populateLocationAndResourceGroupDropdown();
}
});
const azureLocationLabel = this._view.modelBuilder.text().withProps({
value: constants.LOCATION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._azureLocationDropdown = this._view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._azureLocationDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._location = this.migrationStateModel.getLocation(e.index);
this.populateResourceInstanceDropdown();
}
});
const azureResourceGroupLabel = this._view.modelBuilder.text().withProps({
value: constants.RESOURCE_GROUP,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._azureResourceGroupDropdown.onValueChanged((e) => {
if (e.selected) {
this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index);
this.populateResourceInstanceDropdown();
}
});
this._resourceDropdownLabel = this._view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._resourceDropdown = this._view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._resourceDropdown.onValueChanged((e) => {
if (e?.selected &&
e.selected !== constants.NO_MANAGED_INSTANCE_FOUND &&
e.selected !== constants.NO_VIRTUAL_MACHINE_FOUND) {
this.migrationStateModel._sqlMigrationServices = undefined!;
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) {
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(e.index);
} else {
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index);
}
}
});
return this._view.modelBuilder.flexContainer().withItems(
[
this._azureSubscriptionText,
managedInstanceSubscriptionDropdownLabel,
this._managedInstanceSubscriptionDropdown,
azureLocationLabel,
this._azureLocationDropdown,
azureResourceGroupLabel,
this._azureResourceGroupDropdown,
this._resourceDropdownLabel,
this._resourceDropdown
]
).withLayout({
flexFlow: 'column',
}).withProps({
CSSStyles: {
'display': 'none'
}
}).component();
}
private changeTargetType(newTargetType: string) {
if (newTargetType === MigrationTargetType.SQLMI) {
this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI;
this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._miDbs.length, this.migrationStateModel._serverDatabases.length);
this.migrationStateModel._targetType = MigrationTargetType.SQLMI;
this._azureSubscriptionText.value = constants.SELECT_AZURE_MI;
this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs;
} else {
this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM;
this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._vmDbs.length, this.migrationStateModel._serverDatabases.length);
this.migrationStateModel._targetType = MigrationTargetType.SQLVM;
this._azureSubscriptionText.value = constants.SELECT_AZURE_VM;
this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs;
}
this.migrationStateModel.refreshDatabaseBackupPage = true;
this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline';
this.populateResourceInstanceDropdown();
}
@@ -378,17 +419,6 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._assessmentLoader.loading = false;
}
private createAzureSubscriptionText(view: azdata.ModelView): azdata.TextComponent {
const component = view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '13px',
'line-height': '18px'
}
}).component();
return component;
}
private async populateSubscriptionDropdown(): Promise<void> {
if (!this.migrationStateModel._targetSubscription) {
this._managedInstanceSubscriptionDropdown.loading = true;
@@ -518,34 +548,32 @@ export class SKURecommendationPage extends MigrationWizardPage {
text: '',
level: azdata.window.MessageLevel.Error
};
if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) {
this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs;
} else {
this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs;
}
this._azureResourceGroupDropdown.display = (!this._rbg.selectedCardId) ? 'none' : 'inline';
this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline';
if (this.migrationStateModel._assessmentResults) {
const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments.length;
const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length === 0).length;
const miCardText = `${dbWithoutIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`;
const miCardText = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount);
this._rbg.cards[0].descriptions[1].textValue = miCardText;
const vmCardText = `${dbCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._vmDbs.length} selected)`;
const vmCardText = constants.CAN_BE_MIGRATED(dbCount, dbCount);
this._rbg.cards[1].descriptions[1].textValue = vmCardText;
this._rbg.updateProperties({
cards: this._rbg.cards
});
} else {
const miCardText = `${this.migrationStateModel._miDbs.length} selected`;
this._rbg.cards[0].descriptions[1].textValue = miCardText;
const vmCardText = `${this.migrationStateModel._vmDbs.length} selected`;
this._rbg.cards[1].descriptions[1].textValue = vmCardText;
this._rbg.cards[0].descriptions[1].textValue = '';
this._rbg.cards[1].descriptions[1].textValue = '';
this._rbg.updateProperties({
cards: this._rbg.cards

View File

@@ -58,14 +58,18 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
this._view,
constants.ENTER_YOUR_SQL_CREDS,
{
'width': '600px'
'width': '600px',
'font-size': '13px',
}
);
const serverLabel = this._view.modelBuilder.text().withProps({
value: constants.SERVER,
requiredIndicator: true,
width: WIZARD_INPUT_COMPONENT_WIDTH
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
const server = this._view.modelBuilder.inputBox().withProps({
@@ -76,8 +80,11 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
const authenticationTypeLable = this._view.modelBuilder.text().withProps({
value: constants.AUTHENTICATION_TYPE,
requiredIndicator: true,
width: WIZARD_INPUT_COMPONENT_WIDTH
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
const authenticationTypeInput = this._view.modelBuilder.inputBox().withProps({
@@ -88,8 +95,11 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
const usernameLable = this._view.modelBuilder.text().withProps({
value: constants.USERNAME,
requiredIndicator: true,
width: WIZARD_INPUT_COMPONENT_WIDTH
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._usernameInput = this._view.modelBuilder.inputBox().withProps({
value: username,
@@ -103,8 +113,11 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
const passwordLabel = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL,
requiredIndicator: true,
width: WIZARD_INPUT_COMPONENT_WIDTH
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._password = this._view.modelBuilder.inputBox().withProps({
value: (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password,

View File

@@ -5,9 +5,10 @@
import * as azdata from 'azdata';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationMode, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { createHeadingTextComponent, createInformationRow } from './wizardController';
import { getResourceGroupFromId } from '../api/azure';
export class SummaryPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
@@ -43,11 +44,11 @@ export class SummaryPage extends MigrationWizardPage {
createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()),
createHeadingTextComponent(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE),
createInformationRow(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE),
createInformationRow(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)),
createInformationRow(this._view, constants.RESOURCE_GROUP, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.resourceGroup!)),
createInformationRow(this._view, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)),
createInformationRow(this._view, constants.RESOURCE_GROUP, getResourceGroupFromId(this.migrationStateModel._targetServerInstance.id)),
createInformationRow(this._view, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)),
createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL),
createInformationRow(this._view, constants.MODE, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE ? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL : constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL),