SKU recommendation page work (#12050)

This commit is contained in:
Amir Omidi
2020-09-01 09:31:39 -07:00
committed by GitHub
parent 96a6d0674a
commit 53081cfca9
7 changed files with 99 additions and 54 deletions

View File

@@ -34,26 +34,27 @@ export async function getSubscriptions(account: azdata.Account): Promise<Subscri
}
export type AzureProduct = azureResource.AzureGraphResource;
export type SqlManagedInstance = azureResource.AzureGraphResource;
export type SqlManagedInstance = AzureProduct;
export async function getAvailableManagedInstanceProducts(account: azdata.Account, subscription: Subscription): Promise<SqlManagedInstance[]> {
const api = await getAzureCoreAPI();
const result = await api.runGraphQuery<azureResource.AzureGraphResource>(account, subscription, false, 'where type == "microsoft.sql/managedinstances"');
const result = await api.runGraphQuery<SqlManagedInstance>(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<SqlServer[]> {
const api = await getAzureCoreAPI();
const result = await api.runGraphQuery<azureResource.AzureGraphResource>(account, subscription, false, 'where type == "microsoft.sql/servers"');
const result = await api.runGraphQuery<SqlServer>(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<SqlVMServer[]> {
const api = await getAzureCoreAPI();
const result = await api.runGraphQuery<azureResource.AzureGraphResource>(account, subscription, false, 'where type == "microsoft.compute/virtualmachines" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"');
const result = await api.runGraphQuery<SqlVMServer>(account, subscription, false, 'where type == "microsoft.compute/virtualmachines" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"');
return result.resources;
}

View File

@@ -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'),
}
};

View File

@@ -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;
}
}

View File

@@ -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");

View File

@@ -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<azdata.TextComponent> | undefined;
private detailsComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
private chooseTargetComponent: azdata.FormComponent<azdata.DivContainer> | 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<azdata.TextComponent> {
private createStatusComponent(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
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;

View File

@@ -17,7 +17,7 @@ export class WizardController {
}
public async openWizard(profile: azdata.connection.Connection): Promise<void> {
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<void> {
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);