diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 4cd1ba8577..f2e6608c1a 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -12,7 +12,8 @@ "azdata": ">=1.19.0" }, "activationEvents": [ - "onCommand:sqlmigration.start" + "onCommand:sqlmigration.start", + "onCommand:sqlmigration.testDialog" ], "main": "./out/main", "repository": { @@ -28,6 +29,11 @@ "command": "sqlmigration.start", "title": "SQL Migration Start", "category": "SQL Migration" + }, + { + "command": "sqlmigration.testDialog", + "title": "SQL Migration test dialog", + "category": "SQL Migration" } ] }, diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts new file mode 100644 index 0000000000..bc6a7021d3 --- /dev/null +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MigrationStateModel } from '../../models/stateMachine'; +import { SqlDatabaseTree } from './sqlDatabasesTree'; +import { SqlAssessmentResultList } from './sqlAssessmentResultsList'; +import { SqlAssessmentResult } from './sqlAssessmentResult'; + + +export class AssessmentResultsDialog { + + private static readonly OkButtonText: string = 'OK'; + private static readonly CancelButtonText: string = 'Cancel'; + + private _isOpen: boolean = false; + private dialog: azdata.window.Dialog | undefined; + + // Dialog Name for Telemetry + public dialogName: string | undefined; + + private _tree: SqlDatabaseTree; + private _list: SqlAssessmentResultList; + private _result: SqlAssessmentResult; + + constructor(public ownerUri: string, public model: MigrationStateModel, public title: string) { + this._tree = new SqlDatabaseTree(); + this._list = new SqlAssessmentResultList(); + this._result = new SqlAssessmentResult(); + } + + private async initializeDialog(dialog: azdata.window.Dialog): Promise { + return new Promise((resolve, reject) => { + dialog.registerContent(async (view) => { + try { + const treeComponent = await this._tree.createComponent(view); + const separator1 = view.modelBuilder.separator().component(); + const listComponent = await this._list.createComponent(view); + const separator2 = view.modelBuilder.separator().component(); + const resultComponent = await this._result.createComponent(view); + + const flex = view.modelBuilder.flexContainer().withItems([treeComponent, separator1, listComponent, separator2, resultComponent]); + + view.initializeModel(flex.component()); + resolve(); + } catch (ex) { + reject(ex); + } + }); + }); + } + + public async openDialog(dialogName?: string) { + if (!this._isOpen) { + this._isOpen = true; + this.dialog = azdata.window.createModelViewDialog(this.title, this.title, true); + + 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()); + + const dialogSetupPromises: Thenable[] = []; + + dialogSetupPromises.push(this.initializeDialog(this.dialog)); + azdata.window.openDialog(this.dialog); + + await Promise.all(dialogSetupPromises); + } + } + + protected async execute() { + this._isOpen = false; + } + + protected async cancel() { + this._isOpen = false; + } + + + public get isOpen(): boolean { + return this._isOpen; + } +} diff --git a/extensions/sql-migration/src/dialog/assessmentResults/model/assessmentDialogComponent.ts b/extensions/sql-migration/src/dialog/assessmentResults/model/assessmentDialogComponent.ts new file mode 100644 index 0000000000..a1e9878519 --- /dev/null +++ b/extensions/sql-migration/src/dialog/assessmentResults/model/assessmentDialogComponent.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export abstract class AssessmentDialogComponent { + + abstract async createComponent(view: azdata.ModelView): Promise; +} diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts new file mode 100644 index 0000000000..9250ebf733 --- /dev/null +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AssessmentDialogComponent } from './model/assessmentDialogComponent'; + +export class SqlAssessmentResult extends AssessmentDialogComponent { + async createComponent(view: azdata.ModelView): Promise { + + const title = this.createTitleComponent(view); + const impact = this.createImpactComponent(view); + const recommendation = this.createRecommendationComponent(view); + const moreInfo = this.createMoreInfoComponent(view); + const impactedObjects = this.createImpactedObjectsComponent(view); + + return view.modelBuilder.divContainer().withItems([title, impact, recommendation, moreInfo, impactedObjects]).component(); + } + + private createTitleComponent(view: azdata.ModelView): azdata.TextComponent { + const title = view.modelBuilder.text().withProperties({ + value: 'Azure SQL Managed Instance does not support multiple log files', // TODO: Get this string from the actual results + }); + + return title.component(); + } + + private createImpactComponent(view: azdata.ModelView): azdata.TextComponent { + const impact = view.modelBuilder.text().withProperties({ + title: 'Impact', // TODO localize + value: 'SQL Server allows a database to log transactions across multiple files. This databases uses multiple log files' // TODO: Get this string from the actual results + }); + + return impact.component(); + } + + private createRecommendationComponent(view: azdata.ModelView): azdata.TextComponent { + const recommendation = view.modelBuilder.text().withProperties({ + title: 'Recommendation', // TODO localize + value: 'Azure SQL Managed Instance allows a single log file per database only. Please delete all but one of the log files before migrating this database.' // TODO: Get this string from the actual results + }); + + return recommendation.component(); + } + + private createMoreInfoComponent(view: azdata.ModelView): azdata.TextComponent { + const moreInfo = view.modelBuilder.text().withProperties({ + title: 'More info', // TODO localize + value: '{0}', + links: [ + { + text: 'Managed instance T-SQL differences - Azure SQL Database', // TODO: Get this string from the actual results + url: 'https://microsoft.com' // TODO: Get this string from the actual results + } + ] + }); + + return moreInfo.component(); + } + + private createImpactedObjectsComponent(view: azdata.ModelView): azdata.TableComponent { + const impactedObjects = view.modelBuilder.table().withProperties({ + title: 'Impacted Objects', + columns: [ + 'Type', // TODO localize + 'Name', + ], + data: [ + ['Database', 'AAAW2008P7'] // TODO: Get this string from the actual results + ] + }); + + return impactedObjects.component(); + } +} diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts new file mode 100644 index 0000000000..368c3dd3ca --- /dev/null +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AssessmentDialogComponent } from './model/assessmentDialogComponent'; + +export class SqlAssessmentResultList extends AssessmentDialogComponent { + async createComponent(view: azdata.ModelView): Promise { + + return view.modelBuilder.divContainer().withItems([ + this.createListComponent(view) + ] + ).component(); + } + + private createListComponent(view: azdata.ModelView): azdata.ListBoxComponent { + const list = view.modelBuilder.listBox().withProperties({ + values: [ + 'Filestream not supported in Azure SQL Managed Instance', + 'Number of Log files per database something something', + ] + }); + + return list.component(); + } +} diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts new file mode 100644 index 0000000000..8405c4cb20 --- /dev/null +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AssessmentDialogComponent } from './model/assessmentDialogComponent'; + +export class SqlDatabaseTree extends AssessmentDialogComponent { + async createComponent(view: azdata.ModelView): Promise { + + return view.modelBuilder.divContainer().withItems([ + this.createTableComponent(view) + ] + ).component(); + } + + private createTableComponent(view: azdata.ModelView): azdata.DeclarativeTableComponent { + + const table = view.modelBuilder.declarativeTable().withProperties( + { + columns: [ + { + displayName: 'Database', // TODO localize + valueType: azdata.DeclarativeDataType.string, + width: 50, + isReadOnly: true, + showCheckAll: true + }, + { + displayName: '', // Incidents + valueType: azdata.DeclarativeDataType.string, + width: 5, + isReadOnly: true, + showCheckAll: false + } + ], + data: [ + ['DB1', '1'], + ['DB2', '0'] + ], + width: '200px' + } + ); + + return table.component(); + } +} diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts index 4b884ae299..34949882df 100644 --- a/extensions/sql-migration/src/main.ts +++ b/extensions/sql-migration/src/main.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import { WizardController } from './wizard/wizardController'; +import { AssessmentResultsDialog } from './dialog/assessmentResults/assessmentResultsDialog'; class SQLMigration { @@ -22,8 +23,13 @@ class SQLMigration { const connection = await azdata.connection.openConnectionDialog(); const wizardController = new WizardController(this.context); - wizardController.openWizard(connection); + await wizardController.openWizard(connection); }), + + vscode.commands.registerCommand('sqlmigration.testDialog', async () => { + let dialog = new AssessmentResultsDialog('ownerUri', undefined!, 'Assessment Dialog'); + await dialog.openDialog(); + }) ]; this.context.subscriptions.push(...commandDisposables); diff --git a/extensions/sql-migration/src/wizard/assessmentResultsDialog.ts b/extensions/sql-migration/src/wizard/assessmentResultsDialog.ts deleted file mode 100644 index bfec36bdcd..0000000000 --- a/extensions/sql-migration/src/wizard/assessmentResultsDialog.ts +++ /dev/null @@ -1,133 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 afb47e108f..23b6055dff 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -10,7 +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'; +import { AssessmentResultsDialog } from '../dialog/assessmentResults/assessmentResultsDialog'; export class SKURecommendationPage extends MigrationWizardPage { // For future reference: DO NOT EXPOSE WIZARD DIRECTLY THROUGH HERE. @@ -40,7 +40,7 @@ export class SKURecommendationPage extends MigrationWizardPage { }).component(); assessmentLink.onDidClick(async () => { let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog'); - dialog.openDialog(); + await dialog.openDialog(); }); const assessmentFormLink = {