From 8bb6b5fc1a72f5e40f4da9812cdefda3cb4a6f5e Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Mon, 23 May 2022 14:33:18 -0700 Subject: [PATCH] Redoing Execution Plan Comparison Editor (#19375) * Adding code for execution plan comparison editor * Handling floating promises and fixing a loc string * Fixing some polygon stuff * Fixing azdatagraph null check bug * Adding progress notification for similar areas * Removing some floating promises * Fixing button enabled state --- .../icons/images/execution-plan-compare.svg | 1 + .../icons/images/execution-plan.svg | 1 + .../theme-seti/icons/vs-seti-icon-theme.json | 10 +- package.json | 2 +- remote/package.json | 2 +- remote/web/package.json | 2 +- remote/web/yarn.lock | 6 +- remote/yarn.lock | 6 +- src/sql/azdata.proposed.d.ts | 10 +- .../api/browser/mainThreadDataProtocol.ts | 3 +- .../executionPlan/browser/azdataGraphView.ts | 50 +- .../browser/compareExecutionPlanInput.ts | 57 ++ .../executionPlan/browser/constants.ts | 84 +++ .../browser/executionPlanComparisonEditor.ts | 84 +++ .../executionPlanComparisonEditorView.ts | 675 ++++++++++++++++++ .../executionPlanComparisonPropertiesView.ts | 268 +++++++ .../browser/executionPlanContribution.ts | 40 +- .../browser/executionPlanPropertiesView.ts | 4 +- .../executionPlanPropertiesViewBase.ts | 58 +- .../browser/executionPlanView.ts | 27 +- .../browser/executionPlanViewHeader.ts | 51 +- .../browser/images/actionIcons/add.svg | 1 + .../actionIcons/execution-plan-compare.svg | 1 + .../executionPlanEdtiorTabIcon.svg | 1 + .../browser/images/actionIcons/resetZoom.svg | 1 + .../browser/images/actionIcons/settings.svg | 1 + .../actionIcons/splitScreenHorizontally.svg | 1 + .../actionIcons/splitScreenVertically.svg | 1 + .../browser/media/executionPlan.css | 215 +++++- .../browser/widgets/nodeSearchWidget.ts | 5 +- .../common/executionPlanInput.ts | 5 +- .../common/executionPlanService.ts | 18 + .../executionPlan/common/interfaces.ts | 5 +- yarn.lock | 6 +- 34 files changed, 1601 insertions(+), 101 deletions(-) create mode 100644 extensions/theme-seti/icons/images/execution-plan-compare.svg create mode 100644 extensions/theme-seti/icons/images/execution-plan.svg create mode 100644 src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts create mode 100644 src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts create mode 100644 src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts create mode 100644 src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts create mode 100644 src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/add.svg create mode 100644 src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/execution-plan-compare.svg create mode 100644 src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/executionPlanEdtiorTabIcon.svg create mode 100644 src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/resetZoom.svg create mode 100644 src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/settings.svg create mode 100644 src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenHorizontally.svg create mode 100644 src/sql/workbench/contrib/executionPlan/browser/images/actionIcons/splitScreenVertically.svg 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 @@ +BranchCompare_16x \ 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 @@ +Add_16x \ 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 @@ +BranchCompare_16x \ 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 @@ +ResetView_16x \ 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 @@ +SplitScreenHorizontal_16x \ 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 @@ +SplitScreenVertical_16x \ 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"