Add migration service calls to migration wizard (#12090)

* Hook up migration apis WIP

* WIP 2

* WIP 3

* Fix build breaks

* Break 2

* WIP 4

* SKU recommendation page work

* WIP 5

* A few minor fixes

* Merge main

* Add back assessment dialog

* Address feedback

Co-authored-by: Amir Omidi <amomidi@microsoft.com>
This commit is contained in:
Karl Burtram
2020-09-02 16:15:39 -07:00
committed by GitHub
parent 65e253ae0d
commit c5e90d0236
11 changed files with 290 additions and 15 deletions

View File

@@ -41,6 +41,7 @@ export const SchemaCompareService = 'schemaCompareService';
export const LanguageExtensionService = 'languageExtensionService';
export const objectExplorerPrefix: string = 'objectexplorer://';
export const SqlAssessmentService = 'sqlAssessmentService';
export const SqlMigrationService = 'sqlMigrationService';
export const NotebookConvertService = 'notebookConvertService';
export enum BuiltInCommands {

View File

@@ -1001,3 +1001,19 @@ export namespace ProfilerSessionCreatedNotification {
}
// ------------------------------- < SQL Profiler > ------------------------------------
/// ------------------------------- <Sql Migration> -----------------------------
export interface SqlAssessmentResult extends azdata.ResultStatus {
items: mssql.SqlMigrationAssessmentResultItem[];
}
export interface SqlMigrationAssessmentParams {
ownerUri: string;
}
export namespace GetSqlMigrationAssessmentItemsRequest {
export const type = new RequestType<SqlAssessmentParams, SqlAssessmentResult, void, void>('migration/getassessments');
}
// ------------------------------- <Sql Migration> -----------------------------

View File

@@ -43,6 +43,8 @@ export interface IExtension {
readonly dacFx: IDacFxService;
readonly sqlAssessment: ISqlAssessmentService;
readonly sqlMigration: ISqlMigrationService;
}
/**
@@ -495,3 +497,43 @@ export interface ISqlAssessmentService {
getAssessmentItems(ownerUri: string, targetType: azdata.sqlAssessment.SqlAssessmentTargetType): Promise<azdata.SqlAssessmentResult>;
generateAssessmentScript(items: azdata.SqlAssessmentResultItem[], targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Promise<azdata.ResultStatus>;
}
/**
* Sql Migration
*/
// SqlMigration interfaces -----------------------------------------------------------------------
export interface SqlMigrationImpactedObjectInfo {
name: string;
impactDetail: string;
objectType: string;
}
export interface SqlMigrationAssessmentResultItem {
rulesetVersion: string;
rulesetName: string;
targetType: azdata.sqlAssessment.SqlAssessmentTargetType;
targetName: string;
checkId: string;
tags: string[];
displayName: string;
description: string;
helpLink: string;
level: string;
timestamp: string;
kind: azdata.sqlAssessment.SqlAssessmentResultItemKind;
message: string;
appliesToMigrationTargetPlatform: string;
issueCategory: string;
impactedObjects: SqlMigrationImpactedObjectInfo[];
}
export interface SqlMigrationAssessmentResult extends azdata.ResultStatus {
items: SqlMigrationAssessmentResultItem[];
}
export interface ISqlMigrationService {
getAssessments(ownerUri: string): Promise<SqlMigrationAssessmentResult | undefined>;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { AppContext } from './appContext';
import { IExtension, ICmsService, IDacFxService, ISchemaCompareService, MssqlObjectExplorerBrowser, ILanguageExtensionService, ISqlAssessmentService } from './mssql';
import { IExtension, ICmsService, IDacFxService, ISchemaCompareService, MssqlObjectExplorerBrowser, ILanguageExtensionService, ISqlAssessmentService, ISqlMigrationService } from './mssql';
import * as constants from './constants';
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
import * as azdata from 'azdata';
@@ -33,6 +33,9 @@ export function createMssqlApi(context: AppContext): IExtension {
},
get sqlAssessment() {
return context.getService<ISqlAssessmentService>(constants.SqlAssessmentService);
},
get sqlMigration() {
return context.getService<ISqlMigrationService>(constants.SqlMigrationService);
}
};
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as mssql from '../mssql';
import { AppContext } from '../appContext';
import { SqlOpsDataClient, ISqlOpsFeature } from 'dataprotocol-client';
import { ClientCapabilities } from 'vscode-languageclient';
import * as constants from '../constants';
import * as contracts from '../contracts';
export class SqlMigrationService implements mssql.ISqlMigrationService {
public static asFeature(context: AppContext): ISqlOpsFeature {
return class extends SqlMigrationService {
constructor(client: SqlOpsDataClient) {
super(context, client);
}
fillClientCapabilities(capabilities: ClientCapabilities): void {
}
initialize(): void {
}
};
}
private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) {
context.registerService(constants.SqlMigrationService, this);
}
async getAssessments(ownerUri: string): Promise<mssql.SqlMigrationAssessmentResult | undefined> {
let params: contracts.SqlMigrationAssessmentParams = { ownerUri: ownerUri };
try {
return this.client.sendRequest(contracts.GetSqlMigrationAssessmentItemsRequest.type, params);
}
catch (e) {
this.client.logFailedRequest(contracts.GetSqlMigrationAssessmentItemsRequest.type, e);
}
return undefined;
}
}

View File

@@ -24,6 +24,7 @@ import * as nls from 'vscode-nls';
import { LanguageExtensionService } from './languageExtension/languageExtensionService';
import { SqlAssessmentService } from './sqlAssessment/sqlAssessmentService';
import { NotebookConvertService } from './notebookConvert/notebookConvertService';
import { SqlMigrationService } from './sqlMigration/sqlMigrationService';
const localize = nls.loadMessageBundle();
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
@@ -163,7 +164,8 @@ function getClientOptions(context: AppContext): ClientOptions {
CmsService.asFeature(context),
SqlAssessmentService.asFeature(context),
NotebookConvertService.asFeature(context),
ProfilerFeature
ProfilerFeature,
SqlMigrationService.asFeature(context),
],
outputChannel: new CustomOutputChannel()
};

View File

@@ -5,6 +5,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as mssql from '../../../mssql';
import { SKURecommendations } from './externalContract';
export enum State {
@@ -42,10 +43,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
private _currentState: State;
private _gatheringInformationError: string | undefined;
private _skuRecommendations: SKURecommendations | undefined;
private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined;
constructor(
private readonly _extensionContext: vscode.ExtensionContext,
private readonly _sourceConnection: azdata.connection.Connection
private readonly _sourceConnection: azdata.connection.Connection,
public readonly migrationService: mssql.ISqlMigrationService
) {
this._currentState = State.INIT;
}
@@ -66,6 +69,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._stateChangeEventEmitter.fire({ oldState, newState: this.currentState });
}
public get assessmentResults(): mssql.SqlMigrationAssessmentResultItem[] | undefined {
return this._assessmentResults;
}
public set assessmentResults(assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined) {
this._assessmentResults = assessmentResults;
}
public get gatheringInformationError(): string | undefined {
return this._gatheringInformationError;
}

View File

@@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* 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 * as mssql from '../../../mssql';
import { MigrationStateModel } from '../models/stateMachine';
export class AssessmentResultsDialog {
private static readonly OkButtonText: string = 'OK';
private static readonly CancelButtonText: string = 'Cancel';
// protected _onSuccess: vscode.EventEmitter<T> = new vscode.EventEmitter<T>();
protected _isOpen: boolean = false;
// public readonly onSuccess: vscode.Event<T> = this._onSuccess.event;
public dialog: azdata.window.Dialog | undefined;
private assessmentTable: azdata.TableComponent | undefined;
// Dialog Name for Telemetry
public dialogName: string | undefined;
constructor(public ownerUri: string, public model: MigrationStateModel, public title: string) {
}
protected async updateModel(): Promise<void> {
return undefined;
}
protected async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
dialog.registerContent(async view => {
this.assessmentTable = view.modelBuilder.table()
.withProperties({
columns: [
'Target',
'Target Name',
'Rule ID',
'Rule Name',
'Description',
'Impacted Objects'
],
data: [],
height: 700,
width: 1100
}).component();
let formModel = view.modelBuilder.formContainer()
.withFormItems([
{
components: [{
component: this.assessmentTable,
title: 'Results',
layout: {
info: 'Assessment Results'
}
}],
title: 'Assessment Results'
}]).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel);
let data = this.convertAssessmentToData(this.model.assessmentResults);
this.assessmentTable.data = data;
});
}
private convertAssessmentToData(assessments: mssql.SqlMigrationAssessmentResultItem[] | undefined): Array<string | number>[] {
let result: Array<string | number>[] = [];
if (assessments) {
assessments.forEach(assessment => {
if (assessment.impactedObjects && assessment.impactedObjects.length > 0) {
assessment.impactedObjects.forEach(impactedObject => {
this.addAssessmentColumn(result, assessment, impactedObject);
});
} else {
this.addAssessmentColumn(result, assessment, undefined);
}
});
}
return result;
}
private addAssessmentColumn(
result: Array<string | number>[],
assessment: mssql.SqlMigrationAssessmentResultItem,
impactedObject: mssql.SqlMigrationImpactedObjectInfo | undefined): void {
let cols = [];
cols.push(assessment.appliesToMigrationTargetPlatform);
cols.push(assessment.displayName);
cols.push(assessment.checkId);
cols.push(assessment.rulesetName);
cols.push(assessment.description);
cols.push(impactedObject?.name ?? '');
result.push(cols);
}
public async openDialog(dialogName?: string) {
if (!this._isOpen) {
this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(this.title, this.title, true);
// await this.model.initialize();
await this.initializeDialog(this.dialog);
this.dialog.okButton.label = AssessmentResultsDialog.OkButtonText;
this.dialog.okButton.onClick(async () => await this.execute());
this.dialog.cancelButton.label = AssessmentResultsDialog.CancelButtonText;
this.dialog.cancelButton.onClick(async () => await this.cancel());
azdata.window.openDialog(this.dialog);
}
}
protected async execute() {
this.updateModel();
// await this.model.save();
this._isOpen = false;
// this._onSuccess.fire(this.model);
}
protected async cancel() {
this._isOpen = false;
}
public get isOpen(): boolean {
return this._isOpen;
}
}

View File

@@ -10,6 +10,7 @@ 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 './assessmentResultsDialog';
export class SKURecommendationPage extends MigrationWizardPage {
// For future reference: DO NOT EXPOSE WIZARD DIRECTLY THROUGH HERE.
@@ -30,14 +31,30 @@ export class SKURecommendationPage extends MigrationWizardPage {
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);
const assessmentLink = view.modelBuilder.hyperlink()
.withProperties<azdata.HyperlinkComponentProperties>({
label: 'View Assessment Results',
url: ''
}).component();
assessmentLink.onDidClick(async () => {
let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog');
dialog.openDialog();
});
const assessmentFormLink = {
title: '',
component: assessmentLink,
};
this.view = view;
const form = view.modelBuilder.formContainer().withFormItems(
[
this.igComponent,
this.detailsComponent,
this.chooseTargetComponent
this.chooseTargetComponent,
assessmentFormLink
]
);
@@ -95,7 +112,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
rbg.component().cards.push({
id: product.name,
icon: imagePath,
label: 'Some Label'
label: product.name
});
});

View File

@@ -32,11 +32,15 @@ export class SourceConfigurationPage extends MigrationWizardPage {
).component();
await view.initializeModel(form);
let connectionUri: string = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnection.connectionId);
this.migrationStateModel.migrationService.getAssessments(connectionUri).then(results => {
if (results) {
this.migrationStateModel.assessmentResults = results.items;
this.migrationStateModel.currentState = State.TARGET_SELECTION;
}
});
}
// private async createInformationGatheredPage(view: azdata.ModelView){
// }
private async enterErrorState() {
const component = this.gatheringInfoComponent.component as azdata.TextComponent;

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
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';
@@ -17,11 +18,13 @@ export class WizardController {
}
public async openWizard(profile: azdata.connection.Connection): Promise<void> {
const stateModel = new MigrationStateModel(this.extensionContext, profile);
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
if (api) {
const stateModel = new MigrationStateModel(this.extensionContext, profile, api.sqlMigration);
this.extensionContext.subscriptions.push(stateModel);
this.createWizard(stateModel);
}
}
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide');