mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
skuRecommendationPage & azdata change (#11863)
* skuRecommendationPage * fix
This commit is contained in:
30
extensions/sql-migration/src/models/externalContract.ts
Normal file
30
extensions/sql-migration/src/models/externalContract.ts
Normal file
@@ -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<T> 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<SKURecommendations> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,9 +6,24 @@
|
|||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import { MigrationStateModel, StateChangeEvent } from './stateMachine';
|
import { MigrationStateModel, StateChangeEvent } from './stateMachine';
|
||||||
export abstract class MigrationWizardPage {
|
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<void>;
|
public registerWizardContent(): Promise<void> {
|
||||||
|
return new Promise<void>(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<void>;
|
||||||
|
|
||||||
public getwizardPage(): azdata.window.WizardPage {
|
public getwizardPage(): azdata.window.WizardPage {
|
||||||
return this.wizardPage;
|
return this.wizardPage;
|
||||||
@@ -48,5 +63,19 @@ export abstract class MigrationWizardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract async handleStateChange(e: StateChangeEvent): Promise<void>;
|
protected abstract async handleStateChange(e: StateChangeEvent): Promise<void>;
|
||||||
|
|
||||||
|
public canEnter(): Promise<boolean> {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public canLeave(): Promise<boolean> {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async goToNextPage(): Promise<void> {
|
||||||
|
const current = this.wizard.currentPage;
|
||||||
|
await this.wizard.setCurrentPage(current + 1);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,58 @@
|
|||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* 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 {
|
export type MigrationProductType = 'AzureSQLMI' | 'AzureSQLVM';
|
||||||
name: string;
|
export interface MigrationProduct {
|
||||||
learnMoreLink: string | undefined;
|
readonly type: MigrationProductType;
|
||||||
icon: string;
|
}
|
||||||
|
|
||||||
|
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 {
|
export interface SKURecommendation {
|
||||||
product: Product;
|
product: MigrationProduct;
|
||||||
migratableDatabases: number;
|
checks: Checks;
|
||||||
totalDatabases: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { SKURecommendations } from './externalContract';
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
INIT,
|
INIT,
|
||||||
@@ -28,6 +29,7 @@ export interface Model {
|
|||||||
readonly sourceConnection: azdata.connection.Connection;
|
readonly sourceConnection: azdata.connection.Connection;
|
||||||
readonly currentState: State;
|
readonly currentState: State;
|
||||||
gatheringInformationError: string | undefined;
|
gatheringInformationError: string | undefined;
|
||||||
|
skuRecommendations: SKURecommendations | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StateChangeEvent {
|
export interface StateChangeEvent {
|
||||||
@@ -39,6 +41,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
|||||||
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
|
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
|
||||||
private _currentState: State;
|
private _currentState: State;
|
||||||
private _gatheringInformationError: string | undefined;
|
private _gatheringInformationError: string | undefined;
|
||||||
|
private _skuRecommendations: SKURecommendations | undefined;
|
||||||
|
|
||||||
constructor(private readonly _sourceConnection: azdata.connection.Connection) {
|
constructor(private readonly _sourceConnection: azdata.connection.Connection) {
|
||||||
this._currentState = State.INIT;
|
this._currentState = State.INIT;
|
||||||
@@ -68,6 +71,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
|||||||
this._gatheringInformationError = error;
|
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<StateChangeEvent> {
|
public get stateChangeEvent(): vscode.Event<StateChangeEvent> {
|
||||||
return this._stateChangeEventEmitter.event;
|
return this._stateChangeEventEmitter.event;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
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 => {
|
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.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 => {
|
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.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 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");
|
||||||
|
|||||||
98
extensions/sql-migration/src/wizard/skuRecommendationPage.ts
Normal file
98
extensions/sql-migration/src/wizard/skuRecommendationPage.ts
Normal file
@@ -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<azdata.TextComponent> | undefined;
|
||||||
|
private detailsComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
|
||||||
|
private async initialState(view: azdata.ModelView) {
|
||||||
|
this.igComponent = this.createIGComponent(view);
|
||||||
|
this.detailsComponent = this.createDetailsComponent(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createIGComponent(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
|
||||||
|
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
value: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
component: component.component(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createDetailsComponent(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
|
||||||
|
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
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<void> {
|
||||||
|
this.constructDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onPageLeave(): Promise<void> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||||
|
switch (e.newState) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,26 +10,12 @@ import { MigrationStateModel, StateChangeEvent, State } from '../models/stateMac
|
|||||||
import { Disposable } from 'vscode';
|
import { Disposable } from 'vscode';
|
||||||
|
|
||||||
export class SourceConfigurationPage extends MigrationWizardPage {
|
export class SourceConfigurationPage extends MigrationWizardPage {
|
||||||
constructor(migrationStateModel: MigrationStateModel) {
|
// For future reference: DO NOT EXPOSE WIZARD DIRECTLY THROUGH HERE.
|
||||||
super(azdata.window.createWizardPage(SOURCE_CONFIGURATION_PAGE_TITLE), migrationStateModel);
|
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||||
|
super(wizard, azdata.window.createWizardPage(SOURCE_CONFIGURATION_PAGE_TITLE), migrationStateModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async registerWizardContent(): Promise<void> {
|
protected async registerContent(view: azdata.ModelView) {
|
||||||
return new Promise<void>(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) {
|
|
||||||
await this.initialState(view);
|
await this.initialState(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +44,7 @@ export class SourceConfigurationPage extends MigrationWizardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async enterTargetSelectionState() {
|
private async enterTargetSelectionState() {
|
||||||
|
this.goToNextPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region component builders
|
//#region component builders
|
||||||
@@ -91,8 +77,11 @@ export class SourceConfigurationPage extends MigrationWizardPage {
|
|||||||
case State.COLLECTION_SOURCE_INFO_ERROR:
|
case State.COLLECTION_SOURCE_INFO_ERROR:
|
||||||
return this.enterErrorState();
|
return this.enterErrorState();
|
||||||
case State.TARGET_SELECTION:
|
case State.TARGET_SELECTION:
|
||||||
// TODO: Allow pressing next in this state
|
|
||||||
return this.enterTargetSelectionState();
|
return this.enterTargetSelectionState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async canLeave(): Promise<boolean> {
|
||||||
|
return this.migrationStateModel.currentState === State.TARGET_SELECTION;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class WizardController {
|
|||||||
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
|
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
|
||||||
const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide');
|
const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide');
|
||||||
wizard.generateScriptButton.enabled = false;
|
wizard.generateScriptButton.enabled = false;
|
||||||
const sourceConfigurationPage = new SourceConfigurationPage(stateModel);
|
const sourceConfigurationPage = new SourceConfigurationPage(wizard, stateModel);
|
||||||
|
|
||||||
const pages: MigrationWizardPage[] = [sourceConfigurationPage];
|
const pages: MigrationWizardPage[] = [sourceConfigurationPage];
|
||||||
|
|
||||||
@@ -42,6 +42,14 @@ export class WizardController {
|
|||||||
await pages[newPage]?.onPageEnter();
|
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 Promise.all(wizardSetupPromises);
|
||||||
await pages[0].onPageEnter();
|
await pages[0].onPageEnter();
|
||||||
|
|||||||
4
src/sql/azdata.d.ts
vendored
4
src/sql/azdata.d.ts
vendored
@@ -2733,8 +2733,8 @@ declare module 'azdata' {
|
|||||||
focus(): Thenable<void>;
|
focus(): Thenable<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormComponent {
|
export interface FormComponent<T extends Component = Component> {
|
||||||
component: Component;
|
component: T;
|
||||||
title: string;
|
title: string;
|
||||||
actions?: Component[];
|
actions?: Component[];
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user