mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-18 09:35:39 -05:00
537 lines
18 KiB
TypeScript
537 lines
18 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { ExecutionPlanPropertiesViewBase, PropertiesSortType } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase';
|
|
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
|
import * as azdata from 'azdata';
|
|
import { localize } from 'vs/nls';
|
|
import { iconCssFormatter, textFormatter } from 'sql/base/browser/ui/table/formatters';
|
|
import { isString } from 'vs/base/common/types';
|
|
import { removeLineBreaks } from 'sql/base/common/strings';
|
|
import * as DOM from 'vs/base/browser/dom';
|
|
import { InternalExecutionPlanElement } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
|
|
import { executionPlanComparisonPropertiesDifferent } from 'sql/workbench/contrib/executionPlan/browser/constants';
|
|
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
|
|
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
import { Codicon } from 'vs/base/common/codicons';
|
|
import { deepClone } from 'vs/base/common/objects';
|
|
|
|
export enum ExecutionPlanCompareOrientation {
|
|
Horizontal = 'horizontal',
|
|
Vertical = 'vertical'
|
|
}
|
|
|
|
function getTopOperationLabel(target: string): string {
|
|
return localize('nodePropertyViewTopOperation', 'Top operation: {0}', target);
|
|
}
|
|
|
|
function getBottomOperationLabel(target: string): string {
|
|
return localize('nodePropertyViewBottomOperation', 'Bottom operation: {0}', target);
|
|
}
|
|
|
|
function getLeftOperationLabel(target: string): string {
|
|
return localize('nodePropertyViewLeftOperation', 'Left operation: {0}', target);
|
|
}
|
|
|
|
function getRightOperationLabel(target: string): string {
|
|
return localize('nodePropertyViewRightOperation', 'Right operation: {0}', target);
|
|
}
|
|
|
|
const notEqualTitle = localize('nodePropertyViewNameNotEqualTitle', 'Not equal to');
|
|
const lessThanTitle = localize('nodePropertyViewNameLessThanTitle', 'Less than');
|
|
const greaterThanTitle = localize('nodePropertyViewNameGreaterThanTitle', 'Greater than');
|
|
const equivalentPropertiesRowHeader = localize('nodePropertyViewNameEquivalentPropertiesRowHeader', 'Equivalent Properties');
|
|
const topTitleColumnHeader = localize('nodePropertyViewNameValueColumnTopHeader', "Value (Top Plan)");
|
|
const leftTitleColumnHeader = localize('nodePropertyViewNameValueColumnLeftHeader', "Value (Left Plan)");
|
|
const rightTitleColumnHeader = localize('nodePropertyViewNameValueColumnRightHeader', "Value (Right Plan)");
|
|
const bottomTitleColumnHeader = localize('nodePropertyViewNameValueColumnBottomHeader', "Value (Bottom Plan)");
|
|
|
|
export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanPropertiesViewBase {
|
|
private _model: ExecutionPlanComparisonPropertiesViewModel;
|
|
private _primaryContainer: HTMLElement;
|
|
private _secondaryContainer: HTMLElement;
|
|
private _orientation: ExecutionPlanCompareOrientation = ExecutionPlanCompareOrientation.Horizontal;
|
|
private _primaryTarget: string;
|
|
private _secondaryTarget: string;
|
|
|
|
public constructor(
|
|
parentContainer: HTMLElement,
|
|
@IThemeService themeService: IThemeService,
|
|
@IInstantiationService instantiationService: IInstantiationService,
|
|
@IContextMenuService contextMenuService: IContextMenuService,
|
|
@IContextViewService contextViewService: IContextViewService
|
|
) {
|
|
super(parentContainer, themeService, instantiationService, contextMenuService, contextViewService);
|
|
this._model = <ExecutionPlanComparisonPropertiesViewModel>{};
|
|
this._parentContainer.style.display = 'none';
|
|
const header = DOM.$('.compare-operation-name');
|
|
|
|
this._primaryContainer = DOM.$('.compare-operation-name-text');
|
|
header.appendChild(this._primaryContainer);
|
|
|
|
this._secondaryContainer = DOM.$('.compare-operation-name-text');
|
|
header.appendChild(this._secondaryContainer);
|
|
|
|
this.setHeader(header);
|
|
}
|
|
|
|
public setPrimaryElement(e: InternalExecutionPlanElement): void {
|
|
this._model.primaryElement = e;
|
|
if ((<azdata.executionPlan.ExecutionPlanNode>e).name) {
|
|
this._primaryTarget = removeLineBreaks((<azdata.executionPlan.ExecutionPlanNode>e).name);
|
|
} else {
|
|
this._primaryTarget = localize('executionPlanPropertiesEdgeOperationName', "Edge");
|
|
}
|
|
|
|
const primaryTitleText = this._orientation === ExecutionPlanCompareOrientation.Horizontal
|
|
? getTopOperationLabel(this._primaryTarget)
|
|
: getLeftOperationLabel(this._primaryTarget);
|
|
|
|
this._primaryContainer.innerText = primaryTitleText;
|
|
this._primaryContainer.title = primaryTitleText;
|
|
this.refreshPropertiesTable();
|
|
}
|
|
|
|
public setSecondaryElement(e: InternalExecutionPlanElement): void {
|
|
this._model.secondaryElement = e;
|
|
if ((<azdata.executionPlan.ExecutionPlanNode>e)?.name) {
|
|
this._secondaryTarget = removeLineBreaks((<azdata.executionPlan.ExecutionPlanNode>e).name);
|
|
} else {
|
|
this._secondaryTarget = localize('executionPlanPropertiesEdgeOperationName', "Edge");
|
|
}
|
|
|
|
const secondaryTitleText = this._orientation === ExecutionPlanCompareOrientation.Horizontal
|
|
? getBottomOperationLabel(this._secondaryTarget)
|
|
: getRightOperationLabel(this._secondaryTarget);
|
|
|
|
this._secondaryContainer.innerText = secondaryTitleText;
|
|
this._secondaryContainer.title = secondaryTitleText;
|
|
this.refreshPropertiesTable();
|
|
}
|
|
|
|
private updatePropertyContainerTitles(): void {
|
|
const [primaryTitleText, secondaryTitleText] = this._orientation === ExecutionPlanCompareOrientation.Horizontal
|
|
? [getTopOperationLabel(this._primaryTarget), getBottomOperationLabel(this._secondaryTarget)]
|
|
: [getLeftOperationLabel(this._primaryTarget), getRightOperationLabel(this._secondaryTarget)];
|
|
|
|
this._primaryContainer.innerText = primaryTitleText;
|
|
this._primaryContainer.title = primaryTitleText;
|
|
this._secondaryContainer.innerText = secondaryTitleText;
|
|
this._secondaryContainer.title = secondaryTitleText;
|
|
|
|
this.updatePropertiesTableColumnHeaders();
|
|
}
|
|
|
|
public updatePropertiesTableColumnHeaders() {
|
|
const columns: Slick.Column<Slick.SlickData>[] = this.getPropertyTableColumns();
|
|
|
|
this.updateTableColumns(columns);
|
|
}
|
|
|
|
public refreshPropertiesTable() {
|
|
const columns: Slick.Column<Slick.SlickData>[] = this.getPropertyTableColumns();
|
|
|
|
let primaryProps = [];
|
|
if (this._model.primaryElement?.properties) {
|
|
primaryProps = this._model.primaryElement.properties;
|
|
}
|
|
|
|
let secondaryProps = [];
|
|
if (this._model.secondaryElement?.properties) {
|
|
secondaryProps = this._model.secondaryElement.properties;
|
|
}
|
|
|
|
let tableRows = this.convertPropertiesToTableRows(primaryProps, secondaryProps);
|
|
tableRows = this.sortPropertiesByDisplayValueEquivalency(tableRows);
|
|
this.populateTable(columns, tableRows);
|
|
}
|
|
|
|
private getPropertyTableColumns() {
|
|
const columns: Slick.Column<Slick.SlickData>[] = [];
|
|
|
|
if (this._model.primaryElement) {
|
|
columns.push({
|
|
id: 'name',
|
|
name: localize('nodePropertyViewNameNameColumnHeader', "Name"),
|
|
field: 'name',
|
|
width: 200,
|
|
headerCssClass: 'prop-table-header',
|
|
formatter: textFormatter
|
|
});
|
|
columns.push({
|
|
id: 'value1',
|
|
name: getPropertyViewNameValueColumnTopHeaderForOrientation(this._orientation),
|
|
field: 'primary',
|
|
width: 150,
|
|
headerCssClass: 'prop-table-header',
|
|
formatter: textFormatter
|
|
});
|
|
}
|
|
if (this._model.secondaryElement) {
|
|
columns.push({
|
|
id: 'comparison',
|
|
name: '',
|
|
field: 'icon',
|
|
width: 40,
|
|
headerCssClass: 'prop-table-header',
|
|
formatter: iconCssFormatter
|
|
});
|
|
|
|
columns.push({
|
|
id: 'value2',
|
|
name: getPropertyViewNameValueColumnBottomHeaderForOrientation(this._orientation),
|
|
field: 'secondary',
|
|
width: 150,
|
|
headerCssClass: 'prop-table-header',
|
|
formatter: textFormatter
|
|
});
|
|
}
|
|
return columns;
|
|
}
|
|
|
|
public sortPropertiesAlphabetically(props: Map<string, TablePropertiesMapEntry>): Map<string, TablePropertiesMapEntry> {
|
|
return new Map([...props.entries()].sort((a, b) => {
|
|
if (!a[1]?.name && !b[1]?.name) {
|
|
return 0;
|
|
} else if (!a[1]?.name) {
|
|
return -1;
|
|
} else if (!b[1]?.name) {
|
|
return 1;
|
|
} else {
|
|
return a[1].name.localeCompare(b[1].name);
|
|
}
|
|
}));
|
|
}
|
|
|
|
public sortPropertiesByImportance(props: Map<string, TablePropertiesMapEntry>): Map<string, TablePropertiesMapEntry> {
|
|
return new Map([...props.entries()].sort((a, b) => {
|
|
if (!a[1]?.displayOrder && !b[1]?.displayOrder) {
|
|
return 0;
|
|
}
|
|
else if (!a[1]?.displayOrder) {
|
|
return -1;
|
|
}
|
|
else if (!b[1]?.displayOrder) {
|
|
return 1;
|
|
}
|
|
else {
|
|
return a[1].displayOrder - b[1].displayOrder;
|
|
}
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* This method will sort properties by having those with different values appear at the top,
|
|
* and similar values appearing at the bottom of the table.
|
|
*
|
|
* An example of this sort of sorting looks like this:
|
|
*
|
|
* Name Value (Top plan) Value (Bottom Plan)
|
|
* -------------------------------------------------------------------------
|
|
* Compile Time 38 37 <unequal>
|
|
* CompileCpu 38 37 <unequal>
|
|
* CompileMemory 5816 6424 <unequal>
|
|
* Estimated Number of Rows 1000 1000 <equal>
|
|
* Optimization Level FULL FULL <equal>
|
|
* RetrievedFromCache false false <equal>
|
|
*
|
|
* @param rows An array of TableRows that contains all the properties that will be organized.
|
|
* @returns A new array of TableRows with unequal values appearing at the top and equal values appearing at the bottom.
|
|
*/
|
|
public sortPropertiesByDisplayValueEquivalency(rows: TableRow[]): TableRow[] {
|
|
const [unequalPropertyRows, equalPropertyRows] = this.splitEqualFromUnequalProperties(rows);
|
|
|
|
const organizedPropertyRows: TableRow[] = [...unequalPropertyRows];
|
|
|
|
if (equalPropertyRows.length > 0) {
|
|
const equivalentPropertiesRow: TableRow = new Object() as TableRow;
|
|
equivalentPropertiesRow.name = equivalentPropertiesRowHeader;
|
|
equivalentPropertiesRow.expanded = false;
|
|
equivalentPropertiesRow.treeGridChildren = equalPropertyRows;
|
|
|
|
organizedPropertyRows.push(equivalentPropertiesRow);
|
|
}
|
|
|
|
return organizedPropertyRows;
|
|
}
|
|
|
|
private splitEqualFromUnequalProperties(rows: TableRow[]): [TableRow[], TableRow[]] {
|
|
const unequalRows: TableRow[] = [];
|
|
const equalRows: TableRow[] = [];
|
|
|
|
for (let row of rows) {
|
|
const treeGridChildren = row.treeGridChildren;
|
|
|
|
if (treeGridChildren?.length > 0) {
|
|
const [unequalSubRows, equalSubRows] = this.splitEqualFromUnequalProperties(treeGridChildren);
|
|
|
|
if (unequalSubRows.length > 0) {
|
|
const currentRow = deepClone(row);
|
|
currentRow.treeGridChildren = unequalSubRows;
|
|
currentRow.expanded = true;
|
|
|
|
currentRow.icon = {
|
|
iconCssClass: executionPlanComparisonPropertiesDifferent,
|
|
title: notEqualTitle
|
|
};
|
|
|
|
unequalRows.push(currentRow);
|
|
}
|
|
|
|
if (equalSubRows.length > 0) {
|
|
const currentRow = deepClone(row);
|
|
currentRow.treeGridChildren = equalSubRows;
|
|
|
|
equalRows.push(currentRow);
|
|
}
|
|
}
|
|
else {
|
|
const primary = row.primary;
|
|
const secondary = row.secondary;
|
|
|
|
if (primary && secondary && primary.text === secondary.title) {
|
|
equalRows.push(row);
|
|
}
|
|
else {
|
|
unequalRows.push(row);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [unequalRows, equalRows];
|
|
}
|
|
|
|
public sortPropertiesReverseAlphabetically(props: Map<string, TablePropertiesMapEntry>): Map<string, TablePropertiesMapEntry> {
|
|
return new Map([...props.entries()].sort((a, b) => {
|
|
if (!a[1]?.displayOrder && !b[1]?.displayOrder) {
|
|
return 0;
|
|
} else if (!a[1]?.displayOrder) {
|
|
return -1;
|
|
} else if (!b[1]?.displayOrder) {
|
|
return 1;
|
|
} else {
|
|
return b[1].displayOrder - a[1].displayOrder;
|
|
}
|
|
}));
|
|
}
|
|
|
|
private convertPropertiesToTableRows(primaryNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[], secondaryNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): TableRow[] {
|
|
const rows: TableRow[] = [];
|
|
let propertiesMap: Map<string, TablePropertiesMapEntry> = new Map();
|
|
|
|
if (primaryNode) {
|
|
primaryNode.forEach(p => {
|
|
propertiesMap.set(p.name, {
|
|
primaryProp: p,
|
|
secondaryProp: undefined,
|
|
displayOrder: p.displayOrder,
|
|
name: p.name
|
|
});
|
|
});
|
|
}
|
|
|
|
if (secondaryNode) {
|
|
secondaryNode.forEach(p => {
|
|
if (propertiesMap.has(p.name)) {
|
|
propertiesMap.get(p.name).secondaryProp = p;
|
|
} else {
|
|
propertiesMap.set(p.name, {
|
|
primaryProp: undefined,
|
|
secondaryProp: p,
|
|
displayOrder: p.displayOrder,
|
|
name: p.name
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
switch (this.sortType) {
|
|
case PropertiesSortType.DisplayOrder:
|
|
propertiesMap = this.sortPropertiesByImportance(propertiesMap);
|
|
break;
|
|
case PropertiesSortType.Alphabetical:
|
|
propertiesMap = this.sortPropertiesAlphabetically(propertiesMap);
|
|
break;
|
|
case PropertiesSortType.ReverseAlphabetical:
|
|
propertiesMap = this.sortPropertiesReverseAlphabetically(propertiesMap);
|
|
break;
|
|
}
|
|
|
|
propertiesMap.forEach((v, k) => {
|
|
let row: TableRow = new Object() as TableRow;
|
|
row.name = {
|
|
text: k
|
|
};
|
|
|
|
const primaryProp = v.primaryProp;
|
|
const secondaryProp = v.secondaryProp;
|
|
|
|
if (primaryProp && secondaryProp) {
|
|
row.displayOrder = v.primaryProp.displayOrder;
|
|
|
|
let diffIcon = new Object() as DiffIcon;
|
|
if (v.primaryProp.displayValue !== v.secondaryProp.displayValue) {
|
|
switch (v.primaryProp.dataType) {
|
|
case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.Boolean:
|
|
diffIcon.iconClass = executionPlanComparisonPropertiesDifferent;
|
|
diffIcon.title = notEqualTitle;
|
|
break;
|
|
case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.Number:
|
|
if (v.primaryProp.betterValue === sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyBetterValue.None) {
|
|
diffIcon.title = notEqualTitle;
|
|
diffIcon.iconClass = executionPlanComparisonPropertiesDifferent;
|
|
} else {
|
|
diffIcon = (parseFloat(v.primaryProp.displayValue) > parseFloat(v.secondaryProp.displayValue))
|
|
? { iconClass: Codicon.chevronRight.classNames, title: greaterThanTitle }
|
|
: { iconClass: Codicon.chevronLeft.classNames, title: lessThanTitle };
|
|
}
|
|
break;
|
|
case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.String:
|
|
diffIcon.iconClass = executionPlanComparisonPropertiesDifferent;
|
|
diffIcon.title = notEqualTitle;
|
|
break;
|
|
default:
|
|
diffIcon.iconClass = executionPlanComparisonPropertiesDifferent;
|
|
diffIcon.title = notEqualTitle;
|
|
break;
|
|
}
|
|
}
|
|
|
|
row.primary = {
|
|
text: removeLineBreaks(v.primaryProp.displayValue, ' ')
|
|
};
|
|
|
|
row.icon = {
|
|
iconCssClass: diffIcon.iconClass ?? '',
|
|
title: diffIcon.title ?? ''
|
|
};
|
|
|
|
row.secondary = {
|
|
title: removeLineBreaks(v.secondaryProp.displayValue, ' '),
|
|
};
|
|
|
|
if ((primaryProp && !isString(primaryProp.value)) || (secondaryProp && !isString(secondaryProp.value))) {
|
|
const parentRowStyling = ' parent-row-styling';
|
|
|
|
row.name.iconCssClass = !row.name.iconCssClass ? parentRowStyling : row.name.iconCssClass + parentRowStyling;
|
|
row.primary.iconCssClass = !row.primary.iconCssClass ? parentRowStyling : row.primary.iconCssClass + parentRowStyling;
|
|
row.icon.iconCssClass = !row.icon.iconCssClass ? parentRowStyling : row.icon.iconCssClass + parentRowStyling;
|
|
row.secondary.iconCssClass = !row.secondary.iconCssClass ? parentRowStyling : row.secondary.iconCssClass + parentRowStyling;
|
|
}
|
|
|
|
rows.push(row);
|
|
|
|
const topPropValue = isString(primaryProp.value) ? undefined : primaryProp.value;
|
|
const bottomPropValue = isString(secondaryProp.value) ? undefined : secondaryProp.value;
|
|
row.treeGridChildren = this.convertPropertiesToTableRows(topPropValue, bottomPropValue);
|
|
|
|
} else if (primaryProp && !secondaryProp) {
|
|
row.displayOrder = v.primaryProp.displayOrder;
|
|
|
|
row.primary = {
|
|
text: v.primaryProp.displayValue
|
|
};
|
|
|
|
row.icon = {
|
|
iconCssClass: executionPlanComparisonPropertiesDifferent,
|
|
title: notEqualTitle
|
|
};
|
|
|
|
rows.push(row);
|
|
|
|
if (!isString(primaryProp.value)) {
|
|
row.name.iconCssClass += ` parent-row-styling`;
|
|
row.primary.iconCssClass += ` parent-row-styling`;
|
|
row.treeGridChildren = this.convertPropertiesToTableRows(primaryProp.value, undefined);
|
|
}
|
|
} else if (!primaryProp && secondaryProp) {
|
|
row.displayOrder = v.secondaryProp.displayOrder;
|
|
|
|
row.secondary = {
|
|
title: v.secondaryProp.displayValue,
|
|
iconCssClass: ''
|
|
};
|
|
|
|
row.icon = {
|
|
iconCssClass: executionPlanComparisonPropertiesDifferent,
|
|
title: notEqualTitle
|
|
};
|
|
|
|
rows.push(row);
|
|
|
|
if (!isString(secondaryProp.value)) {
|
|
row.name.iconCssClass += ` parent-row-styling`;
|
|
row.secondary.iconCssClass += ` parent-row-styling`;
|
|
row.treeGridChildren = this.convertPropertiesToTableRows(undefined, secondaryProp.value);
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
return rows;
|
|
}
|
|
|
|
set orientation(value: ExecutionPlanCompareOrientation) {
|
|
if (this._orientation === value) {
|
|
return;
|
|
}
|
|
|
|
this._orientation = value;
|
|
this.updatePropertyContainerTitles();
|
|
}
|
|
}
|
|
|
|
function getPropertyViewNameValueColumnTopHeaderForOrientation(orientation: ExecutionPlanCompareOrientation): string {
|
|
if (orientation === ExecutionPlanCompareOrientation.Horizontal) {
|
|
return topTitleColumnHeader;
|
|
}
|
|
else {
|
|
return leftTitleColumnHeader;
|
|
}
|
|
}
|
|
|
|
function getPropertyViewNameValueColumnBottomHeaderForOrientation(orientation: ExecutionPlanCompareOrientation): string {
|
|
if (orientation === ExecutionPlanCompareOrientation.Horizontal) {
|
|
return bottomTitleColumnHeader;
|
|
}
|
|
else {
|
|
return rightTitleColumnHeader;
|
|
}
|
|
}
|
|
|
|
export interface ExecutionPlanComparisonPropertiesViewModel {
|
|
primaryElement: InternalExecutionPlanElement,
|
|
secondaryElement: InternalExecutionPlanElement
|
|
}
|
|
|
|
interface TablePropertiesMapEntry {
|
|
primaryProp: azdata.executionPlan.ExecutionPlanGraphElementProperty,
|
|
secondaryProp: azdata.executionPlan.ExecutionPlanGraphElementProperty,
|
|
displayOrder: number,
|
|
name: string
|
|
}
|
|
|
|
interface DiffIcon {
|
|
iconClass: string;
|
|
title: string;
|
|
}
|
|
|
|
interface TableRow extends Slick.SlickData {
|
|
displayOrder: number;
|
|
icon: RowContent;
|
|
name: RowContent | string;
|
|
primary: RowContent;
|
|
secondary: RowContent;
|
|
expanded: boolean;
|
|
treeGridChildren: TableRow[];
|
|
}
|
|
|
|
interface RowContent {
|
|
iconCssClass?: string;
|
|
text?: string;
|
|
title?: string;
|
|
}
|