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;