From f51e5c370bc092ef3f2dd986d90d37ad4c81e22c Mon Sep 17 00:00:00 2001 From: Lewis Sanchez <87730006+lewis-sanchez@users.noreply.github.com> Date: Fri, 14 Oct 2022 14:50:25 -0700 Subject: [PATCH] Registers all disposable items for query execution plans (#20851) --- .../executionPlan/browser/azdataGraphView.ts | 31 +++- .../browser/compareExecutionPlanInput.ts | 1 + .../browser/executionPlanComparisonEditor.ts | 3 +- .../executionPlanComparisonEditorView.ts | 150 +++++++++++------- .../executionPlanComparisonPropertiesView.ts | 6 +- .../browser/executionPlanContribution.ts | 6 +- .../browser/executionPlanEditor.ts | 2 + .../browser/executionPlanFileView.ts | 18 ++- .../browser/executionPlanPropertiesView.ts | 7 + .../executionPlanPropertiesViewBase.ts | 42 +++-- .../executionPlan/browser/executionPlanTab.ts | 9 +- .../browser/executionPlanTreeTab.ts | 76 +++++---- .../browser/executionPlanView.ts | 127 ++++++++------- .../browser/executionPlanViewHeader.ts | 18 ++- .../browser/executionPlanWidgetController.ts | 1 + .../executionPlan/browser/topOperationsTab.ts | 74 ++++++--- .../browser/widgets/customZoomWidget.ts | 23 +-- .../widgets/highlightExpensiveNodeWidget.ts | 20 +-- .../browser/widgets/nodeSearchWidget.ts | 39 ++--- .../contrib/query/browser/gridPanel.ts | 4 +- 20 files changed, 402 insertions(+), 255 deletions(-) diff --git a/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts b/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts index 42ec761a64..9364971d46 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/azdataGraphView.ts @@ -14,13 +14,14 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { foreground } from 'vs/platform/theme/common/colorRegistry'; import { generateUuid } from 'vs/base/common/uuid'; +import { Disposable } from 'vs/base/common/lifecycle'; const azdataGraph = azdataGraphModule(); /** * This view holds the azdataGraph diagram and provides different * methods to manipulate the azdataGraph */ -export class AzdataGraphView { +export class AzdataGraphView extends Disposable { private _diagram: any; private _diagramModel: AzDataGraphCell; @@ -37,6 +38,8 @@ export class AzdataGraphView { private _executionPlan: azdata.executionPlan.ExecutionPlanGraph, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, ) { + super(); + this._diagramModel = this.populate(this._executionPlan.root); let queryPlanConfiguration = { @@ -58,13 +61,13 @@ export class AzdataGraphView { this._diagram.graph.setCellsDisconnectable(false); // preventing graph edges to be disconnected from source and target nodes. this._diagram.graph.tooltipHandler.delay = 700; // increasing delay for tooltips - registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + this._register(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const iconLabelColor = theme.getColor(foreground); if (iconLabelColor) { this._diagram.setTextFontColor(iconLabelColor); this._diagram.setEdgeColor(iconLabelColor); } - }); + })); } private initializeGraphEvents(): void { @@ -74,10 +77,12 @@ export class AzdataGraphView { if (this._cellInFocus?.id === evt.properties.removed[0]?.id) { return; } + const newSelection = evt.properties.removed[0]; if (!newSelection) { return; } + this._onElementSelectedEmitter.fire(this.getElementById(newSelection.id)); this.centerElement(this.getElementById(newSelection.id)); this._cellInFocus = evt.properties.removed[0]; @@ -102,7 +107,9 @@ export class AzdataGraphView { } else { cell = this._diagram.graph.model.getCell((this._executionPlan.root).id); } + this._diagram.graph.getSelectionModel().setCell(cell); + if (bringToCenter) { this.centerElement(element); } @@ -116,6 +123,7 @@ export class AzdataGraphView { if (cell?.id) { return this.getElementById(cell.id); } + return undefined; } @@ -158,6 +166,7 @@ export class AzdataGraphView { if (level < 1) { throw new Error(localize('invalidExecutionPlanZoomError', "Zoom level cannot be 0 or negative")); } + this._diagram.zoomTo(level); } @@ -168,11 +177,13 @@ export class AzdataGraphView { public getElementById(id: string): InternalExecutionPlanElement | undefined { const nodeStack: azdata.executionPlan.ExecutionPlanNode[] = []; nodeStack.push(this._executionPlan.root); + while (nodeStack.length !== 0) { const currentNode = nodeStack.pop(); if (currentNode.id === id) { return currentNode; } + if (currentNode.edges) { for (let i = 0; i < currentNode.edges.length; i++) { if ((currentNode.edges[i]).id === id) { @@ -180,8 +191,10 @@ export class AzdataGraphView { } } } + nodeStack.push(...currentNode.children); } + return undefined; } @@ -225,12 +238,15 @@ export class AzdataGraphView { matchFound = matchingProp.value < searchQuery.value || matchingProp.value > searchQuery.value; break; } + if (matchFound) { resultNodes.push(currentNode); } } + nodeStack.push(...currentNode.children); } + return resultNodes; } @@ -303,13 +319,14 @@ export class AzdataGraphView { diagramNode.tooltipTitle = node.name; diagramNode.rowCountDisplayString = node.rowCountDisplayString; diagramNode.costDisplayString = node.costDisplayString; - if (!node.id.toString().startsWith(`element-`)) { - node.id = `element-${node.id}`; - } this.expensiveMetricTypes.add(ExpensiveMetricType.Off); + if (!node.id.toString().startsWith(`element-`)) { + node.id = `element-${node.id}`; + } diagramNode.id = node.id; + diagramNode.icon = node.type; diagramNode.metrics = this.populateProperties(node.properties); @@ -401,6 +418,7 @@ export class AzdataGraphView { props.forEach(p => { this._graphElementPropertiesSet.add(p.name); }); + return props.filter(e => isString(e.displayValue) && e.showInTooltip) .sort((a, b) => a.displayOrder - b.displayOrder) .map(e => { @@ -449,6 +467,7 @@ export class AzdataGraphView { } else { this._diagram.graph.tooltipHandler.setEnabled(true); } + return this._diagram.graph.tooltipHandler.enabled; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts b/src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts index de34160b56..6c7f0b46f2 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput.ts @@ -29,6 +29,7 @@ export class ExecutionPlanComparisonInput extends EditorInput { const existingNames = this._editorService.editors.map(editor => editor.getName()); let i = 1; this._editorName = `${this.editorNamePrefix}_${i}`; + while (existingNames.includes(this._editorName)) { i++; this._editorName = `${this.editorNamePrefix}_${i}`; diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts index a8295c5dc6..662ac91421 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor.ts @@ -66,7 +66,8 @@ export class ExecutionPlanComparisonEditor extends EditorPane { // 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); + input._executionPlanComparisonView = this._register(this._instantiationService.createInstance(ExecutionPlanComparisonEditorView, this._editorContainer)); + if (this.input.preloadModel) { if (this.input.preloadModel.topExecutionPlan) { input._executionPlanComparisonView.addExecutionPlanGraph(this.input.preloadModel.topExecutionPlan, this.input.preloadModel.topPlanIndex); diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts index 055f8ddff3..4c169c13a4 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts @@ -34,10 +34,11 @@ import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPl import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget'; import { Button } from 'sql/base/browser/ui/button/button'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { Disposable } from 'vs/base/common/lifecycle'; const ADD_EXECUTION_PLAN_STRING = localize('epCompare.addExecutionPlanLabel', 'Add execution plan'); -export class ExecutionPlanComparisonEditorView { +export class ExecutionPlanComparisonEditorView extends Disposable { public container: HTMLElement; @@ -134,6 +135,7 @@ export class ExecutionPlanComparisonEditorView { @IProgressService private _progressService: IProgressService, @IContextMenuService private _contextMenuService: IContextMenuService ) { + super(); this.container = DOM.$('.comparison-editor'); parentContainer.appendChild(this.container); @@ -144,22 +146,16 @@ export class ExecutionPlanComparisonEditorView { // 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 = this._instantiationService.createInstance(AddExecutionPlanAction); - this._zoomOutAction = new ZoomOutAction(); - this._zoomInAction = new ZoomInAction(); - this._zoomToFitAction = new ZoomToFitAction(); - this._propertiesAction = this._instantiationService.createInstance(PropertiesAction); - this._toggleOrientationAction = new ToggleOrientation(); - this._searchNodeAction = this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Primary); - this._searchNodeActionForAddedPlan = this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Added); - this._resetZoomAction = new ZoomReset(); - this._toggleTooltipAction = new ActionBarToggleTooltip(); + this._addExecutionPlanAction = this._register(this._instantiationService.createInstance(AddExecutionPlanAction)); + this._zoomOutAction = this._register(new ZoomOutAction()); + this._zoomInAction = this._register(new ZoomInAction()); + this._zoomToFitAction = this._register(new ZoomToFitAction()); + this._propertiesAction = this._register(this._instantiationService.createInstance(PropertiesAction)); + this._toggleOrientationAction = this._register(new ToggleOrientation()); + this._searchNodeAction = this._register(this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Primary)); + this._searchNodeActionForAddedPlan = this._register(this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Added)); + this._resetZoomAction = this._register(new ZoomReset()); + this._toggleTooltipAction = this._register(new ActionBarToggleTooltip()); const content: ITaskbarContent[] = [ { action: this._addExecutionPlanAction }, { action: this._zoomInAction }, @@ -172,6 +168,12 @@ export class ExecutionPlanComparisonEditorView { { action: this._searchNodeActionForAddedPlan }, { action: this._toggleTooltipAction } ]; + + this._taskbarContainer = DOM.$('.editor-toolbar'); + this._taskbar = this._register(new Taskbar(this._taskbarContainer, { + orientation: ActionsOrientation.HORIZONTAL, + })); + this._taskbar.context = this; this._taskbar.setContent(content); this.container.appendChild(this._taskbarContainer); } @@ -192,9 +194,10 @@ export class ExecutionPlanComparisonEditorView { this._placeholderContainer = DOM.$('.placeholder'); const contextMenuAction = [ - this._instantiationService.createInstance(AddExecutionPlanAction) + this._register(this._instantiationService.createInstance(AddExecutionPlanAction)) ]; - this._placeholderContainer.oncontextmenu = (e: MouseEvent) => { + + this._register(DOM.addDisposableListener(this._placeholderContainer, DOM.EventType.CONTEXT_MENU, (e: MouseEvent) => { if (contextMenuAction) { this._contextMenuService.showContextMenu({ getAnchor: () => { @@ -207,24 +210,25 @@ export class ExecutionPlanComparisonEditorView { getActionsContext: () => (self) }); } - }; + })); this._placeholderInfoboxContainer = DOM.$('.placeholder-infobox'); - this._placeholderButton = new Button(this._placeholderInfoboxContainer, { secondary: true }); - attachButtonStyler(this._placeholderButton, this.themeService); + this._placeholderButton = this._register(new Button(this._placeholderInfoboxContainer, { secondary: true })); + this._register(attachButtonStyler(this._placeholderButton, this.themeService)); this._placeholderButton.label = ADD_EXECUTION_PLAN_STRING; this._placeholderButton.ariaLabel = ADD_EXECUTION_PLAN_STRING; this._placeholderButton.enabled = true; - this._placeholderButton.onDidClick(e => { - const addExecutionPlanAction = this._instantiationService.createInstance(AddExecutionPlanAction); - addExecutionPlanAction.run(self); - }); - this._placeholderLoading = new LoadingSpinner(this._placeholderContainer, { + this._register(this._placeholderButton.onDidClick(e => { + const addExecutionPlanAction = this._register(this._instantiationService.createInstance(AddExecutionPlanAction)); + addExecutionPlanAction.run(self); + })); + + this._placeholderLoading = this._register(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"); @@ -232,9 +236,10 @@ export class ExecutionPlanComparisonEditorView { 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 = this._register(new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._topPlanDropdownContainer)); this._topPlanDropdown.render(this._topPlanDropdownContainer); - this._topPlanDropdown.onDidSelect(async (e) => { + + this._register(this._topPlanDropdown.onDidSelect(async (e) => { this.activeBottomPlanDiagram?.clearSubtreePolygon(); this.activeTopPlanDiagram?.clearSubtreePolygon(); @@ -247,19 +252,21 @@ export class ExecutionPlanComparisonEditorView { this._activeTopPlanIndex = e.index; await this.getSkeletonNodes(); - }); - attachSelectBoxStyler(this._topPlanDropdown, this.themeService); + })); + + this._register(attachSelectBoxStyler(this._topPlanDropdown, this.themeService)); this._topPlanContainer.appendChild(this._topPlanDropdownContainer); - this._topPlanRecommendations = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._topPlanContainer, undefined); + this._topPlanRecommendations = this._register(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 = this._register(new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._bottomPlanDropdownContainer)); this._bottomPlanDropdown.render(this._bottomPlanDropdownContainer); - this._bottomPlanDropdown.onDidSelect(async (e) => { + + this._register(this._bottomPlanDropdown.onDidSelect(async (e) => { this.activeBottomPlanDiagram?.clearSubtreePolygon(); this.activeTopPlanDiagram?.clearSubtreePolygon(); @@ -272,24 +279,27 @@ export class ExecutionPlanComparisonEditorView { this._activeBottomPlanIndex = e.index; await this.getSkeletonNodes(); - }); - attachSelectBoxStyler(this._bottomPlanDropdown, this.themeService); + })); + + this._register(attachSelectBoxStyler(this._bottomPlanDropdown, this.themeService)); this._bottomPlanContainer.appendChild(this._bottomPlanDropdownContainer); - this._bottomPlanRecommendations = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._bottomPlanContainer, undefined); + this._bottomPlanRecommendations = this._register(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 }); + this._verticalSash = this._register(new Sash(this._sashContainer, new VerticalSash(this), { orientation: Orientation.VERTICAL, size: 3 })); let originalWidth; let change = 0; - this._verticalSash.onDidStart((e: ISashEvent) => { + + this._register(this._verticalSash.onDidStart((e: ISashEvent) => { originalWidth = this._topPlanContainer.offsetWidth; - }); - this._verticalSash.onDidChange((evt: ISashEvent) => { + })); + + this._register(this._verticalSash.onDidChange((evt: ISashEvent) => { change = evt.startX - evt.currentX; const newWidth = originalWidth - change; if (newWidth < 200) { @@ -297,14 +307,16 @@ export class ExecutionPlanComparisonEditorView { } 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 }); + this._horizontalSash = this._register(new Sash(this._sashContainer, new HorizontalSash(this), { orientation: Orientation.HORIZONTAL, size: 3 })); let startHeight; - this._horizontalSash.onDidStart((e: ISashEvent) => { + + this._register(this._horizontalSash.onDidStart((e: ISashEvent) => { startHeight = this._topPlanContainer.offsetHeight; - }); - this._horizontalSash.onDidChange((evt: ISashEvent) => { + })); + + this._register(this._horizontalSash.onDidChange((evt: ISashEvent) => { change = evt.startY - evt.currentY; const newHeight = startHeight - change; if (newHeight < 200) { @@ -312,12 +324,12 @@ export class ExecutionPlanComparisonEditorView { } 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._propertiesView = this._register(this._instantiationService.createInstance(ExecutionPlanComparisonPropertiesView, this._propertiesContainer)); this._planComparisonContainer.appendChild(this._propertiesContainer); } @@ -343,6 +355,7 @@ export class ExecutionPlanComparisonEditorView { canSelectMany: false, canSelectFiles: true }); + if (openedFileUris?.length === 1) { this._placeholderInfoboxContainer.style.display = 'none'; this._placeholderLoading.loading = true; @@ -354,6 +367,7 @@ export class ExecutionPlanComparisonEditorView { }); await this.addExecutionPlanGraph(executionPlanGraphs.graphs, 0); } + this._placeholderInfoboxContainer.style.display = ''; this._placeholderLoading.loading = false; this._placeholderInfoboxContainer.style.display = ''; @@ -367,6 +381,7 @@ export class ExecutionPlanComparisonEditorView { public async addExecutionPlanGraph(executionPlanGraphs: azdata.executionPlan.ExecutionPlanGraph[], preSelectIndex: number): Promise { if (!this._topPlanDiagramModels) { this._topPlanDiagramModels = executionPlanGraphs; + this._topPlanDropdown.setOptions(executionPlanGraphs.map((e, index) => { return { text: this.createQueryDropdownPrefixString(e.query, index + 1, executionPlanGraphs.length) @@ -377,26 +392,30 @@ export class ExecutionPlanComparisonEditorView { const graphContainer = DOM.$('.plan-diagram'); this._topPlanDiagramContainers.push(graphContainer); this._topPlanContainer.appendChild(graphContainer); - const diagram = this._instantiationService.createInstance(AzdataGraphView, graphContainer, e); - diagram.onElementSelected(e => { + + const diagram = this._register(this._instantiationService.createInstance(AzdataGraphView, graphContainer, e)); + this._register(diagram.onElementSelected(e => { this._propertiesView.setPrimaryElement(e); + const id = e.id.replace(`element-`, ''); if (this._topSimilarNode.has(id)) { const similarNode = this._topSimilarNode.get(id); if (this.activeBottomPlanDiagram) { - const element = this.activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); if (similarNode.matchingNodesId.find(m => this.activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { return; } + const element = this.activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); this.activeBottomPlanDiagram.selectElement(element); } } - }); + })); + this.topPlanDiagrams.push(diagram); graphContainer.style.display = 'none'; }); + this._topPlanDropdown.select(preSelectIndex); this._propertiesView.setPrimaryElement(executionPlanGraphs[0].root); this._propertiesAction.enabled = true; @@ -408,43 +427,51 @@ export class ExecutionPlanComparisonEditorView { this._searchNodeAction.enabled = true; } else { this._bottomPlanDiagramModels = executionPlanGraphs; + this._bottomPlanDropdown.setOptions(executionPlanGraphs.map((e, index) => { return { text: this.createQueryDropdownPrefixString(e.query, index + 1, executionPlanGraphs.length) }; })); + 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 => { + const diagram = this._register(this._instantiationService.createInstance(AzdataGraphView, graphContainer, e)); + + this._register(diagram.onElementSelected(e => { this._propertiesView.setSecondaryElement(e); + const id = e.id.replace(`element-`, ''); if (this._bottomSimilarNode.has(id)) { const similarNode = this._bottomSimilarNode.get(id); if (this.activeTopPlanDiagram) { - const element = this.activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); if (this.activeTopPlanDiagram.getSelectedElement() && similarNode.matchingNodesId.find(m => this.activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { return; } + const element = this.activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); this.activeTopPlanDiagram.selectElement(element); } } - }); + })); + this.bottomPlanDiagrams.push(diagram); graphContainer.style.display = 'none'; }); + this._bottomPlanDropdown.select(preSelectIndex); this._propertiesView.setSecondaryElement(executionPlanGraphs[0].root); this._addExecutionPlanAction.enabled = false; this._searchNodeActionForAddedPlan.enabled = true; + if (!this._areTooltipsEnabled) { this.activeBottomPlanDiagram.toggleTooltip(); } } + this.refreshSplitView(); } @@ -463,19 +490,24 @@ export class ExecutionPlanComparisonEditorView { 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 currentRequestId = generateUuid(); this._latestRequestUuid = currentRequestId; + const result = await this._executionPlanService.compareExecutionPlanGraph(this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile, this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile); + if (currentRequestId !== this._latestRequestUuid) { return; } + this.getSimilarSubtrees(result.firstComparisonResult); this.getSimilarSubtrees(result.secondComparisonResult, true); + let colorIndex = 0; this._polygonRootsMap.forEach((v, k) => { if (this.activeTopPlanDiagram && this.activeBottomPlanDiagram) { @@ -494,6 +526,7 @@ export class ExecutionPlanComparisonEditorView { 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, @@ -502,6 +535,7 @@ export class ExecutionPlanComparisonEditorView { } } 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; @@ -779,7 +813,7 @@ class SearchNodeAction extends Action { .withAdditionalProperties({ source: 'ComparisonView' }) .send(); - let nodeSearchWidget = this._instantiationService.createInstance(NodeSearchWidget, widgetController, executionPlan); + let nodeSearchWidget = this._register(this._instantiationService.createInstance(NodeSearchWidget, widgetController, executionPlan)); widgetController.toggleWidget(nodeSearchWidget); } } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts index 9f0c403c3c..aff0afd87d 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts @@ -135,10 +135,11 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti const columns: Slick.Column[] = this.getPropertyTableColumns(); let primaryProps = []; - let secondaryProps = []; if (this._model.primaryElement?.properties) { primaryProps = this._model.primaryElement.properties; } + + let secondaryProps = []; if (this._model.secondaryElement?.properties) { secondaryProps = this._model.secondaryElement.properties; } @@ -440,6 +441,7 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti }; rows.push(row); + if (!isString(primaryProp.value)) { row.name.iconCssClass += ` parent-row-styling`; row.primary.iconCssClass += ` parent-row-styling`; @@ -459,6 +461,7 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti }; rows.push(row); + if (!isString(secondaryProp.value)) { row.name.iconCssClass += ` parent-row-styling`; row.secondary.iconCssClass += ` parent-row-styling`; @@ -475,6 +478,7 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti if (this._orientation === value) { return; } + this._orientation = value; this.updatePropertyContainerTitles(); } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts index 332737746f..f5c0711f1c 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanContribution.ts @@ -61,7 +61,7 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen } }); - this._editorResolverService.registerEditor( + this._register(this._editorResolverService.registerEditor( this.getGlobForFileExtensions(supportedFileFormats), { id: ExecutionPlanEditor.ID, @@ -74,11 +74,11 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen graphFileContent: undefined, graphFileType: undefined }; + const executionPlanInput = this._register(this._instantiationService.createInstance(ExecutionPlanInput, editorInput.resource, executionPlanGraphInfo)); - const executionPlanInput = this._instantiationService.createInstance(ExecutionPlanInput, editorInput.resource, executionPlanGraphInfo); return { editor: executionPlanInput, options: editorInput.options, group: group }; } - ); + )); } private getGlobForFileExtensions(extensions: string[]): string { diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts index 882a6f175d..16185c2b87 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts @@ -50,11 +50,13 @@ export class ExecutionPlanEditor extends EditorPane { override clearInput(): void { const currentInput = this.input as ExecutionPlanInput; + // clearing old input view if present in the editor if (currentInput?._executionPlanFileViewUUID) { const oldView = this._viewCache.executionPlanFileViewMap.get(currentInput._executionPlanFileViewUUID); oldView.onHide(this._parentContainer); } + super.clearInput(); } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanFileView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanFileView.ts index ddf5cbb98b..c575294be8 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanFileView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanFileView.ts @@ -14,8 +14,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { contrastBorder, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView'; +import { Disposable } from 'vs/base/common/lifecycle'; -export class ExecutionPlanFileView { +export class ExecutionPlanFileView extends Disposable { private _parent: HTMLElement; private _loadingSpinner: LoadingSpinner; private _loadingErrorInfoBox: InfoBox; @@ -30,6 +31,7 @@ export class ExecutionPlanFileView { @IInstantiationService private instantiationService: IInstantiationService, @IExecutionPlanService private executionPlanService: IExecutionPlanService ) { + super(); } public render(parent: HTMLElement): void { @@ -48,9 +50,6 @@ export class ExecutionPlanFileView { } } - dispose() { - } - /** * Adds executionPlanGraph to the graph controller. * @param newGraphs ExecutionPlanGraphs to be added. @@ -58,7 +57,7 @@ export class ExecutionPlanFileView { public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) { if (newGraphs) { newGraphs.forEach(g => { - const ep = this.instantiationService.createInstance(ExecutionPlanView, this._container, this._executionPlanViews.length + 1, this, this._queryResultsView); + const ep = this._register(this.instantiationService.createInstance(ExecutionPlanView, this._container, this._executionPlanViews.length + 1, this, this._queryResultsView)); ep.model = g; this._executionPlanViews.push(ep); this.graphs.push(g); @@ -76,10 +75,11 @@ export class ExecutionPlanFileView { * @returns */ public async loadGraphFile(graphFile: azdata.executionPlan.ExecutionPlanGraphInfo) { - this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true }); + this._loadingSpinner = this._register(new LoadingSpinner(this._container, { showText: true, fullSize: true })); this._loadingSpinner.loadingMessage = localize('loadingExecutionPlanFile', "Generating execution plans"); try { this._loadingSpinner.loading = true; + if (this._planCache.has(graphFile.graphFileContent)) { this.addGraphs(this._planCache.get(graphFile.graphFileContent)); return; @@ -91,13 +91,15 @@ export class ExecutionPlanFileView { this.addGraphs(graphs); this._planCache.set(graphFile.graphFileContent, graphs); } + this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingComplete', "Execution plans are generated"); } catch (e) { - this._loadingErrorInfoBox = this.instantiationService.createInstance(InfoBox, this._container, { + this._loadingErrorInfoBox = this._register(this.instantiationService.createInstance(InfoBox, this._container, { text: e.toString(), style: 'error', isClickable: false - }); + })); + this._loadingErrorInfoBox.isClickable = false; this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan"); } finally { diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts index e6984028f8..50323acfd8 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts @@ -116,11 +116,14 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase if (!properties) { return []; } + const sortedProperties = this.sortProperties(properties); const rows: Slick.SlickData[] = []; + sortedProperties.forEach((property, index) => { let row = {}; row['name'] = property.name; + if (!isString(property.value)) { // Styling values in the parent row differently to make them more apparent and standout compared to the rest of the cells. row['name'] = { @@ -131,12 +134,15 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase }; row['tootltip'] = property.displayValue; row['treeGridChildren'] = this.convertPropertiesToTableRows(property.value); + } else { row['value'] = removeLineBreaks(property.displayValue, ' '); row['tooltip'] = property.displayValue; } + rows.push(row); }); + return rows; } @@ -165,6 +171,7 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase rows.push(row); row['name'] = p.name; row['parent'] = parentIndex; + if (!isString(p.value)) { // Styling values in the parent row differently to make them more apparent and standout compared to the rest of the cells. row['name'] = { diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts index 0b62babd22..5cb5146a75 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts @@ -75,15 +75,19 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme this.resizeSash = this._register(new Sash(sashContainer, this, { orientation: Orientation.VERTICAL, size: 3 })); let originalWidth = 0; + this._register(this.resizeSash.onDidStart((e: ISashEvent) => { originalWidth = this._parentContainer.clientWidth; })); + this._register(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`; })); @@ -105,7 +109,7 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme this._titleActions = this._register(new ActionBar(this._titleBarActionsContainer, { orientation: ActionsOrientation.HORIZONTAL, context: this })); - this._titleActions.pushAction([new ClosePropertyViewAction()], { icon: true, label: false }); + this._titleActions.pushAction([this._register(new ClosePropertyViewAction())], { icon: true, label: false }); this._headerContainer = DOM.$('.header'); propertiesContent.appendChild(this._headerContainer); @@ -115,24 +119,28 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme this._headerActionsContainer = DOM.$('.table-action-bar'); this._searchAndActionBarContainer.appendChild(this._headerActionsContainer); + this._headerActions = this._register(new ActionBar(this._headerActionsContainer, { orientation: ActionsOrientation.HORIZONTAL, context: this })); + this._headerActions.pushAction([ - new SortPropertiesByDisplayOrderAction(), - new SortPropertiesAlphabeticallyAction(), - new SortPropertiesReverseAlphabeticallyAction(), - new ExpandAllPropertiesAction(), - new CollapseAllPropertiesAction() + this._register(new SortPropertiesByDisplayOrderAction()), + this._register(new SortPropertiesAlphabeticallyAction()), + this._register(new SortPropertiesReverseAlphabeticallyAction()), + this._register(new ExpandAllPropertiesAction()), + this._register(new CollapseAllPropertiesAction()) ], { icon: true, label: false }); this._propertiesSearchInputContainer = DOM.$('.table-search'); this._propertiesSearchInputContainer.classList.add('codicon', filterIconClassNames); + this._propertiesSearchInput = this._register(new InputBox(this._propertiesSearchInputContainer, this._contextViewService, { ariaDescription: propertiesSearchDescription, placeholder: searchPlaceholder })); - attachInputBoxStyler(this._propertiesSearchInput, this._themeService); + + this._register(attachInputBoxStyler(this._propertiesSearchInput, this._themeService)); this._propertiesSearchInput.element.classList.add('codicon', filterIconClassNames); this._searchAndActionBarContainer.appendChild(this._propertiesSearchInputContainer); this._register(this._propertiesSearchInput.onDidChange(e => { @@ -156,11 +164,12 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme editable: true, autoEdit: false })); - attachTableStyler(this._tableComponent, this._themeService); + + this._register(attachTableStyler(this._tableComponent, this._themeService)); this._tableComponent.setSelectionModel(this._selectionModel); const contextMenuAction = [ - this._instantiationService.createInstance(CopyTableData), + this._register(this._instantiationService.createInstance(CopyTableData)), ]; this._register(this._tableComponent.onContextMenu(e => { @@ -184,10 +193,10 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme }).observe(_parentContainer); } - public getCopyString(): string { - const selectedDataRange = this._selectionModel.getSelectedRanges()[0]; let csvString = ''; + + const selectedDataRange = this._selectionModel.getSelectedRanges()[0]; if (selectedDataRange) { const data = []; @@ -204,10 +213,12 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme } data.push(row); } + csvString = data.map(row => row.map(x => `${x}`).join('\t') ).join('\n'); } + return csvString; } @@ -315,6 +326,7 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme -1) ); } + this._tableComponent.rerenderGrid(); } @@ -322,15 +334,18 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme let resultData: Slick.SlickData[] = []; data.forEach(dataRow => { let includeRow = false; + const columns = this._tableComponent.grid.getColumns(); for (let i = 0; i < columns.length; i++) { let dataValue = ''; + let rawDataValue = dataRow[columns[i].field]; if (isString(rawDataValue)) { dataValue = rawDataValue; } else if (rawDataValue !== undefined) { dataValue = rawDataValue.text ?? rawDataValue.title; } + if (dataValue?.toLowerCase().includes(search.toLowerCase())) { includeRow = true; break; @@ -348,9 +363,11 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme if (rowClone['treeGridChildren'] !== undefined) { rowClone['expanded'] = true; } + resultData.push(rowClone); } }); + return { include: resultData.length > 0, data: resultData }; } @@ -358,13 +375,16 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme if (nestedData === undefined || nestedData.length === 0) { return rows; } + nestedData.forEach((dataRow) => { rows.push(dataRow); dataRow['parent'] = parentIndex; + if (dataRow['treeGridChildren']) { this.flattenTableData(dataRow['treeGridChildren'], rows.length - 1, rows); } }); + return rows; } } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanTab.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanTab.ts index 606734c15a..3c37abc0ac 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanTab.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanTab.ts @@ -13,6 +13,7 @@ import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/brows import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache'; import { generateUuid } from 'vs/base/common/uuid'; import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; export class ExecutionPlanTab implements IPanelTab { public readonly title = localize('executionPlanTitle', "Query Plan (Preview)"); @@ -27,6 +28,7 @@ export class ExecutionPlanTab implements IPanelTab { } public dispose() { + dispose(this.view); } public clear() { @@ -35,7 +37,7 @@ export class ExecutionPlanTab implements IPanelTab { } -export class ExecutionPlanTabView implements IPanelView { +export class ExecutionPlanTabView extends Disposable implements IPanelView { private _container: HTMLElement = DOM.$('.execution-plan-tab'); private _input: ExecutionPlanState; private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance(); @@ -45,6 +47,7 @@ export class ExecutionPlanTabView implements IPanelView { private _queryResultsView: QueryResultsView, @IInstantiationService private _instantiationService: IInstantiationService, ) { + super(); } public set state(newInput: ExecutionPlanState) { @@ -63,7 +66,7 @@ export class ExecutionPlanTabView implements IPanelView { } else { // creating a new view for the new input newInput.executionPlanFileViewUUID = generateUuid(); - newView = this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView); + newView = this._register(this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView)); newView.onShow(this._container); newView.addGraphs( newInput.graphs @@ -88,7 +91,7 @@ export class ExecutionPlanTabView implements IPanelView { if (currentView) { currentView.onHide(this._container); this._input.graphs = []; - currentView = this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView); + currentView = this._register(this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView)); this._viewCache.executionPlanFileViewMap.set(this._input.executionPlanFileViewUUID, currentView); currentView.render(this._container); } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanTreeTab.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanTreeTab.ts index 5d2a584725..0e023715da 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanTreeTab.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanTreeTab.ts @@ -75,6 +75,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { while (this._container.firstChild) { this._container.removeChild(this._container.firstChild); } + this._input.graphs.forEach((g, i) => { this.convertExecutionPlanGraphToTreeGrid(g, i); }); @@ -84,13 +85,16 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { let dataMap: { [key: string]: any }[] = []; const columnValues: string[] = []; const stack: { node: azdata.executionPlan.ExecutionPlanNode, parentIndex: number }[] = []; + stack.push({ node: graph.root, parentIndex: -1, }); + while (stack.length !== 0) { const treeGridNode = stack.pop(); const row: { [key: string]: any } = {}; + treeGridNode.node.topOperationsData.forEach((d, i) => { let displayText = d.displayValue.toString(); @@ -104,10 +108,12 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { columnValues.splice(i, 0, d.columnName); } }); + row['nodeId'] = treeGridNode.node.id; row['parent'] = treeGridNode.parentIndex; row['parentNodeId'] = dataMap[treeGridNode.parentIndex] ? dataMap[treeGridNode.parentIndex]['nodeId'] : undefined; row['expanded'] = true; + if (treeGridNode.node.children) { treeGridNode.node.children.forEach(c => stack.push({ node: c, @@ -132,26 +138,29 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { const topOperationContainer = DOM.$('.top-operations-container'); this._container.appendChild(topOperationContainer); - const header = this._instantiationService.createInstance(ExecutionPlanViewHeader, topOperationContainer, { + + const header = this._register(this._instantiationService.createInstance(ExecutionPlanViewHeader, topOperationContainer, { planIndex: index, - }); + })); header.query = graph.query; header.relativeCost = graph.root.relativeCost; + const tableContainer = DOM.$('.table-container'); topOperationContainer.appendChild(tableContainer); this._planTreeContainers.push(topOperationContainer); let copyHandler = new CopyKeybind(); this._register(copyHandler.onCopy(e => { + let csvString = ''; const selectedDataRange = selectionModel.getSelectedRanges()[0]; - let csvString = ''; if (selectedDataRange) { const data = []; for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { const dataRow = treeGrid.getData().getItem(rowIndex); const row = []; + for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { const dataItem = dataRow[treeGrid.grid.getColumns()[colIndex].field]; if (dataItem) { @@ -160,8 +169,10 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { row.push(' '); } } + data.push(row); } + csvString = data.map(row => row.map(x => `${x}`).join('\t') ).join('\n'); @@ -174,21 +185,23 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { } - this._instantiationService.createInstance(CopyTableData).run({ + this._register(this._instantiationService.createInstance(CopyTableData)).run({ selectedText: csvString }); })); const selectionModel = new CellSelectionModel(); - const treeGrid = new TreeGrid(tableContainer, { + const treeGrid = this._register(new TreeGrid(tableContainer, { columns: columns, sorter: (args) => { const sortColumn = args.sortCol.field; + let data = deepClone(dataMap); if (data.length === 0) { - data = treeGrid.getData().getItems(); + data = this._register(treeGrid.getData()).getItems(); } + const sortedData = []; const rootRow = data[0]; const stack: { row: Slick.SlickData, originalIndex: number }[] = []; @@ -196,8 +209,9 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { while (stack.length !== 0) { const currentTreeGridRow = stack.pop(); - let currentTreeGridRowChildren: { row: Slick.SlickData, originalIndex: number }[] = []; sortedData.push(currentTreeGridRow.row); + + let currentTreeGridRowChildren: { row: Slick.SlickData, originalIndex: number }[] = []; for (let i = 0; i < data.length; i++) { if (data[i].parentNodeId === currentTreeGridRow.row.nodeId) { currentTreeGridRowChildren.push({ row: data[i], originalIndex: i }); @@ -207,6 +221,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { currentTreeGridRowChildren = currentTreeGridRowChildren.sort((a, b) => { const aRow = a.row; const bRow = b.row; + let result = -1; if (!aRow[sortColumn]) { result = 1; @@ -217,6 +232,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { const dataType = aRow[sortColumn].dataType; const aText = aRow[sortColumn].text; const bText = bRow[sortColumn].text; + if (aText === bText) { result = 0; } else { @@ -232,6 +248,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { } } } + return args.sortAsc ? result : -result; }); @@ -241,6 +258,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { stack.push(...currentTreeGridRowChildren); } + dataMap = sortedData; treeGrid.setData(sortedData); } @@ -249,24 +267,23 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { forceFitColumns: false, defaultColumnWidth: 120, showRowNumber: true - }); + })); + treeGrid.setSelectionModel(selectionModel); treeGrid.setData(dataMap); - treeGrid.registerPlugin(copyHandler); - treeGrid.setTableTitle(localize('topOperationsTableTitle', "Execution Plan Tree")); - this._treeGrids.push(treeGrid); + const contextMenuAction = [ - this._instantiationService.createInstance(CopyTableData), - this._instantiationService.createInstance(CopyTableDataWithHeader), - this._instantiationService.createInstance(SelectAll) + this._register(this._instantiationService.createInstance(CopyTableData)), + this._register(this._instantiationService.createInstance(CopyTableDataWithHeader)), + this._register(this._instantiationService.createInstance(SelectAll)) ]; this._register(treeGrid.onKeyDown((evt: ITableKeyboardEvent) => { if (evt.event.ctrlKey && (evt.event.key === 'a' || evt.event.key === 'A')) { - selectionModel.setSelectedRanges([new Slick.Range(0, 0, treeGrid.getData().getLength() - 1, treeGrid.grid.getColumns().length - 1)]); + selectionModel.setSelectedRanges([new Slick.Range(0, 0, this._register(treeGrid.getData()).getLength() - 1, treeGrid.grid.getColumns().length - 1)]); treeGrid.focus(); evt.event.preventDefault(); evt.event.stopPropagation(); @@ -274,14 +291,16 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { })); this._register(treeGrid.onContextMenu(e => { - const selectedDataRange = selectionModel.getSelectedRanges()[0]; let csvString = ''; let csvStringWithHeader = ''; + + const selectedDataRange = selectionModel.getSelectedRanges()[0]; if (selectedDataRange) { const data = []; for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { - const dataRow = treeGrid.getData().getItem(rowIndex); + const dataRow = this._register(treeGrid.getData()).getItem(rowIndex); // TODO lewissanchez: ask if it's okay to register disposable data providers like this. + const row = []; for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { const dataItem = dataRow[treeGrid.grid.getColumns()[colIndex].field]; @@ -291,14 +310,15 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { row.push(''); } } + data.push(row); } + csvString = data.map(row => row.map(x => `${x}`).join('\t') ).join('\n'); const columns = []; - for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { columns.push(treeGrid.grid.getColumns()[colIndex].name); } @@ -318,11 +338,13 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { }); })); - attachTableStyler(treeGrid, this._themeService); + + this._register(attachTableStyler(treeGrid, this._themeService)); new ResizeObserver((e) => { treeGrid.layout(new DOM.Dimension(tableContainer.clientWidth, tableContainer.clientHeight)); }).observe(tableContainer); + return treeGrid; } @@ -330,10 +352,13 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView { this._container.style.width = dimension.width + 'px'; this._container.style.height = dimension.height + 'px'; } + remove?(): void { } + onShow?(): void { } + onHide?(): void { } } @@ -342,14 +367,11 @@ export class CopyTableData extends Action { public static ID = 'ept.CopyTableData'; public static LABEL = localize('ept.topOperationsCopyTableData', "Copy"); - constructor( - @IClipboardService private _clipboardService: IClipboardService - ) { + constructor(@IClipboardService private _clipboardService: IClipboardService) { super(CopyTableData.ID, CopyTableData.LABEL, ''); } public override async run(context: ContextMenuModel): Promise { - this._clipboardService.writeText(context.selectedText); } } @@ -358,14 +380,11 @@ export class CopyTableDataWithHeader extends Action { public static ID = 'ept.CopyTableDataWithHeader'; public static LABEL = localize('ept.topOperationsCopyWithHeader', "Copy with Header"); - constructor( - @IClipboardService private _clipboardService: IClipboardService - ) { + constructor(@IClipboardService private _clipboardService: IClipboardService) { super(CopyTableDataWithHeader.ID, CopyTableDataWithHeader.LABEL, ''); } public override async run(context: ContextMenuModel): Promise { - this._clipboardService.writeText(context.selectionTextWithHeader); } } @@ -374,8 +393,7 @@ export class SelectAll extends Action { public static ID = 'ept.SelectAllTableData'; public static LABEL = localize('ept.topOperationsSelectAll', "Select All"); - constructor( - ) { + constructor() { super(SelectAll.ID, SelectAll.LABEL, ''); } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts index e69daf2ea8..be7a826e35 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts @@ -38,8 +38,9 @@ import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPla import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView'; import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView'; import { HighlightExpensiveOperationWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget'; +import { Disposable } from 'vs/base/common/lifecycle'; -export class ExecutionPlanView implements ISashLayoutProvider { +export class ExecutionPlanView extends Disposable implements ISashLayoutProvider { // Underlying execution plan displayed in the view private _model?: azdata.executionPlan.ExecutionPlanGraph; @@ -87,6 +88,8 @@ export class ExecutionPlanView implements ISashLayoutProvider { @IWorkspaceContextService public workspaceContextService: IWorkspaceContextService, @IEditorService private _editorService: IEditorService ) { + super(); + // parent container for query plan. this.container = DOM.$('.execution-plan'); this._parent.appendChild(this.container); @@ -94,19 +97,20 @@ export class ExecutionPlanView implements ISashLayoutProvider { this._parent.appendChild(sashContainer); // resizing sash for the query plan. - const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL, size: 3 }); + const sash = this._register(new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL, size: 3 })); let originalHeight = this.container.offsetHeight; let originalTableHeight = 0; let change = 0; - sash.onDidStart((e: ISashEvent) => { + + this._register(sash.onDidStart((e: ISashEvent) => { originalHeight = this.container.offsetHeight; originalTableHeight = this.propertiesView.tableHeight; - }); + })); /** * Using onDidChange for the smooth resizing of the graph diagram */ - sash.onDidChange((evt: ISashEvent) => { + this._register(sash.onDidChange((evt: ISashEvent) => { change = evt.startY - evt.currentY; const newHeight = originalHeight - change; if (newHeight < 200) { @@ -118,14 +122,14 @@ export class ExecutionPlanView implements ISashLayoutProvider { */ this.container.style.minHeight = '200px'; this.container.style.flex = `0 0 ${newHeight}px`; - }); + })); /** * Resizing properties window table only once at the end as it is a heavy operation and worsens the smooth resizing experience */ - sash.onDidEnd(() => { + this._register(sash.onDidEnd(() => { this.propertiesView.tableHeight = originalTableHeight - change; - }); + })); this._planContainer = DOM.$('.plan'); this.container.appendChild(this._planContainer); @@ -139,14 +143,14 @@ export class ExecutionPlanView implements ISashLayoutProvider { this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight; this._planContainer.appendChild(this._planHeaderContainer); - this.planHeader = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._planHeaderContainer, { + this.planHeader = this._register(this._instantiationService.createInstance(ExecutionPlanViewHeader, this._planHeaderContainer, { planIndex: this._graphIndex, - }); + })); // container properties this._propContainer = DOM.$('.properties'); this.container.appendChild(this._propContainer); - this.propertiesView = this._instantiationService.createInstance(ExecutionPlanPropertiesView, this._propContainer); + this.propertiesView = this._register(this._instantiationService.createInstance(ExecutionPlanPropertiesView, this._propContainer)); this._widgetContainer = DOM.$('.plan-action-container'); this._planContainer.appendChild(this._widgetContainer); @@ -155,56 +159,56 @@ export class ExecutionPlanView implements ISashLayoutProvider { // container that holds action bar icons this._actionBarContainer = DOM.$('.action-bar-container'); this.container.appendChild(this._actionBarContainer); - this._actionBar = new ActionBar(this._actionBarContainer, { + this._actionBar = this._register(new ActionBar(this._actionBarContainer, { orientation: ActionsOrientation.VERTICAL, context: this - }); + })); - this.actionBarToggleTopTip = new ActionBarToggleTooltip(); + this.actionBarToggleTopTip = this._register(new ActionBarToggleTooltip()); const actionBarActions = [ - new SavePlanFile(), - new OpenPlanFile(), - this._instantiationService.createInstance(OpenQueryAction, 'ActionBar'), - new Separator(), - this._instantiationService.createInstance(ZoomInAction, 'ActionBar'), - this._instantiationService.createInstance(ZoomOutAction, 'ActionBar'), - this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar'), - this._instantiationService.createInstance(CustomZoomAction, 'ActionBar'), - new Separator(), - this._instantiationService.createInstance(SearchNodeAction, 'ActionBar'), - this._instantiationService.createInstance(PropertiesAction, 'ActionBar'), - this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar'), - this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ActionBar'), + this._register(new SavePlanFile()), + this._register(new OpenPlanFile()), + this._register(this._instantiationService.createInstance(OpenQueryAction, 'ActionBar')), + this._register(new Separator()), + this._register(this._instantiationService.createInstance(ZoomInAction, 'ActionBar')), + this._register(this._instantiationService.createInstance(ZoomOutAction, 'ActionBar')), + this._register(this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar')), + this._register(this._instantiationService.createInstance(CustomZoomAction, 'ActionBar')), + this._register(new Separator()), + this._register(this._instantiationService.createInstance(SearchNodeAction, 'ActionBar')), + this._register(this._instantiationService.createInstance(PropertiesAction, 'ActionBar')), + this._register(this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar')), + this._register(this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ActionBar')), this.actionBarToggleTopTip ]; // Setting up context menu - this.contextMenuToggleTooltipAction = new ContextMenuTooltipToggle(); + this.contextMenuToggleTooltipAction = this._register(new ContextMenuTooltipToggle()); const contextMenuAction = [ - new SavePlanFile(), - new OpenPlanFile(), - this._instantiationService.createInstance(OpenQueryAction, 'ContextMenu'), - new Separator(), - this._instantiationService.createInstance(ZoomInAction, 'ContextMenu'), - this._instantiationService.createInstance(ZoomOutAction, 'ContextMenu'), - this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu'), - this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu'), - new Separator(), - this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu'), - this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'), - this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu'), - this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ContextMenu'), + this._register(new SavePlanFile()), + this._register(new OpenPlanFile()), + this._register(this._instantiationService.createInstance(OpenQueryAction, 'ContextMenu')), + this._register(new Separator()), + this._register(this._instantiationService.createInstance(ZoomInAction, 'ContextMenu')), + this._register(this._instantiationService.createInstance(ZoomOutAction, 'ContextMenu')), + this._register(this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu')), + this._register(this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu')), + this._register(new Separator()), + this._register(this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu')), + this._register(this._instantiationService.createInstance(PropertiesAction, 'ContextMenu')), + this._register(this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu')), + this._register(this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ContextMenu')), this.contextMenuToggleTooltipAction, - new Separator(), + this._register(new Separator()), ]; if (this._queryResultsView) { - actionBarActions.push(this._instantiationService.createInstance(TopOperationsAction)); - contextMenuAction.push(this._instantiationService.createInstance(TopOperationsAction)); + actionBarActions.push(this._register(this._instantiationService.createInstance(TopOperationsAction))); + contextMenuAction.push(this._register(this._instantiationService.createInstance(TopOperationsAction))); } this._actionBar.pushAction(actionBarActions, { icon: true, label: false }); const self = this; - this._planContainer.oncontextmenu = (e: MouseEvent) => { + this._register(DOM.addDisposableListener(this._planContainer, DOM.EventType.CONTEXT_MENU, (e: MouseEvent) => { if (contextMenuAction) { this._contextMenuService.showContextMenu({ getAnchor: () => { @@ -217,34 +221,37 @@ export class ExecutionPlanView implements ISashLayoutProvider { getActionsContext: () => (self) }); } - }; + })); - this.container.onkeydown = (e: KeyboardEvent) => { + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'f') { - let searchNodeAction = self._instantiationService.createInstance(SearchNodeAction, 'HotKey'); + let searchNodeAction = self._register(self._instantiationService.createInstance(SearchNodeAction, 'HotKey')); searchNodeAction.run(self); e.stopPropagation(); } - }; + })); } getHorizontalSashTop(sash: Sash): number { return 0; } + getHorizontalSashLeft?(sash: Sash): number { return 0; } + getHorizontalSashWidth?(sash: Sash): number { return this.container.clientWidth; } private createPlanDiagram(container: HTMLElement) { - this.executionPlanDiagram = this._instantiationService.createInstance(AzdataGraphView, container, this._model); - this.executionPlanDiagram.onElementSelected(e => { + this.executionPlanDiagram = this._register(this._instantiationService.createInstance(AzdataGraphView, container, this._model)); + + this._register(this.executionPlanDiagram.onElementSelected(e => { container.focus(); this.propertiesView.graphElement = e; - }); + })); } @@ -253,9 +260,11 @@ export class ExecutionPlanView implements ISashLayoutProvider { if (this._model) { this.planHeader.graphIndex = this._graphIndex; this.planHeader.query = graph.query; + if (graph.recommendations) { this.planHeader.recommendations = graph.recommendations; } + let diagramContainer = DOM.$('.diagram'); this.createPlanDiagram(diagramContainer); @@ -266,13 +275,13 @@ export class ExecutionPlanView implements ISashLayoutProvider { * the graph control. To scroll the individual graphs, users should * use the scroll bars. */ - diagramContainer.addEventListener('wheel', e => { + this._register(DOM.addDisposableListener(diagramContainer, DOM.EventType.WHEEL, (e: WheelEvent) => { //Hiding all tooltips when we scroll. const element = document.getElementsByClassName('mxTooltip'); for (let i = 0; i < element.length; i++) { (element[i]).style.visibility = 'hidden'; } - }); + })); this._planContainer.appendChild(diagramContainer); @@ -301,10 +310,10 @@ export class ExecutionPlanView implements ISashLayoutProvider { } public compareCurrentExecutionPlan() { - this._editorService.openEditor(this._instantiationService.createInstance(ExecutionPlanComparisonInput, { + this._editorService.openEditor(this._register(this._instantiationService.createInstance(ExecutionPlanComparisonInput, { topExecutionPlan: this._executionPlanFileView.graphs, topPlanIndex: this._graphIndex - 1 - }), { + })), { pinned: true }); } @@ -466,7 +475,7 @@ export class CustomZoomAction extends Action { .withAdditionalProperties({ source: this.source }) .send(); - context.widgetController.toggleWidget(context._instantiationService.createInstance(CustomZoomWidget, context.widgetController, context.executionPlanDiagram)); + context.widgetController.toggleWidget(this._register(context._instantiationService.createInstance(CustomZoomWidget, context.widgetController, context.executionPlanDiagram))); } } @@ -486,7 +495,7 @@ export class SearchNodeAction extends Action { .withAdditionalProperties({ source: this.source }) .send(); - context.widgetController.toggleWidget(context._instantiationService.createInstance(NodeSearchWidget, context.widgetController, context.executionPlanDiagram)); + context.widgetController.toggleWidget(this._register(context._instantiationService.createInstance(NodeSearchWidget, context.widgetController, context.executionPlanDiagram))); } } @@ -600,6 +609,6 @@ export class HighlightExpensiveOperationAction extends Action { .withAdditionalProperties({ source: this.source }) .send(); - context.widgetController.toggleWidget(context._instantiationService.createInstance(HighlightExpensiveOperationWidget, context.widgetController, context.executionPlanDiagram)); + context.widgetController.toggleWidget(this._register(context._instantiationService.createInstance(HighlightExpensiveOperationWidget, context.widgetController, context.executionPlanDiagram))); } } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts index 326cec04dc..666e572c4a 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader.ts @@ -11,8 +11,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; import { Button } from 'sql/base/browser/ui/button/button'; import { removeLineBreaks } from 'sql/base/common/strings'; +import { Disposable } from 'vs/base/common/lifecycle'; -export class ExecutionPlanViewHeader { +export class ExecutionPlanViewHeader extends Disposable { private _graphIndex: number; // Index of the graph in the view private _relativeCost: number; // Relative cost of the graph to the script @@ -28,7 +29,9 @@ export class ExecutionPlanViewHeader { public constructor( private _parentContainer: HTMLElement, headerData: PlanHeaderData | undefined, - @IInstantiationService public readonly _instantiationService: IInstantiationService) { + @IInstantiationService public readonly _instantiationService: IInstantiationService + ) { + super(); if (headerData) { this._graphIndex = headerData.planIndex; @@ -67,6 +70,7 @@ export class ExecutionPlanViewHeader { recommendations.forEach(r => { r.displayString = removeLineBreaks(r.displayString); }); + this._recommendations = recommendations; this.renderRecommendations(); } @@ -97,19 +101,19 @@ export class ExecutionPlanViewHeader { while (this._recommendationsContainer.firstChild) { this._recommendationsContainer.removeChild(this._recommendationsContainer.firstChild); } - this._recommendations.forEach(r => { - const link = new Button(this._recommendationsContainer, { + this._recommendations.forEach(r => { + const link = this._register(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._register(link.onDidClick(e => { this._instantiationService.invokeFunction(openNewQuery, undefined, r.queryWithDescription, RunQueryOnConnectionMode.none); - }); + })); }); } } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts index 7076f93e2a..e195489d04 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController.ts @@ -15,6 +15,7 @@ export class ExecutionPlanWidgetController { private addWidget(widget: ExecutionPlanWidgetBase) { if (widget.identifier && !this._executionPlanWidgetMap.has(widget.identifier)) { this._executionPlanWidgetMap.set(widget.identifier, widget); + if (widget.container) { widget.container.classList.add('child'); this._parentContainer.appendChild(widget.container); diff --git a/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts b/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts index c5dc406800..f27ead4ba5 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts @@ -69,9 +69,11 @@ export class TopOperationsTabView extends Disposable implements IPanelView { public scrollToIndex(index: number) { index = index - 1; this._topOperationsContainers[index].scrollIntoView(true); + this._tables.forEach(t => { t.getSelectionModel().setSelectedRanges([]); }); + this._tables[index].getSelectionModel().setSelectedRanges([new Slick.Range(0, 1, 0, 1)]); this._tables[index].focus(); } @@ -95,6 +97,7 @@ export class TopOperationsTabView extends Disposable implements IPanelView { while (this._container.firstChild) { this._container.removeChild(this._container.firstChild); } + this._input.graphs.forEach((g, i) => { this.convertExecutionPlanGraphToTable(g, i); }); @@ -102,17 +105,19 @@ export class TopOperationsTabView extends Disposable implements IPanelView { public convertExecutionPlanGraphToTable(graph: azdata.executionPlan.ExecutionPlanGraph, index: number): Table { - const dataMap: { [key: string]: any }[] = []; const columnValues: string[] = []; const stack: azdata.executionPlan.ExecutionPlanNode[] = []; stack.push(...graph.root.children); + while (stack.length !== 0) { const node = stack.pop(); const row: { [key: string]: any } = {}; + node.topOperationsData.forEach((d, i) => { let displayText = d.displayValue.toString(); + if (i === 0) { row[d.columnName] = { displayText: displayText, @@ -126,14 +131,18 @@ export class TopOperationsTabView extends Disposable implements IPanelView { dataType: d.dataType }; } + if (columnValues.indexOf(d.columnName) === -1) { columnValues.splice(i, 0, d.columnName); } }); + row['nodeId'] = node.id; + if (node.children) { node.children.forEach(c => stack.push(c)); } + row[TABLE_SORT_COLUMN_KEY] = node.cost; dataMap.push(row); } @@ -165,18 +174,19 @@ export class TopOperationsTabView extends Disposable implements IPanelView { headerContainer.appendChild(headerSearchBarContainer); headerContainer.classList.add('codicon', filterIconClassNames); - const topOperationsSearchInput = new InputBox(headerSearchBarContainer, this._contextViewService, { + const topOperationsSearchInput = this._register(new InputBox(headerSearchBarContainer, this._contextViewService, { ariaDescription: topOperationsSearchDescription, placeholder: searchPlaceholder - }); - attachInputBoxStyler(topOperationsSearchInput, this._themeService); + })); + this._register(attachInputBoxStyler(topOperationsSearchInput, this._themeService)); topOperationsSearchInput.element.classList.add('codicon', filterIconClassNames); - const header = this._instantiationService.createInstance(ExecutionPlanViewHeader, headerInfoContainer, { + const header = this._register(this._instantiationService.createInstance(ExecutionPlanViewHeader, headerInfoContainer, { planIndex: index, - }); + })); header.query = graph.query; header.relativeCost = graph.root.relativeCost; + const tableContainer = DOM.$('.table-container'); topOperationContainer.appendChild(tableContainer); this._topOperationsContainers.push(topOperationContainer); @@ -186,14 +196,15 @@ export class TopOperationsTabView extends Disposable implements IPanelView { let copyHandler = new CopyKeybind(); this._register(copyHandler.onCopy(e => { + let csvString = ''; const selectedDataRange = selectionModel.getSelectedRanges()[0]; - let csvString = ''; if (selectedDataRange) { const data = []; for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { - const dataRow = table.getData().getItem(rowIndex); + const dataRow = this._register(table.getData()).getItem(rowIndex); + const row = []; for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { const dataItem = dataRow[table.columns[colIndex].field]; @@ -203,8 +214,10 @@ export class TopOperationsTabView extends Disposable implements IPanelView { row.push(' '); } } + data.push(row); } + csvString = data.map(row => row.map(x => `${x}`).join('\t') ).join('\n'); @@ -214,7 +227,6 @@ export class TopOperationsTabView extends Disposable implements IPanelView { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { columns.push(table.columns[colIndex].name); } - } this._instantiationService.createInstance(CopyTableData).run({ @@ -224,11 +236,12 @@ export class TopOperationsTabView extends Disposable implements IPanelView { const selectionModel = new CellSelectionModel(); - const table = new Table(tableContainer, { + const table = this._register(new Table(tableContainer, { columns: columns, sorter: (args) => { const column = args.sortCol.field; - const sortedData = table.getData().getItems().sort((a, b) => { + + const sortedData = this._register(table.getData()).getItems().sort((a, b) => { let result = -1; if (!a[column]) { @@ -256,8 +269,10 @@ export class TopOperationsTabView extends Disposable implements IPanelView { } } } + return args.sortAsc ? result : -result; }); + table.setData(sortedData); } }, { @@ -265,13 +280,13 @@ export class TopOperationsTabView extends Disposable implements IPanelView { forceFitColumns: false, defaultColumnWidth: 120, showRowNumber: true - }); + })); + table.setSelectionModel(selectionModel); table.setData(dataMap); - table.registerPlugin(copyHandler); - table.setTableTitle(localize('topOperationsTableTitle', "Top Operations")); + this._register(table.onClick(e => { if (e.cell.cell === 1) { const row = table.getData().getItem(e.cell.row); @@ -283,17 +298,13 @@ export class TopOperationsTabView extends Disposable implements IPanelView { })); this._tables.push(table); - const contextMenuAction = [ - this._instantiationService.createInstance(CopyTableData), - this._instantiationService.createInstance(CopyTableDataWithHeader), - this._instantiationService.createInstance(SelectAll) - ]; this._register(topOperationsSearchInput.onDidChange(e => { const filter = e.toLowerCase(); if (filter) { const filteredData = dataMap.filter(row => { let includeRow = false; + for (let i = 0; i < columns.length; i++) { const columnField = columns[i].field; if (row[columnField]) { @@ -303,18 +314,21 @@ export class TopOperationsTabView extends Disposable implements IPanelView { } } } + return includeRow; }); + table.setData(filteredData); } else { table.setData(dataMap); } + table.rerenderGrid(); })); this._register(table.onKeyDown((evt: ITableKeyboardEvent) => { if (evt.event.ctrlKey && (evt.event.key === 'a' || evt.event.key === 'A')) { - selectionModel.setSelectedRanges([new Slick.Range(0, 1, table.getData().getLength() - 1, table.columns.length - 1)]); + selectionModel.setSelectedRanges([new Slick.Range(0, 1, this._register(table.getData()).getLength() - 1, table.columns.length - 1)]); table.focus(); evt.event.preventDefault(); evt.event.stopPropagation(); @@ -322,15 +336,17 @@ export class TopOperationsTabView extends Disposable implements IPanelView { })); this._register(table.onContextMenu(e => { - const selectedDataRange = selectionModel.getSelectedRanges()[0]; let csvString = ''; let csvStringWithHeader = ''; + + const selectedDataRange = selectionModel.getSelectedRanges()[0]; if (selectedDataRange) { const data = []; for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { - const dataRow = table.getData().getItem(rowIndex); + const dataRow = this._register(table.getData()).getItem(rowIndex); const row = []; + for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { const dataItem = dataRow[table.columns[colIndex].field]; if (dataItem) { @@ -339,14 +355,15 @@ export class TopOperationsTabView extends Disposable implements IPanelView { row.push(''); } } + data.push(row); } + csvString = data.map(row => row.map(x => `${x}`).join('\t') ).join('\n'); const columns = []; - for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { columns.push(table.columns[colIndex].name); } @@ -354,6 +371,12 @@ export class TopOperationsTabView extends Disposable implements IPanelView { csvStringWithHeader = columns.join('\t') + '\n' + csvString; } + const contextMenuAction = [ + this._register(this._instantiationService.createInstance(CopyTableData)), + this._register(this._instantiationService.createInstance(CopyTableDataWithHeader)), + this._register(this._instantiationService.createInstance(SelectAll)) + ]; + this._contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => contextMenuAction, @@ -364,13 +387,14 @@ export class TopOperationsTabView extends Disposable implements IPanelView { selectionTextWithHeader: csvStringWithHeader }) }); - })); - attachTableStyler(table, this._themeService); + + this._register(attachTableStyler(table, this._themeService)); new ResizeObserver((e) => { table.layout(new DOM.Dimension(tableContainer.clientWidth, tableContainer.clientHeight)); }).observe(tableContainer); + return table; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts index edc021d8f9..d12109bb2e 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget.ts @@ -34,11 +34,12 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase { // Custom zoom input box const zoomValueLabel = localize("qpZoomValueLabel", 'Zoom (percent)'); - this.customZoomInputBox = new InputBox(this.container, this.contextViewService, { + + this.customZoomInputBox = this._register(new InputBox(this.container, this.contextViewService, { type: 'number', ariaLabel: zoomValueLabel, flexibleWidth: false - }); + })); this._register(attachInputBoxStyler(this.customZoomInputBox, this.themeService)); const currentZoom = this.executionPlanDiagram.getZoomLevel(); @@ -48,28 +49,28 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase { // Setting up keyboard shortcuts const self = this; - this.customZoomInputBox.element.onkeydown = async (ev) => { + this._register(DOM.addDisposableListener(this.customZoomInputBox.element, DOM.EventType.KEY_DOWN, async (ev: KeyboardEvent) => { if (ev.key === 'Enter') { - await new CustomZoomAction().run(self); + await this._register(new CustomZoomAction()).run(self); } else if (ev.key === 'Escape') { this.widgetController.removeWidget(self); } - }; + })); - const applyButton = new Button(this.container, { + const applyButton = this._register(new Button(this.container, { title: localize('customZoomApplyButtonTitle', "Apply Zoom") - }); + })); applyButton.setWidth('60px'); applyButton.label = localize('customZoomApplyButton', "Apply"); this._register(applyButton.onDidClick(async e => { - await new CustomZoomAction().run(self); + await this._register(new CustomZoomAction()).run(self); })); // Adding action bar - this._actionBar = new ActionBar(this.container); + this._actionBar = this._register(new ActionBar(this.container)); this._actionBar.context = this; - this._actionBar.pushAction(new CancelZoom(), { label: false, icon: true }); + this._actionBar.pushAction(this._register(new CancelZoom()), { label: false, icon: true }); } // Setting initial focus to input box @@ -93,7 +94,7 @@ export class CustomZoomAction extends Action { context.widgetController.removeWidget(context); } else { context.notificationService.error( - localize('invalidCustomZoomError', "Select a zoom value between 1 to 200") + localize('invalidCustomZoomError', "Select a zoom value between 1 to 200") // TODO lewissanchez: Ask Aasim about this error message after removing zoom limit. ); } } diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget.ts index 194d57e273..95b79b167e 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget.ts @@ -84,7 +84,7 @@ export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase { this.container.appendChild(this._expenseMetricSelectBoxContainer); const selectBoxOptions = this.getSelectBoxOptionsFromExecutionPlanDiagram(); - this.expenseMetricSelectBox = new SelectBox(selectBoxOptions, COST_STRING, this.contextViewService, this._expenseMetricSelectBoxContainer); + this.expenseMetricSelectBox = this._register(new SelectBox(selectBoxOptions, COST_STRING, this.contextViewService, this._expenseMetricSelectBoxContainer)); this.expenseMetricSelectBox.setAriaLabel(SELECT_EXPENSE_METRIC_TITLE); this.expenseMetricSelectBox.render(this._expenseMetricSelectBoxContainer); @@ -119,19 +119,15 @@ export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase { })); // Apply Button - const highlightExpensiveOperationAction = new HighlightExpensiveOperationAction(); - this._register(highlightExpensiveOperationAction); - - const clearHighlightExpensiveOperationAction = new TurnOffExpensiveHighlightingOperationAction(); - this._register(clearHighlightExpensiveOperationAction); - - const cancelHighlightExpensiveOperationAction = new CancelHIghlightExpensiveOperationAction(); - this._register(cancelHighlightExpensiveOperationAction); + const highlightExpensiveOperationAction = this._register(new HighlightExpensiveOperationAction()); + const clearHighlightExpensiveOperationAction = this._register(new TurnOffExpensiveHighlightingOperationAction()); + const cancelHighlightExpensiveOperationAction = this._register(new CancelHIghlightExpensiveOperationAction()); const self = this; - const applyButton = new Button(this.container, { + const applyButton = this._register(new Button(this.container, { title: localize('highlightExpensiveOperationButtonTitle', 'Highlight Expensive Operation') - }); + })); + applyButton.label = localize('highlightExpensiveOperationApplyButton', 'Apply'); this._register(applyButton.onDidClick(async e => { @@ -146,7 +142,7 @@ export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase { })); // Adds Action bar - this._actionBar = new ActionBar(this.container); + this._actionBar = this._register(new ActionBar(this.container)); this._actionBar.context = this; this._actionBar.pushAction(cancelHighlightExpensiveOperationAction, { label: false, icon: true }); } diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts index b141316fd7..5555f49dd7 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts @@ -57,20 +57,24 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { // property name dropdown this._propertyNameSelectBoxContainer = DOM.$('.search-widget-property-name-select-box .dropdown-container'); this.container.appendChild(this._propertyNameSelectBoxContainer); + this._propertyNameSelectBoxContainer.style.width = '150px'; + const propDropdownOptions = this._executionPlanDiagram.getUniqueElementProperties(); - this._propertyNameSelectBox = new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer); + this._propertyNameSelectBox = this._register(new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer)); this._propertyNameSelectBox.setAriaLabel(SELECT_PROPERTY_TITLE); this._register(attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService)); - this._propertyNameSelectBoxContainer.style.width = '150px'; this._propertyNameSelectBox.render(this._propertyNameSelectBoxContainer); + this._register(this._propertyNameSelectBox.onDidSelect(e => { this._usePreviousSearchResult = false; })); // search type dropdown this._searchTypeSelectBoxContainer = DOM.$('.search-widget-search-type-select-box .dropdown-container'); + this._searchTypeSelectBoxContainer.style.width = '100px'; this.container.appendChild(this._searchTypeSelectBoxContainer); - this._searchTypeSelectBox = new SelectBox([ + + this._searchTypeSelectBox = this._register(new SelectBox([ EQUALS_DISPLAY_STRING, CONTAINS_DISPLAY_STRING, GREATER_DISPLAY_STRING, @@ -78,11 +82,11 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { GREATER_EQUAL_DISPLAY_STRING, LESSER_EQUAL_DISPLAY_STRING, LESSER_AND_GREATER_DISPLAY_STRING - ], EQUALS_DISPLAY_STRING, this.contextViewService, this._searchTypeSelectBoxContainer); + ], EQUALS_DISPLAY_STRING, this.contextViewService, this._searchTypeSelectBoxContainer)); this._searchTypeSelectBox.setAriaLabel(SELECT_SEARCH_TYPE_TITLE); this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer); this._register(attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService)); - this._searchTypeSelectBoxContainer.style.width = '100px'; + this._register(this._searchTypeSelectBox.onDidSelect(e => { this._usePreviousSearchResult = false; switch (e.selected) { @@ -110,27 +114,21 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { })); // search text input box - this._searchTextInputBox = new InputBox(this.container, this.contextViewService, {}); + this._searchTextInputBox = this._register(new InputBox(this.container, this.contextViewService, {})); this._searchTextInputBox.setAriaLabel(ENTER_SEARCH_VALUE_TITLE); - this._register(attachInputBoxStyler(this._searchTextInputBox, this.themeService)); this._searchTextInputBox.element.style.marginLeft = '5px'; + this._register(attachInputBoxStyler(this._searchTextInputBox, this.themeService)); this._register(this._searchTextInputBox.onDidChange(e => { this._usePreviousSearchResult = false; })); - // setting up key board shortcuts - const goToPreviousMatchAction = new GoToPreviousMatchAction(); - this._register(goToPreviousMatchAction); - - const goToNextMatchAction = new GoToNextMatchAction(); - this._register(goToNextMatchAction); - - const cancelSearchAction = new CancelSearch(); - this._register(cancelSearchAction); + const goToPreviousMatchAction = this._register(new GoToPreviousMatchAction()); + const goToNextMatchAction = this._register(new GoToNextMatchAction()); + const cancelSearchAction = this._register(new CancelSearch()); const self = this; - this._searchTextInputBox.element.onkeydown = async e => { + this._register(DOM.addDisposableListener(this._searchTextInputBox.element, DOM.EventType.KEY_DOWN, async (e: KeyboardEvent) => { if (e.key === 'Enter' && e.shiftKey) { await goToPreviousMatchAction.run(self); } else if (e.key === 'Enter') { @@ -138,10 +136,10 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { } else if (e.key === 'Escape') { await cancelSearchAction.run(self); } - }; + })); // Adding action bar - this._actionBar = new ActionBar(this.container); + this._actionBar = this._register(new ActionBar(this.container)); this._actionBar.context = this; this._actionBar.pushAction(goToPreviousMatchAction, { label: false, icon: true }); this._actionBar.pushAction(goToNextMatchAction, { label: false, icon: true }); @@ -155,6 +153,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { public searchNodes(): void { this._currentSearchResultIndex = 0; + this._searchResults = this._executionPlanDiagram.searchNodes({ propertyName: this._propertyNameSelectBox.value, value: this._searchTextInputBox.value, @@ -171,6 +170,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { this._executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]); this._executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]); + this._currentSearchResultIndex = this._currentSearchResultIndex === this._searchResults.length - 1 ? this._currentSearchResultIndex = 0 : this._currentSearchResultIndex = ++this._currentSearchResultIndex; @@ -183,6 +183,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { this._executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]); this._executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]); + this._currentSearchResultIndex = this._currentSearchResultIndex === 0 ? this._currentSearchResultIndex = this._searchResults.length - 1 : this._currentSearchResultIndex = --this._currentSearchResultIndex; diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index 7712830be9..d6e74955cc 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -726,7 +726,7 @@ export abstract class GridTableBase extends Disposable implements IView { graphFileType: result.queryExecutionPlanFileExtension }; - const executionPlanInput = this.instantiationService.createInstance(ExecutionPlanInput, undefined, executionPlanGraphInfo); + const executionPlanInput = this._register(this.instantiationService.createInstance(ExecutionPlanInput, undefined, executionPlanGraphInfo)); await this.editorService.openEditor(executionPlanInput); } else { @@ -800,7 +800,7 @@ export abstract class GridTableBase extends Disposable implements IView { this.currentHeight = size; } // Table is always called with Orientation as VERTICAL - this.table.layout(size, Orientation.VERTICAL); + this.table?.layout(size, Orientation.VERTICAL); } public get minimumSize(): number {