diff --git a/extensions/theme-seti/icons/images/execution-plan-compare.svg b/extensions/theme-seti/icons/images/execution-plan-compare.svg
new file mode 100644
index 0000000000..bca3dcf1fa
--- /dev/null
+++ b/extensions/theme-seti/icons/images/execution-plan-compare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/theme-seti/icons/images/execution-plan.svg b/extensions/theme-seti/icons/images/execution-plan.svg
new file mode 100644
index 0000000000..98b27841be
--- /dev/null
+++ b/extensions/theme-seti/icons/images/execution-plan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json
index d18512a88e..948112acc1 100644
--- a/extensions/theme-seti/icons/vs-seti-icon-theme.json
+++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json
@@ -1529,6 +1529,12 @@
},
"table-graphedge": {
"iconPath": "./images/table-graphedge.svg"
+ },
+ "execution-plan": {
+ "iconPath": "./images/execution-plan.svg"
+ },
+ "execution-plan-compare": {
+ "iconPath": "./images/execution-plan-compare.svg"
}
},
"file": "_default",
@@ -1830,7 +1836,9 @@
"table-basic": "table-basic",
"table-temporal": "table-temporal",
"table-graphnode": "table-graphnode",
- "table-graphedge": "table-graphedge"
+ "table-graphedge": "table-graphedge",
+ "execution-plan": "execution-plan",
+ "execution-plan-compare": "execution-plan-compare"
},
"languageIds": {
"bat": "_windows",
diff --git a/package.json b/package.json
index ae561b1fc0..72ae737532 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,7 @@
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"applicationinsights": "1.0.8",
- "azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
+ "azdataGraph": "github:Microsoft/azdataGraph#0.0.26",
"chart.js": "^2.9.4",
"chokidar": "3.5.2",
"graceful-fs": "4.2.6",
diff --git a/remote/package.json b/remote/package.json
index 04758be304..818749b5af 100644
--- a/remote/package.json
+++ b/remote/package.json
@@ -16,7 +16,7 @@
"applicationinsights": "1.0.8",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
- "azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
+ "azdataGraph": "github:Microsoft/azdataGraph#0.0.26",
"chart.js": "^2.9.4",
"chokidar": "3.5.2",
"cookie": "^0.4.0",
diff --git a/remote/web/package.json b/remote/web/package.json
index 255dc44b69..5f15b4e0e9 100644
--- a/remote/web/package.json
+++ b/remote/web/package.json
@@ -15,7 +15,7 @@
"@vscode/vscode-languagedetection": "1.0.18",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
- "azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
+ "azdataGraph": "github:Microsoft/azdataGraph#0.0.26",
"chart.js": "^2.9.4",
"gridstack": "^3.1.3",
"kburtram-query-plan": "2.6.1",
diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock
index 44686c48af..d018916256 100644
--- a/remote/web/yarn.lock
+++ b/remote/web/yarn.lock
@@ -150,9 +150,9 @@ array-uniq@^1.0.2:
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
-"azdataGraph@github:Microsoft/azdataGraph#0.0.21":
- version "0.0.21"
- resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
+"azdataGraph@github:Microsoft/azdataGraph#0.0.26":
+ version "0.0.26"
+ resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/a5f94e53cb655bc44f1a2727653bb403942e1cf9"
chalk@^2.3.0, chalk@^2.4.1:
version "2.4.2"
diff --git a/remote/yarn.lock b/remote/yarn.lock
index 0bdbd4ec92..5ca07562a1 100644
--- a/remote/yarn.lock
+++ b/remote/yarn.lock
@@ -198,9 +198,9 @@ array-uniq@^1.0.2:
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
-"azdataGraph@github:Microsoft/azdataGraph#0.0.21":
- version "0.0.21"
- resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
+"azdataGraph@github:Microsoft/azdataGraph#0.0.26":
+ version "0.0.26"
+ resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/a5f94e53cb655bc44f1a2727653bb403942e1cf9"
binary-extensions@^2.0.0:
version "2.0.0"
diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts
index 5992975869..7f29b6b802 100644
--- a/src/sql/azdata.proposed.d.ts
+++ b/src/sql/azdata.proposed.d.ts
@@ -1232,6 +1232,10 @@ declare module 'azdata' {
}
export interface ExecutionPlanNode {
+ /**
+ * Unique id given to node by the provider
+ */
+ id: string;
/**
* Type of the node. This property determines the icon that is displayed for it
*/
@@ -1365,6 +1369,10 @@ declare module 'azdata' {
* File type for execution plan. This will be the file type of the editor when the user opens the graph file
*/
graphFileType: string;
+ /**
+ * Index of the execution plan in the file content
+ */
+ planIndexInFile?: number;
}
export interface GetExecutionPlanResult extends ResultStatus {
@@ -1391,7 +1399,7 @@ declare module 'azdata' {
/**
* List of matching nodes for the ExecutionGraphComparisonResult.
*/
- matchingNodes: ExecutionGraphComparisonResult[];
+ matchingNodesId: number[];
/**
* The parent of the ExecutionGraphComparisonResult.
*/
diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts
index 5c5891b6f0..f2a7d47768 100644
--- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts
+++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts
@@ -553,7 +553,8 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData
public $registerExecutionPlanProvider(providerId: string, handle: number): void {
this._executionPlanService.registerProvider(providerId, {
- getExecutionPlan: (planFile: azdata.executionPlan.ExecutionPlanGraphInfo) => this._proxy.$getExecutionPlan(handle, planFile)
+ getExecutionPlan: (planFile: azdata.executionPlan.ExecutionPlanGraphInfo) => this._proxy.$getExecutionPlan(handle, planFile),
+ compareExecutionPlanGraph: (firstPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo, secondPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo) => this._proxy.$compareExecutionPlanGraph(handle, firstPlanFile, secondPlanFile)
});
}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts b/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts
index ad61363ea7..b18f7bf1ca 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as azdataGraphModule from 'azdataGraph';
-import type * as azdata from 'azdata';
+import * as azdata from 'azdata';
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { isString } from 'vs/base/common/types';
@@ -12,7 +12,8 @@ import { badgeIconPaths, executionPlanNodeIconPaths } from 'sql/workbench/contri
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
-import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry';
+import { foreground } from 'vs/platform/theme/common/colorRegistry';
+import { generateUuid } from 'vs/base/common/uuid';
const azdataGraph = azdataGraphModule();
/**
@@ -23,7 +24,6 @@ export class AzdataGraphView {
private _diagram: any;
private _diagramModel: AzDataGraphCell;
- private _uniqueElementId: number = -1;
private _cellInFocus: AzDataGraphCell;
private _graphElementPropertiesSet: Set = new Set();
@@ -51,11 +51,6 @@ export class AzdataGraphView {
this._diagram.graph.tooltipHandler.delay = 700; // increasing delay for tooltips
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
- const iconBackground = theme.getColor(editorBackground);
- if (iconBackground) {
- this._diagram.setIconBackgroundColor(iconBackground);
- }
-
const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) {
this._diagram.setTextFontColor(iconLabelColor);
@@ -99,7 +94,7 @@ export class AzdataGraphView {
if (element) {
cell = this._diagram.graph.model.getCell(element.id);
} else {
- cell = this._diagram.graph.model.getCell((this._executionPlan.root).id);
+ cell = this._diagram.graph.model.getCell((this._executionPlan.root).id);
}
this._diagram.graph.getSelectionModel().setCell(cell);
if (bringToCenter) {
@@ -125,7 +120,6 @@ export class AzdataGraphView {
this._diagram.zoomIn();
}
-
/**
* Zooms out of the diagram
*/
@@ -163,7 +157,7 @@ export class AzdataGraphView {
* @param id id of the diagram element
*/
public getElementById(id: string): InternalExecutionPlanElement | undefined {
- const nodeStack: InternalExecutionPlanNode[] = [];
+ const nodeStack: azdata.executionPlan.ExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) {
const currentNode = nodeStack.pop();
@@ -185,10 +179,10 @@ export class AzdataGraphView {
/**
* Searches the diagram nodes based on the search query provided.
*/
- public searchNodes(searchQuery: SearchQuery): InternalExecutionPlanNode[] {
- const resultNodes: InternalExecutionPlanNode[] = [];
+ public searchNodes(searchQuery: SearchQuery): azdata.executionPlan.ExecutionPlanNode[] {
+ const resultNodes: azdata.executionPlan.ExecutionPlanNode[] = [];
- const nodeStack: InternalExecutionPlanNode[] = [];
+ const nodeStack: azdata.executionPlan.ExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) {
@@ -287,13 +281,14 @@ export class AzdataGraphView {
});
}
- private populate(node: InternalExecutionPlanNode): AzDataGraphCell {
+ private populate(node: azdata.executionPlan.ExecutionPlanNode): AzDataGraphCell {
let diagramNode: AzDataGraphCell = {};
diagramNode.label = node.subtext.join(this.textResourcePropertiesService.getEOL(undefined));
diagramNode.tooltipTitle = node.name;
- const nodeId = this.createGraphElementId();
- diagramNode.id = nodeId;
- node.id = nodeId;
+ if (!node.id.toString().startsWith(`element-`)) {
+ node.id = `element-${node.id}`;
+ }
+ diagramNode.id = node.id;
if (node.type) {
diagramNode.icon = node.type;
@@ -383,8 +378,7 @@ export class AzdataGraphView {
}
private createGraphElementId(): string {
- this._uniqueElementId += 1;
- return `element-${this._uniqueElementId}`;
+ return `element-${generateUuid()}`;
}
/**
@@ -406,13 +400,15 @@ export class AzdataGraphView {
}
return this._diagram.graph.tooltipHandler.enabled;
}
-}
-export interface InternalExecutionPlanNode extends azdata.executionPlan.ExecutionPlanNode {
- /**
- * Unique internal id given to graph node by ADS.
- */
- id?: string;
+ public drawSubtreePolygon(subtreeRoot: string, fillColor: string, borderColor: string): void {
+ const drawPolygon = this._diagram.graph.model.getCell(`element-${subtreeRoot}`);
+ this._diagram.drawPolygon(drawPolygon, fillColor, borderColor);
+ }
+
+ public clearSubtreePolygon(): void {
+ this._diagram.removeDrawnPolygons();
+ }
}
export interface InternalExecutionPlanEdge extends azdata.executionPlan.ExecutionPlanEdge {
@@ -422,7 +418,7 @@ export interface InternalExecutionPlanEdge extends azdata.executionPlan.Executio
id?: string;
}
-export type InternalExecutionPlanElement = InternalExecutionPlanEdge | InternalExecutionPlanNode;
+export type InternalExecutionPlanElement = InternalExecutionPlanEdge | azdata.executionPlan.ExecutionPlanNode;
export interface AzDataGraphCell {
/**
diff --git a/src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts b/src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts
new file mode 100644
index 0000000000..5b21c5f160
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts
@@ -0,0 +1,57 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { localize } from 'vs/nls';
+import { EditorInput } from 'vs/workbench/common/editor/editorInput';
+import { URI } from 'vs/base/common/uri';
+import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
+import { ExecutionPlanComparisonEditorView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView';
+
+export class ExecutionPlanComparisonInput extends EditorInput {
+ public static ID: string = 'workbench.editorinputs.compareExecutionPlanInput';
+ public static SCHEME: string = 'compareExecutionPlanInput';
+ private readonly editorNamePrefix = localize('epCompare.editorName', "Compare Execution Plans");
+ private _editorName: string;
+
+ // Caching the views for faster tab switching
+ public _executionPlanComparisonView: ExecutionPlanComparisonEditorView;
+
+ constructor(
+ public preloadModel: ExecutionPlanComparisonEditorModel | undefined,
+ @IEditorService private readonly _editorService: IEditorService
+ ) {
+ super();
+
+ // Getting name for the editor
+ const existingNames = this._editorService.editors.map(editor => editor.getName());
+ let i = 0;
+ this._editorName = `${this.editorNamePrefix} ${i}`;
+ while (existingNames.includes(this._editorName)) {
+ i++;
+ this._editorName = `${this.editorNamePrefix} ${i}`;
+ }
+ }
+
+ get typeId(): string {
+ return ExecutionPlanComparisonInput.ID;
+ }
+
+ get resource(): URI {
+ return URI.from({
+ scheme: ExecutionPlanComparisonInput.SCHEME,
+ path: 'execution-plan-compare'
+ });
+ }
+
+ public override getName(): string {
+ return this._editorName;
+ }
+}
+
+export interface ExecutionPlanComparisonEditorModel {
+ topExecutionPlan?: azdata.executionPlan.ExecutionPlanGraph[];
+ bottomExecutionPlan?: azdata.executionPlan.ExecutionPlanGraph[];
+}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/constants.ts b/src/sql/workbench/contrib/executionPlan/browser/constants.ts
index 8b5db56b1c..1ff84cad82 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/constants.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/constants.ts
@@ -273,3 +273,87 @@ export const zoomToFitIconClassNames = 'ep-zoom-to-fit-icon';
export const zoomIconClassNames = 'ep-zoom-icon';
export const enableTooltipIconClassName = 'ep-enable-tooltip-icon';
export const disableTooltipIconClassName = 'ep-disable-tooltip-icon';
+export const addIconClassName = 'ep-add-icon';
+export const settingsIconClassName = 'ep-settings-icon';
+export const splitScreenHorizontallyIconClassName = 'ep-split-screen-horizontally-icon';
+export const splitScreenVerticallyIconClassName = 'ep-split-screen-vertically-icon';
+export const resetZoomIconClassName = 'ep-reset-zoom-icon';
+export const executionPlanCompareIconClassName = 'ep-plan-compare-icon';
+
+/**
+ * Plan comparison polygon border colors
+ */
+export const polygonBorderColor: string[] = [
+ `rgba(0, 188, 242)`, // "themeMain blue"
+ `rgba(236, 0, 140)`, // "themeError pink"
+ `rgba(0, 216, 204)`, // "h2 blue"
+ `rgba(236, 0, 140)`, // "b0 orange"
+ `rgba(255, 140, 0)`, // "themeWarning orange"
+ `rgba(127, 186, 0)`, // "themeSuccess green"
+ `rgba(252, 214, 241)`, // "paletteDiffDel light pink"
+ `rgba(252, 209, 22)`, // "a1 gold"
+ `rgba(68,35,89)`, // "e1 dark purple"
+ `rgba(0, 114, 198)`, // "g1 blue"
+ `rgba(160, 165, 168)`, // "i1 green"
+ `rgba(255, 140, 0)`, // "k1 grey"
+ `rgba(199, 241, 199)`, // "paletteDiffAdd light green"
+ `rgba(0, 24, 143)`, // "d0 pink",
+ `rgba(186, 216, 10)`, // "f0 royal blue"
+ `rgba(255, 252, 158)`, // "h0 seafoam green"
+ `rgba(221, 89, 0)`, // "j0 yellow green"
+ `rgba(155, 79, 150)`, // "a2 light yellow"
+ `rgba(109, 194, 233)`, // "c2 burnt orange"
+ `rgba(85, 212, 85)`, // "e2 purple"
+ `rgba(180, 0, 158)`, // "d1 purple"
+ `rgba(0, 32, 80)`, // "f1 navy blue"
+ `rgba(0, 130, 114)`, // "h1 blue green"
+ `rgba(127, 186, 0)`, // "j1 yellow green"
+ `rgba(255, 241, 0)`, // "a0 bright yellow"
+ `rgba(104, 33, 122)`, // "e0 purple"
+ `rgba(0, 188, 242)`, // "g0 sky blue"
+ `rgba(0, 158, 73)`, // "i0 green"
+ `rgba(187, 194, 202)`, // "k0 grey"
+ `rgba(255, 185, 0)`, // "b2 gold"
+ `rgba(244, 114, 208)`, // "d2 pink"
+ `rgba(70, 104, 197)`, // "f2 blue purple"
+ `rgba(226, 229, 132)`, // "j2 khaki"
+];
+
+/**
+ * Plan comparison polygon fill colors
+ */
+export const polygonFillColor: string[] = [
+ `rgba(0, 188, 242, 0.1)`, // "themeMain blue"
+ `rgba(236, 0, 140, 0.1)`, // "themeError pink"
+ `rgba(0, 216, 204, 0.1)`, // "h2 blue"
+ `rgba(236, 0, 140, 0.1)`, // "b0 orange"
+ `rgba(255, 140, 0, 0.1)`, // "themeWarning orange"
+ `rgba(127, 186, 0, 0.1)`, // "themeSuccess green"
+ `rgba(252, 214, 241, 0.1)`, // "paletteDiffDel light pink"
+ `rgba(252, 209, 22, 0.1)`, // "a1 gold"
+ `rgba(68,35,89, 0.1)`, // "e1 dark purple"
+ `rgba(0, 114, 198, 0.1)`, // "g1 blue"
+ `rgba(160, 165, 168, 0.1)`, // "i1 green"
+ `rgba(255, 140, 0, 0.1)`, // "k1 grey"
+ `rgba(199, 241, 199, 0.1)`, // "paletteDiffAdd light green"
+ `rgba(0, 24, 143, 0.1)`, // "d0 pink",
+ `rgba(186, 216, 10, 0.1)`, // "f0 royal blue"
+ `rgba(255, 252, 158, 0.1)`, // "h0 seafoam green"
+ `rgba(221, 89, 0, 0.1)`, // "j0 yellow green"
+ `rgba(155, 79, 150, 0.1)`, // "a2 light yellow"
+ `rgba(109, 194, 233, 0.1)`, // "c2 burnt orange"
+ `rgba(85, 212, 85, 0.1)`, // "e2 purple"
+ `rgba(180, 0, 158, 0.1)`, // "d1 purple"
+ `rgba(0, 32, 80, 0.1)`, // "f1 navy blue"
+ `rgba(0, 130, 114, 0.1)`, // "h1 blue green"
+ `rgba(127, 186, 0, 0.1)`, // "j1 yellow green"
+ `rgba(255, 241, 0, 0.1)`, // "a0 bright yellow"
+ `rgba(104, 33, 122, 0.1)`, // "e0 purple"
+ `rgba(0, 188, 242, 0.1)`, // "g0 sky blue"
+ `rgba(0, 158, 73, 0.1)`, // "i0 green"
+ `rgba(187, 194, 202, 0.1)`, // "k0 grey"
+ `rgba(255, 185, 0, 0.1)`, // "b2 gold"
+ `rgba(244, 114, 208, 0.1)`, // "d2 pink"
+ `rgba(70, 104, 197, 0.1)`, // "f2 blue purple"
+ `rgba(226, 229, 132, 0.1)`, // "j2 khaki"
+];
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts
new file mode 100644
index 0000000000..eee77f459e
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts
@@ -0,0 +1,84 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import 'vs/css!./media/executionPlan';
+import { localize } from 'vs/nls';
+import * as DOM from 'vs/base/browser/dom';
+import { IStorageService } from 'vs/platform/storage/common/storage';
+import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
+import { IThemeService } from 'vs/platform/theme/common/themeService';
+import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
+import { IEditorOptions } from 'vs/platform/editor/common/editor';
+import { IEditorOpenContext } from 'vs/workbench/common/editor';
+import { CancellationToken } from 'vs/base/common/cancellation';
+import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
+import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
+import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+import { ExecutionPlanComparisonEditorView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView';
+
+
+export class ExecutionPlanComparisonEditor extends EditorPane {
+ public static ID: string = 'workbench.editor.compareExecutionPlan';
+ public static LABEL: string = localize('compareExecutionPlanEditor', "Compare Execution Plan Editor");
+
+ private _editorContainer: HTMLElement;
+
+ constructor(
+ @IInstantiationService private _instantiationService: IInstantiationService,
+ @ITelemetryService telemetryService: ITelemetryService,
+ @IThemeService themeService: IThemeService,
+ @IStorageService storageService: IStorageService,
+ @IContextViewService readonly contextViewService: IContextViewService
+ ) {
+ super(ExecutionPlanComparisonEditor.ID, telemetryService, themeService, storageService);
+ }
+
+ protected createEditor(parent: HTMLElement): void {
+ this._editorContainer = DOM.$('.eps-container');
+ parent.appendChild(this._editorContainer);
+ }
+
+ public override get input(): ExecutionPlanComparisonInput {
+ return super.input;
+ }
+
+ layout(dimension: DOM.Dimension): void {
+ this._editorContainer.style.width = dimension.width + 'px';
+ this._editorContainer.style.height = dimension.height + 'px';
+ }
+
+ public override async setInput(input: ExecutionPlanComparisonInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise {
+ const oldInput = this.input as ExecutionPlanComparisonInput;
+
+ // returning when new input is the same as current input
+ if (oldInput && input.matches(oldInput)) {
+ return Promise.resolve();
+ }
+
+ super.setInput(input, options, context, token);
+
+ // removing existing comparison containers
+ while (this._editorContainer.firstChild) {
+ this._editorContainer.removeChild(this._editorContainer.firstChild);
+ }
+
+ // creating a new comparison view if the new input does not already have a cached one.
+ if (!input._executionPlanComparisonView) {
+ input._executionPlanComparisonView = this._instantiationService.createInstance(ExecutionPlanComparisonEditorView, this._editorContainer);
+ if (this.input.preloadModel) {
+ if (this.input.preloadModel.topExecutionPlan) {
+ input._executionPlanComparisonView.addExecutionPlanGraph(this.input.preloadModel.topExecutionPlan);
+ }
+ if (this.input.preloadModel.bottomExecutionPlan) {
+ input._executionPlanComparisonView.addExecutionPlanGraph(this.input.preloadModel.bottomExecutionPlan);
+ }
+ }
+ } else { // Getting the cached comparison view from the input and adding it to the base editor node.
+ this._editorContainer.appendChild(input._executionPlanComparisonView.container);
+ }
+ }
+}
+
+
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts
new file mode 100644
index 0000000000..954f5c8548
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts
@@ -0,0 +1,675 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
+import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
+import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
+import { ExecutionPlanComparisonPropertiesView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView';
+import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
+import { IHorizontalSashLayoutProvider, ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
+import { Action } from 'vs/base/common/actions';
+import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
+import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
+import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+import { IStorageService } from 'vs/platform/storage/common/storage';
+import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
+import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
+import * as DOM from 'vs/base/browser/dom';
+import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
+import { localize } from 'vs/nls';
+import { addIconClassName, openPropertiesIconClassNames, polygonBorderColor, polygonFillColor, resetZoomIconClassName, splitScreenHorizontallyIconClassName, splitScreenVerticallyIconClassName, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
+import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
+import { extname } from 'vs/base/common/path';
+import { INotificationService } from 'vs/platform/notification/common/notification';
+import { InfoBox } from 'sql/workbench/browser/ui/infoBox/infoBox';
+import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
+import { errorForeground, listHoverBackground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
+import { ExecutionPlanViewHeader } from 'sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader';
+import { attachSelectBoxStyler } from 'sql/platform/theme/common/styler';
+import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
+
+
+export class ExecutionPlanComparisonEditorView {
+
+ public container: HTMLElement;
+
+ private _taskbarContainer: HTMLElement;
+ private _taskbar: Taskbar;
+ private _addExecutionPlanAction: Action;
+ private _zoomInAction: Action;
+ private _zoomOutAction: Action;
+ private _zoomToFitAction: Action;
+ private _resetZoomAction: Action;
+ private _propertiesAction: Action;
+ private _toggleOrientationAction: Action;
+
+ private _planComparisonContainer: HTMLElement;
+
+ private _propertiesContainer: HTMLElement;
+ private _propertiesView: ExecutionPlanComparisonPropertiesView;
+
+ public planSplitViewContainer: HTMLElement;
+
+ private _sashContainer: HTMLElement;
+ private _horizontalSash: Sash;
+ private _verticalSash: Sash;
+ private _orientation: 'horizontal' | 'vertical' = 'horizontal';
+
+ private _placeholderContainer: HTMLElement;
+ private _placeholderInfoboxContainer: HTMLElement;
+ private _placeholderInfobox: InfoBox;
+ private _placeholderLoading: LoadingSpinner;
+
+ private _topPlanContainer: HTMLElement;
+ private _topPlanDropdown: SelectBox;
+ private _topPlanDropdownContainer: HTMLElement;
+ private _topPlanDiagramContainers: HTMLElement[] = [];
+ public topPlanDiagrams: AzdataGraphView[] = [];
+ private _topPlanDiagramModels: azdata.executionPlan.ExecutionPlanGraph[];
+ private _activeTopPlanIndex: number = 0;
+ private _topPlanRecommendations: ExecutionPlanViewHeader;
+ private _topSimilarNode: Map = new Map();
+ private _polygonRootsMap: Map = new Map();
+
+ private get _activeTopPlanDiagram(): AzdataGraphView {
+ if (this.topPlanDiagrams.length > 0) {
+ return this.topPlanDiagrams[this._activeTopPlanIndex];
+ }
+ return undefined;
+ }
+
+ private _bottomPlanContainer: HTMLElement;
+ private _bottomPlanDropdown: SelectBox;
+ private _bottomPlanDropdownContainer: HTMLElement;
+ private _bottomPlanDiagramContainers: HTMLElement[] = [];
+ public bottomPlanDiagrams: AzdataGraphView[] = [];
+ private _bottomPlanDiagramModels: azdata.executionPlan.ExecutionPlanGraph[];
+ private _activeBottomPlanIndex: number = 0;
+ private _bottomPlanRecommendations: ExecutionPlanViewHeader;
+ private _bottomSimilarNode: Map = new Map();
+
+
+ private get _activeBottomPlanDiagram(): AzdataGraphView {
+ if (this.bottomPlanDiagrams.length > 0) {
+ return this.bottomPlanDiagrams[this._activeBottomPlanIndex];
+ }
+ return undefined;
+ }
+
+ constructor(
+ parentContainer: HTMLElement,
+ @IInstantiationService private _instantiationService: IInstantiationService,
+ @ITelemetryService telemetryService: ITelemetryService,
+ @IThemeService private themeService: IThemeService,
+ @IStorageService storageService: IStorageService,
+ @IExecutionPlanService private _executionPlanService: IExecutionPlanService,
+ @IFileDialogService private _fileDialogService: IFileDialogService,
+ @IContextViewService readonly contextViewService: IContextViewService,
+ @ITextFileService private readonly _textFileService: ITextFileService,
+ @INotificationService private _notificationService: INotificationService,
+ @IProgressService private _progressService: IProgressService
+ ) {
+
+ this.container = DOM.$('.comparison-editor');
+ parentContainer.appendChild(this.container);
+ this.initializeToolbar();
+ this.initializePlanComparison();
+ this.refreshSplitView();
+ }
+
+ // creating and adding editor toolbar actions
+ private initializeToolbar(): void {
+ this._taskbarContainer = DOM.$('.editor-toolbar');
+ this._taskbar = new Taskbar(this._taskbarContainer, {
+ orientation: ActionsOrientation.HORIZONTAL,
+
+ });
+ this._taskbar.context = this;
+ this._addExecutionPlanAction = new AddExecutionPlanAction();
+ this._zoomOutAction = new ZoomOutAction();
+ this._zoomInAction = new ZoomInAction();
+ this._zoomToFitAction = new ZoomToFitAction();
+ this._propertiesAction = new PropertiesAction();
+ this._toggleOrientationAction = new ToggleOrientation();
+ this._resetZoomAction = new ZoomReset();
+ const content: ITaskbarContent[] = [
+ { action: this._addExecutionPlanAction },
+ { action: this._zoomInAction },
+ { action: this._zoomOutAction },
+ { action: this._zoomToFitAction },
+ { action: this._resetZoomAction },
+ { action: this._toggleOrientationAction },
+ { action: this._propertiesAction }
+ ];
+ this._taskbar.setContent(content);
+ this.container.appendChild(this._taskbarContainer);
+ }
+
+ private initializePlanComparison(): void {
+ this._planComparisonContainer = DOM.$('.plan-comparison-container');
+ this.container.appendChild(this._planComparisonContainer);
+ this.initializeSplitView();
+ this.initializeProperties();
+ }
+
+ private initializeSplitView(): void {
+ this.planSplitViewContainer = DOM.$('.split-view-container');
+ this._planComparisonContainer.appendChild(this.planSplitViewContainer);
+
+ this._placeholderContainer = DOM.$('.placeholder');
+ this._placeholderInfoboxContainer = DOM.$('.placeholder-infobox');
+ this._placeholderLoading = new LoadingSpinner(this._placeholderContainer, {
+ fullSize: true,
+ showText: true
+ });
+ this._placeholderContainer.appendChild(this._placeholderInfoboxContainer);
+ this._placeholderLoading.loadingMessage = localize('epComapre.LoadingPlanMessage', "Loading execution plan");
+ this._placeholderLoading.loadingCompletedMessage = localize('epComapre.LoadingPlanCompleteMessage', "Execution plan successfully loaded");
+ this._placeholderInfobox = this._instantiationService.createInstance(InfoBox, this._placeholderInfoboxContainer, {
+ style: 'information',
+ text: ''
+ });
+ this._placeholderInfobox.text = localize('epComapre.placeholderInfoboxText', "Add execution plans to compare");
+
+ this._topPlanContainer = DOM.$('.plan-container');
+ this.planSplitViewContainer.appendChild(this._topPlanContainer);
+ this._topPlanDropdownContainer = DOM.$('.dropdown-container');
+ this._topPlanDropdown = new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._topPlanDropdownContainer);
+ this._topPlanDropdown.render(this._topPlanDropdownContainer);
+ this._topPlanDropdown.onDidSelect(async (e) => {
+ this._activeBottomPlanDiagram.clearSubtreePolygon();
+ this._activeTopPlanDiagram.clearSubtreePolygon();
+ this._topPlanDiagramContainers.forEach(c => {
+ c.style.display = 'none';
+ });
+ this._topPlanDiagramContainers[e.index].style.display = '';
+ this.topPlanDiagrams[e.index].selectElement(undefined);
+ this._propertiesView.setTopElement(this._topPlanDiagramModels[e.index].root);
+ this._topPlanRecommendations.recommendations = this._topPlanDiagramModels[e.index].recommendations;
+ this._activeTopPlanIndex = e.index;
+ await this.getSkeletonNodes();
+ });
+ attachSelectBoxStyler(this._topPlanDropdown, this.themeService);
+ this._topPlanContainer.appendChild(this._topPlanDropdownContainer);
+ this._topPlanRecommendations = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._topPlanContainer, undefined);
+
+ this.initializeSash();
+
+ this._bottomPlanContainer = DOM.$('.plan-container');
+ this.planSplitViewContainer.appendChild(this._bottomPlanContainer);
+ this._bottomPlanDropdownContainer = DOM.$('.dropdown-container');
+ this._bottomPlanDropdown = new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._bottomPlanDropdownContainer);
+ this._bottomPlanDropdown.render(this._bottomPlanDropdownContainer);
+ this._bottomPlanDropdown.onDidSelect(async (e) => {
+ this._activeBottomPlanDiagram.clearSubtreePolygon();
+ this._activeTopPlanDiagram.clearSubtreePolygon();
+ this._bottomPlanDiagramContainers.forEach(c => {
+ c.style.display = 'none';
+ });
+ this._bottomPlanDiagramContainers[e.index].style.display = '';
+ this.bottomPlanDiagrams[e.index].selectElement(undefined);
+ this._propertiesView.setTopElement(this._bottomPlanDiagramModels[e.index].root);
+ this._bottomPlanRecommendations.recommendations = this._bottomPlanDiagramModels[e.index].recommendations;
+ this._activeBottomPlanIndex = e.index;
+ await this.getSkeletonNodes();
+ });
+ attachSelectBoxStyler(this._bottomPlanDropdown, this.themeService);
+
+ this._bottomPlanContainer.appendChild(this._bottomPlanDropdownContainer);
+ this._bottomPlanRecommendations = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._bottomPlanContainer, undefined);
+ }
+
+ private initializeSash(): void {
+ this._sashContainer = DOM.$('.sash-container');
+ this.planSplitViewContainer.appendChild(this._sashContainer);
+ this._verticalSash = new Sash(this._sashContainer, new VerticalSash(this), { orientation: Orientation.VERTICAL, size: 3 });
+
+ let originalWidth;
+ let change = 0;
+ this._verticalSash.onDidStart((e: ISashEvent) => {
+ originalWidth = this._topPlanContainer.offsetWidth;
+ });
+ this._verticalSash.onDidChange((evt: ISashEvent) => {
+ change = evt.startX - evt.currentX;
+ const newWidth = originalWidth - change;
+ if (newWidth < 200) {
+ return;
+ }
+ this._topPlanContainer.style.minWidth = '200px';
+ this._topPlanContainer.style.flex = `0 0 ${newWidth}px`;
+ });
+
+ this._horizontalSash = new Sash(this._sashContainer, new HorizontalSash(this), { orientation: Orientation.HORIZONTAL, size: 3 });
+ let startHeight;
+ this._horizontalSash.onDidStart((e: ISashEvent) => {
+ startHeight = this._topPlanContainer.offsetHeight;
+ });
+ this._horizontalSash.onDidChange((evt: ISashEvent) => {
+ change = evt.startY - evt.currentY;
+ const newHeight = startHeight - change;
+ if (newHeight < 200) {
+ return;
+ }
+ this._topPlanContainer.style.minHeight = '200px';
+ this._topPlanContainer.style.flex = `0 0 ${newHeight}px`;
+ });
+ }
+
+ private initializeProperties(): void {
+ this._propertiesContainer = DOM.$('.properties');
+ this._propertiesView = this._instantiationService.createInstance(ExecutionPlanComparisonPropertiesView, this._propertiesContainer);
+ this._planComparisonContainer.appendChild(this._propertiesContainer);
+ }
+
+ public async openAndAddExecutionPlanFile(): Promise {
+ try {
+ const openedFileUris = await this._fileDialogService.showOpenDialog({
+ filters: [
+ {
+ extensions: await this._executionPlanService.getSupportedExecutionPlanExtensions(),
+ name: localize('epCompare.FileFilterDescription', "Execution Plan Files")
+ }
+ ],
+ canSelectMany: false,
+ canSelectFiles: true
+ });
+ if (openedFileUris?.length === 1) {
+ this._placeholderInfoboxContainer.style.display = 'none';
+ this._placeholderLoading.loading = true;
+ const fileURI = openedFileUris[0];
+ const fileContent = (await this._textFileService.read(fileURI, { acceptTextOnly: true })).value;
+ let executionPlanGraphs = await this._executionPlanService.getExecutionPlan({
+ graphFileContent: fileContent,
+ graphFileType: extname(fileURI.fsPath).replace('.', '')
+ });
+ await this.addExecutionPlanGraph(executionPlanGraphs.graphs);
+ }
+ this._placeholderInfoboxContainer.style.display = '';
+ this._placeholderLoading.loading = false;
+ this._placeholderInfoboxContainer.style.display = '';
+ } catch (e) {
+ this._placeholderLoading.loading = false;
+ this._notificationService.error(e);
+ }
+
+ }
+
+ public async addExecutionPlanGraph(executionPlanGraphs: azdata.executionPlan.ExecutionPlanGraph[]): Promise {
+ if (!this._topPlanDiagramModels) {
+ this._topPlanDiagramModels = executionPlanGraphs;
+ this._topPlanDropdown.setOptions(executionPlanGraphs.map(e => {
+ return {
+ text: e.query
+ };
+ }), 0);
+
+ executionPlanGraphs.forEach((e, i) => {
+ const graphContainer = DOM.$('.plan-diagram');
+ this._topPlanDiagramContainers.push(graphContainer);
+ this._topPlanContainer.appendChild(graphContainer);
+ const diagram = this._instantiationService.createInstance(AzdataGraphView, graphContainer, e);
+ diagram.onElementSelected(e => {
+ this._propertiesView.setTopElement(e);
+ const id = e.id.replace(`element-`, '');
+ if (this._topSimilarNode.has(id)) {
+ const similarNode = this._topSimilarNode.get(id);
+ const element = this._activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]);
+ if (similarNode.matchingNodesId.find(m => this._activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) {
+ return;
+ }
+ this._activeBottomPlanDiagram.selectElement(element);
+ }
+ });
+ this.topPlanDiagrams.push(diagram);
+ graphContainer.style.display = 'none';
+ });
+
+ this._topPlanDiagramContainers[0].style.display = '';
+ this._topPlanRecommendations.recommendations = executionPlanGraphs[0].recommendations;
+ this.topPlanDiagrams[0].selectElement(undefined);
+ this._propertiesView.setTopElement(executionPlanGraphs[0].root);
+ this._propertiesAction.enabled = true;
+ this._zoomInAction.enabled = true;
+ this._zoomOutAction.enabled = true;
+ this._resetZoomAction.enabled = true;
+ this._zoomToFitAction.enabled = true;
+ this._toggleOrientationAction.enabled = true;
+ } else {
+ this._bottomPlanDiagramModels = executionPlanGraphs;
+ this._bottomPlanDropdown.setOptions(executionPlanGraphs.map(e => {
+ return {
+ text: e.query
+ };
+ }), 0);
+ executionPlanGraphs.forEach((e, i) => {
+ const graphContainer = DOM.$('.plan-diagram');
+ this._bottomPlanDiagramContainers.push(graphContainer);
+ this._bottomPlanContainer.appendChild(graphContainer);
+ const diagram = this._instantiationService.createInstance(AzdataGraphView, graphContainer, e);
+ diagram.onElementSelected(e => {
+ this._propertiesView.setBottomElement(e);
+ const id = e.id.replace(`element-`, '');
+ if (this._bottomSimilarNode.has(id)) {
+ const similarNode = this._bottomSimilarNode.get(id);
+ const element = this._activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]);
+ if (similarNode.matchingNodesId.find(m => this._activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) {
+ return;
+ }
+ this._activeTopPlanDiagram.selectElement(element);
+ }
+ });
+ this.bottomPlanDiagrams.push(diagram);
+ graphContainer.style.display = 'none';
+ });
+
+ this._bottomPlanDiagramContainers[0].style.display = '';
+ this._bottomPlanRecommendations.recommendations = executionPlanGraphs[0].recommendations;
+ this.bottomPlanDiagrams[0].selectElement(undefined);
+ this._propertiesView.setBottomElement(executionPlanGraphs[0].root);
+ this._addExecutionPlanAction.enabled = false;
+ await this.getSkeletonNodes();
+ }
+ this.refreshSplitView();
+ }
+
+ private async getSkeletonNodes(): Promise {
+ this._progressService.withProgress(
+ {
+ location: ProgressLocation.Notification,
+ title: localize('epCompare.comparisonProgess', "Loading similar areas in compared plans"),
+ cancellable: false
+ },
+ async (progress) => {
+ this._polygonRootsMap = new Map();
+ this._topSimilarNode = new Map();
+ this._bottomSimilarNode = new Map();
+ if (this._topPlanDiagramModels && this._bottomPlanDiagramModels) {
+ this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile.graphFileType = 'sqlplan';
+ this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile.graphFileType = 'sqlplan';
+ const result = await this._executionPlanService.compareExecutionPlanGraph(this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile,
+ this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile);
+ this.getSimilarSubtrees(result.firstComparisonResult);
+ this.getSimilarSubtrees(result.secondComparisonResult, true);
+ let colorIndex = 0;
+ this._polygonRootsMap.forEach((v, k) => {
+ this._activeTopPlanDiagram.drawSubtreePolygon(v.topPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]);
+ this._activeBottomPlanDiagram.drawSubtreePolygon(v.bottomPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]);
+ colorIndex += 1;
+ });
+ }
+ return;
+ }
+ );
+ }
+
+ private getSimilarSubtrees(comparedNode: azdata.executionPlan.ExecutionGraphComparisonResult, isBottomPlan: boolean = false): void {
+ if (comparedNode.hasMatch) {
+ if (!isBottomPlan) {
+ this._topSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode);
+ if (!this._polygonRootsMap.has(comparedNode.groupIndex)) {
+ this._polygonRootsMap.set(comparedNode.groupIndex, {
+ topPolygon: comparedNode,
+ bottomPolygon: undefined
+ });
+ }
+ } else {
+ this._bottomSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode);
+ if (this._polygonRootsMap.get(comparedNode.groupIndex).bottomPolygon === undefined) {
+ const polygonMapEntry = this._polygonRootsMap.get(comparedNode.groupIndex);
+ polygonMapEntry.bottomPolygon = comparedNode;
+ this._polygonRootsMap.set(comparedNode.groupIndex, polygonMapEntry);
+ }
+ }
+ }
+ comparedNode.children.forEach(c => {
+ this.getSimilarSubtrees(c, isBottomPlan);
+ });
+ }
+
+ public togglePropertiesView(): void {
+ this._propertiesContainer.style.display = this._propertiesContainer.style.display === 'none' ? '' : 'none';
+ }
+
+ public toggleOrientation(): void {
+ if (this._orientation === 'vertical') {
+ this._sashContainer.style.width = '100%';
+ this._sashContainer.style.height = '3px';
+ this.planSplitViewContainer.style.flexDirection = 'column';
+ this._topPlanContainer.style.minHeight = '200px';
+ this._topPlanContainer.style.minWidth = '';
+ this._topPlanContainer.style.flex = '1';
+ this._orientation = 'horizontal';
+ this._toggleOrientationAction.class = splitScreenHorizontallyIconClassName;
+ } else {
+ this._sashContainer.style.width = '3px';
+ this._sashContainer.style.height = '100%';
+ this.planSplitViewContainer.style.flexDirection = 'row';
+ this._topPlanContainer.style.minHeight = '';
+ this._topPlanContainer.style.minWidth = '200px';
+ this._orientation = 'vertical';
+ this._toggleOrientationAction.class = splitScreenVerticallyIconClassName;
+ }
+ this._topPlanContainer.style.flex = '1';
+ this._bottomPlanContainer.style.flex = '1';
+ }
+
+ public refreshSplitView(): void {
+ if (this.planSplitViewContainer.contains(this._topPlanContainer)) {
+ this.planSplitViewContainer.removeChild(this._topPlanContainer);
+ }
+
+ if (this.planSplitViewContainer.contains(this._bottomPlanContainer)) {
+ this.planSplitViewContainer.removeChild(this._bottomPlanContainer);
+ }
+
+ if (this.planSplitViewContainer.contains(this._sashContainer)) {
+ this.planSplitViewContainer.removeChild(this._sashContainer);
+ }
+
+ if (this.planSplitViewContainer.contains(this._placeholderContainer)) {
+ this.planSplitViewContainer.removeChild(this._placeholderContainer);
+ }
+
+ if (!this._topPlanDiagramModels && !this._bottomPlanDiagramModels) {
+ this.planSplitViewContainer.appendChild(this._placeholderContainer);
+ } else if (this._topPlanDiagramModels && !this._bottomPlanDiagramModels) {
+ this.planSplitViewContainer.appendChild(this._topPlanContainer);
+ this.planSplitViewContainer.appendChild(this._sashContainer);
+ this.planSplitViewContainer.appendChild(this._placeholderContainer);
+ } else {
+ this.planSplitViewContainer.appendChild(this._topPlanContainer);
+ this.planSplitViewContainer.appendChild(this._sashContainer);
+ this.planSplitViewContainer.appendChild(this._bottomPlanContainer);
+ }
+ }
+
+ public zoomIn(): void {
+ this._activeTopPlanDiagram.zoomIn();
+ this._activeBottomPlanDiagram.zoomIn();
+ this.syncZoom();
+ }
+
+ public zoomOut(): void {
+ this._activeTopPlanDiagram.zoomOut();
+ this._activeBottomPlanDiagram.zoomOut();
+ this.syncZoom();
+ }
+
+ public zoomToFit(): void {
+ this._activeTopPlanDiagram.zoomToFit();
+ this._activeBottomPlanDiagram.zoomToFit();
+ this.syncZoom();
+ }
+
+ public resetZoom(): void {
+ if (this._activeTopPlanDiagram) {
+ this._activeTopPlanDiagram.setZoomLevel(100);
+ }
+ if (this._activeBottomPlanDiagram) {
+ this._activeBottomPlanDiagram.setZoomLevel(100);
+ }
+ }
+
+ private syncZoom(): void {
+ if (this._activeTopPlanDiagram.getZoomLevel() < this._activeBottomPlanDiagram.getZoomLevel()) {
+ this._activeBottomPlanDiagram.setZoomLevel(this._activeTopPlanDiagram.getZoomLevel());
+ } else {
+ this._activeTopPlanDiagram.setZoomLevel(this._activeBottomPlanDiagram.getZoomLevel());
+ }
+ }
+}
+
+
+class AddExecutionPlanAction extends Action {
+ public static ID = 'ep.AddExecutionPlan';
+ public static LABEL = localize('addExecutionPlanLabel', "Add execution plan");
+ constructor() {
+ super(AddExecutionPlanAction.ID, AddExecutionPlanAction.LABEL, addIconClassName);
+ }
+
+ public override async run(context: ExecutionPlanComparisonEditorView): Promise {
+ await context.openAndAddExecutionPlanFile();
+ }
+
+}
+
+class ZoomInAction extends Action {
+ public static ID = 'ep.zoomIn';
+ public static LABEL = localize('epCompare.zoomInAction', "Zoom In");
+ constructor() {
+ super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames);
+ this.enabled = false;
+ }
+ public override async run(context: ExecutionPlanComparisonEditorView): Promise {
+ context.zoomIn();
+ }
+}
+
+class ZoomOutAction extends Action {
+ public static ID = 'ep.zoomOut';
+ public static LABEL = localize('epCompare.zoomOutAction', "Zoom Out");
+ constructor() {
+ super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames);
+ this.enabled = false;
+ }
+ public override async run(context: ExecutionPlanComparisonEditorView): Promise {
+ context.zoomOut();
+ }
+}
+
+class ZoomToFitAction extends Action {
+ public static ID = 'ep.zoomToFit';
+ public static LABEL = localize('epCompare.zoomToFit', "Zoom to fit");
+
+ constructor() {
+ super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames);
+ this.enabled = false;
+ }
+
+ public override async run(context: ExecutionPlanComparisonEditorView): Promise {
+ context.zoomToFit();
+ }
+}
+
+class ZoomReset extends Action {
+ public static ID = 'ep.resetZoom';
+ public static LABEL = localize('epCompare.zoomReset', "Reset Zoom");
+
+ constructor() {
+ super(ZoomReset.ID, ZoomReset.LABEL, resetZoomIconClassName);
+ this.enabled = false;
+ }
+
+ public override async run(context: ExecutionPlanComparisonEditorView): Promise {
+ context.resetZoom();
+ }
+}
+
+class ToggleOrientation extends Action {
+ public static ID = 'ep.toggleOrientation';
+ public static LABEL = localize('epCompare.toggleOrientation', "Toggle Orientation");
+
+ constructor() {
+ super(ToggleOrientation.ID, ToggleOrientation.LABEL, splitScreenHorizontallyIconClassName);
+ this.enabled = false;
+ }
+
+ public override async run(context: ExecutionPlanComparisonEditorView): Promise {
+ context.toggleOrientation();
+ }
+}
+
+class PropertiesAction extends Action {
+ public static ID = 'epCompare.comparePropertiesAction';
+ public static LABEL = localize('epCompare.comparePropertiesAction', "Properties");
+ constructor() {
+ super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames);
+ this.enabled = false;
+ }
+ public override async run(context: ExecutionPlanComparisonEditorView): Promise {
+ context.togglePropertiesView();
+ }
+}
+
+class HorizontalSash implements IHorizontalSashLayoutProvider {
+ constructor(private _context: ExecutionPlanComparisonEditorView) {
+ }
+ getHorizontalSashTop(sash: Sash): number {
+ return 0;
+ }
+ getHorizontalSashLeft?(sash: Sash): number {
+ return 0;
+ }
+ getHorizontalSashWidth?(sash: Sash): number {
+ return this._context.planSplitViewContainer.clientWidth;
+ }
+
+}
+
+class VerticalSash implements IVerticalSashLayoutProvider {
+ constructor(private _context: ExecutionPlanComparisonEditorView) {
+
+ }
+ getVerticalSashLeft(sash: Sash): number {
+ return 0;
+ }
+ getVerticalSashTop?(sash: Sash): number {
+ return 0;
+ }
+ getVerticalSashHeight?(sash: Sash): number {
+ return this._context.planSplitViewContainer.clientHeight;
+ }
+
+}
+
+registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
+ const separatorColor = theme.getColor(errorForeground);
+ if (separatorColor) {
+ collector.addRule(`
+ .designer-component .issues-container .issue-item .issue-icon.codicon-error {
+ color: ${separatorColor};
+ }`);
+ }
+ const recommendationsColor = theme.getColor(textLinkForeground);
+ if (recommendationsColor) {
+ collector.addRule(`
+ .eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .recommendations {
+ color: ${recommendationsColor};
+ }
+ `);
+ }
+ const menuBackgroundColor = theme.getColor(listHoverBackground);
+ if (menuBackgroundColor) {
+ collector.addRule(`
+ .eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .recommendations {
+ background-color: ${menuBackgroundColor};
+ }
+ `);
+ }
+});
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts
new file mode 100644
index 0000000000..0ac75875a2
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts
@@ -0,0 +1,268 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 type * as azdata from 'azdata';
+import { localize } from 'vs/nls';
+import { 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';
+
+export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanPropertiesViewBase {
+ private _model: ExecutionPlanComparisonPropertiesViewModel;
+ private _topOperationNameContainer: HTMLElement;
+ private _bottomOperationNameContainer: HTMLElement;
+
+ public constructor(
+ parentContainer: HTMLElement,
+ @IThemeService themeService: IThemeService,
+ ) {
+ super(parentContainer, themeService);
+ this._model = {};
+ this._parentContainer.style.display = 'none';
+ const header = DOM.$('.compare-operation-name');
+ this._topOperationNameContainer = DOM.$('.compare-operation-name-text');
+ header.appendChild(this._topOperationNameContainer);
+ this._bottomOperationNameContainer = DOM.$('.compare-operation-name-text');
+ header.appendChild(this._bottomOperationNameContainer);
+ this.setHeader(header);
+ }
+
+
+ public setTopElement(e: InternalExecutionPlanElement): void {
+ this._model.topElement = e;
+ let target;
+ if ((e).name) {
+ target = removeLineBreaks((e).name);
+ } else {
+ target = localize('executionPlanPropertiesEdgeOperationName', "Edge");
+ }
+ const titleText = localize('executionPlanComparisonPropertiesTopOperation', "Top operation: {0}", target);
+ this._topOperationNameContainer.innerText = titleText;
+ this._bottomOperationNameContainer.title = titleText;
+ this.addDataToTable();
+ }
+
+ public setBottomElement(e: InternalExecutionPlanElement): void {
+ this._model.bottomElement = e;
+ let target;
+ if ((e)?.name) {
+ target = removeLineBreaks((e).name);
+ } else {
+ target = localize('executionPlanPropertiesEdgeOperationName', "Edge");
+ }
+
+ const titleText = localize('executionPlanComparisonPropertiesBottomOperation', "Bottom operation: {0}", target);
+ this._bottomOperationNameContainer.innerText = titleText;
+ this._bottomOperationNameContainer.title = titleText;
+ this.addDataToTable();
+ }
+
+
+ public addDataToTable() {
+ const columns: Slick.Column[] = [
+ ];
+ if (this._model.topElement) {
+ columns.push({
+ id: 'name',
+ name: localize('nodePropertyViewNameNameColumnHeader', "Name"),
+ field: 'name',
+ width: 200,
+ editor: Slick.Editors.Text,
+ headerCssClass: 'prop-table-header',
+ formatter: textFormatter
+ });
+ columns.push({
+ id: 'value',
+ name: localize('nodePropertyViewNameValueColumnTopHeader', "Value (Top Plan)"),
+ field: 'value1',
+ width: 150,
+ editor: Slick.Editors.Text,
+ headerCssClass: 'prop-table-header',
+ formatter: textFormatter
+ });
+ }
+ if (this._model.bottomElement) {
+ columns.push({
+ id: 'value',
+ name: localize('nodePropertyViewNameValueColumnBottomHeader', "Value (Bottom Plan)"),
+ field: 'value2',
+ width: 150,
+ editor: Slick.Editors.Text,
+ headerCssClass: 'prop-table-header',
+ formatter: textFormatter
+ });
+ }
+
+ let topProps = [];
+ let bottomProps = [];
+ if (this._model.topElement?.properties) {
+ topProps = this._model.topElement.properties;
+ }
+ if (this._model.bottomElement?.properties) {
+ bottomProps = this._model.bottomElement.properties;
+ }
+
+ this.populateTable(columns, this.convertPropertiesToTableRows(topProps, bottomProps, -1, 0));
+ }
+
+ public sortPropertiesAlphabetically(props: Map): Map {
+ 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): Map {
+ 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;
+ }
+ }));
+ }
+
+ public sortPropertiesReverseAlphabetically(props: Map): Map {
+ 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(topNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[], bottomNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] {
+ let propertiesMap: Map = new Map();
+
+ if (topNode) {
+ topNode.forEach(p => {
+ propertiesMap.set(p.name, {
+ topProp: p,
+ bottomProp: undefined,
+ displayOrder: p.displayOrder,
+ name: p.name
+ });
+ });
+ }
+
+ if (bottomNode) {
+ bottomNode.forEach(p => {
+ if (propertiesMap.has(p.name)) {
+ propertiesMap.get(p.name).bottomProp = p;
+ } else {
+ propertiesMap.set(p.name, {
+ topProp: undefined,
+ bottomProp: 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 = {};
+ row['name'] = {
+ text: k
+ };
+ row['parent'] = parentIndex;
+
+ const topProp = v.topProp;
+ const bottomProp = v.bottomProp;
+ const parentRowCellStyling = 'font-weight: bold';
+
+ if (topProp && bottomProp) {
+ row['displayOrder'] = v.topProp.displayOrder;
+ row['value1'] = {
+ text: removeLineBreaks(v.topProp.displayValue, ' ')
+ };
+ row['value2'] = {
+ text: removeLineBreaks(v.bottomProp.displayValue, ' ')
+ };
+ if ((topProp && !isString(topProp.value)) || (bottomProp && !isString(bottomProp.value))) {
+ row['name'].style = parentRowCellStyling;
+ row['value1'].style = parentRowCellStyling;
+ row['value2'].style = parentRowCellStyling;
+ }
+ rows.push(row);
+ if (!isString(topProp.value) && !isString(bottomProp.value)) {
+ this.convertPropertiesToTableRows(topProp.value, bottomProp.value, rows.length - 1, indent + 2, rows);
+ } else if (isString(topProp?.value) && !isString(bottomProp.value)) {
+ this.convertPropertiesToTableRows(undefined, bottomProp.value, rows.length - 1, indent + 2, rows);
+ } else if (!isString(topProp.value) && !isString(bottomProp.value)) {
+ this.convertPropertiesToTableRows(topProp.value, undefined, rows.length - 1, indent + 2, rows);
+ }
+ } else if (topProp && !bottomProp) {
+ row['displayOrder'] = v.topProp.displayOrder;
+ row['value1'] = {
+ text: v.topProp.displayValue
+ };
+ rows.push(row);
+ if (!isString(topProp.value)) {
+ row['name'].style = parentRowCellStyling;
+ row['value1'].style = parentRowCellStyling;
+ this.convertPropertiesToTableRows(topProp.value, undefined, rows.length - 1, indent + 2, rows);
+ }
+ } else if (!topProp && bottomProp) {
+ row['displayOrder'] = v.bottomProp.displayOrder;
+ row['value2'] = {
+ text: v.bottomProp.displayValue
+ };
+ rows.push(row);
+ if (!isString(bottomProp.value)) {
+ row['name'].style = parentRowCellStyling;
+ row['value2'].style = parentRowCellStyling;
+ this.convertPropertiesToTableRows(undefined, bottomProp.value, rows.length - 1, indent + 2, rows);
+ }
+ }
+
+ });
+ return rows;
+ }
+}
+
+export interface ExecutionPlanComparisonPropertiesViewModel {
+ topElement: InternalExecutionPlanElement,
+ bottomElement: InternalExecutionPlanElement
+}
+
+interface TablePropertiesMapEntry {
+ topProp: azdata.executionPlan.ExecutionPlanGraphElementProperty,
+ bottomProp: azdata.executionPlan.ExecutionPlanGraphElementProperty,
+ displayOrder: number,
+ name: string
+}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts
index 52c8778419..c3a4124fba 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts
@@ -11,11 +11,17 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Disposable } from 'vs/base/common/lifecycle';
-import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import { ExecutionPlanInput } from 'sql/workbench/contrib/executionPlan/common/executionPlanInput';
import { ExecutionPlanEditor } from 'sql/workbench/contrib/executionPlan/browser/executionPlanEditor';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
+import { ExecutionPlanComparisonEditor } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor';
+import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
+import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
+import { localize } from 'vs/nls';
+import { CommandsRegistry } from 'vs/platform/commands/common/commands';
+import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
// Execution Plan editor registration
@@ -39,7 +45,7 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen
this.registerEditorOverride();
this._capabilitiesService.onCapabilitiesRegistered(e => {
- const newFileFormats = this._executionPlanService.getSupportedExecutionPlanExtensionsForProvider(e.id);
+ const newFileFormats = this._executionPlanService.getSupportedExecutionPlanExtensions(e.id);
if (newFileFormats?.length > 0) {
this._editorResolverService.updateUserAssociations(this.getGlobForFileExtensions(newFileFormats), ExecutionPlanEditor.ID); // Registering new file formats when new providers are registered.
}
@@ -76,3 +82,33 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen
Registry.as(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(ExecutionPlanEditorOverrideContribution, LifecyclePhase.Restored);
+
+const comparisonExecutionPlanEditor = EditorPaneDescriptor.create(
+ ExecutionPlanComparisonEditor,
+ ExecutionPlanComparisonEditor.ID,
+ ExecutionPlanComparisonEditor.LABEL
+);
+
+const COMPARE_EXECUTION_PLAN_COMMAND_ID = 'compareExecutionPlan';
+
+Registry.as(EditorExtensions.EditorPane)
+ .registerEditorPane(comparisonExecutionPlanEditor, [new SyncDescriptor(ExecutionPlanComparisonInput)]);
+
+MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
+ command: {
+ id: COMPARE_EXECUTION_PLAN_COMMAND_ID,
+ title: {
+ value: localize('executionPlanCompareCommandValue', "Compare execution plans"),
+ original: localize('executionPlanCompareCommandOriginalValue', "Compare execution plans")
+ },
+ category: 'Execution Plan'
+ }
+});
+
+CommandsRegistry.registerCommand(COMPARE_EXECUTION_PLAN_COMMAND_ID, (accessors: ServicesAccessor) => {
+ const editorService = accessors.get(IEditorService);
+ const instantiationService = accessors.get(IInstantiationService);
+ editorService.openEditor(instantiationService.createInstance(ExecutionPlanComparisonInput, undefined), {
+ pinned: true
+ });
+});
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts
index 449930dad3..ae839a2072 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts
@@ -33,7 +33,7 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase
public set graphElement(element: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge) {
this._model.graphElement = element;
- this.renderView();
+ this.addDataToTable();
}
public sortPropertiesAlphabetically(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
@@ -79,7 +79,7 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase
});
}
- public renderView(): void {
+ public addDataToTable(): void {
if (this._model.graphElement) {
const nodeName = (this._model.graphElement).name;
this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('executionPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts
index 5c523f0233..1d07094be8 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts
@@ -15,8 +15,9 @@ import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
import { contrastBorder, listHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { TreeGrid } from 'sql/base/browser/ui/table/treeGrid';
+import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
-export abstract class ExecutionPlanPropertiesViewBase {
+export abstract class ExecutionPlanPropertiesViewBase implements IVerticalSashLayoutProvider {
// Title bar with close button action
private _titleBarContainer!: HTMLElement;
private _titleBarTextContainer!: HTMLElement;
@@ -39,6 +40,8 @@ export abstract class ExecutionPlanPropertiesViewBase {
public sortType: PropertiesSortType = PropertiesSortType.DisplayOrder;
+ public resizeSash: Sash;
+
constructor(
public _parentContainer: HTMLElement,
private _themeService: IThemeService
@@ -47,8 +50,28 @@ export abstract class ExecutionPlanPropertiesViewBase {
const sashContainer = DOM.$('.properties-sash');
this._parentContainer.appendChild(sashContainer);
+ this.resizeSash = new Sash(sashContainer, this, { orientation: Orientation.VERTICAL, size: 3 });
+ let originalWidth = 0;
+ this.resizeSash.onDidStart((e: ISashEvent) => {
+ originalWidth = this._parentContainer.clientWidth;
+ });
+ this.resizeSash.onDidChange((evt: ISashEvent) => {
+ const change = evt.startX - evt.currentX;
+ const newWidth = originalWidth + change;
+ if (newWidth < 200) {
+ return;
+ }
+ this._parentContainer.style.flex = `0 0 ${newWidth}px`;
+ propertiesContent.style.width = `${newWidth}px`;
+ });
+ this.resizeSash.onDidEnd(() => {
+ });
+
+ const propertiesContent = DOM.$('.properties-content');
+ this._parentContainer.appendChild(propertiesContent);
+
this._titleBarContainer = DOM.$('.title');
- this._parentContainer.appendChild(this._titleBarContainer);
+ propertiesContent.appendChild(this._titleBarContainer);
this._titleBarTextContainer = DOM.$('h3');
this._titleBarTextContainer.classList.add('text');
@@ -64,10 +87,10 @@ export abstract class ExecutionPlanPropertiesViewBase {
this._titleActions.pushAction([new ClosePropertyViewAction()], { icon: true, label: false });
this._headerContainer = DOM.$('.header');
- this._parentContainer.appendChild(this._headerContainer);
+ propertiesContent.appendChild(this._headerContainer);
this._headerActionsContainer = DOM.$('.table-action-bar');
- this._parentContainer.appendChild(this._headerActionsContainer);
+ propertiesContent.appendChild(this._headerActionsContainer);
this._headerActions = new ActionBar(this._headerActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
@@ -75,7 +98,7 @@ export abstract class ExecutionPlanPropertiesViewBase {
this._tableContainer = DOM.$('.table-container');
- this._parentContainer.appendChild(this._tableContainer);
+ propertiesContent.appendChild(this._tableContainer);
const table = DOM.$('.table');
this._tableContainer.appendChild(table);
@@ -90,9 +113,19 @@ export abstract class ExecutionPlanPropertiesViewBase {
attachTableStyler(this._tableComponent, this._themeService);
new ResizeObserver((e) => {
+ this.resizeSash.layout();
this.resizeTable();
- }).observe(this._parentContainer);
+ }).observe(_parentContainer);
+ }
+ getVerticalSashLeft(sash: Sash): number {
+ return 0;
+ }
+ getVerticalSashTop?(sash: Sash): number {
+ return 0;
+ }
+ getVerticalSashHeight?(sash: Sash): number {
+ return this._parentContainer.clientHeight;
}
public setTitle(v: string): void {
@@ -106,14 +139,12 @@ export abstract class ExecutionPlanPropertiesViewBase {
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
- this.renderView();
}
}
public set tableWidth(value: number) {
if (this._tableWidth !== value) {
this._tableWidth = value;
- this.renderView();
}
}
@@ -125,11 +156,10 @@ export abstract class ExecutionPlanPropertiesViewBase {
return this._tableHeight;
}
- public abstract renderView();
+ public abstract addDataToTable();
public toggleVisibility(): void {
- this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none';
- this.renderView();
+ this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'flex' : 'none';
}
public populateTable(columns: Slick.Column[], data: { [key: string]: string }[]) {
@@ -177,7 +207,7 @@ export class SortPropertiesAlphabeticallyAction extends Action {
public override async run(context: ExecutionPlanPropertiesViewBase): Promise {
context.sortType = PropertiesSortType.Alphabetical;
- context.renderView();
+ context.addDataToTable();
}
}
@@ -191,7 +221,7 @@ export class SortPropertiesReverseAlphabeticallyAction extends Action {
public override async run(context: ExecutionPlanPropertiesViewBase): Promise {
context.sortType = PropertiesSortType.ReverseAlphabetical;
- context.renderView();
+ context.addDataToTable();
}
}
@@ -205,7 +235,7 @@ export class SortPropertiesByDisplayOrderAction extends Action {
public override async run(context: ExecutionPlanPropertiesViewBase): Promise {
context.sortType = PropertiesSortType.DisplayOrder;
- context.renderView();
+ context.addDataToTable();
}
}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts
index d2e3d97b03..501d51b33e 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts
@@ -27,7 +27,7 @@ import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Action } from 'vs/base/common/actions';
import { localize } from 'vs/nls';
-import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
+import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, executionPlanCompareIconClassName, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { URI } from 'vs/base/common/uri';
import { VSBuffer } from 'vs/base/common/buffer';
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
@@ -35,6 +35,7 @@ import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/wi
import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
+import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
export class ExecutionPlanView implements ISashLayoutProvider {
@@ -78,6 +79,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
@IFileDialogService public fileDialogService: IFileDialogService,
@IFileService public fileService: IFileService,
@IWorkspaceContextService public workspaceContextService: IWorkspaceContextService,
+ @IEditorService private _editorService: IEditorService
) {
// parent container for query plan.
this._container = DOM.$('.execution-plan');
@@ -162,6 +164,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar'),
this._instantiationService.createInstance(CustomZoomAction, 'ActionBar'),
this._instantiationService.createInstance(PropertiesAction, 'ActionBar'),
+ new CompareExecutionPlanAction(),
this.actionBarToggleTopTip
];
this._actionBar.pushAction(actionBarActions, { icon: true, label: false });
@@ -178,6 +181,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu'),
this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu'),
this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'),
+ new CompareExecutionPlanAction(),
this.contextMenuToggleTooltipAction
];
const self = this;
@@ -278,6 +282,14 @@ export class ExecutionPlanView implements ISashLayoutProvider {
public hideActionBar() {
this._actionBarContainer.style.display = 'none';
}
+
+ public compareCurrentExecutionPlan() {
+ this._editorService.openEditor(this._instantiationService.createInstance(ExecutionPlanComparisonInput, {
+ topExecutionPlan: [this._model]
+ }), {
+ pinned: true
+ });
+ }
}
type ExecutionPlanActionSource = 'ContextMenu' | 'ActionBar' | 'HotKey';
@@ -513,3 +525,16 @@ export class ContextMenuTooltipToggle extends Action {
}
}
}
+
+export class CompareExecutionPlanAction extends Action {
+ public static ID = 'ep.tooltipToggleContextMenu';
+ public static COMPARE_PLAN = localize('executionPlanCompareExecutionPlanAction', "Compare execution plan");
+
+ constructor() {
+ super(CompareExecutionPlanAction.COMPARE_PLAN, CompareExecutionPlanAction.COMPARE_PLAN, executionPlanCompareIconClassName);
+ }
+
+ public override async run(context: ExecutionPlanView): Promise {
+ context.compareCurrentExecutionPlan();
+ }
+}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts
index 2b66fb937e..710485e7d2 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts
@@ -27,13 +27,15 @@ export class ExecutionPlanViewHeader {
public constructor(
private _parentContainer: HTMLElement,
- headerData: PlanHeaderData,
+ headerData: PlanHeaderData | undefined,
@IInstantiationService public readonly _instantiationService: IInstantiationService) {
- this._graphIndex = headerData.planIndex;
- this._relativeCost = headerData.relativeCost;
- this._query = headerData.query;
- this._recommendations = headerData.recommendations ?? [];
+ if (headerData) {
+ 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');
@@ -84,28 +86,31 @@ export class ExecutionPlanViewHeader {
}
private renderQueryText(): void {
- this._queryContainer.innerText = this._query;
+ if (this._query) {
+ this._queryContainer.innerText = this._query;
+ }
}
private renderRecommendations(): void {
- while (this._recommendationsContainer.firstChild) {
- this._recommendationsContainer.removeChild(this._recommendationsContainer.firstChild);
+ if (this._recommendations) {
+ 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);
+ });
+ });
}
- 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);
- });
- });
-
}
}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/add.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/add.svg
new file mode 100644
index 0000000000..458f400000
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/execution-plan-compare.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/execution-plan-compare.svg
new file mode 100644
index 0000000000..bca3dcf1fa
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/execution-plan-compare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/executionPlanEdtiorTabIcon.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/executionPlanEdtiorTabIcon.svg
new file mode 100644
index 0000000000..98b27841be
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/executionPlanEdtiorTabIcon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/resetZoom.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/resetZoom.svg
new file mode 100644
index 0000000000..f596302dc4
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/resetZoom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/settings.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/settings.svg
new file mode 100644
index 0000000000..ac44ecdcf5
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/settings.svg
@@ -0,0 +1 @@
+
diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenHorizontally.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenHorizontally.svg
new file mode 100644
index 0000000000..f27cea683d
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenHorizontally.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenVertically.svg b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenVertically.svg
new file mode 100644
index 0000000000..9809c557c6
--- /dev/null
+++ b/src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenVertically.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css
index 5366e7dd98..be07d02d4a 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css
+++ b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css
@@ -109,24 +109,34 @@ However we always want it to be the width of the container it is resizing.
position: relative;
}
-/* Properties view in execution plan */
-.eps-container .execution-plan .properties {
+.eps-container .properties {
flex: 0 0 500px;
+ flex-direction: row;
+}
+
+.eps-container .properties-sash {
+ position: relative;
+ height: 100%;
+ width: 1px;
+}
+
+/* Properties view in execution plan */
+.properties-content {
overflow: hidden;
width: 500px;
- height: calc( 100% - 2px );
+ height: calc(100% - 2px);
display: flex;
flex-direction: column;
- border-left: 1px solid;
+ border-left: 3px solid;
+ border-color: var(--separator-border)
}
/* Title container of the properties view */
-.eps-container .execution-plan .properties .title {
+.eps-container .properties .title {
line-height: 30px;
height: 22px;
font-size: 11px;
font-weight: bold;
- text-transform: uppercase;
overflow: hidden;
display: flex;
align-items: center;
@@ -137,7 +147,7 @@ However we always want it to be the width of the container it is resizing.
}
/* text in title container of properties view */
-.eps-container .execution-plan .properties .title .text {
+.eps-container .properties .title .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -148,7 +158,7 @@ However we always want it to be the width of the container it is resizing.
}
/* action bar in the title container for the properties view. This contains the close icon */
-.eps-container .execution-plan .properties .title .action-bar {
+.eps-container .properties .title .action-bar {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -160,7 +170,7 @@ However we always want it to be the width of the container it is resizing.
}
/* Operation name styling in the properties view. */
-.eps-container .execution-plan .properties .operation-name {
+.eps-container .properties .operation-name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -174,8 +184,8 @@ However we always want it to be the width of the container it is resizing.
}
/* Properties table container in properties view */
-.eps-container .execution-plan .properties .table-container {
- overflow-y: scroll;
+.eps-container .properties .table-container {
+ overflow: hidden;
flex: 1;
flex-grow: 1;
}
@@ -183,6 +193,7 @@ However we always want it to be the width of the container it is resizing.
/* Action bar for the execution plan */
.eps-container .execution-plan .action-bar-container {
flex: 0 0 25px;
+ border-left: 1px solid var(--separator-border);
}
/* styling for the column headers in the properties table */
@@ -195,7 +206,6 @@ However we always want it to be the width of the container it is resizing.
-webkit-margin-before: 0;
-webkit-margin-after: 0;
font-weight: bold;
- text-transform: uppercase;
}
.eps-container .properties-header {
@@ -331,3 +341,184 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
}
+
+.ep-add-icon {
+ background-image: url(../images/actionIcons/add.svg);
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.ep-settings-icon {
+ background-image: url(../images/actionIcons/settings.svg);
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.ep-split-screen-horizontally-icon {
+ background-image: url(../images/actionIcons/splitScreenHorizontally.svg);
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.ep-split-screen-vertically-icon {
+ background-image: url(../images/actionIcons/splitScreenVertically.svg);
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.ep-reset-zoom-icon {
+ background-image: url(../images/actionIcons/resetZoom.svg);
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.ep-plan-compare-icon {
+ background-image: url(../images/actionIcons/execution-plan-compare.svg);
+ background-size: 16px 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.eps-container .comparison-editor {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.eps-container .comparison-editor .editor-toolbar {
+ width: 100%;
+ flex: 0 0 auto;
+ border-bottom: 2px solid;
+ border-color: var(--separator-border);
+}
+
+.eps-container .comparison-editor .plan-comparison-container {
+ width: 100%;
+ height: 100%;
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+}
+
+.eps-container .comparison-editor .plan-comparison-container {
+ width: 100%;
+ height: 100%;
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+}
+
+.eps-container .comparison-editor .plan-comparison-container {
+ width: 100%;
+ height: calc(100% - 25px);
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .dropdown-container {
+ flex: 0;
+ padding: 3px;
+ border-bottom: 1px solid var(--separator-border);
+}
+
+/* each link in execution plan recommendations */
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .recommendations > a {
+ width: fit-content;
+ align-items: left;
+ text-align: left;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .sash-container {
+ flex: 0 0 2px;
+ width: 100%;
+ position: relative;
+ background-color: var(--separator-border);
+}
+
+/*
+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.
+*/
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .sash-container .horizontal {
+ width: 100% !important;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .sash-container .vertical {
+ height: 100% !important;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .properties {
+ display: flex;
+ flex: 0 0 500px;
+ flex-direction: row;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .placeholder {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .placeholder .infobox-container {
+ width: fit-content;
+ height: fit-content;
+}
+
+
+.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-diagram {
+ flex: 1;
+ overflow: scroll;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+
+.eps-container .comparison-editor .plan-comparison-container .properties .compare-operation-name {
+ display: flex;
+ flex-direction: row;
+}
+
+.eps-container .comparison-editor .plan-comparison-container .properties .compare-operation-name-text {
+ flex: 1;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: 13px;
+ -webkit-margin-before: 0;
+ -webkit-margin-after: 0;
+ margin-top: 3px;
+ margin-bottom: 5px;
+ margin-left: 5px;
+}
+
+.eps-container .comparison-editor .properties .table-container {
+ overflow: hidden;
+}
diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts
index 46162e1edc..d81577e703 100644
--- a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts
+++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts
@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import * as azdata from 'azdata';
import { ExecutionPlanWidgetBase } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
@@ -14,7 +15,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Action } from 'vs/base/common/actions';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
-import { AzdataGraphView, InternalExecutionPlanNode, SearchType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
+import { AzdataGraphView, SearchType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
const CONTAINS_DISPLAY_STRING = localize("executionPlanSearchTypeContains", 'Contains');
@@ -35,7 +36,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
private _selectedSearchType: SearchType = SearchType.Equals;
private _searchTextInputBox: InputBox;
- private _searchResults: InternalExecutionPlanNode[] = [];
+ private _searchResults: azdata.executionPlan.ExecutionPlanNode[] = [];
private _currentSearchResultIndex = 0;
private _usePreviousSearchResult: boolean = false;
diff --git a/src/sql/workbench/contrib/executionPlan/common/executionPlanInput.ts b/src/sql/workbench/contrib/executionPlan/common/executionPlanInput.ts
index 76b8c1f084..f27058d7eb 100644
--- a/src/sql/workbench/contrib/executionPlan/common/executionPlanInput.ts
+++ b/src/sql/workbench/contrib/executionPlan/common/executionPlanInput.ts
@@ -67,6 +67,9 @@ export class ExecutionPlanInput extends EditorInput {
}
get resource(): URI | undefined {
- return undefined;
+ return URI.from({
+ scheme: ExecutionPlanInput.SCHEMA,
+ path: 'execution-plan'
+ });
}
}
diff --git a/src/sql/workbench/services/executionPlan/common/executionPlanService.ts b/src/sql/workbench/services/executionPlan/common/executionPlanService.ts
index 6b29d4eb7b..3fc58cfad2 100644
--- a/src/sql/workbench/services/executionPlan/common/executionPlanService.ts
+++ b/src/sql/workbench/services/executionPlan/common/executionPlanService.ts
@@ -133,5 +133,23 @@ export class ExecutionPlanService implements IExecutionPlanService {
return this._capabilitiesService.getCapabilities(providerId).connection.supportedExecutionPlanFileExtensions;
}
+ getSupportedExecutionPlanExtensions(providerId: string): string[] | undefined {
+ if (providerId) {
+ return this._capabilitiesService.getCapabilities(providerId).connection.supportedExecutionPlanFileExtensions;
+ } else {
+ const supportedFileExtensionsSet: Set = new Set();
+
+ Object.keys(this._capabilitiesService.providers).forEach(v => {
+ const extensions = this._capabilitiesService.getCapabilities(v).connection.supportedExecutionPlanFileExtensions;
+ if (extensions) {
+ extensions.forEach(ext => {
+ supportedFileExtensionsSet.add(ext);
+ });
+ }
+ });
+
+ return [...supportedFileExtensionsSet];
+ }
+ }
_serviceBrand: undefined;
}
diff --git a/src/sql/workbench/services/executionPlan/common/interfaces.ts b/src/sql/workbench/services/executionPlan/common/interfaces.ts
index 1517353017..8e4a9f9ebb 100644
--- a/src/sql/workbench/services/executionPlan/common/interfaces.ts
+++ b/src/sql/workbench/services/executionPlan/common/interfaces.ts
@@ -30,7 +30,8 @@ export interface IExecutionPlanService {
compareExecutionPlanGraph(firstPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo, secondPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo): Promise;
/**
- * Get execution plan file extensions supported by the provider.
+ * Get execution plan file extensions supported by all registered providers.
+ * @param providerId optional parameter to get extensions only supported by a particular provider.
*/
- getSupportedExecutionPlanExtensionsForProvider(providerId: string): string[];
+ getSupportedExecutionPlanExtensions(providerId?: string): string[];
}
diff --git a/yarn.lock b/yarn.lock
index 4be4b29623..f77a0d5dbf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1831,9 +1831,9 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
-"azdataGraph@github:Microsoft/azdataGraph#0.0.21":
- version "0.0.21"
- resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
+"azdataGraph@github:Microsoft/azdataGraph#0.0.26":
+ version "0.0.26"
+ resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/a5f94e53cb655bc44f1a2727653bb403942e1cf9"
azure-storage@^2.10.2:
version "2.10.2"