Splits the work of the assessment dialog into smaller managable chunks (#12172)

* Splits the work of the assessment dialog into smaller managable chunks

* Use the new assessment dialog page
This commit is contained in:
Amir Omidi
2020-09-08 17:22:23 -07:00
committed by GitHub
parent 9ed274fb39
commit f56e09cfa1
9 changed files with 264 additions and 137 deletions

View File

@@ -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<void> {
return new Promise<void>((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<void>[] = [];
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;
}
}

View File

@@ -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<azdata.Component>;
}

View File

@@ -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<azdata.Component> {
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<azdata.TextComponentProperties>({
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<azdata.TextComponentProperties>({
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<azdata.TextComponentProperties>({
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<azdata.TextComponentProperties>({
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<azdata.TableComponentProperties>({
title: 'Impacted Objects',
columns: [
'Type', // TODO localize
'Name',
],
data: [
['Database', 'AAAW2008P7'] // TODO: Get this string from the actual results
]
});
return impactedObjects.component();
}
}

View File

@@ -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<azdata.Component> {
return view.modelBuilder.divContainer().withItems([
this.createListComponent(view)
]
).component();
}
private createListComponent(view: azdata.ModelView): azdata.ListBoxComponent {
const list = view.modelBuilder.listBox().withProperties<azdata.ListBoxProperties>({
values: [
'Filestream not supported in Azure SQL Managed Instance',
'Number of Log files per database something something',
]
});
return list.component();
}
}

View File

@@ -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<azdata.Component> {
return view.modelBuilder.divContainer().withItems([
this.createTableComponent(view)
]
).component();
}
private createTableComponent(view: azdata.ModelView): azdata.DeclarativeTableComponent {
const table = view.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>(
{
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();
}
}

View File

@@ -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);

View File

@@ -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<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,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 = {