diff --git a/extensions/mssql/src/constants.ts b/extensions/mssql/src/constants.ts index c43f1ea4f7..f415329b73 100644 --- a/extensions/mssql/src/constants.ts +++ b/extensions/mssql/src/constants.ts @@ -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 { diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 2dd681ebf9..2d0bdce4d3 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1001,3 +1001,19 @@ export namespace ProfilerSessionCreatedNotification { } // ------------------------------- < SQL Profiler > ------------------------------------ + +/// ------------------------------- ----------------------------- + +export interface SqlAssessmentResult extends azdata.ResultStatus { + items: mssql.SqlMigrationAssessmentResultItem[]; +} + +export interface SqlMigrationAssessmentParams { + ownerUri: string; +} + +export namespace GetSqlMigrationAssessmentItemsRequest { + export const type = new RequestType('migration/getassessments'); +} + +// ------------------------------- ----------------------------- diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index e9a3e19ebe..66f10b325c 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -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; generateAssessmentScript(items: azdata.SqlAssessmentResultItem[], targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Promise; } + + +/** + * 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; +} diff --git a/extensions/mssql/src/mssqlApiFactory.ts b/extensions/mssql/src/mssqlApiFactory.ts index 8402eab20c..43ce8d6b8a 100644 --- a/extensions/mssql/src/mssqlApiFactory.ts +++ b/extensions/mssql/src/mssqlApiFactory.ts @@ -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(constants.SqlAssessmentService); + }, + get sqlMigration() { + return context.getService(constants.SqlMigrationService); } }; } diff --git a/extensions/mssql/src/sqlMigration/sqlMigrationService.ts b/extensions/mssql/src/sqlMigration/sqlMigrationService.ts new file mode 100644 index 0000000000..4051fc610f --- /dev/null +++ b/extensions/mssql/src/sqlMigration/sqlMigrationService.ts @@ -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 { + 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; + } +} diff --git a/extensions/mssql/src/sqlToolsServer.ts b/extensions/mssql/src/sqlToolsServer.ts index 02c55a437e..5f3cbf8041 100644 --- a/extensions/mssql/src/sqlToolsServer.ts +++ b/extensions/mssql/src/sqlToolsServer.ts @@ -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() }; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 360f19bbb7..66c9297fc6 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 * 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; } diff --git a/extensions/sql-migration/src/wizard/assessmentResultsDialog.ts b/extensions/sql-migration/src/wizard/assessmentResultsDialog.ts new file mode 100644 index 0000000000..bfec36bdcd --- /dev/null +++ b/extensions/sql-migration/src/wizard/assessmentResultsDialog.ts @@ -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 = new vscode.EventEmitter(); + protected _isOpen: boolean = false; + // public readonly onSuccess: vscode.Event = 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 { + return undefined; + } + + protected async initializeDialog(dialog: azdata.window.Dialog): Promise { + 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[] { + let result: Array[] = []; + 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[], + 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; + } +} diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index c3c1365498..ad663ae80a 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -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({ + 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 }); }); diff --git a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts index a89657f146..194b169e33 100644 --- a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts +++ b/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts @@ -32,12 +32,16 @@ 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; component.value = COLLECTING_SOURCE_CONFIGURATIONS_ERROR(this.migrationStateModel.gatheringInformationError); diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 94e5f5fda1..f30a45b89e 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -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,10 +18,12 @@ export class WizardController { } public async openWizard(profile: azdata.connection.Connection): Promise { - const stateModel = new MigrationStateModel(this.extensionContext, profile); - this.extensionContext.subscriptions.push(stateModel); - - this.createWizard(stateModel); + 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 {