Target Page Changes (#14421)

* target page changes

* remove SourceConfigurationPage

* fix build errors

* fixed build errors

* passing selected Dbs on to migration state model

* code cleanup

* fix build errors
This commit is contained in:
Christopher Suh
2021-02-24 20:02:36 -05:00
committed by GitHub
parent 00feb955d9
commit f461d2aa14
7 changed files with 249 additions and 66 deletions

View File

@@ -6,10 +6,15 @@
import * as azdata from 'azdata';
import { MigrationStateModel } from '../../models/stateMachine';
import { SqlDatabaseTree } from './sqlDatabasesTree';
import { SqlAssessmentResultList } from './sqlAssessmentResultsList';
import { SqlAssessmentResult } from './sqlAssessmentResult';
import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
export type Issues = {
description: string,
recommendation: string,
moreInfo: string,
impactedObjects: SqlMigrationImpactedObjectInfo[],
rowNumber: number
};
export class AssessmentResultsDialog {
private static readonly OkButtonText: string = 'OK';
@@ -17,33 +22,40 @@ export class AssessmentResultsDialog {
private _isOpen: boolean = false;
private dialog: azdata.window.Dialog | undefined;
private _model: MigrationStateModel;
// Dialog Name for Telemetry
public dialogName: string | undefined;
private _tree: SqlDatabaseTree;
private _list: SqlAssessmentResultList;
private _result: SqlAssessmentResult;
constructor(public ownerUri: string, public model: MigrationStateModel, public title: string) {
this._tree = new SqlDatabaseTree();
this._list = new SqlAssessmentResultList();
this._result = new SqlAssessmentResult();
this._model = model;
let assessmentData = this.parseData(this._model);
this._tree = new SqlDatabaseTree(assessmentData);
}
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
return new Promise<void>((resolve, reject) => {
dialog.registerContent(async (view) => {
try {
// const resultComponent = await this._tree.createComponentResult(view);
const treeComponent = await this._tree.createComponent(view);
const separator1 = view.modelBuilder.separator().component();
const listComponent = await this._list.createComponent(view);
const separator2 = view.modelBuilder.separator().component();
const resultComponent = await this._result.createComponent(view);
const flex = view.modelBuilder.flexContainer().withItems([treeComponent, separator1, listComponent, separator2, resultComponent]);
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
height: '100%',
width: '100%'
}).withProps({
CSSStyles: {
'margin-top': '10px'
}
}).component();
flex.addItem(treeComponent, { flex: '0 0 auto' });
// flex.addItem(resultComponent, { flex: '1 1 auto' });
view.initializeModel(flex.component());
view.initializeModel(flex);
resolve();
} catch (ex) {
reject(ex);
@@ -72,7 +84,48 @@ export class AssessmentResultsDialog {
}
}
private parseData(model: MigrationStateModel): Map<string, Issues[]> {
// if there are multiple issues for the same DB, need to consolidate
// map DB name -> Assessment result items (issues)
// map assessment result items to description, recommendation, more info & impacted objects
let dbMap = new Map<string, Issues[]>();
model.assessmentResults?.forEach((element) => {
let issues: Issues;
issues = {
description: element.description,
recommendation: element.message,
moreInfo: element.helpLink,
impactedObjects: element.impactedObjects,
rowNumber: 0
};
if (element.targetName.includes(':')) {
let spliceIndex = element.targetName.indexOf(':');
let dbName = element.targetName.slice(spliceIndex + 1);
let dbIssues = dbMap.get(element.targetName);
if (dbIssues) {
dbMap.set(dbName, dbIssues.concat([issues]));
} else {
dbMap.set(dbName, [issues]);
}
} else {
let dbIssues = dbMap.get(element.targetName);
if (dbIssues) {
dbMap.set(element.targetName, dbIssues.concat([issues]));
} else {
dbMap.set(element.targetName, [issues]);
}
}
});
return dbMap;
}
protected async execute() {
// this.model._migrationDbs = this._tree.selectedDbs();
this._isOpen = false;
}

View File

@@ -4,8 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { AssessmentDialogComponent } from './model/assessmentDialogComponent';
import { Issues } from './assessmentResultsDialog';
export class SqlDatabaseTree extends AssessmentDialogComponent {
// private _assessmentData: Map<string, Issues[]>;
constructor(assessmentData: Map<string, Issues[]>) {
super();
// this._assessmentData = assessmentData;
}
async createComponent(view: azdata.ModelView): Promise<azdata.Component> {
return view.modelBuilder.divContainer().withItems([
@@ -89,4 +98,5 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
return table.component();
}
}

View File

@@ -5,7 +5,7 @@
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export type MigrationProductType = 'AzureSQLMI' | 'AzureSQLVM' | 'AzureSQL';
export type MigrationProductType = 'AzureSQLMI' | 'AzureSQLVM';
export interface MigrationProduct {
readonly type: MigrationProductType;
}
@@ -53,9 +53,5 @@ export const ProductLookupTable: { [key in MigrationProductType]: Product } = {
'AzureSQLVM': {
type: 'AzureSQLVM',
name: localize('sql.migration.products.azuresqlvm.name', 'Azure SQL Virtual Machine (Customer managed)'),
},
'AzureSQL': {
type: 'AzureSQL',
name: localize('sql.migration.products.azuresql.name', 'Azure SQL'),
}
};

View File

@@ -85,6 +85,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _targetManagedInstance!: SqlManagedInstance;
public _databaseBackup!: DatabaseBackupModel;
public _migrationDbs!: string[];
public _storageAccounts!: StorageAccount[];
public _fileShares!: azureResource.FileShare[];
public _blobContainers!: azureResource.BlobContainer[];

View File

@@ -25,7 +25,7 @@ export const SKU_RECOMMENDATION_ALL_SUCCESSFUL = (databaseCount: number): string
export const SKU_RECOMMENDATION_SOME_SUCCESSFUL = (migratableCount: number, databaseCount: number): string => {
return localize('sql.migration.wizard.sku.some', "Based on the results of our source configuration scans, {0} out of {1} of your databases can be migrated to Azure SQL.", migratableCount, databaseCount);
};
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose a target");
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose a target Azure SQL");
export const SKU_RECOMMENDATION_NONE_SUCCESSFUL = localize('sql.migration.sku.none', "Based on the results of our source configuration scans, none of your databases can be migrated to Azure SQL.");
@@ -34,7 +34,7 @@ export const SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE = localize('sql.migratio
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 CONGRATULATIONS = localize('sql.migration.generic.congratulations', "Congratulations");
export const CONGRATULATIONS = localize('sql.migration.generic.congratulations', "Congratulations!");
// Accounts page

View File

@@ -8,62 +8,92 @@ import * as path from 'path';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { Product, ProductLookupTable } from '../models/product';
import { SKU_RECOMMENDATION_PAGE_TITLE, SKU_RECOMMENDATION_CHOOSE_A_TARGET } from '../models/strings';
import { Disposable } from 'vscode';
import { AssessmentResultsDialog } from '../dialog/assessmentResults/assessmentResultsDialog';
import { getAvailableManagedInstanceProducts, getSubscriptions, SqlManagedInstance, Subscription } from '../api/azure';
import * as constants from '../models/strings';
import { azureResource } from 'azureResource';
// import { SqlMigrationService } from '../../../../extensions/mssql/src/sqlMigration/sqlMigrationService';
export class SKURecommendationPage extends MigrationWizardPage {
// For future reference: DO NOT EXPOSE WIZARD DIRECTLY THROUGH HERE.
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(SKU_RECOMMENDATION_PAGE_TITLE), migrationStateModel);
super(wizard, azdata.window.createWizardPage(constants.SKU_RECOMMENDATION_PAGE_TITLE), migrationStateModel);
}
protected async registerContent(view: azdata.ModelView) {
await this.initialState(view);
}
private igComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
private detailsComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
private chooseTargetComponent: azdata.FormComponent<azdata.DivContainer> | undefined;
private view: azdata.ModelView | undefined;
private _igComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
private _detailsComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
private _chooseTargetComponent: azdata.FormComponent<azdata.DivContainer> | undefined;
private _azureSubscriptionText: azdata.FormComponent<azdata.TextComponent> | undefined;
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
private _managedInstanceDropdown!: azdata.DropDownComponent;
private _subscriptionDropdownValues: azdata.CategoryValue[] = [];
private _subscriptionMap: Map<string, Subscription> = new Map();
private _view: azdata.ModelView | undefined;
private async initialState(view: azdata.ModelView) {
this.igComponent = this.createStatusComponent(view); // The first component giving basic information
this.detailsComponent = this.createDetailsComponent(view); // The details of what can be moved
this.chooseTargetComponent = this.createChooseTargetComponent(view);
this._igComponent = this.createStatusComponent(view); // The first component giving basic information
this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved
this._chooseTargetComponent = this.createChooseTargetComponent(view);
this._azureSubscriptionText = this.createAzureSubscriptionText(view);
const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text().withProps({
value: constants.SUBSCRIPTION
}).component();
this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().component();
this._managedInstanceSubscriptionDropdown.onValueChanged((e) => {
this.populateManagedInstanceDropdown();
});
const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE
}).component();
this._managedInstanceDropdown = view.modelBuilder.dropDown().component();
const assessmentLink = view.modelBuilder.hyperlink()
.withProperties<azdata.HyperlinkComponentProperties>({
label: 'View Assessment Results',
url: ''
}).component();
assessmentLink.onDidClick(async () => {
let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog');
await dialog.openDialog();
const targetContainer = view.modelBuilder.flexContainer().withItems(
[
managedInstanceSubscriptionDropdownLabel,
this._managedInstanceSubscriptionDropdown,
managedInstanceDropdownLabel,
this._managedInstanceDropdown
]
).withLayout({
flexFlow: 'column'
}).component();
let connectionUri: string = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
this.migrationStateModel.migrationService.getAssessments(connectionUri).then(results => {
if (results) {
this.migrationStateModel.assessmentResults = results.items;
}
});
const assessmentFormLink = {
title: '',
component: assessmentLink,
};
this.view = view;
const form = view.modelBuilder.formContainer().withFormItems(
this._view = view;
const formContainer = view.modelBuilder.formContainer().withFormItems(
[
this.igComponent,
this.detailsComponent,
this.chooseTargetComponent,
assessmentFormLink
this._igComponent,
this._detailsComponent,
this._chooseTargetComponent,
this._azureSubscriptionText,
{
component: targetContainer
},
]
);
await view.initializeModel(form.component());
await view.initializeModel(formContainer.component());
}
private createStatusComponent(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: '',
CSSStyles: {
'font-size': '18px'
}
});
return {
@@ -87,27 +117,34 @@ export class SKURecommendationPage extends MigrationWizardPage {
const component = view.modelBuilder.divContainer();
return {
title: SKU_RECOMMENDATION_CHOOSE_A_TARGET,
title: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
component: component.component()
};
}
private constructDetails(): void {
this.chooseTargetComponent?.component.clearItems();
this._chooseTargetComponent?.component.clearItems();
this.igComponent!.component.value = 'Test';
this.detailsComponent!.component.value = 'Test';
if (this.migrationStateModel.assessmentResults) {
}
this._igComponent!.component.value = constants.CONGRATULATIONS;
// either: SKU_RECOMMENDATION_ALL_SUCCESSFUL or SKU_RECOMMENDATION_SOME_SUCCESSFUL or SKU_RECOMMENDATION_NONE_SUCCESSFUL
this._detailsComponent!.component.value = constants.SKU_RECOMMENDATION_SOME_SUCCESSFUL(1, 1);
this.constructTargets();
}
private constructTargets(): void {
const products: Product[] = Object.values(ProductLookupTable);
const rbg = this.view!.modelBuilder.radioCardGroup();
rbg.component().cards = [];
rbg.component().orientation = azdata.Orientation.Vertical;
rbg.component().iconHeight = '30px';
rbg.component().iconWidth = '30px';
const rbg = this._view!.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
cards: [],
cardWidth: '600px',
cardHeight: '60px',
orientation: azdata.Orientation.Vertical,
iconHeight: '30px',
iconWidth: '30px'
});
products.forEach((product) => {
const imagePath = path.resolve(this.migrationStateModel.getExtensionPath(), 'media', product.icon ?? 'ads.svg');
@@ -144,12 +181,104 @@ export class SKURecommendationPage extends MigrationWizardPage {
});
});
this.chooseTargetComponent?.component.addItem(rbg.component());
rbg.component().onLinkClick(async (value) => {
//check which card is being selected, and open correct dialog based on link
console.log(value);
let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog');
await dialog.openDialog();
});
this._chooseTargetComponent?.component.addItem(rbg.component());
}
private createAzureSubscriptionText(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: 'Select an Azure subscription and an Azure SQL Managed Instance for your target.', //TODO: Localize
});
return {
title: '',
component: component.component(),
};
}
private async populateSubscriptionDropdown(): Promise<void> {
this._managedInstanceSubscriptionDropdown.loading = true;
this._managedInstanceDropdown.loading = true;
let subscriptions: azureResource.AzureResourceSubscription[] = [];
try {
subscriptions = await getSubscriptions(this.migrationStateModel._azureAccount);
subscriptions.forEach((subscription) => {
this._subscriptionMap.set(subscription.id, subscription);
this._subscriptionDropdownValues.push({
name: subscription.id,
displayName: subscription.name + ' - ' + subscription.id,
});
});
if (!this._subscriptionDropdownValues || this._subscriptionDropdownValues.length === 0) {
this._subscriptionDropdownValues = [
{
displayName: constants.NO_SUBSCRIPTIONS_FOUND,
name: ''
}
];
}
this._managedInstanceSubscriptionDropdown.values = this._subscriptionDropdownValues;
} catch (error) {
this.setEmptyDropdownPlaceHolder(this._managedInstanceSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
this._managedInstanceDropdown.loading = false;
}
this.populateManagedInstanceDropdown();
this._managedInstanceSubscriptionDropdown.loading = false;
}
private async populateManagedInstanceDropdown(): Promise<void> {
this._managedInstanceDropdown.loading = true;
let mis: SqlManagedInstance[] = [];
let miValues: azdata.CategoryValue[] = [];
try {
const subscriptionId = (<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).name;
mis = await getAvailableManagedInstanceProducts(this.migrationStateModel._azureAccount, this._subscriptionMap.get(subscriptionId)!);
mis.forEach((mi) => {
miValues.push({
name: mi.name,
displayName: mi.name
});
});
if (!miValues || miValues.length === 0) {
miValues = [
{
displayName: constants.NO_MANAGED_INSTANCE_FOUND,
name: ''
}
];
}
this._managedInstanceDropdown.values = miValues;
} catch (error) {
this.setEmptyDropdownPlaceHolder(this._managedInstanceDropdown, constants.NO_MANAGED_INSTANCE_FOUND);
}
this._managedInstanceDropdown.loading = false;
}
private setEmptyDropdownPlaceHolder(dropDown: azdata.DropDownComponent, placeholder: string): void {
dropDown.values = [{
displayName: placeholder,
name: ''
}];
}
private eventListener: Disposable | undefined;
public async onPageEnter(): Promise<void> {
this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e));
this.populateSubscriptionDropdown();
this.constructDetails();
}

View File

@@ -6,7 +6,6 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as mssql from '../../../mssql';
import { MigrationStateModel } from '../models/stateMachine';
// import { SourceConfigurationPage } from './sourceConfigurationPage';
import { WIZARD_TITLE } from '../models/strings';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { SKURecommendationPage } from './skuRecommendationPage';
@@ -14,7 +13,6 @@ import { SKURecommendationPage } from './skuRecommendationPage';
import { DatabaseBackupPage } from './databaseBackupPage';
import { AccountsSelectionPage } from './accountsSelectionPage';
import { IntergrationRuntimePage } from './integrationRuntimePage';
import { TempTargetSelectionPage } from './tempTargetSelectionPage';
import { SummaryPage } from './summaryPage';
export const WIZARD_INPUT_COMPONENT_WIDTH = '400px';
@@ -36,11 +34,9 @@ export class WizardController {
const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide');
wizard.generateScriptButton.enabled = false;
wizard.generateScriptButton.hidden = true;
// const sourceConfigurationPage = new SourceConfigurationPage(wizard, stateModel);
const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel);
// const subscriptionSelectionPage = new SubscriptionSelectionPage(wizard, stateModel);
const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel);
const tempTargetSelectionPage = new TempTargetSelectionPage(wizard, stateModel);
const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel);
const integrationRuntimePage = new IntergrationRuntimePage(wizard, stateModel);
const summaryPage = new SummaryPage(wizard, stateModel);
@@ -48,8 +44,6 @@ export class WizardController {
const pages: MigrationWizardPage[] = [
// subscriptionSelectionPage,
azureAccountsPage,
tempTargetSelectionPage,
// sourceConfigurationPage,
skuRecommendationPage,
databaseBackupPage,
integrationRuntimePage,