Adding properties view and action bar to graph and fixing styling. (#18185)

* Adding properties view and action bar to graph.

* Open Graph File

* replacing innerhtml with  innertext

* Fixing floating promises

* Fixed typo

* renaming

* Fixing hardcoded colors and comments

* Removing todo and hardcoded colors

* renaming method

* removed unused contract

* Fixed path in comment

* converting div to button

* adding checks to table width and height setter

* Make method name more meaningful

* adding method return types

* concising repeated logic

* removing unused styling

* better sorting logic

* Fixing graph parsing
Renaming some stuff
Implementing IDisposable

* Fixing bad props logic

* Fixed image loading issue

* Removing hardcoded colors

* Adding comments to localize and handling undefined cases in sort

* Changed ch to px

* moving util function to strings
This commit is contained in:
Aasim Khan
2022-02-02 14:17:01 -08:00
committed by GitHub
parent de5090e47a
commit e40c31559a
15 changed files with 1222 additions and 525 deletions

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as azdata from 'azdata';
export class QueryPlan2State {
graphs: azdata.ExecutionPlanGraph[] = [];
clearQueryPlan2State() {
this.graphs = [];
}
}

View File

@@ -12,6 +12,7 @@ import { QueryPlanState } from 'sql/workbench/common/editor/query/queryPlanState
import { GridPanelState } from 'sql/workbench/common/editor/query/gridTableState';
import { QueryModelViewState } from 'sql/workbench/common/editor/query/modelViewState';
import { URI } from 'vs/base/common/uri';
import { QueryPlan2State } from 'sql/workbench/common/editor/query/queryPlan2State';
export class ResultsViewState {
public readonly gridPanelState: GridPanelState = new GridPanelState();
@@ -19,6 +20,7 @@ export class ResultsViewState {
public readonly queryPlanState: QueryPlanState = new QueryPlanState();
public readonly topOperationsState = new TopOperationsState();
public readonly dynamicModelViewTabsState: Map<string, QueryModelViewState> = new Map<string, QueryModelViewState>();
public readonly queryPlan2State: QueryPlan2State = new QueryPlan2State();
public activeTab?: string;
public readonly visibleTabs: Set<string> = new Set<string>();
@@ -27,6 +29,7 @@ export class ResultsViewState {
this.gridPanelState.dispose();
this.chartState.dispose();
this.queryPlanState.dispose();
this.queryPlan2State.clearQueryPlan2State();
this.dynamicModelViewTabsState.forEach((state: QueryModelViewState, identifier: string) => {
state.dispose();
});

View File

@@ -185,7 +185,7 @@ export class QueryResultsView extends Disposable {
this._panelView = this._register(new TabbedPanel(container, { showHeaderWhenSingleView: true }));
this._register(attachTabbedPanelStyler(this._panelView, themeService));
this.qpTab = this._register(new QueryPlanTab());
this.qp2Tab = this._register(new QueryPlan2Tab());
this.qp2Tab = this._register(this.instantiationService.createInstance(QueryPlan2Tab));
this.topOperationsTab = this._register(new TopOperationsTab(instantiationService));
this._panelView.pushTab(this.resultsTab);
@@ -254,6 +254,8 @@ export class QueryResultsView extends Disposable {
if (!this.input.state.visibleTabs.has(this.qp2Tab.identifier)) {
this.showPlan2();
}
// Adding graph to state and tab as they become available
this.input.state.queryPlan2State.graphs.push(...e.planGraphs);
this.qp2Tab.view.addGraphs(e.planGraphs);
}
}));
@@ -334,6 +336,7 @@ export class QueryResultsView extends Disposable {
if (input) {
this.resultsTab.view.state = input.state.gridPanelState;
this.qpTab.view.setState(input.state.queryPlanState);
this.qp2Tab.view.addGraphs(input.state.queryPlan2State.graphs);
this.topOperationsTab.view.setState(input.state.topOperationsState);
this.chartTab.view.state = input.state.chartState;
this.dynamicModelViewTabs.forEach((dynamicTab: QueryModelViewTab) => {
@@ -454,6 +457,7 @@ export class QueryResultsView extends Disposable {
public hidePlan2() {
if (this._panelView.contains(this.qp2Tab)) {
this.qp2Tab.clear();
this.input.state.queryPlan2State.clearQueryPlan2State();
this._panelView.removeTab(this.qp2Tab.identifier);
}
}

View File

@@ -1,23 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { QueryPlan2 } from 'sql/workbench/contrib/queryplan2/browser/queryPlan';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { localize } from 'vs/nls';
export class PropertiesAction extends Action {
public static ID = 'qp.propertiesAction';
public static LABEL = localize('queryPlanPropertiesActionLabel', "Properties");
constructor() {
super(PropertiesAction.ID, PropertiesAction.LABEL, Codicon.listUnordered.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
context.propContainer.style.visibility = context.propContainer.style.visibility === 'visible' ? 'hidden' : 'visible';
}
}

View File

@@ -0,0 +1,250 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
let imageBasePath = require.toUrl('./images/icons/');
export let queryPlanNodeIconPaths =
{
// generic icons
iteratorCatchAll: imageBasePath + 'iterator_catch_all.png',
cursorCatchAll: imageBasePath + 'cursor_catch_all.png',
languageConstructCatchAll: imageBasePath + 'language_construct_catch_all.png',
// operator icons
adaptiveJoin: imageBasePath + 'adaptive_join.png',
assert: imageBasePath + 'assert.png',
bitmap: imageBasePath + 'bitmap.png',
clusteredIndexDelete: imageBasePath + 'clustered_index_delete.png',
clusteredIndexInsert: imageBasePath + 'clustered_index_insert.png',
clusteredIndexScan: imageBasePath + 'clustered_index_scan.png',
clusteredIndexSeek: imageBasePath + 'clustered_index_seek.png',
clusteredIndexUpdate: imageBasePath + 'clustered_index_update.png',
clusteredIndexMerge: imageBasePath + 'clustered_index_merge.png',
clusteredUpdate: imageBasePath + 'clustered_update.png',
collapse: imageBasePath + 'collapse.png',
computeScalar: imageBasePath + 'compute_scalar.png',
concatenation: imageBasePath + 'concatenation.png',
constantScan: imageBasePath + 'constant_scan.png',
deletedScan: imageBasePath + 'deleted_scan.png',
filter: imageBasePath + 'filter.png',
hashMatch: imageBasePath + 'hash_match.png',
indexDelete: imageBasePath + 'index_delete.png',
indexInsert: imageBasePath + 'index_insert.png',
indexScan: imageBasePath + 'index_scan.png',
columnstoreIndexDelete: imageBasePath + 'columnstore_index_delete.png',
columnstoreIndexInsert: imageBasePath + 'columnstore_index_insert.png',
columnstoreIndexMerge: imageBasePath + 'columnstore_index_merge.png',
columnstoreIndexScan: imageBasePath + 'columnstore_index_scan.png',
columnstoreIndexUpdate: imageBasePath + 'columnstore_index_update.png',
indexSeek: imageBasePath + 'index_seek.png',
indexSpool: imageBasePath + 'index_spool.png',
indexUpdate: imageBasePath + 'index_update.png',
insertedScan: imageBasePath + 'inserted_scan.png',
logRowScan: imageBasePath + 'log_row_scan.png',
mergeInterval: imageBasePath + 'merge_interval.png',
mergeJoin: imageBasePath + 'merge_join.png',
nestedLoops: imageBasePath + 'nested_loops.png',
parallelism: imageBasePath + 'parallelism.png',
parameterTableScan: imageBasePath + 'parameter_table_scan.png',
print: imageBasePath + 'print.png',
rank: imageBasePath + 'rank.png',
foreignKeyReferencesCheck: imageBasePath + 'foreign_key_references_check.png',
remoteDelete: imageBasePath + 'remote_delete.png',
remoteIndexScan: imageBasePath + 'remote_index_scan.png',
remoteIndexSeek: imageBasePath + 'remote_index_seek.png',
remoteInsert: imageBasePath + 'remote_insert.png',
remoteQuery: imageBasePath + 'remote_query.png',
remoteScan: imageBasePath + 'remote_scan.png',
remoteUpdate: imageBasePath + 'remote_update.png',
ridLookup: imageBasePath + 'rid_lookup.png',
rowCountSpool: imageBasePath + 'row_count_spool.png',
segment: imageBasePath + 'segment.png',
sequence: imageBasePath + 'sequence.png',
sequenceProject: imageBasePath + 'sequence_project.png',
sort: imageBasePath + 'sort.png',
split: imageBasePath + 'split.png',
streamAggregate: imageBasePath + 'stream_aggregate.png',
switchStatement: imageBasePath + 'switch.png',
tableValuedFunction: imageBasePath + 'table_valued_function.png',
tableDelete: imageBasePath + 'table_delete.png',
tableInsert: imageBasePath + 'table_insert.png',
tableScan: imageBasePath + 'table_scan.png',
tableSpool: imageBasePath + 'table_spool.png',
tableUpdate: imageBasePath + 'table_update.png',
tableMerge: imageBasePath + 'table_merge.png',
tfp: imageBasePath + 'predict.png',
top: imageBasePath + 'top.png',
udx: imageBasePath + 'udx.png',
batchHashTableBuild: imageBasePath + 'batch_hash_table_build.png',
windowSpool: imageBasePath + 'table_spool.png',
windowAggregate: imageBasePath + 'window_aggregate.png',
// cursor operators
fetchQuery: imageBasePath + 'fetch_query.png',
populateQuery: imageBasePath + 'population_query.png',
refreshQuery: imageBasePath + 'refresh_query.png',
// shiloh operators
result: imageBasePath + 'result.png',
aggregate: imageBasePath + 'aggregate.png',
assign: imageBasePath + 'assign.png',
arithmeticExpression: imageBasePath + 'arithmetic_expression.png',
bookmarkLookup: imageBasePath + 'bookmark_lookup.png',
convert: imageBasePath + 'convert.png',
declare: imageBasePath + 'declare.png',
deleteOperator: imageBasePath + 'delete.png',
dynamic: imageBasePath + 'dynamic.png',
hashMatchRoot: imageBasePath + 'hash_match_root.png',
hashMatchTeam: imageBasePath + 'hash_match_team.png',
ifOperator: imageBasePath + 'if.png',
insert: imageBasePath + 'insert.png',
intrinsic: imageBasePath + 'intrinsic.png',
keyset: imageBasePath + 'keyset.png',
locate: imageBasePath + 'locate.png',
populationQuery: imageBasePath + 'population_query.png',
setFunction: imageBasePath + 'set_function.png',
snapshot: imageBasePath + 'snapshot.png',
spool: imageBasePath + 'spool.png',
tsql: imageBasePath + 'sql.png',
update: imageBasePath + 'update.png',
// fake operators
keyLookup: imageBasePath + 'bookmark_lookup.png',
// PDW operators
apply: imageBasePath + 'apply.png',
broadcast: imageBasePath + 'broadcast.png',
computeToControlNode: imageBasePath + 'compute_to_control_node.png',
constTableGet: imageBasePath + 'const_table_get.png',
controlToComputeNodes: imageBasePath + 'control_to_compute_nodes.png',
externalBroadcast: imageBasePath + 'external_broadcast.png',
externalExport: imageBasePath + 'external_export.png',
externalLocalStreaming: imageBasePath + 'external_local_streaming.png',
externalRoundRobin: imageBasePath + 'external_round_robin.png',
externalShuffle: imageBasePath + 'external_shuffle.png',
get: imageBasePath + 'get.png',
groupByApply: imageBasePath + 'apply.png',
groupByAggregate: imageBasePath + 'group_by_aggregate.png',
join: imageBasePath + 'join.png',
localCube: imageBasePath + 'intrinsic.png',
project: imageBasePath + 'project.png',
shuffle: imageBasePath + 'shuffle.png',
singleSourceRoundRobin: imageBasePath + 'single_source_round_robin.png',
singleSourceShuffle: imageBasePath + 'single_source_shuffle.png',
trim: imageBasePath + 'trim.png',
union: imageBasePath + 'union.png',
unionAll: imageBasePath + 'union_all.png'
};

View File

@@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import type * as azdata from 'azdata';
import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { Table } from 'sql/base/browser/ui/table/table';
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
import { isString } from 'vs/base/common/types';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
export class GraphElementPropertiesView {
// Title bar with close button action
private _propertiesTitle!: HTMLElement;
private _titleText!: HTMLElement;
private _titleActionBarContainer!: HTMLElement;
private _titleActionBar: ActionBar;
// Div that holds the name of the element selected
private _operationName!: HTMLElement;
// Action bar that contains sorting option for the table
private _tableActionBarContainer!: HTMLElement;
private _tableActionBar!: ActionBar;
// Properties table
private _table: Table<Slick.SlickData>;
private _dataView: TableDataView<Slick.SlickData>;
private _data: { [key: string]: string }[];
private _tableContainer!: HTMLElement;
private _actualTable!: HTMLElement;
// Table dimensions.
private _tableWidth = 485;
private _tableHeight = 420;
public constructor(
private _parentContainer: HTMLElement,
private _themeService: IThemeService,
private _model: GraphElementPropertyViewData = <GraphElementPropertyViewData>{}
) {
this._parentContainer.style.display = 'none';
this._propertiesTitle = DOM.$('.title');
this._parentContainer.appendChild(this._propertiesTitle);
this._titleText = DOM.$('h3');
this._titleText.classList.add('text');
this._titleText.innerText = localize('nodePropertyViewTitle', "Properties");
this._propertiesTitle.appendChild(this._titleText);
this._titleActionBarContainer = DOM.$('.action-bar');
this._propertiesTitle.appendChild(this._titleActionBarContainer);
this._titleActionBar = new ActionBar(this._titleActionBarContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._titleActionBar.pushAction([new ClosePropertyViewAction()], { icon: true, label: false });
this._operationName = DOM.$('h3');
this._operationName.classList.add('operation-name');
this._parentContainer.appendChild(this._operationName);
this._tableActionBarContainer = DOM.$('.table-action-bar');
this._parentContainer.appendChild(this._tableActionBarContainer);
this._tableActionBar = new ActionBar(this._tableActionBarContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._tableActionBar.pushAction([new SortPropertiesByDisplayOrderAction(), new SortPropertiesAlphabeticallyAction()], { icon: true, label: false });
this._tableContainer = DOM.$('.table-container');
this._parentContainer.appendChild(this._tableContainer);
this._actualTable = DOM.$('.table');
this._tableContainer.appendChild(this._actualTable);
this._dataView = new TableDataView();
this._data = [];
const columns: Slick.Column<Slick.SlickData>[] = [
{
id: 'name',
name: localize('nodePropertyViewNameNameColumnHeader', "Name"),
field: 'name',
width: 250,
editor: Slick.Editors.Text,
headerCssClass: 'prop-table-header'
},
{
id: 'value',
name: localize('nodePropertyViewNameValueColumnHeader', "Value"),
field: 'value',
width: 250,
editor: Slick.Editors.Text,
headerCssClass: 'prop-table-header'
}
];
this._table = new Table(this._actualTable, {
dataProvider: this._dataView, columns: columns
}, {
rowHeight: RESULTS_GRID_DEFAULTS.rowHeight,
forceFitColumns: true,
defaultColumnWidth: 120
});
attachTableStyler(this._table, this._themeService);
}
public set graphElement(element: azdata.ExecutionPlanNode | azdata.ExecutionPlanEdge) {
this._model.graphElement = element;
this.sortPropertiesByImportance();
this.renderView();
}
public sortPropertiesAlphabetically(): void {
this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => {
if (!a?.name && !b?.name) {
return 0;
} else if (!a?.name) {
return -1;
} else if (!b?.name) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
this.renderView();
}
public sortPropertiesByImportance(): void {
this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => {
if (!a?.displayOrder && !b?.displayOrder) {
return 0;
} else if (!a?.displayOrder) {
return -1;
} else if (!b?.displayOrder) {
return 1;
} else {
return a.displayOrder - b.displayOrder;
}
});
this.renderView();
}
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
this.renderView();
}
}
public get tableHeight(): number {
return this._tableHeight;
}
public set tableWidth(value: number) {
if (this._tableWidth !== value) {
this._tableWidth = value;
this.renderView();
}
}
public get tableWidth(): number {
return this._tableWidth;
}
private renderView(): void {
if (this._model.graphElement) {
const nodeName = (<azdata.ExecutionPlanNode>this._model.graphElement).name;
this._operationName.innerText = nodeName ?? localize('queryPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
}
this._tableContainer.scrollTo(0, 0);
this._dataView.clear();
this._data = this.convertPropertiesToTableRows(this._model.graphElement.properties, -1, 0);
this._dataView.push(this._data);
this._table.setData(this._dataView);
this._table.autosizeColumns();
this._table.updateRowCount();
this._table.layout(new DOM.Dimension(this._tableWidth, this._tableHeight));
this._table.resizeCanvas();
}
private convertPropertiesToTableRows(props: azdata.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] {
if (!props) {
return rows;
}
props.forEach((p, i) => {
let row = {};
row['name'] = '\t'.repeat(indent) + p.name;
row['parent'] = parentIndex;
rows.push(row);
if (!isString(p.value)) {
row['value'] = '';
this.convertPropertiesToTableRows(p.value, rows.length - 1, indent + 2, rows);
} else {
row['value'] = p.value;
}
});
return rows;
}
public toggleVisibility(): void {
this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none';
this.renderView();
}
}
export interface GraphElementPropertyViewData {
graphElement: azdata.ExecutionPlanNode | azdata.ExecutionPlanEdge;
}
export class ClosePropertyViewAction extends Action {
public static ID = 'qp.propertiesView.close';
public static LABEL = localize('queryPlanPropertyViewClose', "Close");
constructor() {
super(ClosePropertyViewAction.ID, ClosePropertyViewAction.LABEL, Codicon.close.classNames);
}
public override async run(context: GraphElementPropertiesView): Promise<void> {
context.toggleVisibility();
}
}
export class SortPropertiesAlphabeticallyAction extends Action {
public static ID = 'qp.propertiesView.sortByAlphabet';
public static LABEL = localize('queryPlanPropertyViewSortAlphabetically', "Alphabetical");
constructor() {
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, Codicon.sortPrecedence.classNames);
}
public override async run(context: GraphElementPropertiesView): Promise<void> {
context.sortPropertiesAlphabetically();
}
}
export class SortPropertiesByDisplayOrderAction extends Action {
public static ID = 'qp.propertiesView.sortByDisplayOrder';
public static LABEL = localize('queryPlanPropertyViewSortByDisplayOrde', "Categorized");
constructor() {
super(SortPropertiesByDisplayOrderAction.ID, SortPropertiesByDisplayOrderAction.LABEL, Codicon.listOrdered.classNames);
}
public override async run(context: GraphElementPropertiesView): Promise<void> {
context.sortPropertiesByImportance();
}
}

View File

@@ -3,67 +3,213 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.qp-container .query-plan {
/* Styling for the a queryplan container in the tab */
.qps-container .query-plan {
width: 100%;
height: 500px;
display: flex;
overflow: hidden;
}
/* horizontal height resizing sash container that is below a queryplan */
.qps-container .query-plan-sash {
width: 100%;
height: 3px;
position: relative;
}
/*
The actual sash element constructed by code. Important is used here because the width of the sash is fixed.
However we always want it to be the width of the container it is resizing.
*/
.qps-container .query-plan-sash > div {
width: 100% !important;
}
/* Container that contains showplan header, description and graph diagram */
.qps-container .query-plan .plan {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
border: 1px solid;
}
/* Container that contains views made by the action-bar actions */
.qps-container .query-plan .plan .plan-action-container {
display: flex;
flex-direction: column;
position: absolute;
right: 0;
}
/* views created by the action-bar actions */
.qps-container .query-plan .plan .plan-action-container .child {
flex: 0 0 25px;
margin-left: auto;
}
/* Search node action view */
.qps-container .query-plan .plan .plan-action-container .search-node-view {
display: flex;
flex-direction: row;
border: 1px solid;
padding: 5px;
height: auto;
width: 470px;
}
/* input bar styling in search node action view */
.qps-container .query-plan .plan .plan-action-container .search-node-view .search-bar-container{
margin-left: 5px;
}
/* styling for select element in search node action view */
.qps-container .query-plan .plan .plan-action-container .search-node-view .search-bar-container > select{
height: 100%;
}
/* Custom zoom action view */
.qps-container .query-plan .plan .plan-action-container .custom-zoom-view {
display: flex;
flex-direction: row;
border: 1px solid;
padding: 5px;
height: auto;
width: 180px;
}
/* query plan header that contains the relative query cost, query statement and recommendations */
.qps-container .query-plan .plan .header {
font-family: 'Courier New', Courier, monospace;
border-bottom: solid 1px;
font-weight: bolder;
padding-left: 5px;
font-size: 13px;
}
/* each link in query plan recommendations */
.qps-container .query-plan .plan .header .recommendations > a {
width: fit-content;
align-items: left;
text-align: left;
}
/* graph diagram in query plan */
.qps-container .query-plan .plan .diagram {
width: 100%;
height: 100%;
overflow: scroll;
}
/* Properties view in query plan */
.qps-container .query-plan .properties {
flex: 0 0 500px;
overflow: hidden;
width: 500px;
height: 100%;
border: 1px solid;
display: flex;
flex-direction: column;
border: 1px solid;
}
/* Title container of the properties view */
.qps-container .query-plan .properties .title {
line-height: 22px;
height: 22px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
overflow: hidden;
display: flex;
align-items: center;
box-sizing: border-box;
margin-top: 3px;
margin-bottom: 5px;
padding-left: 5px;
border-bottom: solid 1px;
flex: 0 0 25px;
}
/* text in title container of properties view */
.qps-container .query-plan .properties .title .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 11px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
flex: 1;
}
/* action bar in the title container for the properties view. This contains the close icon */
.qps-container .query-plan .properties .title .action-bar {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 11px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
flex: 0 0 30px;
}
/* Operation name styling in the properties view. */
.qps-container .query-plan .properties .operation-name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 13px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
flex: 0 0 25px;
margin-top: 3px;
margin-bottom: 5px;
margin-left: 5px;
}
/* Properties table container in properties view */
.qps-container .query-plan .properties .table-container {
overflow-y: scroll;
flex: 1;
flex-grow: 1;
border-top: 1px solid;
}
.qp-container .query-plan .actionbar-container {
height: 100%;
width: 30px;
position: absolute;
top: 0px;
right: 0px;
/* Action bar for the query plan */
.qps-container .query-plan .action-bar-container {
flex: 0 0 25px;
}
.qp-container .query-plan .properties-container {
position: absolute;
top: 0px;
right: 32px;
height: 100%;
overflow-y: scroll;
background-color: #eeeeee;
width: 510px;
}
.qp-container .prop-table-header {
/* styling for the column headers in the properties table */
.qps-container .properties .table-container .prop-table-header {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 11px;
min-width: 10px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
font-weight: bold;
text-transform: uppercase;
}
.qp-container .properties-header {
.qps-container .properties-header {
font-weight: bold;
padding: 5px;
}
.qp-container .properties-toggle {
.qps-container .properties-toggle {
height: 9px;
width: 9px;
display: inline-block;
}
.qp-container .properties-toggle .expand {
.qps-container .properties-toggle .expand {
background: url(../images/expand.gif) no-repeat center center;
}
.qp-container .properties-toggle .collapse {
.qps-container .properties-toggle .collapse {
background: url(../images/collapse.gif) no-repeat center center;
}
.qp-container .mxTooltip {
-webkit-box-shadow: 3px 3px 12px #C0C0C0;
-moz-box-shadow: 3px 3px 12px #C0C0C0;
box-shadow: 3px 3px 12px #C0C0C0;
background: #FFFFCC;
border-style: solid;
border-width: 1px;
border-color: black;
font-family: Arial;
font-size: 8pt;
position: absolute;
cursor: default;
padding: 4px;
color: black;
z-index: 3;
}

View File

@@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as azdata from 'azdata';
import * as DOM from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { Button } from 'sql/base/browser/ui/button/button';
import { removeLineBreaks } from 'sql/base/common/strings';
export class PlanHeader {
private _graphIndex: number; // Index of the graph in the view
private _relativeCost: number; // Relative cost of the graph to the script
private _graphIndexAndCostContainer: HTMLElement; //Container that holds the graph index and relative cost
private _query: string;
private _queryContainer: HTMLElement; // container that holds query text
private _recommendations: azdata.ExecutionPlanRecommendations[];
private _recommendationsContainer: HTMLElement; // container that holds graph recommendations
public constructor(
private _parentContainer: HTMLElement,
headerData: PlanHeaderData,
@IInstantiationService public readonly _instantiationService: IInstantiationService) {
this._graphIndex = headerData.planIndex;
this._relativeCost = headerData.relativeCost;
this._query = headerData.query;
this._recommendations = headerData.recommendations ?? [];
this._graphIndexAndCostContainer = DOM.$('.index-row');
this._queryContainer = DOM.$('.query-row');
this._recommendationsContainer = DOM.$('.recommendations');
this._parentContainer.appendChild(this._graphIndexAndCostContainer);
this._parentContainer.appendChild(this._queryContainer);
this._parentContainer.appendChild(this._recommendationsContainer);
this.renderGraphIndexAndCost();
this.renderQueryText();
this.renderRecommendations();
}
public set graphIndex(index: number) {
this._graphIndex = index;
this.renderGraphIndexAndCost();
}
public set relativeCost(cost: number) {
this._relativeCost = cost;
this.renderGraphIndexAndCost();
}
public set query(query: string) {
this._query = removeLineBreaks(query);
this.renderQueryText();
}
public set recommendations(recommendations: azdata.ExecutionPlanRecommendations[]) {
recommendations.forEach(r => {
r.displayString = removeLineBreaks(r.displayString);
});
this._recommendations = recommendations;
this.renderRecommendations();
}
private renderGraphIndexAndCost(): void {
if (this._graphIndex && this._relativeCost) {
this._graphIndexAndCostContainer.innerText = localize(
{
key: 'planHeaderIndexAndCost',
comment: [
'{0} is the index of the graph in the execution plan tab',
'{1} is the relative cost in percentage of the graph to the rest of the graphs in execution plan tab '
]
},
"Query {0}: Query cost (relative to the script): {1}%", this._graphIndex, this._relativeCost.toFixed(2));
}
}
private renderQueryText(): void {
this._queryContainer.innerText = this._query;
}
private renderRecommendations(): void {
while (this._recommendationsContainer.firstChild) {
this._recommendationsContainer.removeChild(this._recommendationsContainer.firstChild);
}
this._recommendations.forEach(r => {
const link = new Button(this._recommendationsContainer, {
title: r.displayString,
secondary: true,
});
link.label = r.displayString;
//Enabling on click action for recommendations. It will open the recommendation File
link.onDidClick(e => {
this._instantiationService.invokeFunction(openNewQuery, undefined, r.queryWithDescription, RunQueryOnConnectionMode.none);
});
});
}
}
export interface PlanHeaderData {
planIndex?: number;
relativeCost?: number;
query?: string;
recommendations?: azdata.ExecutionPlanRecommendations[];
}

View File

@@ -6,16 +6,33 @@
import 'vs/css!./media/queryPlan2';
import type * as azdata from 'azdata';
import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { dispose } from 'vs/base/common/lifecycle';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import { PropertiesAction } from 'sql/workbench/contrib/queryplan2/browser/actions/propertiesAction';
import * as azdataGraphModule from 'azdataGraph';
import { escape } from 'sql/base/common/strings';
import { queryPlanNodeIconPaths } from 'sql/workbench/contrib/queryplan2/browser/constants';
import { isString } from 'vs/base/common/types';
import { PlanHeader } from 'sql/workbench/contrib/queryplan2/browser/planHeader';
import { GraphElementPropertiesView } from 'sql/workbench/contrib/queryplan2/browser/graphElementPropertiesView';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { editorBackground, foreground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { ISashEvent, ISashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
let azdataGraph = azdataGraphModule();
export class QueryPlan2Tab implements IPanelTab {
@@ -23,8 +40,10 @@ export class QueryPlan2Tab implements IPanelTab {
public readonly identifier = 'QueryPlan2Tab';
public readonly view: QueryPlan2View;
constructor() {
this.view = new QueryPlan2View();
constructor(
@IInstantiationService instantiationService: IInstantiationService,
) {
this.view = instantiationService.createInstance(QueryPlan2View);
}
public dispose() {
@@ -34,12 +53,18 @@ export class QueryPlan2Tab implements IPanelTab {
public clear() {
this.view.clear();
}
}
export class QueryPlan2View implements IPanelView {
private _qps?: QueryPlan2[] = [];
private _graphs?: azdata.QueryPlanGraph[] = [];
private _container = DOM.$('.qp-container');
private _graphs?: azdata.ExecutionPlanGraph[] = [];
private _container = DOM.$('.qps-container');
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
) {
}
public render(container: HTMLElement): void {
container.appendChild(this._container);
@@ -63,14 +88,16 @@ export class QueryPlan2View implements IPanelView {
DOM.clearNode(this._container);
}
public addGraphs(newGraphs: azdata.QueryPlanGraph[]) {
newGraphs.forEach(g => {
const qp2 = new QueryPlan2(this._container, this._qps.length + 1);
qp2.graph = g;
this._qps.push(qp2);
this._graphs.push(g);
this.updateRelativeCosts();
});
public addGraphs(newGraphs: azdata.ExecutionPlanGraph[] | undefined) {
if (newGraphs) {
newGraphs.forEach(g => {
const qp2 = this.instantiationService.createInstance(QueryPlan2, this._container, this._qps.length + 1);
qp2.graph = g;
this._qps.push(qp2);
this._graphs.push(g);
this.updateRelativeCosts();
});
}
}
private updateRelativeCosts() {
@@ -80,420 +107,130 @@ export class QueryPlan2View implements IPanelView {
if (sum > 0) {
this._qps.forEach(qp => {
qp.relativeCost = ((qp.graph.root.subTreeCost + qp.graph.root.cost) / sum) * 100;
qp.planHeader.relativeCost = ((qp.graph.root.subTreeCost + qp.graph.root.cost) / sum) * 100;
});
}
}
}
export class QueryPlan2 {
private _graph?: azdata.QueryPlanGraph;
private _relativeCost?: globalThis.Text;
private _actionBar: ActionBar;
private _table: Slick.Grid<any>;
private _dataView: Slick.Data.DataView<any>;
private _container: HTMLElement;
private _actionBarContainer: HTMLElement;
private _data: any[];
private _iconMap: any = new Object();
private _iconPaths: any = new Object();
export class QueryPlan2 implements ISashLayoutProvider {
private _graph?: azdata.ExecutionPlanGraph;
public propContainer: HTMLElement;
private _container: HTMLElement;
private _actionBarContainer: HTMLElement;
private _actionBar: ActionBar;
public planHeader: PlanHeader;
private _planContainer: HTMLElement;
private _planHeaderContainer: HTMLElement;
public propertiesView: GraphElementPropertiesView;
private _propContainer: HTMLElement;
private _azdataGraphDiagram: any;
constructor(
parent: HTMLElement,
private _graphIndex: number,
@IInstantiationService public readonly _instantiationService: IInstantiationService,
@IThemeService private readonly _themeService: IThemeService,
@IContextViewService public readonly contextViewService: IContextViewService,
@IUntitledTextEditorService private readonly _untitledEditorService: IUntitledTextEditorService,
@IEditorService private readonly editorService: IEditorService
) {
// parent container for query plan.
this._container = DOM.$('.query-plan');
parent.appendChild(this._container);
const sashContainer = DOM.$('.query-plan-sash');
parent.appendChild(sashContainer);
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL });
let originalHeight = this._container.offsetHeight;
let originalTableHeight = 0;
let change = 0;
sash.onDidStart((e: ISashEvent) => {
originalHeight = this._container.offsetHeight;
originalTableHeight = this.propertiesView.tableHeight;
});
this._actionBarContainer = DOM.$('.actionbar-container');
/**
* Using onDidChange for the smooth resizing of the graph diagram
*/
sash.onDidChange((evt: ISashEvent) => {
change = evt.startY - evt.currentY;
const newHeight = originalHeight - change;
if (newHeight < 200) {
return;
}
this._container.style.height = `${newHeight}px`;
});
/**
* Resizing properties window table only once at the end as it is a heavy operation and worsens the smooth resizing experience
*/
sash.onDidEnd(() => {
this.propertiesView.tableHeight = originalTableHeight - change;
});
this._planContainer = DOM.$('.plan');
this._container.appendChild(this._planContainer);
// container that holds plan header info
this._planHeaderContainer = DOM.$('.header');
this._planContainer.appendChild(this._planHeaderContainer);
this.planHeader = this._instantiationService.createInstance(PlanHeader, this._planHeaderContainer, {
planIndex: this._graphIndex,
});
// container properties
this._propContainer = DOM.$('.properties');
this._container.appendChild(this._propContainer);
this.propertiesView = new GraphElementPropertiesView(this._propContainer, this._themeService);
// container that holds actionbar icons
this._actionBarContainer = DOM.$('.action-bar-container');
this._container.appendChild(this._actionBarContainer);
this._actionBar = new ActionBar(this._actionBarContainer, {
orientation: ActionsOrientation.VERTICAL, context: this
});
this.propContainer = DOM.$('.properties-container');
const propHeader = document.createElement('div');
propHeader.className = 'properties-header';
propHeader.innerText = 'Properties';
this.propContainer.appendChild(propHeader);
this.propContainer.style.visibility = 'hidden';
this._dataView = new Slick.Data.DataView({ inlineFilters: false });
let self = this;
this._data = [];
const TaskNameFormatter = function (row, cell, value, columnDef, dataContext) {
value = escape(value);
const spacer = '<span style="display:inline-block;height:1px;width' + (15 * dataContext['indent']) + 'px"></span>';
const idx = self._dataView.getIdxById(dataContext.id);
if (self._data[idx + 1] && self._data[idx + 1].indent > self._data[idx].indent) {
if (dataContext._collapsed) {
return spacer + '<span class="properties-toggle expand"></span>&nbsp;' + value;
} else {
return spacer + '<span class="properties-toggle collapse"></span>&nbsp;' + value;
}
} else {
return spacer + '<span class="properties-toggle"></span>&nbsp;' + value;
}
};
const columns: Slick.Column<any>[] = [
{
id: 'name',
name: 'Name',
field: 'name',
width: 250,
editor: Slick.Editors.Text,
formatter: TaskNameFormatter,
headerCssClass: 'prop-table-header'
},
{
id: 'value',
name: 'Value',
field: 'propValue',
width: 250,
editor: Slick.Editors.Text,
headerCssClass: 'prop-table-header'
}
];
const options: Slick.GridOptions<any> = {
editable: false,
enableAddRow: false,
enableCellNavigation: true,
autoHeight: true
};
const tableContainer = DOM.$('.table-container');
tableContainer.style.height = '500px';
tableContainer.style.width = '490px';
this.propContainer.appendChild(tableContainer);
this._table = new Slick.Grid(tableContainer, this._dataView, columns, options);
this._table.onClick.subscribe((e: any, args) => {
const item = this._dataView.getItem(args.row);
if (item) {
item._collapsed = !item._collapsed;
this._dataView.updateItem(item.id, item);
}
e.stopImmediatePropagation();
});
this._dataView.setFilter((item) => {
if (item.parent !== null) {
let parent = this._data[item.parent];
while (parent) {
if (parent._collapsed) {
return false;
}
parent = this._data[parent.parent];
}
}
return true;
});
// wire up model events to drive the grid
this._dataView.onRowCountChanged.subscribe((e, args) => {
this._table.updateRowCount();
this._table.render();
});
this._dataView.onRowsChanged.subscribe((e, args) => {
this._table.invalidateRows(args.rows);
this._table.render();
});
const actions = [
new PropertiesAction()
new SaveXml(),
new OpenGraphFile(),
new OpenQueryAction(),
new SearchNodeAction(),
new ZoomInAction(),
new ZoomOutAction(),
new ZoomToFitAction(),
new CustomZoomAction(),
new PropertiesAction(),
];
this._actionBar.push(actions, { icon: true, label: false });
this._actionBar.pushAction(actions, { icon: true, label: false });
this._iconMap['Adaptive_Join_32x.ico'] = 'adaptiveJoin';
this._iconMap['Assert_32x.ico'] = 'assert';
this._iconMap['Bitmap_32x.ico'] = 'bitmap';
this._iconMap['Clustered_index_delete_32x.ico'] = 'clusteredIndexDelete';
this._iconMap['Clustered_index_insert_32x.ico'] = 'ClusteredIndexInsert';
this._iconMap['Clustered_index_scan_32x.ico'] = 'ClusteredIndexScan';
this._iconMap['Clustered_index_seek_32x.ico'] = 'ClusteredIndexSeek';
this._iconMap['Clustered_index_update_32x.ico'] = 'ClusteredIndexUpdate';
this._iconMap['Clustered_index_merge_32x.icoo'] = 'ClusteredIndexMerge';
this._iconMap['Filter_32x.ico'] = 'filter';
this._iconMap['Clustered_index_scan_32x.ico'] = 'clusteredIndexScan';
this._iconMap['Clustered_index_seek_32x.ico'] = 'clusteredIndexSeek';
this._iconMap['Compute_scalar_32x.ico'] = 'computeScalar';
this._iconMap['Concatenation_32x.ico'] = 'concatenation';
this._iconMap['Concatenation_32x.ico'] = 'concatenation';
this._iconMap['Nested_loops_32x.ico'] = 'nestedLoops';
this._iconMap['Result_32x.ico'] = 'result';
this._iconMap['Table_spool_32x.ico'] = 'tableSpool';
this._iconMap['Top_32x.ico'] = 'top';
let imageBasePath = URI.parse(decodeURI(require.toUrl('./images/icons/'))).fsPath;
this._iconPaths =
{
// generic icons
iteratorCatchAll: imageBasePath + 'iterator_catch_all.png',
cursorCatchAll: imageBasePath + 'cursor_catch_all.png',
languageConstructCatchAll: imageBasePath + 'language_construct_catch_all.png',
// operator icons
adaptiveJoin: imageBasePath + 'adaptive_join.png',
assert: imageBasePath + 'assert.png',
bitmap: imageBasePath + 'bitmap.png',
clusteredIndexDelete: imageBasePath + 'clustered_index_delete.png',
clusteredIndexInsert: imageBasePath + 'clustered_index_insert.png',
clusteredIndexScan: imageBasePath + 'clustered_index_scan.png',
clusteredIndexSeek: imageBasePath + 'clustered_index_seek.png',
clusteredIndexUpdate: imageBasePath + 'clustered_index_update.png',
clusteredIndexMerge: imageBasePath + 'clustered_index_merge.png',
clusteredUpdate: imageBasePath + 'clustered_update.png',
collapse: imageBasePath + 'collapse.png',
computeScalar: imageBasePath + 'compute_scalar.png',
concatenation: imageBasePath + 'concatenation.png',
constantScan: imageBasePath + 'constant_scan.png',
deletedScan: imageBasePath + 'deleted_scan.png',
filter: imageBasePath + 'filter.png',
hashMatch: imageBasePath + 'hash_match.png',
indexDelete: imageBasePath + 'index_delete.png',
indexInsert: imageBasePath + 'index_insert.png',
indexScan: imageBasePath + 'index_scan.png',
columnstoreIndexDelete: imageBasePath + 'columnstore_index_delete.png',
columnstoreIndexInsert: imageBasePath + 'columnstore_index_insert.png',
columnstoreIndexMerge: imageBasePath + 'columnstore_index_merge.png',
columnstoreIndexScan: imageBasePath + 'columnstore_index_scan.png',
columnstoreIndexUpdate: imageBasePath + 'columnstore_index_update.png',
indexSeek: imageBasePath + 'index_seek.png',
indexSpool: imageBasePath + 'index_spool.png',
indexUpdate: imageBasePath + 'index_update.png',
insertedScan: imageBasePath + 'inserted_scan.png',
logRowScan: imageBasePath + 'log_row_scan.png',
mergeInterval: imageBasePath + 'merge_interval.png',
mergeJoin: imageBasePath + 'merge_join.png',
nestedLoops: imageBasePath + 'nested_loops.png',
parallelism: imageBasePath + 'parallelism.png',
parameterTableScan: imageBasePath + 'parameter_table_scan.png',
print: imageBasePath + 'print.png',
rank: imageBasePath + 'rank.png',
foreignKeyReferencesCheck: imageBasePath + 'foreign_key_references_check.png',
remoteDelete: imageBasePath + 'remote_delete.png',
remoteIndexScan: imageBasePath + 'remote_index_scan.png',
remoteIndexSeek: imageBasePath + 'remote_index_seek.png',
remoteInsert: imageBasePath + 'remote_insert.png',
remoteQuery: imageBasePath + 'remote_query.png',
remoteScan: imageBasePath + 'remote_scan.png',
remoteUpdate: imageBasePath + 'remote_update.png',
ridLookup: imageBasePath + 'rid_lookup.png',
rowCountSpool: imageBasePath + 'row_count_spool.png',
segment: imageBasePath + 'segment.png',
sequence: imageBasePath + 'sequence.png',
sequenceProject: imageBasePath + 'sequence_project.png',
sort: imageBasePath + 'sort.png',
split: imageBasePath + 'split.png',
streamAggregate: imageBasePath + 'stream_aggregate.png',
switchStatement: imageBasePath + 'switch.png',
tableValuedFunction: imageBasePath + 'table_valued_function.png',
tableDelete: imageBasePath + 'table_delete.png',
tableInsert: imageBasePath + 'table_insert.png',
tableScan: imageBasePath + 'table_scan.png',
tableSpool: imageBasePath + 'table_spool.png',
tableUpdate: imageBasePath + 'table_update.png',
tableMerge: imageBasePath + 'table_merge.png',
tfp: imageBasePath + 'predict.png',
top: imageBasePath + 'top.png',
udx: imageBasePath + 'udx.png',
batchHashTableBuild: imageBasePath + 'batch_hash_table_build.png',
windowSpool: imageBasePath + 'table_spool.png',
windowAggregate: imageBasePath + 'window_aggregate.png',
// cursor operators
fetchQuery: imageBasePath + 'fetch_query.png',
populateQuery: imageBasePath + 'population_query.png',
refreshQuery: imageBasePath + 'refresh_query.png',
// shiloh operators
result: imageBasePath + 'result.png',
aggregate: imageBasePath + 'aggregate.png',
assign: imageBasePath + 'assign.png',
arithmeticExpression: imageBasePath + 'arithmetic_expression.png',
bookmarkLookup: imageBasePath + 'bookmark_lookup.png',
convert: imageBasePath + 'convert.png',
declare: imageBasePath + 'declare.png',
deleteOperator: imageBasePath + 'delete.png',
dynamic: imageBasePath + 'dynamic.png',
hashMatchRoot: imageBasePath + 'hash_match_root.png',
hashMatchTeam: imageBasePath + 'hash_match_team.png',
ifOperator: imageBasePath + 'if.png',
insert: imageBasePath + 'insert.png',
intrinsic: imageBasePath + 'intrinsic.png',
keyset: imageBasePath + 'keyset.png',
locate: imageBasePath + 'locate.png',
populationQuery: imageBasePath + 'population_query.png',
setFunction: imageBasePath + 'set_function.png',
snapshot: imageBasePath + 'snapshot.png',
spool: imageBasePath + 'spool.png',
tsql: imageBasePath + 'sql.png',
update: imageBasePath + 'update.png',
// fake operators
keyLookup: imageBasePath + 'bookmark_lookup.png',
// PDW operators
apply: imageBasePath + 'apply.png',
broadcast: imageBasePath + 'broadcast.png',
computeToControlNode: imageBasePath + 'compute_to_control_node.png',
constTableGet: imageBasePath + 'const_table_get.png',
controlToComputeNodes: imageBasePath + 'control_to_compute_nodes.png',
externalBroadcast: imageBasePath + 'external_broadcast.png',
externalExport: imageBasePath + 'external_export.png',
externalLocalStreaming: imageBasePath + 'external_local_streaming.png',
externalRoundRobin: imageBasePath + 'external_round_robin.png',
externalShuffle: imageBasePath + 'external_shuffle.png',
get: imageBasePath + 'get.png',
groupByApply: imageBasePath + 'apply.png',
groupByAggregate: imageBasePath + 'group_by_aggregate.png',
join: imageBasePath + 'join.png',
localCube: imageBasePath + 'intrinsic.png',
project: imageBasePath + 'project.png',
shuffle: imageBasePath + 'shuffle.png',
singleSourceRoundRobin: imageBasePath + 'single_source_round_robin.png',
singleSourceShuffle: imageBasePath + 'single_source_shuffle.png',
trim: imageBasePath + 'trim.png',
union: imageBasePath + 'union.png',
unionAll: imageBasePath + 'union_all.png'
};
}
private populate(node: azdata.QueryPlanGraphNode, diagramNode: any): any {
getHorizontalSashTop(sash: Sash): number {
return 0;
}
getHorizontalSashLeft?(sash: Sash): number {
return 0;
}
getHorizontalSashWidth?(sash: Sash): number {
return this._container.clientWidth;
}
private populate(node: azdata.ExecutionPlanNode, diagramNode: any): any {
diagramNode.label = node.name;
if (node.properties && node.properties.length > 0) {
diagramNode.metrics = node.properties.map(e => { return { name: e.name, value: e.formattedValue.substring(0, 75) }; });
diagramNode.metrics = this.populateProperties(node.properties);
}
let icon = this._iconMap[node.type];
if (icon) {
diagramNode.icon = icon;
if (node.type) {
diagramNode.icon = node.type;
}
if (node.children) {
@@ -502,65 +239,218 @@ export class QueryPlan2 {
diagramNode.children.push(this.populate(node.children[i], new Object()));
}
}
if (node.edges) {
diagramNode.edges = [];
for (let i = 0; i < node.edges.length; i++) {
diagramNode.edges.push(this.populateEdges(node.edges[i], new Object()));
}
}
return diagramNode;
}
private createPlanDiagram(container: HTMLDivElement): void {
let diagramRoot: any = new Object();
let graphRoot: azdata.QueryPlanGraphNode = this._graph.root;
this.populate(graphRoot, diagramRoot);
private populateEdges(edge: azdata.ExecutionPlanEdge, diagramEdge: any) {
diagramEdge.label = '';
diagramEdge.metrics = this.populateProperties(edge.properties);
diagramEdge.weight = Math.max(0.5, Math.min(0.5 + 0.75 * Math.log10(edge.rowCount), 6));
return diagramEdge;
}
new azdataGraph.azdataQueryPlan(container, diagramRoot, this._iconPaths);
private populateProperties(props: azdata.ExecutionPlanGraphElementProperty[]) {
return props.filter(e => isString(e.value))
.map(e => {
return {
name: e.name,
value: e.value.toString().substring(0, 75)
};
});
}
private createPlanDiagram(container: HTMLElement): void {
let diagramRoot: any = new Object();
let graphRoot: azdata.ExecutionPlanNode = this._graph.root;
this.populate(graphRoot, diagramRoot);
this._azdataGraphDiagram = new azdataGraph.azdataQueryPlan(container, diagramRoot, queryPlanNodeIconPaths);
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const iconBackground = theme.getColor(editorBackground);
if (iconBackground) {
this._azdataGraphDiagram.setIconBackgroundColor(iconBackground);
}
const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) {
this._azdataGraphDiagram.setTextFontColor(iconLabelColor);
}
});
}
public set graph(graph: azdata.QueryPlanGraph | undefined) {
public set graph(graph: azdata.ExecutionPlanGraph | undefined) {
this._graph = graph;
if (this._graph) {
this._container.appendChild(document.createTextNode(localize('queryIndex', "Query {0}: ", this._graphIndex)));
this._relativeCost = document.createTextNode(localize('relativeToTheScript', "(relative to the script):"));
this._container.appendChild(this._relativeCost);
this._container.appendChild(document.createElement('br'));
this._container.appendChild(document.createTextNode(`${graph.query}`));
let diagramContainer = document.createElement('div');
this.planHeader.graphIndex = this._graphIndex;
this.planHeader.query = graph.query;
if (graph.recommendations) {
this.planHeader.recommendations = graph.recommendations;
}
let diagramContainer = DOM.$('.diagram');
this.createPlanDiagram(diagramContainer);
this._container.appendChild(diagramContainer);
this._planContainer.appendChild(diagramContainer);
this._container.appendChild(this.propContainer);
this.setData(this._graph.root.properties);
this._container.appendChild(this._actionBarContainer);
this.propertiesView.graphElement = this._graph.root;
}
}
public get graph(): azdata.QueryPlanGraph | undefined {
public get graph(): azdata.ExecutionPlanGraph | undefined {
return this._graph;
}
public set relativeCost(newCost: number) {
this._relativeCost.nodeValue = localize('relativeToTheScriptWithCost', "(relative to the script): {0}%", newCost.toFixed(2));
public openQuery() {
return this._instantiationService.invokeFunction(openNewQuery, undefined, this.graph.query, RunQueryOnConnectionMode.none).then();
}
public setData(props: azdata.QueryPlanGraphElementProperty[]): void {
this._data = [];
props.forEach((p, i) => {
this._data.push({
id: p.name,
name: p.name,
propValue: p.formattedValue,
_collapsed: true
});
});
this._dataView.beginUpdate();
this._dataView.setItems(this._data);
this._dataView.endUpdate();
this._dataView.refresh();
this._table.autosizeColumns();
this._table.updateRowCount();
this._table.resizeCanvas();
this._table.render();
public async openGraphFile() {
const input = this._untitledEditorService.create({ mode: this.graph.graphFile.graphFileType, initialValue: this.graph.graphFile.graphFileContent });
await input.resolve();
await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None);
input.setDirty(false);
this.editorService.openEditor(input);
}
}
class OpenQueryAction extends Action {
public static ID = 'qp.OpenQueryAction';
public static LABEL = localize('openQueryAction', "Open Query");
constructor() {
super(OpenQueryAction.ID, OpenQueryAction.LABEL, Codicon.dash.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
context.openQuery();
}
}
class PropertiesAction extends Action {
public static ID = 'qp.propertiesAction';
public static LABEL = localize('queryPlanPropertiesActionLabel', "Properties");
constructor() {
super(PropertiesAction.ID, PropertiesAction.LABEL, Codicon.book.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
context.propertiesView.toggleVisibility();
}
}
class ZoomInAction extends Action {
public static ID = 'qp.ZoomInAction';
public static LABEL = localize('queryPlanZoomInActionLabel', "Zoom In");
constructor() {
super(ZoomInAction.ID, ZoomInAction.LABEL, Codicon.zoomIn.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
}
}
class ZoomOutAction extends Action {
public static ID = 'qp.ZoomOutAction';
public static LABEL = localize('queryPlanZoomOutActionLabel', "Zoom Out");
constructor() {
super(ZoomOutAction.ID, ZoomOutAction.LABEL, Codicon.zoomOut.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
}
}
class ZoomToFitAction extends Action {
public static ID = 'qp.FitGraph';
public static LABEL = localize('queryPlanFitGraphLabel', "Zoom to fit");
constructor() {
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, Codicon.debugStop.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
}
}
class SaveXml extends Action {
public static ID = 'qp.saveXML';
public static LABEL = localize('queryPlanSavePlanXML', "Save XML");
constructor() {
super(SaveXml.ID, SaveXml.LABEL, Codicon.save.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
}
}
class CustomZoomAction extends Action {
public static ID = 'qp.customZoom';
public static LABEL = localize('queryPlanCustomZoom', "Custom Zoom");
constructor() {
super(CustomZoomAction.ID, CustomZoomAction.LABEL, Codicon.searchStop.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
}
}
class SearchNodeAction extends Action {
public static ID = 'qp.searchNode';
public static LABEL = localize('queryPlanSearchNodeAction', "SearchNode");
constructor() {
super(SearchNodeAction.ID, SearchNodeAction.LABEL, Codicon.search.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
}
}
class OpenGraphFile extends Action {
public static ID = 'qp.openGraphFile';
public static Label = localize('queryPlanOpenGraphFile', "Open Graph File");
constructor() {
super(OpenGraphFile.ID, OpenGraphFile.Label, Codicon.output.classNames);
}
public override async run(context: QueryPlan2): Promise<void> {
await context.openGraphFile();
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const menuBackgroundColor = theme.getColor(editorBackground);
if (menuBackgroundColor) {
collector.addRule(`
.qps-container .query-plan .plan .plan-action-container .child {
background-color: ${menuBackgroundColor};
}
`);
}
const recommendationsColor = theme.getColor(textLinkForeground);
if (recommendationsColor) {
collector.addRule(`
.qps-container .query-plan .plan .header .recommendations {
color: ${recommendationsColor};
}
`);
}
});
/**
* Registering a feature flag for query plan.
* TODO: This should be removed before taking the feature to public preview.

View File

@@ -16,7 +16,7 @@ import {
EditRevertCellResult,
ExecutionPlanOptions,
queryeditor,
QueryPlanGraph
ExecutionPlanGraph
} from 'azdata';
import { QueryInfo } from 'sql/workbench/services/query/common/queryModelService';
import { IRange } from 'vs/editor/common/core/range';
@@ -34,7 +34,7 @@ export interface IQueryPlanInfo {
export interface IQueryPlan2Info {
providerId: string;
fileUri: string;
planGraphs: QueryPlanGraph[];
planGraphs: ExecutionPlanGraph[];
}
export interface IQueryInfo {

View File

@@ -387,7 +387,7 @@ export default class QueryRunner extends Disposable {
}
}
public handleQueryPlan2Available(queryPlans: azdata.QueryPlanGraph[] | undefined) {
public handleQueryPlan2Available(queryPlans: azdata.ExecutionPlanGraph[] | undefined) {
if (queryPlans) {
this._onQueryPlan2Available.fire({
providerId: mssqlProviderName,