From 53081cfca9e908fd6b0ae02da97a76913d84871e Mon Sep 17 00:00:00 2001 From: Amir Omidi Date: Tue, 1 Sep 2020 09:31:39 -0700 Subject: [PATCH] SKU recommendation page work (#12050) --- extensions/sql-migration/media/ads.svg | 11 +++ extensions/sql-migration/src/api/azure.ts | 13 +-- .../sql-migration/src/models/product.ts | 14 +-- .../sql-migration/src/models/stateMachine.ts | 9 +- .../sql-migration/src/models/strings.ts | 6 +- .../src/wizard/skuRecommendationPage.ts | 86 ++++++++++++------- .../src/wizard/wizardController.ts | 14 +-- 7 files changed, 99 insertions(+), 54 deletions(-) create mode 100644 extensions/sql-migration/media/ads.svg diff --git a/extensions/sql-migration/media/ads.svg b/extensions/sql-migration/media/ads.svg new file mode 100644 index 0000000000..a9f6620868 --- /dev/null +++ b/extensions/sql-migration/media/ads.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index b912fff4b5..167f7e5ecd 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -34,26 +34,27 @@ export async function getSubscriptions(account: azdata.Account): Promise { const api = await getAzureCoreAPI(); - const result = await api.runGraphQuery(account, subscription, false, 'where type == "microsoft.sql/managedinstances"'); + const result = await api.runGraphQuery(account, subscription, false, 'where type == "microsoft.sql/managedinstances"'); return result.resources; } -export type SqlServer = azureResource.AzureGraphResource; +export type SqlServer = AzureProduct; export async function getAvailableSqlServers(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); - const result = await api.runGraphQuery(account, subscription, false, 'where type == "microsoft.sql/servers"'); + const result = await api.runGraphQuery(account, subscription, false, 'where type == "microsoft.sql/servers"'); return result.resources; } -export type SqlVMServer = azureResource.AzureGraphResource; +export type SqlVMServer = AzureProduct; export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); - const result = await api.runGraphQuery(account, subscription, false, 'where type == "microsoft.compute/virtualmachines" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"'); + const result = await api.runGraphQuery(account, subscription, false, 'where type == "microsoft.compute/virtualmachines" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"'); return result.resources; } diff --git a/extensions/sql-migration/src/models/product.ts b/extensions/sql-migration/src/models/product.ts index 6da2d81fde..6acd610293 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'; +export type MigrationProductType = 'AzureSQLMI' | 'AzureSQLVM' | 'AzureSQL'; export interface MigrationProduct { readonly type: MigrationProductType; } @@ -22,12 +22,12 @@ export interface Checks { export interface Product extends MigrationProduct { readonly name: string; - readonly icon: string; readonly learnMoreLink?: string; + readonly icon?: string; } export class Product implements Product { - constructor(public readonly type: MigrationProductType, public readonly name: string, public readonly icon: string, public readonly learnMoreLink?: string) { + constructor(public readonly type: MigrationProductType, public readonly name: string, public readonly icon?: string, public readonly learnMoreLink?: string) { } @@ -45,15 +45,17 @@ export interface SKURecommendation { } -const ProductLookupTable: { [key in MigrationProductType]: Product } = { +export const ProductLookupTable: { [key in MigrationProductType]: Product } = { 'AzureSQLMI': { type: 'AzureSQLMI', name: localize('sql.migration.products.azuresqlmi.name', 'Azure Managed Instance (Microsoft managed)'), - icon: 'TODO', }, 'AzureSQLVM': { type: 'AzureSQLVM', name: localize('sql.migration.products.azuresqlvm.name', 'Azure SQL Virtual Machine (Customer managed)'), - icon: 'TODO', + }, + '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 c9c463cea4..360f19bbb7 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -43,7 +43,10 @@ export class MigrationStateModel implements Model, vscode.Disposable { private _gatheringInformationError: string | undefined; private _skuRecommendations: SKURecommendations | undefined; - constructor(private readonly _sourceConnection: azdata.connection.Connection) { + constructor( + private readonly _extensionContext: vscode.ExtensionContext, + private readonly _sourceConnection: azdata.connection.Connection + ) { this._currentState = State.INIT; } @@ -86,4 +89,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { dispose() { this._stateChangeEventEmitter.dispose(); } + + public getExtensionPath(): string { + return this._extensionContext.extensionPath; + } } diff --git a/extensions/sql-migration/src/models/strings.ts b/extensions/sql-migration/src/models/strings.ts index 09c4cecd02..5a43acac2d 100644 --- a/extensions/sql-migration/src/models/strings.ts +++ b/extensions/sql-migration/src/models/strings.ts @@ -20,11 +20,13 @@ export const COLLECTING_SOURCE_CONFIGURATIONS_ERROR = (error: string = ''): stri export const SKU_RECOMMENDATION_PAGE_TITLE = localize('sql.migration.wizard.sku.title', "Azure SQL Target Selection"); export const SKU_RECOMMENDATION_ALL_SUCCESSFUL = (databaseCount: number): string => { - return localize('sql.migration.sku.all', "Based on the results of our source configuration scans, all {0} of your databases can be migrated to Azure SQL.", databaseCount); + return localize('sql.migration.wizard.sku.all', "Based on the results of our source configuration scans, all {0} of your databases can be migrated to Azure SQL.", databaseCount); }; export const SKU_RECOMMENDATION_SOME_SUCCESSFUL = (migratableCount: number, databaseCount: number): string => { - return localize('sql.migration.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); + 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_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."); export const SUBSCRIPTION_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.subscription.title', "Azure Subscription Selection"); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 2941d8039b..c3c1365498 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import * as path from 'path'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; -import { Product } from '../models/product'; -import { CONGRATULATIONS, SKU_RECOMMENDATION_PAGE_TITLE, SKU_RECOMMENDATION_ALL_SUCCESSFUL } from '../models/strings'; +import { Product, ProductLookupTable } from '../models/product'; +import { SKU_RECOMMENDATION_PAGE_TITLE, SKU_RECOMMENDATION_CHOOSE_A_TARGET } from '../models/strings'; import { Disposable } from 'vscode'; export class SKURecommendationPage extends MigrationWizardPage { @@ -22,12 +23,28 @@ export class SKURecommendationPage extends MigrationWizardPage { private igComponent: azdata.FormComponent | undefined; private detailsComponent: azdata.FormComponent | undefined; + private chooseTargetComponent: azdata.FormComponent | undefined; + private view: azdata.ModelView | undefined; + private async initialState(view: azdata.ModelView) { - this.igComponent = this.createIGComponent(view); - this.detailsComponent = this.createDetailsComponent(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.view = view; + + + const form = view.modelBuilder.formContainer().withFormItems( + [ + this.igComponent, + this.detailsComponent, + this.chooseTargetComponent + ] + ); + + await view.initializeModel(form.component()); } - private createIGComponent(view: azdata.ModelView): azdata.FormComponent { + private createStatusComponent(view: azdata.ModelView): azdata.FormComponent { const component = view.modelBuilder.text().withProperties({ value: '', }); @@ -49,37 +66,40 @@ export class SKURecommendationPage extends MigrationWizardPage { }; } - private constructDetails(): void { - const recommendations = this.migrationStateModel.skuRecommendations?.recommendations; - - if (!recommendations) { - return; - } - - const products = recommendations.map(recommendation => { - return { - checks: recommendation.checks, - product: Product.FromMigrationProduct(recommendation.product) - }; - }); - - const migratableDatabases: number = products?.length ?? 10; // force it to be used - - const allDatabases = 10; - - if (allDatabases === migratableDatabases) { - this.allMigratable(migratableDatabases); - } - - // TODO handle other situations + private createChooseTargetComponent(view: azdata.ModelView) { + const component = view.modelBuilder.divContainer(); + return { + title: SKU_RECOMMENDATION_CHOOSE_A_TARGET, + component: component.component() + }; } - private allMigratable(databaseCount: number): void { - this.igComponent!.title = CONGRATULATIONS; - this.igComponent!.component.value = SKU_RECOMMENDATION_ALL_SUCCESSFUL(databaseCount); - this.detailsComponent!.component.value = ''; // force it to be used - // fill in some of that information + private constructDetails(): void { + this.chooseTargetComponent?.component.clearItems(); + + this.igComponent!.component.value = 'Test'; + this.detailsComponent!.component.value = 'Test'; + this.constructTargets(); + } + + private constructTargets(): void { + const products: Product[] = Object.values(ProductLookupTable); + + const rbg = this.view!.modelBuilder.radioCardGroup(); + rbg.component().cards = []; + + products.forEach((product) => { + const imagePath = path.resolve(this.migrationStateModel.getExtensionPath(), 'media', product.icon ?? 'ads.svg'); + + rbg.component().cards.push({ + id: product.name, + icon: imagePath, + label: 'Some Label' + }); + }); + + this.chooseTargetComponent?.component.addItem(rbg.component()); } private eventListener: Disposable | undefined; diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 49be4a40b4..94e5f5fda1 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -17,7 +17,7 @@ export class WizardController { } public async openWizard(profile: azdata.connection.Connection): Promise { - const stateModel = new MigrationStateModel(profile); + const stateModel = new MigrationStateModel(this.extensionContext, profile); this.extensionContext.subscriptions.push(stateModel); this.createWizard(stateModel); @@ -26,6 +26,8 @@ export class WizardController { private async createWizard(stateModel: MigrationStateModel): Promise { 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); @@ -47,13 +49,13 @@ export class WizardController { }); wizard.registerNavigationValidator(async validator => { - const lastPage = validator.lastPage; + // const lastPage = validator.lastPage; - const canLeave = await pages[lastPage]?.canLeave() ?? true; - const canEnter = await pages[lastPage]?.canEnter() ?? true; + // const canLeave = await pages[lastPage]?.canLeave() ?? true; + // const canEnter = await pages[lastPage]?.canEnter() ?? true; - return canEnter && canLeave; - // return true; + // return canEnter && canLeave; + return true; }); await Promise.all(wizardSetupPromises);