From dbb6b7190860df5bbdfed1226fcab5b6d4c7a475 Mon Sep 17 00:00:00 2001 From: Christopher Suh Date: Thu, 25 Feb 2021 16:05:09 -0500 Subject: [PATCH] Assessment page changes (#14415) * assessment page changes * code cleanup * remove dead code * fixed hardcoded value * fix instance table bug * Revert "fix instance table bug" This reverts commit a924f44e64062a427c9fe4b12c0f368e78e6c04f. * Revert "fixed hardcoded value" This reverts commit 75661c457b6161b03c823d783fa9db97431c563f. --- .../assessmentResultsDialog.ts | 8 +- .../assessmentResults/sqlDatabasesTree.ts | 669 ++++++++++++++++-- 2 files changed, 633 insertions(+), 44 deletions(-) diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index ed9d8821f1..eb08fc1f21 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -33,14 +33,14 @@ export class AssessmentResultsDialog { constructor(public ownerUri: string, public model: MigrationStateModel, public title: string) { this._model = model; let assessmentData = this.parseData(this._model); - this._tree = new SqlDatabaseTree(assessmentData); + this._tree = new SqlDatabaseTree(this._model, assessmentData); } private async initializeDialog(dialog: azdata.window.Dialog): Promise { return new Promise((resolve, reject) => { dialog.registerContent(async (view) => { try { - // const resultComponent = await this._tree.createComponentResult(view); + const resultComponent = await this._tree.createComponentResult(view); const treeComponent = await this._tree.createComponent(view); const flex = view.modelBuilder.flexContainer().withLayout({ @@ -53,7 +53,7 @@ export class AssessmentResultsDialog { } }).component(); flex.addItem(treeComponent, { flex: '0 0 auto' }); - // flex.addItem(resultComponent, { flex: '1 1 auto' }); + flex.addItem(resultComponent, { flex: '1 1 auto' }); view.initializeModel(flex); resolve(); @@ -125,7 +125,7 @@ export class AssessmentResultsDialog { } protected async execute() { - // this.model._migrationDbs = this._tree.selectedDbs(); + this.model._migrationDbs = this._tree.selectedDbs(); this._isOpen = false; } diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index bca2d4f1ac..7299e6065c 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -3,100 +3,689 @@ * 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'; +import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql'; +import { MigrationStateModel } from '../../models/stateMachine'; import { Issues } from './assessmentResultsDialog'; +import { AssessmentDialogComponent } from './model/assessmentDialogComponent'; +type DbIssues = { + name: string, + issues: Issues[] +}; export class SqlDatabaseTree extends AssessmentDialogComponent { - // private _assessmentData: Map; + private _model!: MigrationStateModel; + private instanceTable!: azdata.ComponentBuilder; + private databaseTable!: azdata.ComponentBuilder; + private _assessmentResultsTable!: azdata.ComponentBuilder; + private _impactedObjectsTable!: azdata.ComponentBuilder; + private _assessmentData: Map; - constructor(assessmentData: Map) { + private _recommendation!: azdata.TextComponent; + private _dbName!: azdata.TextComponent; + private _recommendationText!: azdata.TextComponent; + private _descriptionText!: azdata.TextComponent; + private _issues!: Issues; + private _impactedObjects!: SqlMigrationImpactedObjectInfo[]; + private _objectDetailsType!: azdata.TextComponent; + private _objectDetailsName!: azdata.TextComponent; + private _objectDetailsSample!: azdata.TextComponent; + private _moreInfo!: azdata.TextComponent; + + constructor(model: MigrationStateModel, assessmentData: Map) { super(); - // this._assessmentData = assessmentData; + this._assessmentData = assessmentData; + this._model = model; } async createComponent(view: azdata.ModelView): Promise { + const component = view.modelBuilder.flexContainer().withLayout({ + height: '100%', + flexFlow: 'column' + }).withProps({ + CSSStyles: { + 'border-right': 'solid 1px' + }, + }).component(); - return view.modelBuilder.divContainer().withItems([ - this.createTableComponent(view) - ] - ).component(); + component.addItem(this.createSearchComponent(view), { flex: '0 0 auto' }); + component.addItem(this.createInstanceComponent(view), { flex: '0 0 auto' }); + component.addItem(await this.createDatabaseComponent(view), { flex: '1 1 auto' }); + return component; } - private createTableComponent(view: azdata.ModelView): azdata.DeclarativeTableComponent { + private async createDatabaseComponent(view: azdata.ModelView): Promise { - const style: azdata.CssStyles = { + let mapRowIssue = new Map(); + const styleLeft: azdata.CssStyles = { 'border': 'none', - 'text-align': 'left' + 'text-align': 'left', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden' + }; + const styleRight: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'right', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden' }; - const table = view.modelBuilder.declarativeTable().withProps( + this.databaseTable = view.modelBuilder.declarativeTable().withProps( { selectEffect: true, + width: '350px', + CSSStyles: { + 'table-layout': 'fixed' + }, columns: [ { displayName: '', valueType: azdata.DeclarativeDataType.boolean, - width: 5, + width: '10%', isReadOnly: false, showCheckAll: true, - headerCssStyles: style, + headerCssStyles: styleLeft, ariaLabel: 'Database Migration Check' // TODO localize }, { - displayName: 'Database', // TODO localize + displayName: 'Databases', // TODO localize valueType: azdata.DeclarativeDataType.string, - width: 50, + width: '80%', isReadOnly: true, - headerCssStyles: style + headerCssStyles: styleLeft }, { - displayName: '', // Incidents + displayName: 'Issues', // Incidents + valueType: azdata.DeclarativeDataType.string, + width: '10%', + isReadOnly: true, + headerCssStyles: styleRight, + ariaLabel: 'Issue Count' // TODO localize + + } + ], + dataValues: [ + ] + } + ); + + let dbList = await azdata.connection.listDatabases(this._model.sourceConnectionId); + + if (dbList.length > 0) { + let rowNumber = 0; + this._assessmentData.forEach((value, key) => { + this.databaseTable.component().dataValues?.push( + [ + { + value: false, + style: styleLeft + }, + { + value: key, + style: styleLeft + }, + { + value: value.length, + style: styleRight + } + ] + + ); + let dbIssues = { + name: key, + issues: value + }; + mapRowIssue.set(rowNumber, dbIssues); + dbList = dbList.filter(obj => obj !== key); + + rowNumber = rowNumber + 1; + }); + + dbList.forEach((value) => { + this.databaseTable.component().dataValues?.push( + [ + { + value: false, + style: styleLeft + }, + { + value: value, + style: styleLeft + }, + { + value: 0, + style: styleRight + } + ] + + ); + let impactedObjects: SqlMigrationImpactedObjectInfo[] = []; + let issue: Issues[] = [{ + description: 'No Issues', + recommendation: 'No Issues', + moreInfo: 'No Issues', + impactedObjects: impactedObjects, + rowNumber: rowNumber + }]; + let noIssues = { + name: value, + issues: issue + }; + mapRowIssue.set(rowNumber, noIssues); + rowNumber = rowNumber + 1; + }); + } + + this.databaseTable.component().onRowSelected(({ row }) => { + const rowInfo = mapRowIssue.get(row); + if (rowInfo) { + this._assessmentResultsTable.component().dataValues = []; + this._dbName.value = rowInfo.name; + this._recommendation.value = `Assessment Results (${rowInfo.issues.length} issues found)`; + // Need some kind of refresh method for declarative tables + let dataValues: string[][] = []; + rowInfo.issues.forEach(async (issue) => { + dataValues.push([ + issue.description + ]); + + }); + + this._assessmentResultsTable.component().updateProperties({ + data: dataValues + }); + + } + + }); + + + const tableContainer = view.modelBuilder.divContainer().withItems([this.databaseTable.component()]).withProps({ + CSSStyles: { + 'margin-left': '15px', + }, + }).component(); + return tableContainer; + } + + private createSearchComponent(view: azdata.ModelView): azdata.DivContainer { + let resourceSearchBox = view.modelBuilder.inputBox().withProperties({ + placeHolder: 'Search', + ariaLabel: 'searchbar' + }).component(); + + const searchContainer = view.modelBuilder.divContainer().withItems([resourceSearchBox]).withProps({ + CSSStyles: { + 'width': '200px', + 'margin-left': '15px', + 'margin-right': '5px' + + }, + }).component(); + + return searchContainer; + } + + private createInstanceComponent(view: azdata.ModelView): azdata.DivContainer { + const styleLeft: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left' + }; + + const styleRight: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'right' + }; + + this.instanceTable = view.modelBuilder.declarativeTable().withProps( + { + selectEffect: true, + width: '100%', + columns: [ + { + displayName: 'Instance', valueType: azdata.DeclarativeDataType.string, width: 5, isReadOnly: true, - headerCssStyles: style, - ariaLabel: 'Issue Count' // TODO localize + headerCssStyles: styleLeft, + ariaLabel: 'Database Migration Check' // TODO localize + }, + { + displayName: 'Warnings', // TODO localize + valueType: azdata.DeclarativeDataType.string, + width: 1, + isReadOnly: true, + headerCssStyles: styleRight } ], dataValues: [ [ { - value: false, - style + value: 'SQL Server 1', + style: styleLeft }, { - value: 'DB1', - style + value: 2, + style: styleRight + } + ] + ] + }); + + const instanceContainer = view.modelBuilder.divContainer().withItems([this.instanceTable.component()]).withProps({ + CSSStyles: { + 'margin-left': '15px', + }, + }).component(); + + return instanceContainer; + } + + async createComponentResult(view: azdata.ModelView): Promise { + + const topContainer = this.createTopContainer(view); + const bottomContainer = this.createBottomContainer(view); + + const container = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + height: '100%' + }).withProps({ + CSSStyles: { + 'margin-left': '10px' + } + }).component(); + + container.addItem(topContainer, { flex: '0 0 auto' }); + container.addItem(bottomContainer, { flex: '1 1 auto' }); + + return container; + } + + + private createTopContainer(view: azdata.ModelView): azdata.FlexContainer { + const title = this.createTitleComponent(view); + const impact = this.createPlatformComponent(view); + const recommendation = this.createRecommendationComponent(view); + const assessmentResults = this.createAssessmentResultsTitle(view); + + const container = view.modelBuilder.flexContainer().withItems([title, impact, recommendation, assessmentResults]).withLayout({ + flexFlow: 'column' + }).component(); + + return container; + } + + private createBottomContainer(view: azdata.ModelView): azdata.FlexContainer { + + const impactedObjects = this.createImpactedObjectsTable(view); + const rightContainer = this.createAssessmentContainer(view); + + const container = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + height: '100%' + }).withProps({ + CSSStyles: { + 'height': '100%' + } + }).component(); + + container.addItem(impactedObjects, { flex: '0 0 auto' }); + container.addItem(rightContainer, { flex: '1 1 auto' }); + return container; + } + + private createAssessmentContainer(view: azdata.ModelView): azdata.FlexContainer { + const title = this.createAssessmentTitle(view); + + const bottomContainer = this.createDescriptionContainer(view); + + + const container = view.modelBuilder.flexContainer().withItems([title, bottomContainer]).withLayout({ + flexFlow: 'column' + }).withProps({ + CSSStyles: { + 'margin-left': '10px' + } + }).component(); + + return container; + } + + private createDescriptionContainer(view: azdata.ModelView): azdata.FlexContainer { + const description = this.createDescription(view); + const impactedObjects = this.createImpactedObjectsDescription(view); + + + const container = view.modelBuilder.flexContainer().withItems([description, impactedObjects]).withLayout({ + flexFlow: 'row' + }).component(); + + return container; + } + + private createImpactedObjectsDescription(view: azdata.ModelView): azdata.FlexContainer { + const impactedObjectsTitle = view.modelBuilder.text().withProperties({ + value: 'Impacted Objects', + CSSStyles: { + 'font-size': '14px' + } + }).component(); + + const headerStyle: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left' + }; + const rowStyle: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left' + }; + + this._impactedObjectsTable = view.modelBuilder.declarativeTable().withProps( + { + selectEffect: true, + width: '100%', + columns: [ + { + displayName: 'Type', // TODO localize + valueType: azdata.DeclarativeDataType.string, + width: '100%', + isReadOnly: true, + headerCssStyles: headerStyle, + rowCssStyles: rowStyle + }, + { + displayName: 'Name', // TODO localize + valueType: azdata.DeclarativeDataType.string, + width: '100%', + isReadOnly: true, + headerCssStyles: headerStyle, + rowCssStyles: rowStyle + }, + ], + dataValues: [ + [ + { + value: 'Agent Job' }, { - value: 1, - style + value: 'Process Monthly Usage' + } + ] + ] + } + ); + + + this._impactedObjectsTable.component().onRowSelected(({ row }) => { + if (this._dbName.value) { + this._impactedObjects = this._issues.impactedObjects; + } + this._objectDetailsType.value = `Type: ${this._impactedObjects[row].objectType!}`; + this._objectDetailsName.value = `Name: ${this._impactedObjects[row].name}`; + this._objectDetailsSample.value = this._impactedObjects[row].impactDetail; + + + }); + + + + + const objectDetailsTitle = view.modelBuilder.text().withProperties({ + value: 'Object details', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + + this._objectDetailsType = view.modelBuilder.text().withProperties({ + value: 'Type:', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + + this._objectDetailsName = view.modelBuilder.text().withProperties({ + value: 'Name:', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + + this._objectDetailsSample = view.modelBuilder.text().withProperties({ + value: 'Sample', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + + const container = view.modelBuilder.flexContainer().withItems([impactedObjectsTitle, this._impactedObjectsTable.component(), objectDetailsTitle, this._objectDetailsType, this._objectDetailsName, this._objectDetailsSample]).withLayout({ + flexFlow: 'column' + }).component(); + + return container; + } + + private createDescription(view: azdata.ModelView): azdata.FlexContainer { + const descriptionTitle = view.modelBuilder.text().withProperties({ + value: 'Description', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + this._descriptionText = view.modelBuilder.text().withProperties({ + value: 'It is a job step that runs a PowerShell scripts.', + CSSStyles: { + 'font-size': '12px' + } + }).component(); + + const recommendationTitle = view.modelBuilder.text().withProperties({ + value: 'Recommendation', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + this._recommendationText = view.modelBuilder.text().withProperties({ + value: '', + CSSStyles: { + 'font-size': '12px', + 'width': '250px' + } + }).component(); + const moreInfo = view.modelBuilder.text().withProperties({ + value: 'More Info', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + this._moreInfo = view.modelBuilder.text().withProperties({ + value: '', + CSSStyles: { + 'font-size': '12px', + 'width': '250px' + } + }).component(); + + + const container = view.modelBuilder.flexContainer().withItems([descriptionTitle, this._descriptionText, recommendationTitle, this._recommendationText, moreInfo, this._moreInfo]).withLayout({ + flexFlow: 'column' + }).component(); + + return container; + } + + + private createAssessmentTitle(view: azdata.ModelView): azdata.TextComponent { + const title = view.modelBuilder.text().withProperties({ + value: '', + CSSStyles: { + 'font-size': '14px', + 'border-bottom': 'solid 1px' + } + }); + + return title.component(); + } + + private createTitleComponent(view: azdata.ModelView): azdata.TextComponent { + const title = view.modelBuilder.text().withProperties({ + value: 'Target Platform', + CSSStyles: { + 'font-size': '14px', + 'margin-block-start': '0px', + 'margin-block-end': '2px' + } + }); + + return title.component(); + } + + private createPlatformComponent(view: azdata.ModelView): azdata.TextComponent { + const impact = view.modelBuilder.text().withProperties({ + title: 'Platform', // TODO localize + value: 'Azure SQL Managed Instance', + CSSStyles: { + 'font-size': '18px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }); + + return impact.component(); + } + + private createRecommendationComponent(view: azdata.ModelView): azdata.TextComponent { + this._dbName = view.modelBuilder.text().withProperties({ + title: 'Recommendation', // TODO localize + value: 'SQL Server 1', + CSSStyles: { + 'font-size': '14px', + 'font-weight': 'bold' + } + }).component(); + + return this._dbName; + } + + private createAssessmentResultsTitle(view: azdata.ModelView): azdata.TextComponent { + this._recommendation = view.modelBuilder.text().withProperties({ + title: 'Recommendation', // TODO localize + value: 'Assessment Results', + CSSStyles: { + 'font-size': '14px', + 'font-weight': 'bold', + 'border-bottom': 'solid 1px', + 'margin-block-start': '0px', + 'margin-block-end': '0px' + } + }).component(); + + return this._recommendation; + } + + + private createImpactedObjectsTable(view: azdata.ModelView): azdata.DeclarativeTableComponent { + + const headerStyle: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left' + }; + const rowStyle: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'width': '200px', + 'overflow': 'hidden' + }; + + this._assessmentResultsTable = view.modelBuilder.declarativeTable().withProps( + { + selectEffect: true, + width: '200px', + CSSStyles: { + 'table-layout': 'fixed' + }, + columns: [ + { + displayName: '', // TODO localize + valueType: azdata.DeclarativeDataType.string, + width: '100%', + isReadOnly: true, + headerCssStyles: headerStyle, + rowCssStyles: rowStyle + } + ], + dataValues: [ + [ + { + value: 'DB1 Assessment results' } ], [ { - value: true, - style - }, - { - value: 'DB2', - style - }, - { - value: 2, - style + value: 'DB2 Assessment results' } ] - ], + ] } ); - table.component().onRowSelected(({ row }) => { - console.log(row); + this._assessmentResultsTable.component().onRowSelected(({ row }) => { + this._descriptionText.value = this._assessmentResultsTable.component().data![row][0]; + + if (this._dbName.value) { + this._issues = this._assessmentData.get(this._dbName.value)![row]; + this._moreInfo.value = this._issues.moreInfo; + this._impactedObjects = this._issues.impactedObjects; + let data: { value: string; }[][] = []; + this._impactedObjects.forEach(async (impactedObject) => { + data.push([ + { + value: impactedObject.objectType + }, + { + value: impactedObject.name + } + + ]); + }); + + this._impactedObjectsTable.component().updateProperties({ + dataValues: data + }); + } }); - return table.component(); + return this._assessmentResultsTable.component(); + } + + public selectedDbs(): string[] { + let result: string[] = []; + + this.databaseTable.component().dataValues?.forEach((arr) => { + if (arr[0].value === true) { + result.push(arr[1].value.toString()); + } + }); + + return result; } }