diff --git a/extensions/sql-migration/src/models/externalContract.ts b/extensions/sql-migration/src/models/externalContract.ts new file mode 100644 index 0000000000..45f803b0be --- /dev/null +++ b/extensions/sql-migration/src/models/externalContract.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SKURecommendation } from './product'; + +export interface Base { + uuid: string; +} + +export interface BaseRequest extends Base { } + +export interface BaseResponse extends Base { + error?: string; + response: T; +} + +export interface GatherInformationRequest extends BaseRequest { + connection: azdata.connection.Connection; +} + +export interface SKURecommendations { + recommendations: SKURecommendation[]; +} + +export interface GatherInformationResponse extends BaseResponse { +} + diff --git a/extensions/sql-migration/src/models/migrationWizardPage.ts b/extensions/sql-migration/src/models/migrationWizardPage.ts index ba1b4d3681..44972911a5 100644 --- a/extensions/sql-migration/src/models/migrationWizardPage.ts +++ b/extensions/sql-migration/src/models/migrationWizardPage.ts @@ -6,9 +6,24 @@ import * as azdata from 'azdata'; import { MigrationStateModel, StateChangeEvent } from './stateMachine'; export abstract class MigrationWizardPage { - constructor(protected readonly wizardPage: azdata.window.WizardPage, protected readonly migrationStateModel: MigrationStateModel) { } + constructor(private readonly wizard: azdata.window.Wizard, protected readonly wizardPage: azdata.window.WizardPage, protected readonly migrationStateModel: MigrationStateModel) { } - public abstract async registerWizardContent(): Promise; + public registerWizardContent(): Promise { + return new Promise(async (resolve, reject) => { + this.wizardPage.registerContent(async (view) => { + try { + await this.registerContent(view); + resolve(); + } catch (ex) { + reject(ex); + } finally { + reject(new Error()); + } + }); + }); + } + + protected abstract async registerContent(view: azdata.ModelView): Promise; public getwizardPage(): azdata.window.WizardPage { return this.wizardPage; @@ -48,5 +63,19 @@ export abstract class MigrationWizardPage { } protected abstract async handleStateChange(e: StateChangeEvent): Promise; + + public canEnter(): Promise { + return Promise.resolve(true); + } + + public canLeave(): Promise { + return Promise.resolve(true); + } + + protected async goToNextPage(): Promise { + const current = this.wizard.currentPage; + await this.wizard.setCurrentPage(current + 1); + } + } diff --git a/extensions/sql-migration/src/models/product.ts b/extensions/sql-migration/src/models/product.ts index 51b2d9d7ed..6da2d81fde 100644 --- a/extensions/sql-migration/src/models/product.ts +++ b/extensions/sql-migration/src/models/product.ts @@ -2,15 +2,58 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); -export interface Product { - name: string; - learnMoreLink: string | undefined; - icon: string; +export type MigrationProductType = 'AzureSQLMI' | 'AzureSQLVM'; +export interface MigrationProduct { + readonly type: MigrationProductType; +} + +export interface Check { + +} + +export interface Checks { + // fill some information + checks: Check; + // If there is not going to be any more information, use Check[] directly +} + +export interface Product extends MigrationProduct { + readonly name: string; + readonly icon: string; + readonly learnMoreLink?: string; +} + +export class Product implements Product { + constructor(public readonly type: MigrationProductType, public readonly name: string, public readonly icon: string, public readonly learnMoreLink?: string) { + + } + + static FromMigrationProduct(migrationProduct: MigrationProduct) { + // TODO: populatie from some lookup table; + + const product: Product | undefined = ProductLookupTable[migrationProduct.type]; + return new Product(migrationProduct.type, product?.name ?? '', product.icon ?? ''); + } } export interface SKURecommendation { - product: Product; - migratableDatabases: number; - totalDatabases: number; + product: MigrationProduct; + checks: Checks; } + + +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', + } +}; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 28cfcc9a1b..c9c463cea4 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -5,6 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; +import { SKURecommendations } from './externalContract'; export enum State { INIT, @@ -28,6 +29,7 @@ export interface Model { readonly sourceConnection: azdata.connection.Connection; readonly currentState: State; gatheringInformationError: string | undefined; + skuRecommendations: SKURecommendations | undefined; } export interface StateChangeEvent { @@ -39,6 +41,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { private _stateChangeEventEmitter = new vscode.EventEmitter(); private _currentState: State; private _gatheringInformationError: string | undefined; + private _skuRecommendations: SKURecommendations | undefined; constructor(private readonly _sourceConnection: azdata.connection.Connection) { this._currentState = State.INIT; @@ -68,6 +71,14 @@ export class MigrationStateModel implements Model, vscode.Disposable { this._gatheringInformationError = error; } + public get skuRecommendations(): SKURecommendations | undefined { + return this._skuRecommendations; + } + + public set skuRecommendations(recommendations: SKURecommendations | undefined) { + this._skuRecommendations = recommendations; + } + public get stateChangeEvent(): vscode.Event { return this._stateChangeEventEmitter.event; } diff --git a/extensions/sql-migration/src/models/strings.ts b/extensions/sql-migration/src/models/strings.ts index d6b6792c47..5ead4b0b28 100644 --- a/extensions/sql-migration/src/models/strings.ts +++ b/extensions/sql-migration/src/models/strings.ts @@ -18,12 +18,14 @@ export const COLLECTING_SOURCE_CONFIGURATIONS_ERROR = (error: string = ''): stri return localize('sql.migration.collecting_source_configurations.error', "There was an error when gathering information about your data configuration. {0}", error); }; +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); }; - 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); }; - 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 CONGRATULATIONS = localize('sql.migration.generic.congratulations', "Congratulations"); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts new file mode 100644 index 0000000000..7e65d39775 --- /dev/null +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { 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'; + +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); + } + + protected async registerContent(view: azdata.ModelView) { + await this.initialState(view); + } + + private igComponent: azdata.FormComponent | undefined; + private detailsComponent: azdata.FormComponent | undefined; + private async initialState(view: azdata.ModelView) { + this.igComponent = this.createIGComponent(view); + this.detailsComponent = this.createDetailsComponent(view); + } + + private createIGComponent(view: azdata.ModelView): azdata.FormComponent { + const component = view.modelBuilder.text().withProperties({ + value: '', + }); + + return { + title: '', + component: component.component(), + }; + } + + private createDetailsComponent(view: azdata.ModelView): azdata.FormComponent { + const component = view.modelBuilder.text().withProperties({ + value: '', + }); + + return { + title: '', + component: component.component(), + }; + } + + 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 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 + } + + public async onPageEnter(): Promise { + this.constructDetails(); + } + + public async onPageLeave(): Promise { + + } + + protected async handleStateChange(e: StateChangeEvent): Promise { + switch (e.newState) { + + } + } + +} diff --git a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts index ce8932f90c..a89657f146 100644 --- a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts +++ b/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts @@ -10,26 +10,12 @@ import { MigrationStateModel, StateChangeEvent, State } from '../models/stateMac import { Disposable } from 'vscode'; export class SourceConfigurationPage extends MigrationWizardPage { - constructor(migrationStateModel: MigrationStateModel) { - super(azdata.window.createWizardPage(SOURCE_CONFIGURATION_PAGE_TITLE), migrationStateModel); + // For future reference: DO NOT EXPOSE WIZARD DIRECTLY THROUGH HERE. + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { + super(wizard, azdata.window.createWizardPage(SOURCE_CONFIGURATION_PAGE_TITLE), migrationStateModel); } - public async registerWizardContent(): Promise { - return new Promise(async (resolve, reject) => { - this.wizardPage.registerContent(async (view) => { - try { - await this.registerContent(view); - resolve(); - } catch (ex) { - reject(ex); - } finally { - reject(new Error()); - } - }); - }); - } - - private async registerContent(view: azdata.ModelView) { + protected async registerContent(view: azdata.ModelView) { await this.initialState(view); } @@ -58,7 +44,7 @@ export class SourceConfigurationPage extends MigrationWizardPage { } private async enterTargetSelectionState() { - + this.goToNextPage(); } //#region component builders @@ -91,8 +77,11 @@ export class SourceConfigurationPage extends MigrationWizardPage { case State.COLLECTION_SOURCE_INFO_ERROR: return this.enterErrorState(); case State.TARGET_SELECTION: - // TODO: Allow pressing next in this state return this.enterTargetSelectionState(); } } + + public async canLeave(): Promise { + return this.migrationStateModel.currentState === State.TARGET_SELECTION; + } } diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 1021783910..c3d3ccaf11 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -24,7 +24,7 @@ export class WizardController { private async createWizard(stateModel: MigrationStateModel): Promise { const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide'); wizard.generateScriptButton.enabled = false; - const sourceConfigurationPage = new SourceConfigurationPage(stateModel); + const sourceConfigurationPage = new SourceConfigurationPage(wizard, stateModel); const pages: MigrationWizardPage[] = [sourceConfigurationPage]; @@ -42,6 +42,14 @@ export class WizardController { await pages[newPage]?.onPageEnter(); }); + wizard.registerNavigationValidator(async validator => { + const lastPage = validator.lastPage; + + const canLeave = await pages[lastPage]?.canLeave() ?? true; + const canEnter = await pages[lastPage]?.canEnter() ?? true; + + return canEnter && canLeave; + }); await Promise.all(wizardSetupPromises); await pages[0].onPageEnter(); diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 48f438538e..87621f24c3 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -2733,8 +2733,8 @@ declare module 'azdata' { focus(): Thenable; } - export interface FormComponent { - component: Component; + export interface FormComponent { + component: T; title: string; actions?: Component[]; required?: boolean;