From 41516e375cc66f1e913a53970b1e4cc81e4c7bdf Mon Sep 17 00:00:00 2001 From: stuti149 <87131830+stuti149@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:52:18 +0530 Subject: [PATCH] [SQL Migration][New Migration Experience] Instance Summary created. (#24295) * instance summary created * adding resource strings * resolving comment --- .../sql-migration/src/constants/helper.ts | 7 + .../sql-migration/src/constants/strings.ts | 13 + .../sql-migration/src/constants/styles.ts | 17 +- .../assessmentDetailsPage.ts | 6 + .../assessmentDetialsBody.ts | 71 +++- .../assessmentDetailsPage/instanceSummary.ts | 249 ++++++++++++ .../assessmentDetailsPage/treeComponent.ts | 380 ++++++++++++++++++ 7 files changed, 740 insertions(+), 3 deletions(-) create mode 100644 extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts create mode 100644 extensions/sql-migration/src/wizard/assessmentDetailsPage/treeComponent.ts diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts index 0036e7e9b746..29f75bea9cc1 100644 --- a/extensions/sql-migration/src/constants/helper.ts +++ b/extensions/sql-migration/src/constants/helper.ts @@ -91,6 +91,13 @@ export const ValidationErrorCodes = { SqlInfoValidationFailed: '2056' }; +// Color codes for Graph +export const ColorCodes = { + NotReadyState_Red: "#E00B1C", + ReadyState_Green: "#57A300", + ReadyWithWarningState_Amber: "#DB7500" +} + const _dateFormatter = new Intl.DateTimeFormat( undefined, { year: 'numeric', diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 0970701ceda0..0aa97e7ed6b0 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -86,6 +86,19 @@ export const ASSESSMENT_RESULTS_PAGE_TITLE = localize('sql.migration.assessment. export const ASSESSMENT_RESULTS_PAGE_HEADER = localize('sql.migration.assessment.results.header', "View assessment results and select database(s) for migration"); export const DATABASES_ASSESSED_LABEL = localize('sql.migration.database.assessed.label', "Database(s) assessed"); export const MIGRATION_TIME_LABEL = localize('sql.migration.migration.time.label', "Ready for migration"); +export const ASSESSMENT_FINDINGS_LABEL = localize('sql.migration.assessment.findings.label', "Assessment findings"); +export const SUMMARY_TITLE = localize('sql.migration.summary.title', "Summary"); +export const DETAILS_TITLE = localize('sql.migration.details.title', "Details"); +export const ASSESSMENT_SUMMARY_TITLE = localize('sql.migration.assessment.summary.title', "Assessment summary"); +export const READINESS_SECTION_TITLE = localize('sql.migration.readiness.section.title', "Migration readiness of assessed databases in the Server instance"); +export const TOTAL_FINDINGS_LABEL = localize('sql.migration.total.findings.label', "Total findings"); +export const ISSUES_LABEL = localize('sql.migration.issues.label', "Blocking issues"); +export const INSTANCE_FINDING_SUMMARY = localize('sql.migration.instance.finding.summary', "Server instance assessment findings summary"); +export const SEVERITY_FINDINGS_LABEL = localize('sql.migration.severity.findings.label', "Findings by severity"); +export const ASSESSED_DBS_LABEL = localize('sql.migration.assessed.dbs.label', "Assessed databases"); +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"); // 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/constants/styles.ts b/extensions/sql-migration/src/constants/styles.ts index b8c998f2aae6..04a7758e6013 100644 --- a/extensions/sql-migration/src/constants/styles.ts +++ b/extensions/sql-migration/src/constants/styles.ts @@ -70,6 +70,11 @@ export const BIG_NUMBER_CSS = { 'margin': '0', }; +export const SUBTITLE_LABEL_CSS = { + ...BODY_CSS, + 'font-weight': '400', +}; + export const CARD_CSS = { 'width': '190px', 'box-shadow': '0px 1px 4px rgba(0, 0, 0, 0.13)', @@ -83,4 +88,14 @@ export const TOOLBAR_CSS = { 'line-height': '16px', 'font-weight': '400', 'margin': '0', -}; \ No newline at end of file +}; + + +export const CARD_AXES_LABEL = { + 'font-size': '12px', + 'height': '14px', + 'line-height': '14px', + 'margin': '0px', + 'text-align': 'right', + 'font-weight': '600', +}; diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts index 278dac725f61..02ba4e48119b 100644 --- a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetailsPage.ts @@ -41,6 +41,8 @@ export class AssessmentDetailsPage extends MigrationWizardPage { const headerSection = this._header.createAssessmentDetailsHeader(this._view); + const bodySection = this._body.createAssessmentDetailsBody(this._view); + const form = this._view.modelBuilder.formContainer() .withFormItems([ { @@ -48,6 +50,9 @@ export class AssessmentDetailsPage extends MigrationWizardPage { }, { component: headerSection + }, + { + component: bodySection } ]).withProps({ CSSStyles: { 'padding-top': '0' } @@ -57,6 +62,7 @@ export class AssessmentDetailsPage extends MigrationWizardPage { public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { await this._header.populateAssessmentDetailsHeader(); + await this._body._treeComponent.initialize(); } 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 fada2ee60289..b3bb1b46395f 100644 --- a/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetialsBody.ts +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/assessmentDetialsBody.ts @@ -4,19 +4,86 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import * as constants from '../../constants/strings'; import { MigrationStateModel } from '../../models/stateMachine'; +import { MigrationTargetType } from '../../api/utils'; +import { TreeComponent } from './treeComponent'; +import { InstanceSummary } from './instanceSummary'; +// Class that defines ui for body section of assessment result page export class AssessmentDetailsBody { private _view!: azdata.ModelView; + private _model!: MigrationStateModel; + public _treeComponent!: TreeComponent; + private _targetType!: MigrationTargetType; + private _instanceSummary = new InstanceSummary(); + public _warningsOrIssuesListSection!: azdata.ListViewComponent; - constructor(migrationStateModel: MigrationStateModel) { } + constructor(migrationStateModel: MigrationStateModel) { + this._model = migrationStateModel; + this._treeComponent = new TreeComponent(this._model, this._model._targetType) + } - public createAssessmentDetailsHeader(view: azdata.ModelView): azdata.Component { + // function that defines all the components for body section + public createAssessmentDetailsBody(view: azdata.ModelView): azdata.Component { this._view = view; const bodyContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', + }).withProps({ + CSSStyles: { + 'border-top': 'solid 1px' + } }).component(); + // returns the side pane of tree component to select instance and databases. + const treeComponent = this._treeComponent.createTreeComponent( + view, + (this._targetType === MigrationTargetType.SQLVM) + ? this._model._vmDbs + : (this._targetType === MigrationTargetType.SQLMI) + ? this._model._miDbs + : this._model._sqldbDbs); + + // 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); + + bodyContainer.addItem(treeComponent, { flex: "none" }); + bodyContainer.addItem(assessmentFindingsComponent, { flex: "none" }); + bodyContainer.addItem(instanceSummary, { flex: "none" }); + return bodyContainer; } + + // function to create middle section of body that displays list of warnings/ issues. + public createAssessmentFindingComponent(): azdata.FlexContainer { + const assessmentFindingsComponent = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: "190px" + }).withProps({ + CSSStyles: { + "width": "190px" + } + }).component(); + + const findingsSection = this._view.modelBuilder.listView().withProps({ + title: { + text: constants.ASSESSMENT_FINDINGS_LABEL, + style: { "border": "solid 1px" } + }, + options: [{ label: constants.SUMMARY_TITLE, id: "summary" }] + }).component(); + + assessmentFindingsComponent.addItem(findingsSection); + + const warningsOrIssuesListSection = this._view.modelBuilder.listView().withProps({ + title: { text: constants.WARNINGS }, + options: [] // TODO: fill the list of options. + }).component(); + + assessmentFindingsComponent.addItem(warningsOrIssuesListSection); + return assessmentFindingsComponent; + } } diff --git a/extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts new file mode 100644 index 000000000000..119e2ea4d70d --- /dev/null +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/instanceSummary.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ColorCodes } from '../../constants/helper'; + +interface IActionMetadata { + label: string, + count: number, + iconPath?: azdata.IconPath, + color?: string +} + +// Class that defines Instance Summary section. +export class InstanceSummary { + private _view!: azdata.ModelView; + + // 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({ + value: constants.READINESS_SECTION_TITLE, + CSSStyles: { + ...styles.SUBTITLE_LABEL_CSS, + 'margin': '0px', + 'padding-left': '10px' + } + }).component(); + + const assessedDatabasesLabel = view.modelBuilder.text().withProps({ + value: constants.ASSESSED_DBS_LABEL + ":", + CSSStyles: { + ...styles.LIGHT_LABEL_CSS, + 'margin': '0px', + 'padding-left': '10px' + } + }).component(); + + const findingsSummaryTitle = view.modelBuilder.text().withProps({ + value: constants.INSTANCE_FINDING_SUMMARY, + CSSStyles: { + ...styles.SUBTITLE_LABEL_CSS, + 'margin': '0px', + 'padding-left': '10px', + 'margin-top': '30px' + } + }).component(); + + const totalFindingsLabel = view.modelBuilder.text().withProps({ + value: constants.TOTAL_FINDINGS_LABEL + ":", + CSSStyles: { + ...styles.LIGHT_LABEL_CSS, + 'margin': '0px', + 'padding-left': '10px' + } + }).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(); + + const findings = [ + { + label: constants.ISSUES_LABEL, + count: 0, + iconPath: IconPathHelper.error + }, + { + label: constants.WARNINGS, + count: 0, + iconPath: IconPathHelper.warning + }]; + + instanceSummaryContainer.addItems([heading, subHeading, description, assessedDatabasesLabel, this.createGraphComponent(), + findingsSummaryTitle, totalFindingsLabel, findingsSummarySubtitle]); + + instanceSummaryContainer.addItems(findings.map(l => this.createFindingsContainer(l))); + return instanceSummaryContainer; + } + + private createGraphComponent(): azdata.FlexContainer { + const graph = this._view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'display': 'flex', + 'flex-direction': 'column', + } + }).component(); + + 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 + }]; + + // create individual card component for each property in above list + graph.addItems(labels.map(l => this.createBarComponent(l, 8))); + return graph; + } + + private createBarComponent(linkMetaData: IActionMetadata, maxValue: number) { + + const barComponent = this._view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'display': 'flex', + '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 + '%' } }); + } + + } + + return barComponent; + } + + private createYAxisComponent(linkMetaData: IActionMetadata): azdata.Component { + + const yAxisLabelContainer = this._view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'width': '80px', + 'margin-top': '12px', + 'height': '30px', + 'overflow-wrap': 'break-word' + } + }).component(); + + const yAxisLabel = this._view.modelBuilder.text().withProps({ + value: linkMetaData.label + " (" + linkMetaData.count + ")", + CSSStyles: { + ...styles.CARD_AXES_LABEL, + 'overflow-wrap': 'break-word' + }, + }).component(); + + yAxisLabelContainer.addItem(yAxisLabel); + + return yAxisLabelContainer; + } + + private setWidth(value: number, maxValue: number): number { + 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/treeComponent.ts b/extensions/sql-migration/src/wizard/assessmentDetailsPage/treeComponent.ts new file mode 100644 index 000000000000..c7166f86dd1b --- /dev/null +++ b/extensions/sql-migration/src/wizard/assessmentDetailsPage/treeComponent.ts @@ -0,0 +1,380 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as constants from '../../constants/strings'; +import * as styles from '../../constants/styles'; +import { MigrationTargetType, debounce } from '../../api/utils'; +import { MigrationStateModel } from '../../models/stateMachine'; +import { IconPath, IconPathHelper } from '../../constants/iconPathHelper'; +import { selectDatabasesFromList } from '../../constants/helper'; +import { getSourceConnectionProfile } from '../../api/sqlUtils'; +import { SqlMigrationAssessmentResultItem } from '../../service/contracts'; + +const styleLeft: azdata.CssStyles = { + 'border': 'none', + '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 headerLeft: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden', + 'border-bottom': '1px solid' +}; +const headerRight: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'right', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden', + 'border-bottom': '1px solid' +}; + +// Class that defines list of instance and databases to select in assessment results page +export class TreeComponent { + private _view!: azdata.ModelView; + private _instanceTable!: azdata.DeclarativeTableComponent; + private _databaseTable!: azdata.DeclarativeTableComponent; + private _assessmentResultsList!: azdata.ListViewComponent; + private _assessmentContainer!: azdata.FlexContainer; + private _assessmentsTable!: azdata.FlexContainer; + private _noIssuesContainer!: azdata.FlexContainer; + private _recommendation!: azdata.TextComponent; + private _recommendationTitle!: azdata.TextComponent; + private _databaseTableValues!: azdata.DeclarativeTableCellValue[][]; + private _activeIssues!: SqlMigrationAssessmentResultItem[]; + private _serverName!: string; + private _dbNames!: string[]; + private _databaseCount!: azdata.TextComponent; + private _disposables: vscode.Disposable[] = []; + + constructor( + private _model: MigrationStateModel, + private _targetType: MigrationTargetType + ) { } + + // function that creates components for tree section + public createTreeComponent(view: azdata.ModelView, dbs: string[]) { + this._view = view; + const component = view.modelBuilder.flexContainer().withLayout({ + height: '100%', + flexFlow: 'column' + }).withProps({ + CSSStyles: { + 'border-right': 'solid 1px', + 'width': '285px' + }, + }).component(); + + component.addItem(this.createSearchComponent(), { flex: '0 0 auto' }); + component.addItem(this.createInstanceComponent(), { flex: '0 0 auto' }); + component.addItem(this.createDatabaseCount(), { flex: '0 0 auto' }); + component.addItem(this.createDatabaseComponent(dbs), { flex: '1 1 auto', CSSStyles: { 'overflow-y': 'auto' } }); + return component; + } + + // function to create search component. + private createSearchComponent(): azdata.DivContainer { + let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({ + stopEnterPropagation: true, + placeHolder: constants.SEARCH, + width: 260 + }).component(); + + this._disposables.push(resourceSearchBox.onTextChanged(value => this._filterTableList(value))); + + const searchContainer = this._view.modelBuilder.divContainer().withItems([resourceSearchBox]).withProps({ + CSSStyles: { + 'margin': '10px 15px 0px 15px' + } + }).component(); + + return searchContainer; + } + + @debounce(500) + private _filterTableList(value: string): void { + if (this._databaseTableValues && value?.length > 0) { + const filter: number[] = []; + this._databaseTableValues.forEach((row, index) => { + // undo when bug #16445 is fixed + // const flexContainer: azdata.FlexContainer = row[1]?.value as azdata.FlexContainer; + // const textComponent: azdata.TextComponent = flexContainer?.items[1] as azdata.TextComponent; + // const cellText = textComponent?.value?.toLowerCase(); + const text = row[1]?.value as string; + const cellText = text?.toLowerCase(); + const searchText: string = value?.toLowerCase(); + if (cellText?.includes(searchText)) { + filter.push(index); + } + }); + + this._databaseTable.setFilter(filter); + } else { + this._databaseTable.setFilter(undefined); + } + } + + private createInstanceComponent(): azdata.DivContainer { + this._instanceTable = this._view.modelBuilder.declarativeTable().withProps( + { + ariaLabel: constants.SQL_SERVER_INSTANCE, + enableRowSelection: true, + width: 240, + CSSStyles: { + 'table-layout': 'fixed' + }, + columns: [ + { + displayName: constants.INSTANCE, + // undo when bug #16445 is fixed + // valueType: azdata.DeclarativeDataType.component, + valueType: azdata.DeclarativeDataType.string, + width: 190, + isReadOnly: true, + headerCssStyles: headerLeft + }, + { + displayName: constants.WARNINGS, + valueType: azdata.DeclarativeDataType.string, + width: 50, + isReadOnly: true, + headerCssStyles: headerRight + } + ], + }).component(); + + const instanceContainer = this._view.modelBuilder.divContainer().withItems([this._instanceTable]).withProps({ + CSSStyles: { + 'margin': '19px 15px 0px 15px' + } + }).component(); + + return instanceContainer; + } + + public async refreshResults(): Promise { + if (this._targetType === MigrationTargetType.SQLMI || + this._targetType === MigrationTargetType.SQLDB) { + if (this._activeIssues?.length === 0) { + /// show no issues here + await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' }); + await this._assessmentContainer.updateCssStyles({ 'display': 'none' }); + await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' }); + } else { + await this._assessmentContainer.updateCssStyles({ 'display': 'flex' }); + await this._assessmentsTable.updateCssStyles({ 'display': 'flex', 'border-right': 'solid 1px' }); + await this._noIssuesContainer.updateCssStyles({ 'display': 'none' }); + } + } else { + await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' }); + await this._assessmentContainer.updateCssStyles({ 'display': 'none' }); + await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' }); + + this._recommendationTitle.value = constants.ASSESSMENT_RESULTS; + this._recommendation.value = ''; + } + 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._assessmentResultsList.options = assessmentResults; + if (this._assessmentResultsList.options.length) { + this._assessmentResultsList.selectedOptionId = '0'; + + } + } + + private createDatabaseCount(): azdata.TextComponent { + this._databaseCount = this._view.modelBuilder.text().withProps({ + CSSStyles: { + ...styles.BOLD_NOTE_CSS, + 'margin': '0px 15px 0px 15px' + } + }).component(); + return this._databaseCount; + } + + private createDatabaseComponent(dbs: string[]): azdata.DivContainer { + + this._databaseTable = this._view.modelBuilder.declarativeTable().withProps( + { + ariaLabel: constants.DATABASES_TABLE_TILE, + enableRowSelection: true, + width: 230, + CSSStyles: { + 'table-layout': 'fixed' + }, + columns: [ + { + displayName: '', + valueType: azdata.DeclarativeDataType.boolean, + width: 20, + isReadOnly: false, + showCheckAll: true, + headerCssStyles: headerLeft, + }, + { + displayName: constants.DATABASE, + // undo when bug #16445 is fixed + // valueType: azdata.DeclarativeDataType.component, + valueType: azdata.DeclarativeDataType.string, + width: 160, + isReadOnly: true, + headerCssStyles: headerLeft + }, + { + displayName: constants.ISSUES, + valueType: azdata.DeclarativeDataType.string, + width: 50, + isReadOnly: true, + headerCssStyles: headerRight, + } + ] + } + ).component(); + + this._disposables.push(this._databaseTable.onDataChanged(async () => { + await this.updateValuesOnSelection(); + })); + + const tableContainer = this._view.modelBuilder.divContainer().withItems([this._databaseTable]).withProps({ + width: '100%', + CSSStyles: { + 'margin': '0px 15px 0px 15px' + } + }).component(); + return tableContainer; + } + + private async updateValuesOnSelection() { + await this._databaseCount.updateProperties({ + 'value': constants.DATABASES(this.selectedDbs()?.length, this._model._databasesForAssessment?.length) + }); + } + + public selectedDbs(): string[] { + let result: string[] = []; + this._databaseTable.dataValues?.forEach((arr, index) => { + if (arr[0].value === true) { + result.push(this._dbNames[index]); + } + }); + return result; + } + + public async initialize(): Promise { + let instanceTableValues: azdata.DeclarativeTableCellValue[][] = []; + this._databaseTableValues = []; + this._dbNames = this._model._databasesForAssessment; + this._serverName = (await getSourceConnectionProfile()).serverName; + + // pre-select the entire list + const selectedDbs = this._dbNames.filter(db => this._model._databasesForAssessment.includes(db)); + + if (this._targetType === MigrationTargetType.SQLVM || !this._model._assessmentResults) { + instanceTableValues = [[ + { + value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName), + style: styleLeft + }, + { + value: '0', + style: styleRight + } + ]]; + this._dbNames.forEach((db) => { + this._databaseTableValues.push([ + { + value: selectedDbs.includes(db), + style: styleLeft + }, + { + value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, db), + style: styleLeft + }, + { + value: '0', + style: styleRight + } + ]); + }); + } else { + instanceTableValues = [[ + { + value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName), + style: styleLeft + }, + { + value: this._model._assessmentResults?.issues?.filter(issue => issue.appliesToMigrationTargetPlatform === this._targetType).length, + style: styleRight + } + ]]; + this._model._assessmentResults?.databaseAssessments + .sort((db1, db2) => db2.issues?.length - db1.issues?.length); + + // Reset the dbName list so that it is in sync with the table + this._dbNames = this._model._assessmentResults?.databaseAssessments.map(da => da.name); + this._model._assessmentResults?.databaseAssessments.forEach((db) => { + let selectable = true; + if (db.issues.find(issue => issue.databaseRestoreFails && issue.appliesToMigrationTargetPlatform === this._targetType)) { + selectable = false; + } + this._databaseTableValues.push([ + { + value: selectedDbs.includes(db.name) && selectable, + style: styleLeft, + enabled: selectable + }, + { + value: this.createIconTextCell((selectable) ? IconPathHelper.sqlDatabaseLogo : IconPathHelper.sqlDatabaseWarningLogo, db.name), + style: styleLeft + }, + { + value: db.issues.filter(v => v.appliesToMigrationTargetPlatform === this._targetType)?.length, + style: styleRight + } + ]); + }); + } + await this._instanceTable.setDataValues(instanceTableValues); + + this._databaseTableValues = selectDatabasesFromList(this._model._databasesForMigration, this._databaseTableValues); + await this._databaseTable.setDataValues(this._databaseTableValues); + await this.updateValuesOnSelection(); + this._databaseCount.value = constants.DATABASES(0, this._model._databasesForAssessment?.length); + } + + private createIconTextCell(icon: IconPath, text: string): string { + return text; + } + +}