diff --git a/extensions/sql-migration/images/sqlDatabaseNotReady.svg b/extensions/sql-migration/images/sqlDatabaseNotReady.svg new file mode 100644 index 000000000000..4b840fc3a400 --- /dev/null +++ b/extensions/sql-migration/images/sqlDatabaseNotReady.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index 91f8cdf16ede..bebbf438d6bc 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -27,6 +27,7 @@ export class IconPathHelper { public static sqlServerLogo: IconPath; public static sqlDatabaseLogo: IconPath; public static sqlDatabaseWarningLogo: IconPath; + public static sqlDatabaseNotReadyLogo: IconPath; public static cancel: IconPath; public static warning: IconPath; public static info: IconPath; @@ -119,6 +120,10 @@ export class IconPathHelper { light: context.asAbsolutePath('images/sqlDatabaseWarning.svg'), dark: context.asAbsolutePath('images/sqlDatabaseWarning.svg') }; + IconPathHelper.sqlDatabaseNotReadyLogo = { + light: context.asAbsolutePath('images/sqlDatabaseNotReady.svg'), + dark: context.asAbsolutePath('images/sqlDatabaseNotReady.svg') + }; IconPathHelper.cancel = { light: context.asAbsolutePath('images/cancel.svg'), dark: context.asAbsolutePath('images/cancel.svg') diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 0aa97e7ed6b0..f0c7900290fe 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -99,6 +99,16 @@ export const ASSESSED_DBS_LABEL = localize('sql.migration.assessed.dbs.label', " export const NOT_READY = localize('sql.migration.not.ready', "Not ready"); export const READY = localize('sql.migration.ready', "Ready"); export const READY_WARN = localize('sql.migration.ready.warn', "Ready with warnings"); +export const DATABASE_ISSUES_SUMMARY = localize('sql.migration.database.issues.summary', "Database assessment issues summary"); +export const TOTAL_ISSUES_LABEL = localize('sql.migration.total.issues.label', "Total issues found"); +export const SEVERITY_ISSUES_LABEL = localize('sql.migration.severity.issues.label', "Issues by severity"); +export function DB_READINESS_SECTION_TITLE(dbName: string) { + return localize('sql.migration.db.readiness.section.title', "Database {0} migration readiness", dbName); +} +export function NON_READINESS_DESCRIPTION(issueCount: number) { + return localize('sql.migration.non.readiness.description', "The database is not ready to migrate due to {0} blocking issue.", issueCount); +} +export const READINESS_DESCRIPTION = localize('sql.migration.readiness.description', "The database is ready to migrate."); // Assessment results and recommendations export const ASSESSMENT_RESULTS_AND_RECOMMENDATIONS_PAGE_TITLE = localize('sql.migration.assessment.results.and.recommendations.title', "Assessment results and recommendations"); diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts index 02ba4e48119b..1004d738bde1 100644 --- a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts @@ -25,7 +25,7 @@ export class AssessmentDetailsPage extends MigrationWizardPage { azdata.window.createWizardPage(constants.ASSESSMENT_RESULTS_PAGE_TITLE), migrationStateModel); this._header = new AssessmentDetailsHeader(migrationStateModel); - this._body = new AssessmentDetailsBody(migrationStateModel); + this._body = new AssessmentDetailsBody(migrationStateModel, migrationStateModel._targetType); } // function to register Assessment details page content. @@ -62,7 +62,7 @@ export class AssessmentDetailsPage extends MigrationWizardPage { public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { await this._header.populateAssessmentDetailsHeader(); - await this._body._treeComponent.initialize(); + await this._body.populateAssessmentBody(); } public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetialsBody.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetialsBody.ts index b3bb1b46395f..7c95364d3d8d 100644 --- a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetialsBody.ts +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetialsBody.ts @@ -4,11 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import * as constants from '../../constants/strings'; +import * as styles from '../../constants/styles'; import { MigrationStateModel } from '../../models/stateMachine'; import { MigrationTargetType } from '../../api/utils'; import { TreeComponent } from './treeComponent'; import { InstanceSummary } from './instanceSummary'; +import { IconPathHelper } from '../../constants/iconPathHelper'; +import { DatabaseSummary } from './databaseSummary'; +import { IssueSummary } from './issueSummary'; +import { SqlMigrationAssessmentResultItem } from '../../service/contracts'; // Class that defines ui for body section of assessment result page export class AssessmentDetailsBody { @@ -16,11 +22,18 @@ export class AssessmentDetailsBody { private _model!: MigrationStateModel; public _treeComponent!: TreeComponent; private _targetType!: MigrationTargetType; - private _instanceSummary = new InstanceSummary(); + public _instanceSummary = new InstanceSummary(); + private _databaseSummary = new DatabaseSummary(); + private _issueSummary = new IssueSummary(); public _warningsOrIssuesListSection!: azdata.ListViewComponent; + private _findingsSummaryList!: azdata.ListViewComponent; + private _disposables: vscode.Disposable[] = []; + private _activeIssues!: SqlMigrationAssessmentResultItem[]; - constructor(migrationStateModel: MigrationStateModel) { + constructor(migrationStateModel: MigrationStateModel, + targetType: MigrationTargetType) { this._model = migrationStateModel; + this._targetType = targetType; this._treeComponent = new TreeComponent(this._model, this._model._targetType) } @@ -47,16 +60,22 @@ export class AssessmentDetailsBody { // returns middle section of body where list of issues and warnings are displayed. const assessmentFindingsComponent = this.createAssessmentFindingComponent(); - // returns the right section of body which defines summary of selected instance. - const instanceSummary = this._instanceSummary.createInstanceSummaryContainer(view); + // returns the right section of body which defines the result of assessment. + const resultComponent = this.createResultComponent(); bodyContainer.addItem(treeComponent, { flex: "none" }); bodyContainer.addItem(assessmentFindingsComponent, { flex: "none" }); - bodyContainer.addItem(instanceSummary, { flex: "none" }); + bodyContainer.addItem(resultComponent, { flex: "none" }); return bodyContainer; } + // function to populate data for body section + public async populateAssessmentBody(): Promise { + await this._treeComponent.initialize(); + this._instanceSummary.populateInstanceSummaryContainer(this._model, this._model._targetType); + } + // function to create middle section of body that displays list of warnings/ issues. public createAssessmentFindingComponent(): azdata.FlexContainer { const assessmentFindingsComponent = this._view.modelBuilder.flexContainer().withLayout({ @@ -68,7 +87,7 @@ export class AssessmentDetailsBody { } }).component(); - const findingsSection = this._view.modelBuilder.listView().withProps({ + this._findingsSummaryList = this._view.modelBuilder.listView().withProps({ title: { text: constants.ASSESSMENT_FINDINGS_LABEL, style: { "border": "solid 1px" } @@ -76,14 +95,177 @@ export class AssessmentDetailsBody { options: [{ label: constants.SUMMARY_TITLE, id: "summary" }] }).component(); - assessmentFindingsComponent.addItem(findingsSection); + assessmentFindingsComponent.addItem(this._findingsSummaryList); - const warningsOrIssuesListSection = this._view.modelBuilder.listView().withProps({ + this._warningsOrIssuesListSection = this._view.modelBuilder.listView().withProps({ title: { text: constants.WARNINGS }, - options: [] // TODO: fill the list of options. + options: [] }).component(); - assessmentFindingsComponent.addItem(warningsOrIssuesListSection); + assessmentFindingsComponent.addItem(this._warningsOrIssuesListSection); return assessmentFindingsComponent; } + + private createResultComponent(): azdata.FlexContainer { + const resultContainer = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).withProps({ + CSSStyles: { + 'border-left': 'solid 1px' + } + }).component(); + + const heading = this._view.modelBuilder.text().withProps({ + value: constants.DETAILS_TITLE, + CSSStyles: { + ...styles.LABEL_CSS, + "padding": "5px", + "border-bottom": "solid 1px" + } + }).component(); + + const subHeading = this._view.modelBuilder.text().withProps({ + value: constants.ASSESSMENT_SUMMARY_TITLE, + CSSStyles: { + ...styles.LABEL_CSS, + "padding": "5px", + "padding-left": "10px", + "padding-right": "250px", + "border-bottom": "solid 1px" + } + }).component(); + + const bottomContainer = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + + // returns the right section of body which defines summary of selected instance. + const instanceSummary = this._instanceSummary.createInstanceSummaryContainer(this._view); + + // returns the right section of body which defines summary of selected database. + const databaseSummary = this._databaseSummary.createDatabaseSummary(this._view); + databaseSummary.display = 'none'; + + // returns the right section of body which defines summary of selected database. + const issueSummary = this._issueSummary.createIssueSummary(this._view); + issueSummary.display = 'none'; + + bottomContainer.addItems([instanceSummary, databaseSummary, issueSummary]); + + resultContainer.addItems([heading, subHeading, bottomContainer]); + + let _isInstanceSummarySelected = true; + + this._disposables.push(this._treeComponent.instanceTable.onRowSelected(async (e) => { + _isInstanceSummarySelected = true; + this._targetType = this._model?._targetType; + this._activeIssues = this._model._assessmentResults?.issues.filter(issue => issue.appliesToMigrationTargetPlatform === this._targetType); + await instanceSummary.updateCssStyles({ + 'display': 'block' + }); + await databaseSummary.updateCssStyles({ + 'display': 'none' + }); + await issueSummary.updateCssStyles({ + 'display': 'none' + }); + await subHeading.updateProperty('value', constants.ASSESSMENT_SUMMARY_TITLE); + if (this._targetType === MigrationTargetType.SQLMI || + this._targetType === MigrationTargetType.SQLDB) { + await this.refreshResults(); + } + this._instanceSummary.populateInstanceSummaryContainer(this._model, this._targetType); + })); + + this._disposables.push(this._treeComponent.databaseTable.onRowSelected(async (e) => { + _isInstanceSummarySelected = false; + if (this._targetType === MigrationTargetType.SQLMI || + this._targetType === MigrationTargetType.SQLDB) { + this._activeIssues = this._model._assessmentResults?.databaseAssessments[e.row].issues.filter(i => i.appliesToMigrationTargetPlatform === this._targetType); + } else { + this._activeIssues = []; + } + await instanceSummary.updateCssStyles({ + 'display': 'none' + }); + await issueSummary.updateCssStyles({ + 'display': 'none' + }); + await databaseSummary.updateCssStyles({ + 'display': 'block' + }); + await subHeading.updateProperty('value', constants.ASSESSMENT_SUMMARY_TITLE); + await this._databaseSummary.populateDatabaseSummary(this._activeIssues, this._model._assessmentResults?.databaseAssessments[e.row]?.name); + if (this._targetType === MigrationTargetType.SQLMI || + this._targetType === MigrationTargetType.SQLDB) { + await this.refreshResults(); + } + if (this._findingsSummaryList.options.length) { + this._findingsSummaryList.selectedOptionId = '0'; + } + })); + + this._disposables.push(this._findingsSummaryList.onDidClick(async (e: azdata.ListViewClickEvent) => { + if (_isInstanceSummarySelected) { + await instanceSummary.updateCssStyles({ + 'display': 'block' + }); + await databaseSummary.updateCssStyles({ + 'display': 'none' + }); + } + else { + await instanceSummary.updateCssStyles({ + 'display': 'none' + }); + await databaseSummary.updateCssStyles({ + 'display': 'block' + }); + } + await issueSummary.updateCssStyles({ + 'display': 'none' + }); + await subHeading.updateProperty('value', constants.ASSESSMENT_SUMMARY_TITLE); + })); + + this._disposables.push(this._warningsOrIssuesListSection.onDidClick(async (e: azdata.ListViewClickEvent) => { + const selectedIssue = this._activeIssues[parseInt(this._warningsOrIssuesListSection.selectedOptionId!)]; + await instanceSummary.updateCssStyles({ + 'display': 'none' + }); + await databaseSummary.updateCssStyles({ + 'display': 'none' + }); + await issueSummary.updateCssStyles({ + 'display': 'block' + }); + await subHeading.updateProperty('value', selectedIssue?.checkId || ''); + await this._issueSummary.refreshAssessmentDetails(selectedIssue); + })); + + return resultContainer; + } + + public async refreshResults(): Promise { + if (this._targetType === MigrationTargetType.SQLMI || + this._targetType === MigrationTargetType.SQLDB) { + let assessmentResults: azdata.ListViewOption[] = this._activeIssues + .sort((e1, e2) => { + if (e1.databaseRestoreFails) { return -1; } + if (e2.databaseRestoreFails) { return 1; } + return e1.checkId.localeCompare(e2.checkId); + }).filter((v) => { + return v.appliesToMigrationTargetPlatform === this._targetType; + }).map((v, index) => { + return { + id: index.toString(), + label: v.checkId, + icon: v.databaseRestoreFails ? IconPathHelper.error : undefined, + ariaLabel: v.databaseRestoreFails ? constants.BLOCKING_ISSUE_ARIA_LABEL(v.checkId) : v.checkId, + }; + }); + + this._warningsOrIssuesListSection.options = assessmentResults; + } + } } diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/databaseSummary.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/databaseSummary.ts new file mode 100644 index 000000000000..988b20d634b9 --- /dev/null +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/databaseSummary.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../../constants/strings'; +import * as styles from '../../constants/styles'; +import { IconPathHelper } from '../../constants/iconPathHelper'; +import { SqlMigrationAssessmentResultItem } from '../../service/contracts'; + +// Class defines the database summary. +export class DatabaseSummary { + + private _view!: azdata.ModelView; + private _readinessTitle!: azdata.TextComponent; + private _totalIssues!: azdata.TextComponent; + private _readinessDescription!: azdata.TextComponent; + private _readinessIcon!: azdata.ImageComponent; + private _readinessText!: azdata.TextComponent; + + // function that creates database summary ui + public createDatabaseSummary(view: azdata.ModelView): azdata.FlexContainer { + this._view = view; + const databaseSummaryContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + + this._readinessTitle = view.modelBuilder.text().withProps({ + value: "", + CSSStyles: { + ...styles.SUBTITLE_LABEL_CSS, + 'margin': '0px', + 'padding-left': '10px' + } + }).component(); + + const issuesSummaryTitle = view.modelBuilder.text().withProps({ + value: constants.DATABASE_ISSUES_SUMMARY, + CSSStyles: { + ...styles.SUBTITLE_LABEL_CSS, + 'margin': '0px', + 'padding-left': '10px', + 'margin-top': '30px' + } + }).component(); + + this._totalIssues = view.modelBuilder.text().withProps({ + value: "", + CSSStyles: { + ...styles.LIGHT_LABEL_CSS, + 'margin': '0px', + 'padding-left': '10px' + } + }).component(); + + const issuesSummarySubtitle = view.modelBuilder.text().withProps({ + value: constants.SEVERITY_FINDINGS_LABEL, + CSSStyles: { + ...styles.SUBTITLE_LABEL_CSS, + 'margin': '0px', + 'margin-top': '10px', + 'padding-left': '10px' + } + }).component(); + + this._readinessDescription = view.modelBuilder.text().withProps({ + value: "", + CSSStyles: { + ...styles.SUBTITLE_LABEL_CSS, + 'margin': '0px', + 'margin-top': '2px', + 'padding-left': '38px' + } + }).component(); + + databaseSummaryContainer.addItem(this._readinessTitle); + + databaseSummaryContainer.addItem(this.createIconTextContainer()); + + databaseSummaryContainer.addItem(this._readinessDescription); + + databaseSummaryContainer.addItems([ + issuesSummaryTitle, this._totalIssues, issuesSummarySubtitle]); + + return databaseSummaryContainer; + } + + private createIconTextContainer(): azdata.FlexContainer { + const iconTextContainer = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row' + }).withProps({ CSSStyles: { 'padding-left': '10px', 'margin-top': '10px' } }).component(); + + this._readinessIcon = this._view.modelBuilder.image().withProps({ + iconPath: "", + iconHeight: 24, + iconWidth: 24, + width: 24, + height: 24, + CSSStyles: { 'margin-right': '4px' } + }).component(); + + this._readinessText = this._view.modelBuilder.text().withProps({ + value: "", + CSSStyles: { + ...styles.PAGE_SUBTITLE_CSS, + 'font-weight': '400', + 'margin': '0px' + } + }).component(); + + iconTextContainer.addItem(this._readinessIcon, { flex: 'none' }); + iconTextContainer.addItem(this._readinessText, { flex: 'none' }); + + return iconTextContainer; + } + + // function to populate database summary with latest data values. + public async populateDatabaseSummary(issues: SqlMigrationAssessmentResultItem[], dbName: string) { + this._readinessTitle.value = constants.DB_READINESS_SECTION_TITLE(dbName); + this._totalIssues.value = constants.TOTAL_ISSUES_LABEL + ":" + issues.length; + if (issues.length > 0) { + this._readinessDescription.value = constants.NON_READINESS_DESCRIPTION(issues.length); + this._readinessIcon.iconPath = IconPathHelper.sqlDatabaseNotReadyLogo; + this._readinessText.value = constants.NOT_READY; + } + else { + this._readinessDescription.value = constants.READINESS_DESCRIPTION; + this._readinessIcon.iconPath = IconPathHelper.sqlDatabaseLogo; + this._readinessText.value = constants.READY; + } + } + +} diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts index 119e2ea4d70d..b338ac32548c 100644 --- a/extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts @@ -6,12 +6,13 @@ import * as azdata from 'azdata'; import * as constants from '../../constants/strings'; import * as styles from '../../constants/styles'; -import { IconPathHelper } from '../../constants/iconPathHelper'; import { ColorCodes } from '../../constants/helper'; +import { MigrationStateModel } from '../../models/stateMachine'; +import { MigrationTargetType } from '../../api/utils'; interface IActionMetadata { label: string, - count: number, + count?: number, iconPath?: azdata.IconPath, color?: string } @@ -19,34 +20,16 @@ interface IActionMetadata { // Class that defines Instance Summary section. export class InstanceSummary { private _view!: azdata.ModelView; + private _assessedDatabases!: azdata.TextComponent; + private _totalFindingLabels!: azdata.TextComponent; + private _yAxisLabels: azdata.TextComponent[] = []; + private _divs: azdata.FlexContainer[] = []; // function to create instance summary components public createInstanceSummaryContainer(view: azdata.ModelView): azdata.FlexContainer { this._view = view; const instanceSummaryContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' - }).withProps({ - CSSStyles: { - 'border-left': 'solid 1px' - } - }).component(); - - const heading = view.modelBuilder.text().withProps({ - value: constants.DETAILS_TITLE, - CSSStyles: { - ...styles.LABEL_CSS, - "padding": "5px", - "border-bottom": "solid 1px" - } - }).component(); - - const subHeading = view.modelBuilder.text().withProps({ - value: constants.ASSESSMENT_SUMMARY_TITLE, - CSSStyles: { - ...styles.LABEL_CSS, - "padding": "10px", - "border-bottom": "solid 1px" - } }).component(); const description = view.modelBuilder.text().withProps({ @@ -58,8 +41,8 @@ export class InstanceSummary { } }).component(); - const assessedDatabasesLabel = view.modelBuilder.text().withProps({ - value: constants.ASSESSED_DBS_LABEL + ":", + this._assessedDatabases = view.modelBuilder.text().withProps({ + value: "", CSSStyles: { ...styles.LIGHT_LABEL_CSS, 'margin': '0px', @@ -77,8 +60,8 @@ export class InstanceSummary { } }).component(); - const totalFindingsLabel = view.modelBuilder.text().withProps({ - value: constants.TOTAL_FINDINGS_LABEL + ":", + this._totalFindingLabels = view.modelBuilder.text().withProps({ + value: "", CSSStyles: { ...styles.LIGHT_LABEL_CSS, 'margin': '0px', @@ -86,33 +69,45 @@ export class InstanceSummary { } }).component(); - const findingsSummarySubtitle = view.modelBuilder.text().withProps({ - value: constants.SEVERITY_FINDINGS_LABEL, - CSSStyles: { - ...styles.SUBTITLE_LABEL_CSS, - 'margin': '0px', - 'margin-top': '10px', - 'padding-left': '10px' - } - }).component(); + instanceSummaryContainer.addItems([description, this._assessedDatabases, this.createGraphComponent(), + findingsSummaryTitle, this._totalFindingLabels]); - const findings = [ + return instanceSummaryContainer; + } + + public populateInstanceSummaryContainer(model: MigrationStateModel, targetType: MigrationTargetType): void { + + this._assessedDatabases.value = constants.ASSESSED_DBS_LABEL + ": " + model._databasesForAssessment?.length; + this._totalFindingLabels.value = constants.TOTAL_FINDINGS_LABEL + ": " + model._assessmentResults?.issues.filter(issue => issue.appliesToMigrationTargetPlatform === targetType).length; + const readyDbsCount = model._assessmentResults.databaseAssessments.filter((db) => db.issues.filter(issue => issue.appliesToMigrationTargetPlatform === targetType).length === 0).length; + const notReadyDbsCount = model._databasesForAssessment?.length - readyDbsCount; + const readywithWarnDbsCount = 0; + + const labels = [ + { + label: constants.NOT_READY, + count: notReadyDbsCount, + color: ColorCodes.NotReadyState_Red + }, { - label: constants.ISSUES_LABEL, - count: 0, - iconPath: IconPathHelper.error + label: constants.READY_WARN, + count: readywithWarnDbsCount, + color: ColorCodes.ReadyWithWarningState_Amber }, { - label: constants.WARNINGS, - count: 0, - iconPath: IconPathHelper.warning + label: constants.READY, + count: readyDbsCount, + color: ColorCodes.ReadyState_Green }]; - - instanceSummaryContainer.addItems([heading, subHeading, description, assessedDatabasesLabel, this.createGraphComponent(), - findingsSummaryTitle, totalFindingsLabel, findingsSummarySubtitle]); - - instanceSummaryContainer.addItems(findings.map(l => this.createFindingsContainer(l))); - return instanceSummaryContainer; + let i = 0; + this._yAxisLabels.forEach(component => { component.value = labels[i].label + " (" + labels[i].count + ")"; i++; }); + i = 0; + let maxValue = 1; + labels.forEach(label => maxValue = Math.max(label.count, maxValue)); + this._divs.forEach(async (component) => { + const divWidth = this.setWidth(labels[i++].count ?? 0, maxValue); + await component.updateCssStyles({ 'width': divWidth + '%' }); + }); } private createGraphComponent(): azdata.FlexContainer { @@ -126,17 +121,14 @@ export class InstanceSummary { const labels = [ { label: constants.NOT_READY, - count: 1, color: ColorCodes.NotReadyState_Red }, { label: constants.READY_WARN, - count: 1, color: ColorCodes.ReadyWithWarningState_Amber }, { label: constants.READY, - count: 8, color: ColorCodes.ReadyState_Green }]; @@ -153,32 +145,25 @@ export class InstanceSummary { 'flex-direction': 'row' } }).component(); - barComponent.addItem(this.createYAxisComponent(linkMetaData), { 'flex': 'none' }); - if (linkMetaData.count !== 0) { - - const divWidth = this.setWidth(linkMetaData.count, maxValue); - - const division = this._view.modelBuilder.divContainer().withProps({ - CSSStyles: { - 'margin-top': '12px', - 'margin-left': '10px', - 'background-color': linkMetaData.color ?? "", - 'height': '30px', - } - }).component(); - - if (divWidth !== 0) { - barComponent.addItem(division, { CSSStyles: { 'width': divWidth + '%' } }); + const division = this._view.modelBuilder.divContainer().withProps({ + width: '0px', + CSSStyles: { + 'margin-top': '12px', + 'margin-left': '10px', + 'background-color': linkMetaData.color ?? "", + 'height': '30px', + 'width': '0px' } - - } + }).component(); + this._divs.push(division); + barComponent.addItem(division); return barComponent; } - private createYAxisComponent(linkMetaData: IActionMetadata): azdata.Component { + private createYAxisComponent(linkMetaData: IActionMetadata): azdata.FlexContainer { const yAxisLabelContainer = this._view.modelBuilder.flexContainer().withProps({ CSSStyles: { @@ -190,13 +175,15 @@ export class InstanceSummary { }).component(); const yAxisLabel = this._view.modelBuilder.text().withProps({ - value: linkMetaData.label + " (" + linkMetaData.count + ")", + value: "", CSSStyles: { ...styles.CARD_AXES_LABEL, 'overflow-wrap': 'break-word' }, }).component(); + this._yAxisLabels.push(yAxisLabel); + yAxisLabelContainer.addItem(yAxisLabel); return yAxisLabelContainer; @@ -206,44 +193,4 @@ export class InstanceSummary { const width = (Math.sqrt(value) / Math.sqrt(maxValue)) * 75; return width; } - - private createFindingsContainer(linkMetaData: IActionMetadata): azdata.FlexContainer { - const findingContainer = this._view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'flex-direction': 'row', - 'margin': '0px', - 'padding-left': '10px' - } - }).component(); - - const findingImage = this._view.modelBuilder.image().withProps({ - iconPath: linkMetaData.iconPath, - iconHeight: 12, - iconWidth: 12, - width: 12, - height: 19, - CSSStyles: { 'margin-right': '4px' } - }).component(); - - const findingLabel = this._view.modelBuilder.text().withProps({ - value: linkMetaData.label + ":", - CSSStyles: { - ...styles.LIGHT_LABEL_CSS, - 'margin': '0px', 'margin-right': '4px' - } - }).component(); - - const findingCount = this._view.modelBuilder.text().withProps({ - value: String(linkMetaData.count), - CSSStyles: { - ...styles.SUBTITLE_LABEL_CSS, - 'margin': '0px', 'margin-right': '4px' - } - }).component(); - - findingContainer.addItem(findingImage, { flex: 'none' }); - findingContainer.addItem(findingLabel, { flex: 'none' }); - findingContainer.addItem(findingCount, { flex: 'none' }); - return findingContainer; - } } diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/issueSummary.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/issueSummary.ts new file mode 100644 index 000000000000..d05436539fef --- /dev/null +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/issueSummary.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../../constants/strings'; +import * as styles from '../../constants/styles'; +import * as vscode from 'vscode'; +import { SqlMigrationAssessmentResultItem, SqlMigrationImpactedObjectInfo } from '../../service/contracts'; + +const headerLeft: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden', + 'border-bottom': '1px solid' +}; + +export class IssueSummary { + private _view!: azdata.ModelView; + private _recommendationText!: azdata.TextComponent; + private _descriptionText!: azdata.TextComponent; + private _moreInfoTitle!: azdata.TextComponent; + private _moreInfoText!: azdata.HyperlinkComponent; + private _impactedObjects!: SqlMigrationImpactedObjectInfo[]; + private _objectDetailsType!: azdata.TextComponent; + private _objectDetailsName!: azdata.TextComponent; + private _objectDetailsSample!: azdata.TextComponent; + private _impactedObjectsTable!: azdata.DeclarativeTableComponent; + private _disposables: vscode.Disposable[] = []; + + public createIssueSummary(view: azdata.ModelView): azdata.FlexContainer { + this._view = view; + const bottomContainer = this.createDescriptionContainer(); + const container = this._view.modelBuilder.flexContainer() + .withItems([bottomContainer]) + .withLayout({ flexFlow: 'column' }) + .withProps({ CSSStyles: { 'margin-left': '10px' } }) + .component(); + + return container; + } + + private createDescriptionContainer(): azdata.FlexContainer { + const description = this.createDescription(); + const impactedObjects = this.createImpactedObjectsDescription(); + const container = this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'row' }) + .withProps({ CSSStyles: { 'height': '100%' } }) + .component(); + container.addItem(description, { flex: '0 0 auto', CSSStyles: { 'width': '200px', 'margin-right': '35px' } }); + container.addItem(impactedObjects, { flex: '0 0 auto', CSSStyles: { 'width': '280px' } }); + + return container; + } + + private createDescription(): azdata.FlexContainer { + const LABEL_CSS = { + ...styles.LIGHT_LABEL_CSS, + 'width': '200px', + 'margin': '12px 0 0' + }; + const textStyle = { + ...styles.BODY_CSS, + 'width': '200px', + 'word-wrap': 'break-word' + }; + const descriptionTitle = this._view.modelBuilder.text() + .withProps({ + value: constants.DESCRIPTION, + CSSStyles: LABEL_CSS + }).component(); + this._descriptionText = this._view.modelBuilder.text() + .withProps({ + CSSStyles: textStyle + }).component(); + + const recommendationTitle = this._view.modelBuilder.text() + .withProps({ + value: constants.RECOMMENDATION, + CSSStyles: LABEL_CSS + }).component(); + this._recommendationText = this._view.modelBuilder.text() + .withProps({ + CSSStyles: textStyle + }).component(); + + this._moreInfoTitle = this._view.modelBuilder.text() + .withProps({ + value: constants.MORE_INFO, + CSSStyles: LABEL_CSS + }).component(); + this._moreInfoText = this._view.modelBuilder.hyperlink() + .withProps({ + label: '', + url: '', + CSSStyles: textStyle, + ariaLabel: constants.MORE_INFO, + showLinkIcon: true + }).component(); + + const container = this._view.modelBuilder.flexContainer() + .withItems([descriptionTitle, + this._descriptionText, + recommendationTitle, + this._recommendationText, + this._moreInfoTitle, + this._moreInfoText]) + .withLayout({ flexFlow: 'column' }) + .component(); + + return container; + } + + private createImpactedObjectsDescription(): azdata.FlexContainer { + const impactedObjectsTitle = this._view.modelBuilder.text().withProps({ + value: constants.IMPACTED_OBJECTS, + CSSStyles: { + ...styles.LIGHT_LABEL_CSS, + 'width': '280px', + 'margin': '10px 0px 0px 0px', + } + }).component(); + + const rowStyle: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left', + 'border-bottom': '1px solid' + }; + + this._impactedObjectsTable = this._view.modelBuilder.declarativeTable().withProps( + { + ariaLabel: constants.IMPACTED_OBJECTS, + enableRowSelection: true, + width: '100%', + columns: [ + { + displayName: constants.TYPE, + valueType: azdata.DeclarativeDataType.string, + width: '120px', + isReadOnly: true, + headerCssStyles: headerLeft, + rowCssStyles: rowStyle + }, + { + displayName: constants.NAME, + valueType: azdata.DeclarativeDataType.string, + width: '130px', + isReadOnly: true, + headerCssStyles: headerLeft, + rowCssStyles: rowStyle + }, + ], + dataValues: [[{ value: '' }, { value: '' }]], + CSSStyles: { 'margin-top': '12px' } + } + ).component(); + + this._disposables.push(this._impactedObjectsTable.onRowSelected((e) => { + const impactedObject = e.row > -1 ? this._impactedObjects[e.row] : undefined; + this.refreshImpactedObject(impactedObject); + })); + + const objectDetailsTitle = this._view.modelBuilder.text() + .withProps({ + value: constants.OBJECT_DETAILS, + CSSStyles: { + ...styles.LIGHT_LABEL_CSS, + 'margin': '12px 0px 0px 0px', + } + }).component(); + const objectDescriptionStyle = { + ...styles.BODY_CSS, + 'margin': '5px 0px 0px 0px', + 'word-wrap': 'break-word' + }; + this._objectDetailsType = this._view.modelBuilder.text() + .withProps({ + value: constants.TYPES_LABEL, + CSSStyles: objectDescriptionStyle + }).component(); + + this._objectDetailsName = this._view.modelBuilder.text() + .withProps({ + value: constants.NAMES_LABEL, + CSSStyles: objectDescriptionStyle + }).component(); + + this._objectDetailsSample = this._view.modelBuilder.text() + .withProps({ + value: '', + CSSStyles: objectDescriptionStyle + }).component(); + + const container = this._view.modelBuilder.flexContainer() + .withItems([ + impactedObjectsTitle, + this._impactedObjectsTable, + objectDetailsTitle, + this._objectDetailsType, + this._objectDetailsName, + this._objectDetailsSample]) + .withLayout({ flexFlow: 'column' }) + .component(); + + return container; + } + + public refreshImpactedObject(impactedObject?: SqlMigrationImpactedObjectInfo): void { + this._objectDetailsType.value = constants.IMPACT_OBJECT_TYPE(impactedObject?.objectType); + this._objectDetailsName.value = constants.IMPACT_OBJECT_NAME(impactedObject?.name); + this._objectDetailsSample.value = impactedObject?.impactDetail || ''; + } + + public async refreshAssessmentDetails(selectedIssue?: SqlMigrationAssessmentResultItem): Promise { + await this._descriptionText.updateProperty('value', selectedIssue?.description || ''); + await this._recommendationText.updateProperty('value', selectedIssue?.message || constants.NA); + + if (selectedIssue?.helpLink) { + await this._moreInfoTitle.updateProperty('display', 'flex'); + await this._moreInfoText.updateProperties({ + 'display': 'flex', + 'url': selectedIssue?.helpLink || '', + 'label': selectedIssue?.displayName || '', + 'ariaLabel': selectedIssue?.displayName || '', + 'showLinkIcon': true + }); + } else { + await this._moreInfoTitle.updateProperty('display', 'none'); + await this._moreInfoText.updateProperties({ + 'display': 'none', + 'url': '', + 'label': '', + 'ariaLabel': '', + 'showLinkIcon': false + }); + } + + this._impactedObjects = selectedIssue?.impactedObjects || []; + await this._impactedObjectsTable.setDataValues( + this._impactedObjects.map( + (object) => [{ value: object.objectType }, { value: object.name }])); + + this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1; + } +} diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/treeComponent.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/treeComponent.ts index c7166f86dd1b..a8cf8fb656a2 100644 --- a/extensions/sql-migration/src/wizard/assessmentDetailsPage/treeComponent.ts +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/treeComponent.ts @@ -69,6 +69,14 @@ export class TreeComponent { private _targetType: MigrationTargetType ) { } + public get instanceTable() { + return this._instanceTable; + } + + public get databaseTable() { + return this._databaseTable; + } + // function that creates components for tree section public createTreeComponent(view: azdata.ModelView, dbs: string[]) { this._view = view;