diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index bc6a7021d3..ed9d8821f1 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -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 { return new Promise((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 { + // 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(); + + 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; } diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index ac0c5eee1b..bca2d4f1ac 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -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; + + constructor(assessmentData: Map) { + super(); + // this._assessmentData = assessmentData; + } + async createComponent(view: azdata.ModelView): Promise { return view.modelBuilder.divContainer().withItems([ @@ -89,4 +98,5 @@ export class SqlDatabaseTree extends AssessmentDialogComponent { return table.component(); } + } diff --git a/extensions/sql-migration/src/models/product.ts b/extensions/sql-migration/src/models/product.ts index 6acd610293..39d2e1ef9d 100644 --- a/extensions/sql-migration/src/models/product.ts +++ b/extensions/sql-migration/src/models/product.ts @@ -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'), } }; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index d8a00973fa..94423c46c2 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -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[]; diff --git a/extensions/sql-migration/src/models/strings.ts b/extensions/sql-migration/src/models/strings.ts index 4313be0269..ae5e6bf17d 100644 --- a/extensions/sql-migration/src/models/strings.ts +++ b/extensions/sql-migration/src/models/strings.ts @@ -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 diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 23b6055dff..e78aea889e 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -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 | undefined; - private detailsComponent: azdata.FormComponent | undefined; - private chooseTargetComponent: azdata.FormComponent | undefined; - private view: azdata.ModelView | undefined; + private _igComponent: azdata.FormComponent | undefined; + private _detailsComponent: azdata.FormComponent | undefined; + private _chooseTargetComponent: azdata.FormComponent | undefined; + private _azureSubscriptionText: azdata.FormComponent | undefined; + private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent; + private _managedInstanceDropdown!: azdata.DropDownComponent; + private _subscriptionDropdownValues: azdata.CategoryValue[] = []; + private _subscriptionMap: Map = 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({ - 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 { const component = view.modelBuilder.text().withProperties({ 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({ + 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 { + const component = view.modelBuilder.text().withProperties({ + 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 { + 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 { + this._managedInstanceDropdown.loading = true; + let mis: SqlManagedInstance[] = []; + let miValues: azdata.CategoryValue[] = []; + try { + const subscriptionId = (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 { this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e)); + this.populateSubscriptionDropdown(); this.constructDetails(); } diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 0b6a7462d3..70b1e92c12 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -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,