Registers all disposable items for query execution plans (#20851)

This commit is contained in:
Lewis Sanchez
2022-10-14 14:50:25 -07:00
committed by GitHub
parent 55c453700d
commit f51e5c370b
20 changed files with 402 additions and 255 deletions

View File

@@ -14,13 +14,14 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { foreground } from 'vs/platform/theme/common/colorRegistry'; import { foreground } from 'vs/platform/theme/common/colorRegistry';
import { generateUuid } from 'vs/base/common/uuid'; import { generateUuid } from 'vs/base/common/uuid';
import { Disposable } from 'vs/base/common/lifecycle';
const azdataGraph = azdataGraphModule(); const azdataGraph = azdataGraphModule();
/** /**
* This view holds the azdataGraph diagram and provides different * This view holds the azdataGraph diagram and provides different
* methods to manipulate the azdataGraph * methods to manipulate the azdataGraph
*/ */
export class AzdataGraphView { export class AzdataGraphView extends Disposable {
private _diagram: any; private _diagram: any;
private _diagramModel: AzDataGraphCell; private _diagramModel: AzDataGraphCell;
@@ -37,6 +38,8 @@ export class AzdataGraphView {
private _executionPlan: azdata.executionPlan.ExecutionPlanGraph, private _executionPlan: azdata.executionPlan.ExecutionPlanGraph,
@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,
) { ) {
super();
this._diagramModel = this.populate(this._executionPlan.root); this._diagramModel = this.populate(this._executionPlan.root);
let queryPlanConfiguration = { 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.setCellsDisconnectable(false); // preventing graph edges to be disconnected from source and target nodes.
this._diagram.graph.tooltipHandler.delay = 700; // increasing delay for tooltips 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); const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) { if (iconLabelColor) {
this._diagram.setTextFontColor(iconLabelColor); this._diagram.setTextFontColor(iconLabelColor);
this._diagram.setEdgeColor(iconLabelColor); this._diagram.setEdgeColor(iconLabelColor);
} }
}); }));
} }
private initializeGraphEvents(): void { private initializeGraphEvents(): void {
@@ -74,10 +77,12 @@ export class AzdataGraphView {
if (this._cellInFocus?.id === evt.properties.removed[0]?.id) { if (this._cellInFocus?.id === evt.properties.removed[0]?.id) {
return; return;
} }
const newSelection = evt.properties.removed[0]; const newSelection = evt.properties.removed[0];
if (!newSelection) { if (!newSelection) {
return; return;
} }
this._onElementSelectedEmitter.fire(this.getElementById(newSelection.id)); this._onElementSelectedEmitter.fire(this.getElementById(newSelection.id));
this.centerElement(this.getElementById(newSelection.id)); this.centerElement(this.getElementById(newSelection.id));
this._cellInFocus = evt.properties.removed[0]; this._cellInFocus = evt.properties.removed[0];
@@ -102,7 +107,9 @@ export class AzdataGraphView {
} else { } else {
cell = this._diagram.graph.model.getCell((<azdata.executionPlan.ExecutionPlanNode>this._executionPlan.root).id); cell = this._diagram.graph.model.getCell((<azdata.executionPlan.ExecutionPlanNode>this._executionPlan.root).id);
} }
this._diagram.graph.getSelectionModel().setCell(cell); this._diagram.graph.getSelectionModel().setCell(cell);
if (bringToCenter) { if (bringToCenter) {
this.centerElement(element); this.centerElement(element);
} }
@@ -116,6 +123,7 @@ export class AzdataGraphView {
if (cell?.id) { if (cell?.id) {
return this.getElementById(cell.id); return this.getElementById(cell.id);
} }
return undefined; return undefined;
} }
@@ -158,6 +166,7 @@ export class AzdataGraphView {
if (level < 1) { if (level < 1) {
throw new Error(localize('invalidExecutionPlanZoomError', "Zoom level cannot be 0 or negative")); throw new Error(localize('invalidExecutionPlanZoomError', "Zoom level cannot be 0 or negative"));
} }
this._diagram.zoomTo(level); this._diagram.zoomTo(level);
} }
@@ -168,11 +177,13 @@ export class AzdataGraphView {
public getElementById(id: string): InternalExecutionPlanElement | undefined { public getElementById(id: string): InternalExecutionPlanElement | undefined {
const nodeStack: azdata.executionPlan.ExecutionPlanNode[] = []; const nodeStack: azdata.executionPlan.ExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root); nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) { while (nodeStack.length !== 0) {
const currentNode = nodeStack.pop(); const currentNode = nodeStack.pop();
if (currentNode.id === id) { if (currentNode.id === id) {
return currentNode; return currentNode;
} }
if (currentNode.edges) { if (currentNode.edges) {
for (let i = 0; i < currentNode.edges.length; i++) { for (let i = 0; i < currentNode.edges.length; i++) {
if ((<InternalExecutionPlanEdge>currentNode.edges[i]).id === id) { if ((<InternalExecutionPlanEdge>currentNode.edges[i]).id === id) {
@@ -180,8 +191,10 @@ export class AzdataGraphView {
} }
} }
} }
nodeStack.push(...currentNode.children); nodeStack.push(...currentNode.children);
} }
return undefined; return undefined;
} }
@@ -225,12 +238,15 @@ export class AzdataGraphView {
matchFound = matchingProp.value < searchQuery.value || matchingProp.value > searchQuery.value; matchFound = matchingProp.value < searchQuery.value || matchingProp.value > searchQuery.value;
break; break;
} }
if (matchFound) { if (matchFound) {
resultNodes.push(currentNode); resultNodes.push(currentNode);
} }
} }
nodeStack.push(...currentNode.children); nodeStack.push(...currentNode.children);
} }
return resultNodes; return resultNodes;
} }
@@ -303,13 +319,14 @@ export class AzdataGraphView {
diagramNode.tooltipTitle = node.name; diagramNode.tooltipTitle = node.name;
diagramNode.rowCountDisplayString = node.rowCountDisplayString; diagramNode.rowCountDisplayString = node.rowCountDisplayString;
diagramNode.costDisplayString = node.costDisplayString; diagramNode.costDisplayString = node.costDisplayString;
if (!node.id.toString().startsWith(`element-`)) {
node.id = `element-${node.id}`;
}
this.expensiveMetricTypes.add(ExpensiveMetricType.Off); this.expensiveMetricTypes.add(ExpensiveMetricType.Off);
if (!node.id.toString().startsWith(`element-`)) {
node.id = `element-${node.id}`;
}
diagramNode.id = node.id; diagramNode.id = node.id;
diagramNode.icon = node.type; diagramNode.icon = node.type;
diagramNode.metrics = this.populateProperties(node.properties); diagramNode.metrics = this.populateProperties(node.properties);
@@ -401,6 +418,7 @@ export class AzdataGraphView {
props.forEach(p => { props.forEach(p => {
this._graphElementPropertiesSet.add(p.name); this._graphElementPropertiesSet.add(p.name);
}); });
return props.filter(e => isString(e.displayValue) && e.showInTooltip) return props.filter(e => isString(e.displayValue) && e.showInTooltip)
.sort((a, b) => a.displayOrder - b.displayOrder) .sort((a, b) => a.displayOrder - b.displayOrder)
.map(e => { .map(e => {
@@ -449,6 +467,7 @@ export class AzdataGraphView {
} else { } else {
this._diagram.graph.tooltipHandler.setEnabled(true); this._diagram.graph.tooltipHandler.setEnabled(true);
} }
return this._diagram.graph.tooltipHandler.enabled; return this._diagram.graph.tooltipHandler.enabled;
} }

View File

@@ -29,6 +29,7 @@ export class ExecutionPlanComparisonInput extends EditorInput {
const existingNames = this._editorService.editors.map(editor => editor.getName()); const existingNames = this._editorService.editors.map(editor => editor.getName());
let i = 1; let i = 1;
this._editorName = `${this.editorNamePrefix}_${i}`; this._editorName = `${this.editorNamePrefix}_${i}`;
while (existingNames.includes(this._editorName)) { while (existingNames.includes(this._editorName)) {
i++; i++;
this._editorName = `${this.editorNamePrefix}_${i}`; this._editorName = `${this.editorNamePrefix}_${i}`;

View File

@@ -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. // creating a new comparison view if the new input does not already have a cached one.
if (!input._executionPlanComparisonView) { 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) {
if (this.input.preloadModel.topExecutionPlan) { if (this.input.preloadModel.topExecutionPlan) {
input._executionPlanComparisonView.addExecutionPlanGraph(this.input.preloadModel.topExecutionPlan, this.input.preloadModel.topPlanIndex); input._executionPlanComparisonView.addExecutionPlanGraph(this.input.preloadModel.topExecutionPlan, this.input.preloadModel.topPlanIndex);

View File

@@ -34,10 +34,11 @@ import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPl
import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget'; import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget';
import { Button } from 'sql/base/browser/ui/button/button'; import { Button } from 'sql/base/browser/ui/button/button';
import { attachButtonStyler } from 'vs/platform/theme/common/styler'; 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'); const ADD_EXECUTION_PLAN_STRING = localize('epCompare.addExecutionPlanLabel', 'Add execution plan');
export class ExecutionPlanComparisonEditorView { export class ExecutionPlanComparisonEditorView extends Disposable {
public container: HTMLElement; public container: HTMLElement;
@@ -134,6 +135,7 @@ export class ExecutionPlanComparisonEditorView {
@IProgressService private _progressService: IProgressService, @IProgressService private _progressService: IProgressService,
@IContextMenuService private _contextMenuService: IContextMenuService @IContextMenuService private _contextMenuService: IContextMenuService
) { ) {
super();
this.container = DOM.$('.comparison-editor'); this.container = DOM.$('.comparison-editor');
parentContainer.appendChild(this.container); parentContainer.appendChild(this.container);
@@ -144,22 +146,16 @@ export class ExecutionPlanComparisonEditorView {
// creating and adding editor toolbar actions // creating and adding editor toolbar actions
private initializeToolbar(): void { private initializeToolbar(): void {
this._taskbarContainer = DOM.$('.editor-toolbar'); this._addExecutionPlanAction = this._register(this._instantiationService.createInstance(AddExecutionPlanAction));
this._taskbar = new Taskbar(this._taskbarContainer, { this._zoomOutAction = this._register(new ZoomOutAction());
orientation: ActionsOrientation.HORIZONTAL, this._zoomInAction = this._register(new ZoomInAction());
this._zoomToFitAction = this._register(new ZoomToFitAction());
}); this._propertiesAction = this._register(this._instantiationService.createInstance(PropertiesAction));
this._taskbar.context = this; this._toggleOrientationAction = this._register(new ToggleOrientation());
this._addExecutionPlanAction = this._instantiationService.createInstance(AddExecutionPlanAction); this._searchNodeAction = this._register(this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Primary));
this._zoomOutAction = new ZoomOutAction(); this._searchNodeActionForAddedPlan = this._register(this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Added));
this._zoomInAction = new ZoomInAction(); this._resetZoomAction = this._register(new ZoomReset());
this._zoomToFitAction = new ZoomToFitAction(); this._toggleTooltipAction = this._register(new ActionBarToggleTooltip());
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();
const content: ITaskbarContent[] = [ const content: ITaskbarContent[] = [
{ action: this._addExecutionPlanAction }, { action: this._addExecutionPlanAction },
{ action: this._zoomInAction }, { action: this._zoomInAction },
@@ -172,6 +168,12 @@ export class ExecutionPlanComparisonEditorView {
{ action: this._searchNodeActionForAddedPlan }, { action: this._searchNodeActionForAddedPlan },
{ action: this._toggleTooltipAction } { 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._taskbar.setContent(content);
this.container.appendChild(this._taskbarContainer); this.container.appendChild(this._taskbarContainer);
} }
@@ -192,9 +194,10 @@ export class ExecutionPlanComparisonEditorView {
this._placeholderContainer = DOM.$('.placeholder'); this._placeholderContainer = DOM.$('.placeholder');
const contextMenuAction = [ 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) { if (contextMenuAction) {
this._contextMenuService.showContextMenu({ this._contextMenuService.showContextMenu({
getAnchor: () => { getAnchor: () => {
@@ -207,24 +210,25 @@ export class ExecutionPlanComparisonEditorView {
getActionsContext: () => (self) getActionsContext: () => (self)
}); });
} }
}; }));
this._placeholderInfoboxContainer = DOM.$('.placeholder-infobox'); this._placeholderInfoboxContainer = DOM.$('.placeholder-infobox');
this._placeholderButton = new Button(this._placeholderInfoboxContainer, { secondary: true }); this._placeholderButton = this._register(new Button(this._placeholderInfoboxContainer, { secondary: true }));
attachButtonStyler(this._placeholderButton, this.themeService); this._register(attachButtonStyler(this._placeholderButton, this.themeService));
this._placeholderButton.label = ADD_EXECUTION_PLAN_STRING; this._placeholderButton.label = ADD_EXECUTION_PLAN_STRING;
this._placeholderButton.ariaLabel = ADD_EXECUTION_PLAN_STRING; this._placeholderButton.ariaLabel = ADD_EXECUTION_PLAN_STRING;
this._placeholderButton.enabled = true; 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, fullSize: true,
showText: true showText: true
}); }));
this._placeholderContainer.appendChild(this._placeholderInfoboxContainer); this._placeholderContainer.appendChild(this._placeholderInfoboxContainer);
this._placeholderLoading.loadingMessage = localize('epComapre.LoadingPlanMessage', "Loading execution plan"); this._placeholderLoading.loadingMessage = localize('epComapre.LoadingPlanMessage', "Loading execution plan");
this._placeholderLoading.loadingCompletedMessage = localize('epComapre.LoadingPlanCompleteMessage', "Execution plan successfully loaded"); this._placeholderLoading.loadingCompletedMessage = localize('epComapre.LoadingPlanCompleteMessage', "Execution plan successfully loaded");
@@ -232,9 +236,10 @@ export class ExecutionPlanComparisonEditorView {
this._topPlanContainer = DOM.$('.plan-container'); this._topPlanContainer = DOM.$('.plan-container');
this.planSplitViewContainer.appendChild(this._topPlanContainer); this.planSplitViewContainer.appendChild(this._topPlanContainer);
this._topPlanDropdownContainer = DOM.$('.dropdown-container'); 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.render(this._topPlanDropdownContainer);
this._topPlanDropdown.onDidSelect(async (e) => {
this._register(this._topPlanDropdown.onDidSelect(async (e) => {
this.activeBottomPlanDiagram?.clearSubtreePolygon(); this.activeBottomPlanDiagram?.clearSubtreePolygon();
this.activeTopPlanDiagram?.clearSubtreePolygon(); this.activeTopPlanDiagram?.clearSubtreePolygon();
@@ -247,19 +252,21 @@ export class ExecutionPlanComparisonEditorView {
this._activeTopPlanIndex = e.index; this._activeTopPlanIndex = e.index;
await this.getSkeletonNodes(); await this.getSkeletonNodes();
}); }));
attachSelectBoxStyler(this._topPlanDropdown, this.themeService);
this._register(attachSelectBoxStyler(this._topPlanDropdown, this.themeService));
this._topPlanContainer.appendChild(this._topPlanDropdownContainer); 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.initializeSash();
this._bottomPlanContainer = DOM.$('.plan-container'); this._bottomPlanContainer = DOM.$('.plan-container');
this.planSplitViewContainer.appendChild(this._bottomPlanContainer); this.planSplitViewContainer.appendChild(this._bottomPlanContainer);
this._bottomPlanDropdownContainer = DOM.$('.dropdown-container'); 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.render(this._bottomPlanDropdownContainer);
this._bottomPlanDropdown.onDidSelect(async (e) => {
this._register(this._bottomPlanDropdown.onDidSelect(async (e) => {
this.activeBottomPlanDiagram?.clearSubtreePolygon(); this.activeBottomPlanDiagram?.clearSubtreePolygon();
this.activeTopPlanDiagram?.clearSubtreePolygon(); this.activeTopPlanDiagram?.clearSubtreePolygon();
@@ -272,24 +279,27 @@ export class ExecutionPlanComparisonEditorView {
this._activeBottomPlanIndex = e.index; this._activeBottomPlanIndex = e.index;
await this.getSkeletonNodes(); await this.getSkeletonNodes();
}); }));
attachSelectBoxStyler(this._bottomPlanDropdown, this.themeService);
this._register(attachSelectBoxStyler(this._bottomPlanDropdown, this.themeService));
this._bottomPlanContainer.appendChild(this._bottomPlanDropdownContainer); 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 { private initializeSash(): void {
this._sashContainer = DOM.$('.sash-container'); this._sashContainer = DOM.$('.sash-container');
this.planSplitViewContainer.appendChild(this._sashContainer); 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 originalWidth;
let change = 0; let change = 0;
this._verticalSash.onDidStart((e: ISashEvent) => {
this._register(this._verticalSash.onDidStart((e: ISashEvent) => {
originalWidth = this._topPlanContainer.offsetWidth; originalWidth = this._topPlanContainer.offsetWidth;
}); }));
this._verticalSash.onDidChange((evt: ISashEvent) => {
this._register(this._verticalSash.onDidChange((evt: ISashEvent) => {
change = evt.startX - evt.currentX; change = evt.startX - evt.currentX;
const newWidth = originalWidth - change; const newWidth = originalWidth - change;
if (newWidth < 200) { if (newWidth < 200) {
@@ -297,14 +307,16 @@ export class ExecutionPlanComparisonEditorView {
} }
this._topPlanContainer.style.minWidth = '200px'; this._topPlanContainer.style.minWidth = '200px';
this._topPlanContainer.style.flex = `0 0 ${newWidth}px`; 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; let startHeight;
this._horizontalSash.onDidStart((e: ISashEvent) => {
this._register(this._horizontalSash.onDidStart((e: ISashEvent) => {
startHeight = this._topPlanContainer.offsetHeight; startHeight = this._topPlanContainer.offsetHeight;
}); }));
this._horizontalSash.onDidChange((evt: ISashEvent) => {
this._register(this._horizontalSash.onDidChange((evt: ISashEvent) => {
change = evt.startY - evt.currentY; change = evt.startY - evt.currentY;
const newHeight = startHeight - change; const newHeight = startHeight - change;
if (newHeight < 200) { if (newHeight < 200) {
@@ -312,12 +324,12 @@ export class ExecutionPlanComparisonEditorView {
} }
this._topPlanContainer.style.minHeight = '200px'; this._topPlanContainer.style.minHeight = '200px';
this._topPlanContainer.style.flex = `0 0 ${newHeight}px`; this._topPlanContainer.style.flex = `0 0 ${newHeight}px`;
}); }));
} }
private initializeProperties(): void { private initializeProperties(): void {
this._propertiesContainer = DOM.$('.properties'); 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); this._planComparisonContainer.appendChild(this._propertiesContainer);
} }
@@ -343,6 +355,7 @@ export class ExecutionPlanComparisonEditorView {
canSelectMany: false, canSelectMany: false,
canSelectFiles: true canSelectFiles: true
}); });
if (openedFileUris?.length === 1) { if (openedFileUris?.length === 1) {
this._placeholderInfoboxContainer.style.display = 'none'; this._placeholderInfoboxContainer.style.display = 'none';
this._placeholderLoading.loading = true; this._placeholderLoading.loading = true;
@@ -354,6 +367,7 @@ export class ExecutionPlanComparisonEditorView {
}); });
await this.addExecutionPlanGraph(executionPlanGraphs.graphs, 0); await this.addExecutionPlanGraph(executionPlanGraphs.graphs, 0);
} }
this._placeholderInfoboxContainer.style.display = ''; this._placeholderInfoboxContainer.style.display = '';
this._placeholderLoading.loading = false; this._placeholderLoading.loading = false;
this._placeholderInfoboxContainer.style.display = ''; this._placeholderInfoboxContainer.style.display = '';
@@ -367,6 +381,7 @@ export class ExecutionPlanComparisonEditorView {
public async addExecutionPlanGraph(executionPlanGraphs: azdata.executionPlan.ExecutionPlanGraph[], preSelectIndex: number): Promise<void> { public async addExecutionPlanGraph(executionPlanGraphs: azdata.executionPlan.ExecutionPlanGraph[], preSelectIndex: number): Promise<void> {
if (!this._topPlanDiagramModels) { if (!this._topPlanDiagramModels) {
this._topPlanDiagramModels = executionPlanGraphs; this._topPlanDiagramModels = executionPlanGraphs;
this._topPlanDropdown.setOptions(executionPlanGraphs.map((e, index) => { this._topPlanDropdown.setOptions(executionPlanGraphs.map((e, index) => {
return { return {
text: this.createQueryDropdownPrefixString(e.query, index + 1, executionPlanGraphs.length) text: this.createQueryDropdownPrefixString(e.query, index + 1, executionPlanGraphs.length)
@@ -377,26 +392,30 @@ export class ExecutionPlanComparisonEditorView {
const graphContainer = DOM.$('.plan-diagram'); const graphContainer = DOM.$('.plan-diagram');
this._topPlanDiagramContainers.push(graphContainer); this._topPlanDiagramContainers.push(graphContainer);
this._topPlanContainer.appendChild(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); this._propertiesView.setPrimaryElement(e);
const id = e.id.replace(`element-`, ''); const id = e.id.replace(`element-`, '');
if (this._topSimilarNode.has(id)) { if (this._topSimilarNode.has(id)) {
const similarNode = this._topSimilarNode.get(id); const similarNode = this._topSimilarNode.get(id);
if (this.activeBottomPlanDiagram) { if (this.activeBottomPlanDiagram) {
const element = this.activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]);
if (similarNode.matchingNodesId.find(m => this.activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { if (similarNode.matchingNodesId.find(m => this.activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) {
return; return;
} }
const element = this.activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]);
this.activeBottomPlanDiagram.selectElement(element); this.activeBottomPlanDiagram.selectElement(element);
} }
} }
}); }));
this.topPlanDiagrams.push(diagram); this.topPlanDiagrams.push(diagram);
graphContainer.style.display = 'none'; graphContainer.style.display = 'none';
}); });
this._topPlanDropdown.select(preSelectIndex); this._topPlanDropdown.select(preSelectIndex);
this._propertiesView.setPrimaryElement(executionPlanGraphs[0].root); this._propertiesView.setPrimaryElement(executionPlanGraphs[0].root);
this._propertiesAction.enabled = true; this._propertiesAction.enabled = true;
@@ -408,43 +427,51 @@ export class ExecutionPlanComparisonEditorView {
this._searchNodeAction.enabled = true; this._searchNodeAction.enabled = true;
} else { } else {
this._bottomPlanDiagramModels = executionPlanGraphs; this._bottomPlanDiagramModels = executionPlanGraphs;
this._bottomPlanDropdown.setOptions(executionPlanGraphs.map((e, index) => { this._bottomPlanDropdown.setOptions(executionPlanGraphs.map((e, index) => {
return { return {
text: this.createQueryDropdownPrefixString(e.query, index + 1, executionPlanGraphs.length) text: this.createQueryDropdownPrefixString(e.query, index + 1, executionPlanGraphs.length)
}; };
})); }));
executionPlanGraphs.forEach((e, i) => { executionPlanGraphs.forEach((e, i) => {
const graphContainer = DOM.$('.plan-diagram'); const graphContainer = DOM.$('.plan-diagram');
this._bottomPlanDiagramContainers.push(graphContainer); this._bottomPlanDiagramContainers.push(graphContainer);
this._bottomPlanContainer.appendChild(graphContainer); this._bottomPlanContainer.appendChild(graphContainer);
const diagram = this._instantiationService.createInstance(AzdataGraphView, graphContainer, e); const diagram = this._register(this._instantiationService.createInstance(AzdataGraphView, graphContainer, e));
diagram.onElementSelected(e => {
this._register(diagram.onElementSelected(e => {
this._propertiesView.setSecondaryElement(e); this._propertiesView.setSecondaryElement(e);
const id = e.id.replace(`element-`, ''); const id = e.id.replace(`element-`, '');
if (this._bottomSimilarNode.has(id)) { if (this._bottomSimilarNode.has(id)) {
const similarNode = this._bottomSimilarNode.get(id); const similarNode = this._bottomSimilarNode.get(id);
if (this.activeTopPlanDiagram) { 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) { if (this.activeTopPlanDiagram.getSelectedElement() && similarNode.matchingNodesId.find(m => this.activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) {
return; return;
} }
const element = this.activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]);
this.activeTopPlanDiagram.selectElement(element); this.activeTopPlanDiagram.selectElement(element);
} }
} }
}); }));
this.bottomPlanDiagrams.push(diagram); this.bottomPlanDiagrams.push(diagram);
graphContainer.style.display = 'none'; graphContainer.style.display = 'none';
}); });
this._bottomPlanDropdown.select(preSelectIndex); this._bottomPlanDropdown.select(preSelectIndex);
this._propertiesView.setSecondaryElement(executionPlanGraphs[0].root); this._propertiesView.setSecondaryElement(executionPlanGraphs[0].root);
this._addExecutionPlanAction.enabled = false; this._addExecutionPlanAction.enabled = false;
this._searchNodeActionForAddedPlan.enabled = true; this._searchNodeActionForAddedPlan.enabled = true;
if (!this._areTooltipsEnabled) { if (!this._areTooltipsEnabled) {
this.activeBottomPlanDiagram.toggleTooltip(); this.activeBottomPlanDiagram.toggleTooltip();
} }
} }
this.refreshSplitView(); this.refreshSplitView();
} }
@@ -463,19 +490,24 @@ export class ExecutionPlanComparisonEditorView {
this._polygonRootsMap = new Map(); this._polygonRootsMap = new Map();
this._topSimilarNode = new Map(); this._topSimilarNode = new Map();
this._bottomSimilarNode = new Map(); this._bottomSimilarNode = new Map();
if (this._topPlanDiagramModels && this._bottomPlanDiagramModels) { if (this._topPlanDiagramModels && this._bottomPlanDiagramModels) {
this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile.graphFileType = 'sqlplan'; this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile.graphFileType = 'sqlplan';
this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile.graphFileType = 'sqlplan'; this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile.graphFileType = 'sqlplan';
const currentRequestId = generateUuid(); const currentRequestId = generateUuid();
this._latestRequestUuid = currentRequestId; this._latestRequestUuid = currentRequestId;
const result = await this._executionPlanService.compareExecutionPlanGraph(this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile, const result = await this._executionPlanService.compareExecutionPlanGraph(this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile,
this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile); this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile);
if (currentRequestId !== this._latestRequestUuid) { if (currentRequestId !== this._latestRequestUuid) {
return; return;
} }
this.getSimilarSubtrees(result.firstComparisonResult); this.getSimilarSubtrees(result.firstComparisonResult);
this.getSimilarSubtrees(result.secondComparisonResult, true); this.getSimilarSubtrees(result.secondComparisonResult, true);
let colorIndex = 0; let colorIndex = 0;
this._polygonRootsMap.forEach((v, k) => { this._polygonRootsMap.forEach((v, k) => {
if (this.activeTopPlanDiagram && this.activeBottomPlanDiagram) { if (this.activeTopPlanDiagram && this.activeBottomPlanDiagram) {
@@ -494,6 +526,7 @@ export class ExecutionPlanComparisonEditorView {
if (comparedNode.hasMatch) { if (comparedNode.hasMatch) {
if (!isBottomPlan) { if (!isBottomPlan) {
this._topSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode); this._topSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode);
if (!this._polygonRootsMap.has(comparedNode.groupIndex)) { if (!this._polygonRootsMap.has(comparedNode.groupIndex)) {
this._polygonRootsMap.set(comparedNode.groupIndex, { this._polygonRootsMap.set(comparedNode.groupIndex, {
topPolygon: comparedNode, topPolygon: comparedNode,
@@ -502,6 +535,7 @@ export class ExecutionPlanComparisonEditorView {
} }
} else { } else {
this._bottomSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode); this._bottomSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode);
if (this._polygonRootsMap.get(comparedNode.groupIndex).bottomPolygon === undefined) { if (this._polygonRootsMap.get(comparedNode.groupIndex).bottomPolygon === undefined) {
const polygonMapEntry = this._polygonRootsMap.get(comparedNode.groupIndex); const polygonMapEntry = this._polygonRootsMap.get(comparedNode.groupIndex);
polygonMapEntry.bottomPolygon = comparedNode; polygonMapEntry.bottomPolygon = comparedNode;
@@ -779,7 +813,7 @@ class SearchNodeAction extends Action {
.withAdditionalProperties({ source: 'ComparisonView' }) .withAdditionalProperties({ source: 'ComparisonView' })
.send(); .send();
let nodeSearchWidget = this._instantiationService.createInstance(NodeSearchWidget, widgetController, executionPlan); let nodeSearchWidget = this._register(this._instantiationService.createInstance(NodeSearchWidget, widgetController, executionPlan));
widgetController.toggleWidget(nodeSearchWidget); widgetController.toggleWidget(nodeSearchWidget);
} }
} }

View File

@@ -135,10 +135,11 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
const columns: Slick.Column<Slick.SlickData>[] = this.getPropertyTableColumns(); const columns: Slick.Column<Slick.SlickData>[] = this.getPropertyTableColumns();
let primaryProps = []; let primaryProps = [];
let secondaryProps = [];
if (this._model.primaryElement?.properties) { if (this._model.primaryElement?.properties) {
primaryProps = this._model.primaryElement.properties; primaryProps = this._model.primaryElement.properties;
} }
let secondaryProps = [];
if (this._model.secondaryElement?.properties) { if (this._model.secondaryElement?.properties) {
secondaryProps = this._model.secondaryElement.properties; secondaryProps = this._model.secondaryElement.properties;
} }
@@ -440,6 +441,7 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
}; };
rows.push(row); rows.push(row);
if (!isString(primaryProp.value)) { if (!isString(primaryProp.value)) {
row.name.iconCssClass += ` parent-row-styling`; row.name.iconCssClass += ` parent-row-styling`;
row.primary.iconCssClass += ` parent-row-styling`; row.primary.iconCssClass += ` parent-row-styling`;
@@ -459,6 +461,7 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
}; };
rows.push(row); rows.push(row);
if (!isString(secondaryProp.value)) { if (!isString(secondaryProp.value)) {
row.name.iconCssClass += ` parent-row-styling`; row.name.iconCssClass += ` parent-row-styling`;
row.secondary.iconCssClass += ` parent-row-styling`; row.secondary.iconCssClass += ` parent-row-styling`;
@@ -475,6 +478,7 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti
if (this._orientation === value) { if (this._orientation === value) {
return; return;
} }
this._orientation = value; this._orientation = value;
this.updatePropertyContainerTitles(); this.updatePropertyContainerTitles();
} }

View File

@@ -61,7 +61,7 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen
} }
}); });
this._editorResolverService.registerEditor( this._register(this._editorResolverService.registerEditor(
this.getGlobForFileExtensions(supportedFileFormats), this.getGlobForFileExtensions(supportedFileFormats),
{ {
id: ExecutionPlanEditor.ID, id: ExecutionPlanEditor.ID,
@@ -74,11 +74,11 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen
graphFileContent: undefined, graphFileContent: undefined,
graphFileType: 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 }; return { editor: executionPlanInput, options: editorInput.options, group: group };
} }
); ));
} }
private getGlobForFileExtensions(extensions: string[]): string { private getGlobForFileExtensions(extensions: string[]): string {

View File

@@ -50,11 +50,13 @@ export class ExecutionPlanEditor extends EditorPane {
override clearInput(): void { override clearInput(): void {
const currentInput = this.input as ExecutionPlanInput; const currentInput = this.input as ExecutionPlanInput;
// clearing old input view if present in the editor // clearing old input view if present in the editor
if (currentInput?._executionPlanFileViewUUID) { if (currentInput?._executionPlanFileViewUUID) {
const oldView = this._viewCache.executionPlanFileViewMap.get(currentInput._executionPlanFileViewUUID); const oldView = this._viewCache.executionPlanFileViewMap.get(currentInput._executionPlanFileViewUUID);
oldView.onHide(this._parentContainer); oldView.onHide(this._parentContainer);
} }
super.clearInput(); super.clearInput();
} }

View File

@@ -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 { contrastBorder, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView'; 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 _parent: HTMLElement;
private _loadingSpinner: LoadingSpinner; private _loadingSpinner: LoadingSpinner;
private _loadingErrorInfoBox: InfoBox; private _loadingErrorInfoBox: InfoBox;
@@ -30,6 +31,7 @@ export class ExecutionPlanFileView {
@IInstantiationService private instantiationService: IInstantiationService, @IInstantiationService private instantiationService: IInstantiationService,
@IExecutionPlanService private executionPlanService: IExecutionPlanService @IExecutionPlanService private executionPlanService: IExecutionPlanService
) { ) {
super();
} }
public render(parent: HTMLElement): void { public render(parent: HTMLElement): void {
@@ -48,9 +50,6 @@ export class ExecutionPlanFileView {
} }
} }
dispose() {
}
/** /**
* Adds executionPlanGraph to the graph controller. * Adds executionPlanGraph to the graph controller.
* @param newGraphs ExecutionPlanGraphs to be added. * @param newGraphs ExecutionPlanGraphs to be added.
@@ -58,7 +57,7 @@ export class ExecutionPlanFileView {
public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) { public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) {
if (newGraphs) { if (newGraphs) {
newGraphs.forEach(g => { 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; ep.model = g;
this._executionPlanViews.push(ep); this._executionPlanViews.push(ep);
this.graphs.push(g); this.graphs.push(g);
@@ -76,10 +75,11 @@ export class ExecutionPlanFileView {
* @returns * @returns
*/ */
public async loadGraphFile(graphFile: azdata.executionPlan.ExecutionPlanGraphInfo) { 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"); this._loadingSpinner.loadingMessage = localize('loadingExecutionPlanFile', "Generating execution plans");
try { try {
this._loadingSpinner.loading = true; this._loadingSpinner.loading = true;
if (this._planCache.has(graphFile.graphFileContent)) { if (this._planCache.has(graphFile.graphFileContent)) {
this.addGraphs(this._planCache.get(graphFile.graphFileContent)); this.addGraphs(this._planCache.get(graphFile.graphFileContent));
return; return;
@@ -91,13 +91,15 @@ export class ExecutionPlanFileView {
this.addGraphs(graphs); this.addGraphs(graphs);
this._planCache.set(graphFile.graphFileContent, graphs); this._planCache.set(graphFile.graphFileContent, graphs);
} }
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingComplete', "Execution plans are generated"); this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingComplete', "Execution plans are generated");
} catch (e) { } catch (e) {
this._loadingErrorInfoBox = this.instantiationService.createInstance(InfoBox, this._container, { this._loadingErrorInfoBox = this._register(this.instantiationService.createInstance(InfoBox, this._container, {
text: e.toString(), text: e.toString(),
style: 'error', style: 'error',
isClickable: false isClickable: false
}); }));
this._loadingErrorInfoBox.isClickable = false; this._loadingErrorInfoBox.isClickable = false;
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan"); this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan");
} finally { } finally {

View File

@@ -116,11 +116,14 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase
if (!properties) { if (!properties) {
return []; return [];
} }
const sortedProperties = this.sortProperties(properties); const sortedProperties = this.sortProperties(properties);
const rows: Slick.SlickData[] = []; const rows: Slick.SlickData[] = [];
sortedProperties.forEach((property, index) => { sortedProperties.forEach((property, index) => {
let row = {}; let row = {};
row['name'] = property.name; row['name'] = property.name;
if (!isString(property.value)) { 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. // Styling values in the parent row differently to make them more apparent and standout compared to the rest of the cells.
row['name'] = { row['name'] = {
@@ -131,12 +134,15 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase
}; };
row['tootltip'] = property.displayValue; row['tootltip'] = property.displayValue;
row['treeGridChildren'] = this.convertPropertiesToTableRows(property.value); row['treeGridChildren'] = this.convertPropertiesToTableRows(property.value);
} else { } else {
row['value'] = removeLineBreaks(property.displayValue, ' '); row['value'] = removeLineBreaks(property.displayValue, ' ');
row['tooltip'] = property.displayValue; row['tooltip'] = property.displayValue;
} }
rows.push(row); rows.push(row);
}); });
return rows; return rows;
} }
@@ -165,6 +171,7 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase
rows.push(row); rows.push(row);
row['name'] = p.name; row['name'] = p.name;
row['parent'] = parentIndex; row['parent'] = parentIndex;
if (!isString(p.value)) { 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. // Styling values in the parent row differently to make them more apparent and standout compared to the rest of the cells.
row['name'] = { row['name'] = {

View File

@@ -75,15 +75,19 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
this.resizeSash = this._register(new Sash(sashContainer, this, { orientation: Orientation.VERTICAL, size: 3 })); this.resizeSash = this._register(new Sash(sashContainer, this, { orientation: Orientation.VERTICAL, size: 3 }));
let originalWidth = 0; let originalWidth = 0;
this._register(this.resizeSash.onDidStart((e: ISashEvent) => { this._register(this.resizeSash.onDidStart((e: ISashEvent) => {
originalWidth = this._parentContainer.clientWidth; originalWidth = this._parentContainer.clientWidth;
})); }));
this._register(this.resizeSash.onDidChange((evt: ISashEvent) => { this._register(this.resizeSash.onDidChange((evt: ISashEvent) => {
const change = evt.startX - evt.currentX; const change = evt.startX - evt.currentX;
const newWidth = originalWidth + change; const newWidth = originalWidth + change;
if (newWidth < 200) { if (newWidth < 200) {
return; return;
} }
this._parentContainer.style.flex = `0 0 ${newWidth}px`; this._parentContainer.style.flex = `0 0 ${newWidth}px`;
propertiesContent.style.width = `${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, { this._titleActions = this._register(new ActionBar(this._titleBarActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this 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'); this._headerContainer = DOM.$('.header');
propertiesContent.appendChild(this._headerContainer); propertiesContent.appendChild(this._headerContainer);
@@ -115,24 +119,28 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
this._headerActionsContainer = DOM.$('.table-action-bar'); this._headerActionsContainer = DOM.$('.table-action-bar');
this._searchAndActionBarContainer.appendChild(this._headerActionsContainer); this._searchAndActionBarContainer.appendChild(this._headerActionsContainer);
this._headerActions = this._register(new ActionBar(this._headerActionsContainer, { this._headerActions = this._register(new ActionBar(this._headerActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this orientation: ActionsOrientation.HORIZONTAL, context: this
})); }));
this._headerActions.pushAction([ this._headerActions.pushAction([
new SortPropertiesByDisplayOrderAction(), this._register(new SortPropertiesByDisplayOrderAction()),
new SortPropertiesAlphabeticallyAction(), this._register(new SortPropertiesAlphabeticallyAction()),
new SortPropertiesReverseAlphabeticallyAction(), this._register(new SortPropertiesReverseAlphabeticallyAction()),
new ExpandAllPropertiesAction(), this._register(new ExpandAllPropertiesAction()),
new CollapseAllPropertiesAction() this._register(new CollapseAllPropertiesAction())
], { icon: true, label: false }); ], { icon: true, label: false });
this._propertiesSearchInputContainer = DOM.$('.table-search'); this._propertiesSearchInputContainer = DOM.$('.table-search');
this._propertiesSearchInputContainer.classList.add('codicon', filterIconClassNames); this._propertiesSearchInputContainer.classList.add('codicon', filterIconClassNames);
this._propertiesSearchInput = this._register(new InputBox(this._propertiesSearchInputContainer, this._contextViewService, { this._propertiesSearchInput = this._register(new InputBox(this._propertiesSearchInputContainer, this._contextViewService, {
ariaDescription: propertiesSearchDescription, ariaDescription: propertiesSearchDescription,
placeholder: searchPlaceholder placeholder: searchPlaceholder
})); }));
attachInputBoxStyler(this._propertiesSearchInput, this._themeService);
this._register(attachInputBoxStyler(this._propertiesSearchInput, this._themeService));
this._propertiesSearchInput.element.classList.add('codicon', filterIconClassNames); this._propertiesSearchInput.element.classList.add('codicon', filterIconClassNames);
this._searchAndActionBarContainer.appendChild(this._propertiesSearchInputContainer); this._searchAndActionBarContainer.appendChild(this._propertiesSearchInputContainer);
this._register(this._propertiesSearchInput.onDidChange(e => { this._register(this._propertiesSearchInput.onDidChange(e => {
@@ -156,11 +164,12 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
editable: true, editable: true,
autoEdit: false autoEdit: false
})); }));
attachTableStyler(this._tableComponent, this._themeService);
this._register(attachTableStyler(this._tableComponent, this._themeService));
this._tableComponent.setSelectionModel(this._selectionModel); this._tableComponent.setSelectionModel(this._selectionModel);
const contextMenuAction = [ const contextMenuAction = [
this._instantiationService.createInstance(CopyTableData), this._register(this._instantiationService.createInstance(CopyTableData)),
]; ];
this._register(this._tableComponent.onContextMenu(e => { this._register(this._tableComponent.onContextMenu(e => {
@@ -184,10 +193,10 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
}).observe(_parentContainer); }).observe(_parentContainer);
} }
public getCopyString(): string { public getCopyString(): string {
const selectedDataRange = this._selectionModel.getSelectedRanges()[0];
let csvString = ''; let csvString = '';
const selectedDataRange = this._selectionModel.getSelectedRanges()[0];
if (selectedDataRange) { if (selectedDataRange) {
const data = []; const data = [];
@@ -204,10 +213,12 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
} }
data.push(row); data.push(row);
} }
csvString = data.map(row => csvString = data.map(row =>
row.map(x => `${x}`).join('\t') row.map(x => `${x}`).join('\t')
).join('\n'); ).join('\n');
} }
return csvString; return csvString;
} }
@@ -315,6 +326,7 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
-1) -1)
); );
} }
this._tableComponent.rerenderGrid(); this._tableComponent.rerenderGrid();
} }
@@ -322,15 +334,18 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
let resultData: Slick.SlickData[] = []; let resultData: Slick.SlickData[] = [];
data.forEach(dataRow => { data.forEach(dataRow => {
let includeRow = false; let includeRow = false;
const columns = this._tableComponent.grid.getColumns(); const columns = this._tableComponent.grid.getColumns();
for (let i = 0; i < columns.length; i++) { for (let i = 0; i < columns.length; i++) {
let dataValue = ''; let dataValue = '';
let rawDataValue = dataRow[columns[i].field]; let rawDataValue = dataRow[columns[i].field];
if (isString(rawDataValue)) { if (isString(rawDataValue)) {
dataValue = rawDataValue; dataValue = rawDataValue;
} else if (rawDataValue !== undefined) { } else if (rawDataValue !== undefined) {
dataValue = rawDataValue.text ?? rawDataValue.title; dataValue = rawDataValue.text ?? rawDataValue.title;
} }
if (dataValue?.toLowerCase().includes(search.toLowerCase())) { if (dataValue?.toLowerCase().includes(search.toLowerCase())) {
includeRow = true; includeRow = true;
break; break;
@@ -348,9 +363,11 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
if (rowClone['treeGridChildren'] !== undefined) { if (rowClone['treeGridChildren'] !== undefined) {
rowClone['expanded'] = true; rowClone['expanded'] = true;
} }
resultData.push(rowClone); resultData.push(rowClone);
} }
}); });
return { include: resultData.length > 0, data: resultData }; return { include: resultData.length > 0, data: resultData };
} }
@@ -358,13 +375,16 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
if (nestedData === undefined || nestedData.length === 0) { if (nestedData === undefined || nestedData.length === 0) {
return rows; return rows;
} }
nestedData.forEach((dataRow) => { nestedData.forEach((dataRow) => {
rows.push(dataRow); rows.push(dataRow);
dataRow['parent'] = parentIndex; dataRow['parent'] = parentIndex;
if (dataRow['treeGridChildren']) { if (dataRow['treeGridChildren']) {
this.flattenTableData(dataRow['treeGridChildren'], rows.length - 1, rows); this.flattenTableData(dataRow['treeGridChildren'], rows.length - 1, rows);
} }
}); });
return rows; return rows;
} }
} }

View File

@@ -13,6 +13,7 @@ import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/brows
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache'; import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
import { generateUuid } from 'vs/base/common/uuid'; import { generateUuid } from 'vs/base/common/uuid';
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView'; import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
export class ExecutionPlanTab implements IPanelTab { export class ExecutionPlanTab implements IPanelTab {
public readonly title = localize('executionPlanTitle', "Query Plan (Preview)"); public readonly title = localize('executionPlanTitle', "Query Plan (Preview)");
@@ -27,6 +28,7 @@ export class ExecutionPlanTab implements IPanelTab {
} }
public dispose() { public dispose() {
dispose(this.view);
} }
public clear() { 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 _container: HTMLElement = DOM.$('.execution-plan-tab');
private _input: ExecutionPlanState; private _input: ExecutionPlanState;
private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance(); private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
@@ -45,6 +47,7 @@ export class ExecutionPlanTabView implements IPanelView {
private _queryResultsView: QueryResultsView, private _queryResultsView: QueryResultsView,
@IInstantiationService private _instantiationService: IInstantiationService, @IInstantiationService private _instantiationService: IInstantiationService,
) { ) {
super();
} }
public set state(newInput: ExecutionPlanState) { public set state(newInput: ExecutionPlanState) {
@@ -63,7 +66,7 @@ export class ExecutionPlanTabView implements IPanelView {
} else { } else {
// creating a new view for the new input // creating a new view for the new input
newInput.executionPlanFileViewUUID = generateUuid(); newInput.executionPlanFileViewUUID = generateUuid();
newView = this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView); newView = this._register(this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView));
newView.onShow(this._container); newView.onShow(this._container);
newView.addGraphs( newView.addGraphs(
newInput.graphs newInput.graphs
@@ -88,7 +91,7 @@ export class ExecutionPlanTabView implements IPanelView {
if (currentView) { if (currentView) {
currentView.onHide(this._container); currentView.onHide(this._container);
this._input.graphs = []; 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); this._viewCache.executionPlanFileViewMap.set(this._input.executionPlanFileViewUUID, currentView);
currentView.render(this._container); currentView.render(this._container);
} }

View File

@@ -75,6 +75,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
while (this._container.firstChild) { while (this._container.firstChild) {
this._container.removeChild(this._container.firstChild); this._container.removeChild(this._container.firstChild);
} }
this._input.graphs.forEach((g, i) => { this._input.graphs.forEach((g, i) => {
this.convertExecutionPlanGraphToTreeGrid(g, i); this.convertExecutionPlanGraphToTreeGrid(g, i);
}); });
@@ -84,13 +85,16 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
let dataMap: { [key: string]: any }[] = []; let dataMap: { [key: string]: any }[] = [];
const columnValues: string[] = []; const columnValues: string[] = [];
const stack: { node: azdata.executionPlan.ExecutionPlanNode, parentIndex: number }[] = []; const stack: { node: azdata.executionPlan.ExecutionPlanNode, parentIndex: number }[] = [];
stack.push({ stack.push({
node: graph.root, node: graph.root,
parentIndex: -1, parentIndex: -1,
}); });
while (stack.length !== 0) { while (stack.length !== 0) {
const treeGridNode = stack.pop(); const treeGridNode = stack.pop();
const row: { [key: string]: any } = {}; const row: { [key: string]: any } = {};
treeGridNode.node.topOperationsData.forEach((d, i) => { treeGridNode.node.topOperationsData.forEach((d, i) => {
let displayText = d.displayValue.toString(); let displayText = d.displayValue.toString();
@@ -104,10 +108,12 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
columnValues.splice(i, 0, d.columnName); columnValues.splice(i, 0, d.columnName);
} }
}); });
row['nodeId'] = treeGridNode.node.id; row['nodeId'] = treeGridNode.node.id;
row['parent'] = treeGridNode.parentIndex; row['parent'] = treeGridNode.parentIndex;
row['parentNodeId'] = dataMap[treeGridNode.parentIndex] ? dataMap[treeGridNode.parentIndex]['nodeId'] : undefined; row['parentNodeId'] = dataMap[treeGridNode.parentIndex] ? dataMap[treeGridNode.parentIndex]['nodeId'] : undefined;
row['expanded'] = true; row['expanded'] = true;
if (treeGridNode.node.children) { if (treeGridNode.node.children) {
treeGridNode.node.children.forEach(c => stack.push({ treeGridNode.node.children.forEach(c => stack.push({
node: c, node: c,
@@ -132,26 +138,29 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
const topOperationContainer = DOM.$('.top-operations-container'); const topOperationContainer = DOM.$('.top-operations-container');
this._container.appendChild(topOperationContainer); this._container.appendChild(topOperationContainer);
const header = this._instantiationService.createInstance(ExecutionPlanViewHeader, topOperationContainer, {
const header = this._register(this._instantiationService.createInstance(ExecutionPlanViewHeader, topOperationContainer, {
planIndex: index, planIndex: index,
}); }));
header.query = graph.query; header.query = graph.query;
header.relativeCost = graph.root.relativeCost; header.relativeCost = graph.root.relativeCost;
const tableContainer = DOM.$('.table-container'); const tableContainer = DOM.$('.table-container');
topOperationContainer.appendChild(tableContainer); topOperationContainer.appendChild(tableContainer);
this._planTreeContainers.push(topOperationContainer); this._planTreeContainers.push(topOperationContainer);
let copyHandler = new CopyKeybind<any>(); let copyHandler = new CopyKeybind<any>();
this._register(copyHandler.onCopy(e => { this._register(copyHandler.onCopy(e => {
let csvString = '';
const selectedDataRange = selectionModel.getSelectedRanges()[0]; const selectedDataRange = selectionModel.getSelectedRanges()[0];
let csvString = '';
if (selectedDataRange) { if (selectedDataRange) {
const data = []; const data = [];
for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) {
const dataRow = treeGrid.getData().getItem(rowIndex); const dataRow = treeGrid.getData().getItem(rowIndex);
const row = []; const row = [];
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
const dataItem = dataRow[treeGrid.grid.getColumns()[colIndex].field]; const dataItem = dataRow[treeGrid.grid.getColumns()[colIndex].field];
if (dataItem) { if (dataItem) {
@@ -160,8 +169,10 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
row.push(' '); row.push(' ');
} }
} }
data.push(row); data.push(row);
} }
csvString = data.map(row => csvString = data.map(row =>
row.map(x => `${x}`).join('\t') row.map(x => `${x}`).join('\t')
).join('\n'); ).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 selectedText: csvString
}); });
})); }));
const selectionModel = new CellSelectionModel<Slick.SlickData>(); const selectionModel = new CellSelectionModel<Slick.SlickData>();
const treeGrid = new TreeGrid<Slick.SlickData>(tableContainer, { const treeGrid = this._register(new TreeGrid<Slick.SlickData>(tableContainer, {
columns: columns, columns: columns,
sorter: (args) => { sorter: (args) => {
const sortColumn = args.sortCol.field; const sortColumn = args.sortCol.field;
let data = deepClone(dataMap); let data = deepClone(dataMap);
if (data.length === 0) { if (data.length === 0) {
data = treeGrid.getData().getItems(); data = this._register(treeGrid.getData()).getItems();
} }
const sortedData = []; const sortedData = [];
const rootRow = data[0]; const rootRow = data[0];
const stack: { row: Slick.SlickData, originalIndex: number }[] = []; const stack: { row: Slick.SlickData, originalIndex: number }[] = [];
@@ -196,8 +209,9 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
while (stack.length !== 0) { while (stack.length !== 0) {
const currentTreeGridRow = stack.pop(); const currentTreeGridRow = stack.pop();
let currentTreeGridRowChildren: { row: Slick.SlickData, originalIndex: number }[] = [];
sortedData.push(currentTreeGridRow.row); sortedData.push(currentTreeGridRow.row);
let currentTreeGridRowChildren: { row: Slick.SlickData, originalIndex: number }[] = [];
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
if (data[i].parentNodeId === currentTreeGridRow.row.nodeId) { if (data[i].parentNodeId === currentTreeGridRow.row.nodeId) {
currentTreeGridRowChildren.push({ row: data[i], originalIndex: i }); currentTreeGridRowChildren.push({ row: data[i], originalIndex: i });
@@ -207,6 +221,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
currentTreeGridRowChildren = currentTreeGridRowChildren.sort((a, b) => { currentTreeGridRowChildren = currentTreeGridRowChildren.sort((a, b) => {
const aRow = a.row; const aRow = a.row;
const bRow = b.row; const bRow = b.row;
let result = -1; let result = -1;
if (!aRow[sortColumn]) { if (!aRow[sortColumn]) {
result = 1; result = 1;
@@ -217,6 +232,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
const dataType = aRow[sortColumn].dataType; const dataType = aRow[sortColumn].dataType;
const aText = aRow[sortColumn].text; const aText = aRow[sortColumn].text;
const bText = bRow[sortColumn].text; const bText = bRow[sortColumn].text;
if (aText === bText) { if (aText === bText) {
result = 0; result = 0;
} else { } else {
@@ -232,6 +248,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
} }
} }
} }
return args.sortAsc ? result : -result; return args.sortAsc ? result : -result;
}); });
@@ -241,6 +258,7 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
stack.push(...currentTreeGridRowChildren); stack.push(...currentTreeGridRowChildren);
} }
dataMap = sortedData; dataMap = sortedData;
treeGrid.setData(sortedData); treeGrid.setData(sortedData);
} }
@@ -249,24 +267,23 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
forceFitColumns: false, forceFitColumns: false,
defaultColumnWidth: 120, defaultColumnWidth: 120,
showRowNumber: true showRowNumber: true
}); }));
treeGrid.setSelectionModel(selectionModel); treeGrid.setSelectionModel(selectionModel);
treeGrid.setData(dataMap); treeGrid.setData(dataMap);
treeGrid.registerPlugin(copyHandler); treeGrid.registerPlugin(copyHandler);
treeGrid.setTableTitle(localize('topOperationsTableTitle', "Execution Plan Tree")); treeGrid.setTableTitle(localize('topOperationsTableTitle', "Execution Plan Tree"));
this._treeGrids.push(treeGrid); this._treeGrids.push(treeGrid);
const contextMenuAction = [ const contextMenuAction = [
this._instantiationService.createInstance(CopyTableData), this._register(this._instantiationService.createInstance(CopyTableData)),
this._instantiationService.createInstance(CopyTableDataWithHeader), this._register(this._instantiationService.createInstance(CopyTableDataWithHeader)),
this._instantiationService.createInstance(SelectAll) this._register(this._instantiationService.createInstance(SelectAll))
]; ];
this._register(treeGrid.onKeyDown((evt: ITableKeyboardEvent) => { this._register(treeGrid.onKeyDown((evt: ITableKeyboardEvent) => {
if (evt.event.ctrlKey && (evt.event.key === 'a' || evt.event.key === 'A')) { 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(); treeGrid.focus();
evt.event.preventDefault(); evt.event.preventDefault();
evt.event.stopPropagation(); evt.event.stopPropagation();
@@ -274,14 +291,16 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
})); }));
this._register(treeGrid.onContextMenu(e => { this._register(treeGrid.onContextMenu(e => {
const selectedDataRange = selectionModel.getSelectedRanges()[0];
let csvString = ''; let csvString = '';
let csvStringWithHeader = ''; let csvStringWithHeader = '';
const selectedDataRange = selectionModel.getSelectedRanges()[0];
if (selectedDataRange) { if (selectedDataRange) {
const data = []; const data = [];
for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { 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 = []; const row = [];
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
const dataItem = dataRow[treeGrid.grid.getColumns()[colIndex].field]; const dataItem = dataRow[treeGrid.grid.getColumns()[colIndex].field];
@@ -291,14 +310,15 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
row.push(''); row.push('');
} }
} }
data.push(row); data.push(row);
} }
csvString = data.map(row => csvString = data.map(row =>
row.map(x => `${x}`).join('\t') row.map(x => `${x}`).join('\t')
).join('\n'); ).join('\n');
const columns = []; const columns = [];
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
columns.push(treeGrid.grid.getColumns()[colIndex].name); 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) => { new ResizeObserver((e) => {
treeGrid.layout(new DOM.Dimension(tableContainer.clientWidth, tableContainer.clientHeight)); treeGrid.layout(new DOM.Dimension(tableContainer.clientWidth, tableContainer.clientHeight));
}).observe(tableContainer); }).observe(tableContainer);
return treeGrid; return treeGrid;
} }
@@ -330,10 +352,13 @@ export class ExecutionPlanTreeTabView extends Disposable implements IPanelView {
this._container.style.width = dimension.width + 'px'; this._container.style.width = dimension.width + 'px';
this._container.style.height = dimension.height + 'px'; this._container.style.height = dimension.height + 'px';
} }
remove?(): void { remove?(): void {
} }
onShow?(): void { onShow?(): void {
} }
onHide?(): void { onHide?(): void {
} }
} }
@@ -342,14 +367,11 @@ export class CopyTableData extends Action {
public static ID = 'ept.CopyTableData'; public static ID = 'ept.CopyTableData';
public static LABEL = localize('ept.topOperationsCopyTableData', "Copy"); public static LABEL = localize('ept.topOperationsCopyTableData', "Copy");
constructor( constructor(@IClipboardService private _clipboardService: IClipboardService) {
@IClipboardService private _clipboardService: IClipboardService
) {
super(CopyTableData.ID, CopyTableData.LABEL, ''); super(CopyTableData.ID, CopyTableData.LABEL, '');
} }
public override async run(context: ContextMenuModel): Promise<void> { public override async run(context: ContextMenuModel): Promise<void> {
this._clipboardService.writeText(context.selectedText); this._clipboardService.writeText(context.selectedText);
} }
} }
@@ -358,14 +380,11 @@ export class CopyTableDataWithHeader extends Action {
public static ID = 'ept.CopyTableDataWithHeader'; public static ID = 'ept.CopyTableDataWithHeader';
public static LABEL = localize('ept.topOperationsCopyWithHeader', "Copy with Header"); public static LABEL = localize('ept.topOperationsCopyWithHeader', "Copy with Header");
constructor( constructor(@IClipboardService private _clipboardService: IClipboardService) {
@IClipboardService private _clipboardService: IClipboardService
) {
super(CopyTableDataWithHeader.ID, CopyTableDataWithHeader.LABEL, ''); super(CopyTableDataWithHeader.ID, CopyTableDataWithHeader.LABEL, '');
} }
public override async run(context: ContextMenuModel): Promise<void> { public override async run(context: ContextMenuModel): Promise<void> {
this._clipboardService.writeText(context.selectionTextWithHeader); this._clipboardService.writeText(context.selectionTextWithHeader);
} }
} }
@@ -374,8 +393,7 @@ export class SelectAll extends Action {
public static ID = 'ept.SelectAllTableData'; public static ID = 'ept.SelectAllTableData';
public static LABEL = localize('ept.topOperationsSelectAll', "Select All"); public static LABEL = localize('ept.topOperationsSelectAll', "Select All");
constructor( constructor() {
) {
super(SelectAll.ID, SelectAll.LABEL, ''); super(SelectAll.ID, SelectAll.LABEL, '');
} }

View File

@@ -38,8 +38,9 @@ import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPla
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView'; import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView'; import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView';
import { HighlightExpensiveOperationWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget'; 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 // Underlying execution plan displayed in the view
private _model?: azdata.executionPlan.ExecutionPlanGraph; private _model?: azdata.executionPlan.ExecutionPlanGraph;
@@ -87,6 +88,8 @@ export class ExecutionPlanView implements ISashLayoutProvider {
@IWorkspaceContextService public workspaceContextService: IWorkspaceContextService, @IWorkspaceContextService public workspaceContextService: IWorkspaceContextService,
@IEditorService private _editorService: IEditorService @IEditorService private _editorService: IEditorService
) { ) {
super();
// parent container for query plan. // parent container for query plan.
this.container = DOM.$('.execution-plan'); this.container = DOM.$('.execution-plan');
this._parent.appendChild(this.container); this._parent.appendChild(this.container);
@@ -94,19 +97,20 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._parent.appendChild(sashContainer); this._parent.appendChild(sashContainer);
// resizing sash for the query plan. // 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 originalHeight = this.container.offsetHeight;
let originalTableHeight = 0; let originalTableHeight = 0;
let change = 0; let change = 0;
sash.onDidStart((e: ISashEvent) => {
this._register(sash.onDidStart((e: ISashEvent) => {
originalHeight = this.container.offsetHeight; originalHeight = this.container.offsetHeight;
originalTableHeight = this.propertiesView.tableHeight; originalTableHeight = this.propertiesView.tableHeight;
}); }));
/** /**
* Using onDidChange for the smooth resizing of the graph diagram * 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; change = evt.startY - evt.currentY;
const newHeight = originalHeight - change; const newHeight = originalHeight - change;
if (newHeight < 200) { if (newHeight < 200) {
@@ -118,14 +122,14 @@ export class ExecutionPlanView implements ISashLayoutProvider {
*/ */
this.container.style.minHeight = '200px'; this.container.style.minHeight = '200px';
this.container.style.flex = `0 0 ${newHeight}px`; 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 * 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.propertiesView.tableHeight = originalTableHeight - change;
}); }));
this._planContainer = DOM.$('.plan'); this._planContainer = DOM.$('.plan');
this.container.appendChild(this._planContainer); this.container.appendChild(this._planContainer);
@@ -139,14 +143,14 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight; this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight;
this._planContainer.appendChild(this._planHeaderContainer); 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, planIndex: this._graphIndex,
}); }));
// container properties // container properties
this._propContainer = DOM.$('.properties'); this._propContainer = DOM.$('.properties');
this.container.appendChild(this._propContainer); 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._widgetContainer = DOM.$('.plan-action-container');
this._planContainer.appendChild(this._widgetContainer); this._planContainer.appendChild(this._widgetContainer);
@@ -155,56 +159,56 @@ export class ExecutionPlanView implements ISashLayoutProvider {
// container that holds action bar icons // container that holds action bar icons
this._actionBarContainer = DOM.$('.action-bar-container'); this._actionBarContainer = DOM.$('.action-bar-container');
this.container.appendChild(this._actionBarContainer); this.container.appendChild(this._actionBarContainer);
this._actionBar = new ActionBar(this._actionBarContainer, { this._actionBar = this._register(new ActionBar(this._actionBarContainer, {
orientation: ActionsOrientation.VERTICAL, context: this orientation: ActionsOrientation.VERTICAL, context: this
}); }));
this.actionBarToggleTopTip = new ActionBarToggleTooltip(); this.actionBarToggleTopTip = this._register(new ActionBarToggleTooltip());
const actionBarActions = [ const actionBarActions = [
new SavePlanFile(), this._register(new SavePlanFile()),
new OpenPlanFile(), this._register(new OpenPlanFile()),
this._instantiationService.createInstance(OpenQueryAction, 'ActionBar'), this._register(this._instantiationService.createInstance(OpenQueryAction, 'ActionBar')),
new Separator(), this._register(new Separator()),
this._instantiationService.createInstance(ZoomInAction, 'ActionBar'), this._register(this._instantiationService.createInstance(ZoomInAction, 'ActionBar')),
this._instantiationService.createInstance(ZoomOutAction, 'ActionBar'), this._register(this._instantiationService.createInstance(ZoomOutAction, 'ActionBar')),
this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar'), this._register(this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar')),
this._instantiationService.createInstance(CustomZoomAction, 'ActionBar'), this._register(this._instantiationService.createInstance(CustomZoomAction, 'ActionBar')),
new Separator(), this._register(new Separator()),
this._instantiationService.createInstance(SearchNodeAction, 'ActionBar'), this._register(this._instantiationService.createInstance(SearchNodeAction, 'ActionBar')),
this._instantiationService.createInstance(PropertiesAction, 'ActionBar'), this._register(this._instantiationService.createInstance(PropertiesAction, 'ActionBar')),
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar'), this._register(this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar')),
this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ActionBar'), this._register(this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ActionBar')),
this.actionBarToggleTopTip this.actionBarToggleTopTip
]; ];
// Setting up context menu // Setting up context menu
this.contextMenuToggleTooltipAction = new ContextMenuTooltipToggle(); this.contextMenuToggleTooltipAction = this._register(new ContextMenuTooltipToggle());
const contextMenuAction = [ const contextMenuAction = [
new SavePlanFile(), this._register(new SavePlanFile()),
new OpenPlanFile(), this._register(new OpenPlanFile()),
this._instantiationService.createInstance(OpenQueryAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(OpenQueryAction, 'ContextMenu')),
new Separator(), this._register(new Separator()),
this._instantiationService.createInstance(ZoomInAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(ZoomInAction, 'ContextMenu')),
this._instantiationService.createInstance(ZoomOutAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(ZoomOutAction, 'ContextMenu')),
this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu')),
this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu')),
new Separator(), this._register(new Separator()),
this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu')),
this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(PropertiesAction, 'ContextMenu')),
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu')),
this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ContextMenu'), this._register(this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ContextMenu')),
this.contextMenuToggleTooltipAction, this.contextMenuToggleTooltipAction,
new Separator(), this._register(new Separator()),
]; ];
if (this._queryResultsView) { if (this._queryResultsView) {
actionBarActions.push(this._instantiationService.createInstance(TopOperationsAction)); actionBarActions.push(this._register(this._instantiationService.createInstance(TopOperationsAction)));
contextMenuAction.push(this._instantiationService.createInstance(TopOperationsAction)); contextMenuAction.push(this._register(this._instantiationService.createInstance(TopOperationsAction)));
} }
this._actionBar.pushAction(actionBarActions, { icon: true, label: false }); this._actionBar.pushAction(actionBarActions, { icon: true, label: false });
const self = this; const self = this;
this._planContainer.oncontextmenu = (e: MouseEvent) => { this._register(DOM.addDisposableListener(this._planContainer, DOM.EventType.CONTEXT_MENU, (e: MouseEvent) => {
if (contextMenuAction) { if (contextMenuAction) {
this._contextMenuService.showContextMenu({ this._contextMenuService.showContextMenu({
getAnchor: () => { getAnchor: () => {
@@ -217,34 +221,37 @@ export class ExecutionPlanView implements ISashLayoutProvider {
getActionsContext: () => (self) 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') { 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); searchNodeAction.run(self);
e.stopPropagation(); e.stopPropagation();
} }
}; }));
} }
getHorizontalSashTop(sash: Sash): number { getHorizontalSashTop(sash: Sash): number {
return 0; return 0;
} }
getHorizontalSashLeft?(sash: Sash): number { getHorizontalSashLeft?(sash: Sash): number {
return 0; return 0;
} }
getHorizontalSashWidth?(sash: Sash): number { getHorizontalSashWidth?(sash: Sash): number {
return this.container.clientWidth; return this.container.clientWidth;
} }
private createPlanDiagram(container: HTMLElement) { private createPlanDiagram(container: HTMLElement) {
this.executionPlanDiagram = this._instantiationService.createInstance(AzdataGraphView, container, this._model); this.executionPlanDiagram = this._register(this._instantiationService.createInstance(AzdataGraphView, container, this._model));
this.executionPlanDiagram.onElementSelected(e => {
this._register(this.executionPlanDiagram.onElementSelected(e => {
container.focus(); container.focus();
this.propertiesView.graphElement = e; this.propertiesView.graphElement = e;
}); }));
} }
@@ -253,9 +260,11 @@ export class ExecutionPlanView implements ISashLayoutProvider {
if (this._model) { if (this._model) {
this.planHeader.graphIndex = this._graphIndex; this.planHeader.graphIndex = this._graphIndex;
this.planHeader.query = graph.query; this.planHeader.query = graph.query;
if (graph.recommendations) { if (graph.recommendations) {
this.planHeader.recommendations = graph.recommendations; this.planHeader.recommendations = graph.recommendations;
} }
let diagramContainer = DOM.$('.diagram'); let diagramContainer = DOM.$('.diagram');
this.createPlanDiagram(diagramContainer); this.createPlanDiagram(diagramContainer);
@@ -266,13 +275,13 @@ export class ExecutionPlanView implements ISashLayoutProvider {
* the graph control. To scroll the individual graphs, users should * the graph control. To scroll the individual graphs, users should
* use the scroll bars. * use the scroll bars.
*/ */
diagramContainer.addEventListener('wheel', e => { this._register(DOM.addDisposableListener(diagramContainer, DOM.EventType.WHEEL, (e: WheelEvent) => {
//Hiding all tooltips when we scroll. //Hiding all tooltips when we scroll.
const element = document.getElementsByClassName('mxTooltip'); const element = document.getElementsByClassName('mxTooltip');
for (let i = 0; i < element.length; i++) { for (let i = 0; i < element.length; i++) {
(<HTMLElement>element[i]).style.visibility = 'hidden'; (<HTMLElement>element[i]).style.visibility = 'hidden';
} }
}); }));
this._planContainer.appendChild(diagramContainer); this._planContainer.appendChild(diagramContainer);
@@ -301,10 +310,10 @@ export class ExecutionPlanView implements ISashLayoutProvider {
} }
public compareCurrentExecutionPlan() { public compareCurrentExecutionPlan() {
this._editorService.openEditor(this._instantiationService.createInstance(ExecutionPlanComparisonInput, { this._editorService.openEditor(this._register(this._instantiationService.createInstance(ExecutionPlanComparisonInput, {
topExecutionPlan: this._executionPlanFileView.graphs, topExecutionPlan: this._executionPlanFileView.graphs,
topPlanIndex: this._graphIndex - 1 topPlanIndex: this._graphIndex - 1
}), { })), {
pinned: true pinned: true
}); });
} }
@@ -466,7 +475,7 @@ export class CustomZoomAction extends Action {
.withAdditionalProperties({ source: this.source }) .withAdditionalProperties({ source: this.source })
.send(); .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 }) .withAdditionalProperties({ source: this.source })
.send(); .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 }) .withAdditionalProperties({ source: this.source })
.send(); .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)));
} }
} }

View File

@@ -11,8 +11,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { Button } from 'sql/base/browser/ui/button/button'; import { Button } from 'sql/base/browser/ui/button/button';
import { removeLineBreaks } from 'sql/base/common/strings'; 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 _graphIndex: number; // Index of the graph in the view
private _relativeCost: number; // Relative cost of the graph to the script private _relativeCost: number; // Relative cost of the graph to the script
@@ -28,7 +29,9 @@ export class ExecutionPlanViewHeader {
public constructor( public constructor(
private _parentContainer: HTMLElement, private _parentContainer: HTMLElement,
headerData: PlanHeaderData | undefined, headerData: PlanHeaderData | undefined,
@IInstantiationService public readonly _instantiationService: IInstantiationService) { @IInstantiationService public readonly _instantiationService: IInstantiationService
) {
super();
if (headerData) { if (headerData) {
this._graphIndex = headerData.planIndex; this._graphIndex = headerData.planIndex;
@@ -67,6 +70,7 @@ export class ExecutionPlanViewHeader {
recommendations.forEach(r => { recommendations.forEach(r => {
r.displayString = removeLineBreaks(r.displayString); r.displayString = removeLineBreaks(r.displayString);
}); });
this._recommendations = recommendations; this._recommendations = recommendations;
this.renderRecommendations(); this.renderRecommendations();
} }
@@ -97,19 +101,19 @@ export class ExecutionPlanViewHeader {
while (this._recommendationsContainer.firstChild) { while (this._recommendationsContainer.firstChild) {
this._recommendationsContainer.removeChild(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, title: r.displayString,
secondary: true, secondary: true,
}); }));
link.label = r.displayString; link.label = r.displayString;
//Enabling on click action for recommendations. It will open the recommendation File //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); this._instantiationService.invokeFunction(openNewQuery, undefined, r.queryWithDescription, RunQueryOnConnectionMode.none);
}); }));
}); });
} }
} }

View File

@@ -15,6 +15,7 @@ export class ExecutionPlanWidgetController {
private addWidget(widget: ExecutionPlanWidgetBase) { private addWidget(widget: ExecutionPlanWidgetBase) {
if (widget.identifier && !this._executionPlanWidgetMap.has(widget.identifier)) { if (widget.identifier && !this._executionPlanWidgetMap.has(widget.identifier)) {
this._executionPlanWidgetMap.set(widget.identifier, widget); this._executionPlanWidgetMap.set(widget.identifier, widget);
if (widget.container) { if (widget.container) {
widget.container.classList.add('child'); widget.container.classList.add('child');
this._parentContainer.appendChild(widget.container); this._parentContainer.appendChild(widget.container);

View File

@@ -69,9 +69,11 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
public scrollToIndex(index: number) { public scrollToIndex(index: number) {
index = index - 1; index = index - 1;
this._topOperationsContainers[index].scrollIntoView(true); this._topOperationsContainers[index].scrollIntoView(true);
this._tables.forEach(t => { this._tables.forEach(t => {
t.getSelectionModel().setSelectedRanges([]); t.getSelectionModel().setSelectedRanges([]);
}); });
this._tables[index].getSelectionModel().setSelectedRanges([new Slick.Range(0, 1, 0, 1)]); this._tables[index].getSelectionModel().setSelectedRanges([new Slick.Range(0, 1, 0, 1)]);
this._tables[index].focus(); this._tables[index].focus();
} }
@@ -95,6 +97,7 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
while (this._container.firstChild) { while (this._container.firstChild) {
this._container.removeChild(this._container.firstChild); this._container.removeChild(this._container.firstChild);
} }
this._input.graphs.forEach((g, i) => { this._input.graphs.forEach((g, i) => {
this.convertExecutionPlanGraphToTable(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<Slick.SlickData> { public convertExecutionPlanGraphToTable(graph: azdata.executionPlan.ExecutionPlanGraph, index: number): Table<Slick.SlickData> {
const dataMap: { [key: string]: any }[] = []; const dataMap: { [key: string]: any }[] = [];
const columnValues: string[] = []; const columnValues: string[] = [];
const stack: azdata.executionPlan.ExecutionPlanNode[] = []; const stack: azdata.executionPlan.ExecutionPlanNode[] = [];
stack.push(...graph.root.children); stack.push(...graph.root.children);
while (stack.length !== 0) { while (stack.length !== 0) {
const node = stack.pop(); const node = stack.pop();
const row: { [key: string]: any } = {}; const row: { [key: string]: any } = {};
node.topOperationsData.forEach((d, i) => { node.topOperationsData.forEach((d, i) => {
let displayText = d.displayValue.toString(); let displayText = d.displayValue.toString();
if (i === 0) { if (i === 0) {
row[d.columnName] = { row[d.columnName] = {
displayText: displayText, displayText: displayText,
@@ -126,14 +131,18 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
dataType: d.dataType dataType: d.dataType
}; };
} }
if (columnValues.indexOf(d.columnName) === -1) { if (columnValues.indexOf(d.columnName) === -1) {
columnValues.splice(i, 0, d.columnName); columnValues.splice(i, 0, d.columnName);
} }
}); });
row['nodeId'] = node.id; row['nodeId'] = node.id;
if (node.children) { if (node.children) {
node.children.forEach(c => stack.push(c)); node.children.forEach(c => stack.push(c));
} }
row[TABLE_SORT_COLUMN_KEY] = node.cost; row[TABLE_SORT_COLUMN_KEY] = node.cost;
dataMap.push(row); dataMap.push(row);
} }
@@ -165,18 +174,19 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
headerContainer.appendChild(headerSearchBarContainer); headerContainer.appendChild(headerSearchBarContainer);
headerContainer.classList.add('codicon', filterIconClassNames); headerContainer.classList.add('codicon', filterIconClassNames);
const topOperationsSearchInput = new InputBox(headerSearchBarContainer, this._contextViewService, { const topOperationsSearchInput = this._register(new InputBox(headerSearchBarContainer, this._contextViewService, {
ariaDescription: topOperationsSearchDescription, ariaDescription: topOperationsSearchDescription,
placeholder: searchPlaceholder placeholder: searchPlaceholder
}); }));
attachInputBoxStyler(topOperationsSearchInput, this._themeService); this._register(attachInputBoxStyler(topOperationsSearchInput, this._themeService));
topOperationsSearchInput.element.classList.add('codicon', filterIconClassNames); topOperationsSearchInput.element.classList.add('codicon', filterIconClassNames);
const header = this._instantiationService.createInstance(ExecutionPlanViewHeader, headerInfoContainer, { const header = this._register(this._instantiationService.createInstance(ExecutionPlanViewHeader, headerInfoContainer, {
planIndex: index, planIndex: index,
}); }));
header.query = graph.query; header.query = graph.query;
header.relativeCost = graph.root.relativeCost; header.relativeCost = graph.root.relativeCost;
const tableContainer = DOM.$('.table-container'); const tableContainer = DOM.$('.table-container');
topOperationContainer.appendChild(tableContainer); topOperationContainer.appendChild(tableContainer);
this._topOperationsContainers.push(topOperationContainer); this._topOperationsContainers.push(topOperationContainer);
@@ -186,14 +196,15 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
let copyHandler = new CopyKeybind<any>(); let copyHandler = new CopyKeybind<any>();
this._register(copyHandler.onCopy(e => { this._register(copyHandler.onCopy(e => {
let csvString = '';
const selectedDataRange = selectionModel.getSelectedRanges()[0]; const selectedDataRange = selectionModel.getSelectedRanges()[0];
let csvString = '';
if (selectedDataRange) { if (selectedDataRange) {
const data = []; const data = [];
for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { 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 = []; const row = [];
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
const dataItem = dataRow[table.columns[colIndex].field]; const dataItem = dataRow[table.columns[colIndex].field];
@@ -203,8 +214,10 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
row.push(' '); row.push(' ');
} }
} }
data.push(row); data.push(row);
} }
csvString = data.map(row => csvString = data.map(row =>
row.map(x => `${x}`).join('\t') row.map(x => `${x}`).join('\t')
).join('\n'); ).join('\n');
@@ -214,7 +227,6 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
columns.push(table.columns[colIndex].name); columns.push(table.columns[colIndex].name);
} }
} }
this._instantiationService.createInstance(CopyTableData).run({ this._instantiationService.createInstance(CopyTableData).run({
@@ -224,11 +236,12 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
const selectionModel = new CellSelectionModel<Slick.SlickData>(); const selectionModel = new CellSelectionModel<Slick.SlickData>();
const table = new Table<Slick.SlickData>(tableContainer, { const table = this._register(new Table<Slick.SlickData>(tableContainer, {
columns: columns, columns: columns,
sorter: (args) => { sorter: (args) => {
const column = args.sortCol.field; 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; let result = -1;
if (!a[column]) { if (!a[column]) {
@@ -256,8 +269,10 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
} }
} }
} }
return args.sortAsc ? result : -result; return args.sortAsc ? result : -result;
}); });
table.setData(sortedData); table.setData(sortedData);
} }
}, { }, {
@@ -265,13 +280,13 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
forceFitColumns: false, forceFitColumns: false,
defaultColumnWidth: 120, defaultColumnWidth: 120,
showRowNumber: true showRowNumber: true
}); }));
table.setSelectionModel(selectionModel); table.setSelectionModel(selectionModel);
table.setData(dataMap); table.setData(dataMap);
table.registerPlugin(copyHandler); table.registerPlugin(copyHandler);
table.setTableTitle(localize('topOperationsTableTitle', "Top Operations")); table.setTableTitle(localize('topOperationsTableTitle', "Top Operations"));
this._register(table.onClick(e => { this._register(table.onClick(e => {
if (e.cell.cell === 1) { if (e.cell.cell === 1) {
const row = table.getData().getItem(e.cell.row); const row = table.getData().getItem(e.cell.row);
@@ -283,17 +298,13 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
})); }));
this._tables.push(table); this._tables.push(table);
const contextMenuAction = [
this._instantiationService.createInstance(CopyTableData),
this._instantiationService.createInstance(CopyTableDataWithHeader),
this._instantiationService.createInstance(SelectAll)
];
this._register(topOperationsSearchInput.onDidChange(e => { this._register(topOperationsSearchInput.onDidChange(e => {
const filter = e.toLowerCase(); const filter = e.toLowerCase();
if (filter) { if (filter) {
const filteredData = dataMap.filter(row => { const filteredData = dataMap.filter(row => {
let includeRow = false; let includeRow = false;
for (let i = 0; i < columns.length; i++) { for (let i = 0; i < columns.length; i++) {
const columnField = columns[i].field; const columnField = columns[i].field;
if (row[columnField]) { if (row[columnField]) {
@@ -303,18 +314,21 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
} }
} }
} }
return includeRow; return includeRow;
}); });
table.setData(filteredData); table.setData(filteredData);
} else { } else {
table.setData(dataMap); table.setData(dataMap);
} }
table.rerenderGrid(); table.rerenderGrid();
})); }));
this._register(table.onKeyDown((evt: ITableKeyboardEvent) => { this._register(table.onKeyDown((evt: ITableKeyboardEvent) => {
if (evt.event.ctrlKey && (evt.event.key === 'a' || evt.event.key === 'A')) { 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(); table.focus();
evt.event.preventDefault(); evt.event.preventDefault();
evt.event.stopPropagation(); evt.event.stopPropagation();
@@ -322,15 +336,17 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
})); }));
this._register(table.onContextMenu(e => { this._register(table.onContextMenu(e => {
const selectedDataRange = selectionModel.getSelectedRanges()[0];
let csvString = ''; let csvString = '';
let csvStringWithHeader = ''; let csvStringWithHeader = '';
const selectedDataRange = selectionModel.getSelectedRanges()[0];
if (selectedDataRange) { if (selectedDataRange) {
const data = []; const data = [];
for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { 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 = []; const row = [];
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
const dataItem = dataRow[table.columns[colIndex].field]; const dataItem = dataRow[table.columns[colIndex].field];
if (dataItem) { if (dataItem) {
@@ -339,14 +355,15 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
row.push(''); row.push('');
} }
} }
data.push(row); data.push(row);
} }
csvString = data.map(row => csvString = data.map(row =>
row.map(x => `${x}`).join('\t') row.map(x => `${x}`).join('\t')
).join('\n'); ).join('\n');
const columns = []; const columns = [];
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
columns.push(table.columns[colIndex].name); columns.push(table.columns[colIndex].name);
} }
@@ -354,6 +371,12 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
csvStringWithHeader = columns.join('\t') + '\n' + csvString; 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({ this._contextMenuService.showContextMenu({
getAnchor: () => e.anchor, getAnchor: () => e.anchor,
getActions: () => contextMenuAction, getActions: () => contextMenuAction,
@@ -364,13 +387,14 @@ export class TopOperationsTabView extends Disposable implements IPanelView {
selectionTextWithHeader: csvStringWithHeader selectionTextWithHeader: csvStringWithHeader
}) })
}); });
})); }));
attachTableStyler(table, this._themeService);
this._register(attachTableStyler(table, this._themeService));
new ResizeObserver((e) => { new ResizeObserver((e) => {
table.layout(new DOM.Dimension(tableContainer.clientWidth, tableContainer.clientHeight)); table.layout(new DOM.Dimension(tableContainer.clientWidth, tableContainer.clientHeight));
}).observe(tableContainer); }).observe(tableContainer);
return table; return table;
} }

View File

@@ -34,11 +34,12 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
// Custom zoom input box // Custom zoom input box
const zoomValueLabel = localize("qpZoomValueLabel", 'Zoom (percent)'); 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', type: 'number',
ariaLabel: zoomValueLabel, ariaLabel: zoomValueLabel,
flexibleWidth: false flexibleWidth: false
}); }));
this._register(attachInputBoxStyler(this.customZoomInputBox, this.themeService)); this._register(attachInputBoxStyler(this.customZoomInputBox, this.themeService));
const currentZoom = this.executionPlanDiagram.getZoomLevel(); const currentZoom = this.executionPlanDiagram.getZoomLevel();
@@ -48,28 +49,28 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
// Setting up keyboard shortcuts // Setting up keyboard shortcuts
const self = this; 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') { if (ev.key === 'Enter') {
await new CustomZoomAction().run(self); await this._register(new CustomZoomAction()).run(self);
} else if (ev.key === 'Escape') { } else if (ev.key === 'Escape') {
this.widgetController.removeWidget(self); this.widgetController.removeWidget(self);
} }
}; }));
const applyButton = new Button(this.container, { const applyButton = this._register(new Button(this.container, {
title: localize('customZoomApplyButtonTitle', "Apply Zoom") title: localize('customZoomApplyButtonTitle', "Apply Zoom")
}); }));
applyButton.setWidth('60px'); applyButton.setWidth('60px');
applyButton.label = localize('customZoomApplyButton', "Apply"); applyButton.label = localize('customZoomApplyButton', "Apply");
this._register(applyButton.onDidClick(async e => { this._register(applyButton.onDidClick(async e => {
await new CustomZoomAction().run(self); await this._register(new CustomZoomAction()).run(self);
})); }));
// Adding action bar // Adding action bar
this._actionBar = new ActionBar(this.container); this._actionBar = this._register(new ActionBar(this.container));
this._actionBar.context = this; 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 // Setting initial focus to input box
@@ -93,7 +94,7 @@ export class CustomZoomAction extends Action {
context.widgetController.removeWidget(context); context.widgetController.removeWidget(context);
} else { } else {
context.notificationService.error( 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.
); );
} }
} }

View File

@@ -84,7 +84,7 @@ export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase {
this.container.appendChild(this._expenseMetricSelectBoxContainer); this.container.appendChild(this._expenseMetricSelectBoxContainer);
const selectBoxOptions = this.getSelectBoxOptionsFromExecutionPlanDiagram(); 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.setAriaLabel(SELECT_EXPENSE_METRIC_TITLE);
this.expenseMetricSelectBox.render(this._expenseMetricSelectBoxContainer); this.expenseMetricSelectBox.render(this._expenseMetricSelectBoxContainer);
@@ -119,19 +119,15 @@ export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase {
})); }));
// Apply Button // Apply Button
const highlightExpensiveOperationAction = new HighlightExpensiveOperationAction(); const highlightExpensiveOperationAction = this._register(new HighlightExpensiveOperationAction());
this._register(highlightExpensiveOperationAction); const clearHighlightExpensiveOperationAction = this._register(new TurnOffExpensiveHighlightingOperationAction());
const cancelHighlightExpensiveOperationAction = this._register(new CancelHIghlightExpensiveOperationAction());
const clearHighlightExpensiveOperationAction = new TurnOffExpensiveHighlightingOperationAction();
this._register(clearHighlightExpensiveOperationAction);
const cancelHighlightExpensiveOperationAction = new CancelHIghlightExpensiveOperationAction();
this._register(cancelHighlightExpensiveOperationAction);
const self = this; const self = this;
const applyButton = new Button(this.container, { const applyButton = this._register(new Button(this.container, {
title: localize('highlightExpensiveOperationButtonTitle', 'Highlight Expensive Operation') title: localize('highlightExpensiveOperationButtonTitle', 'Highlight Expensive Operation')
}); }));
applyButton.label = localize('highlightExpensiveOperationApplyButton', 'Apply'); applyButton.label = localize('highlightExpensiveOperationApplyButton', 'Apply');
this._register(applyButton.onDidClick(async e => { this._register(applyButton.onDidClick(async e => {
@@ -146,7 +142,7 @@ export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase {
})); }));
// Adds Action bar // Adds Action bar
this._actionBar = new ActionBar(this.container); this._actionBar = this._register(new ActionBar(this.container));
this._actionBar.context = this; this._actionBar.context = this;
this._actionBar.pushAction(cancelHighlightExpensiveOperationAction, { label: false, icon: true }); this._actionBar.pushAction(cancelHighlightExpensiveOperationAction, { label: false, icon: true });
} }

View File

@@ -57,20 +57,24 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
// property name dropdown // property name dropdown
this._propertyNameSelectBoxContainer = DOM.$('.search-widget-property-name-select-box .dropdown-container'); this._propertyNameSelectBoxContainer = DOM.$('.search-widget-property-name-select-box .dropdown-container');
this.container.appendChild(this._propertyNameSelectBoxContainer); this.container.appendChild(this._propertyNameSelectBoxContainer);
this._propertyNameSelectBoxContainer.style.width = '150px';
const propDropdownOptions = this._executionPlanDiagram.getUniqueElementProperties(); 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._propertyNameSelectBox.setAriaLabel(SELECT_PROPERTY_TITLE);
this._register(attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService)); this._register(attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService));
this._propertyNameSelectBoxContainer.style.width = '150px';
this._propertyNameSelectBox.render(this._propertyNameSelectBoxContainer); this._propertyNameSelectBox.render(this._propertyNameSelectBoxContainer);
this._register(this._propertyNameSelectBox.onDidSelect(e => { this._register(this._propertyNameSelectBox.onDidSelect(e => {
this._usePreviousSearchResult = false; this._usePreviousSearchResult = false;
})); }));
// search type dropdown // search type dropdown
this._searchTypeSelectBoxContainer = DOM.$('.search-widget-search-type-select-box .dropdown-container'); this._searchTypeSelectBoxContainer = DOM.$('.search-widget-search-type-select-box .dropdown-container');
this._searchTypeSelectBoxContainer.style.width = '100px';
this.container.appendChild(this._searchTypeSelectBoxContainer); this.container.appendChild(this._searchTypeSelectBoxContainer);
this._searchTypeSelectBox = new SelectBox([
this._searchTypeSelectBox = this._register(new SelectBox([
EQUALS_DISPLAY_STRING, EQUALS_DISPLAY_STRING,
CONTAINS_DISPLAY_STRING, CONTAINS_DISPLAY_STRING,
GREATER_DISPLAY_STRING, GREATER_DISPLAY_STRING,
@@ -78,11 +82,11 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
GREATER_EQUAL_DISPLAY_STRING, GREATER_EQUAL_DISPLAY_STRING,
LESSER_EQUAL_DISPLAY_STRING, LESSER_EQUAL_DISPLAY_STRING,
LESSER_AND_GREATER_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.setAriaLabel(SELECT_SEARCH_TYPE_TITLE);
this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer); this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer);
this._register(attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService)); this._register(attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService));
this._searchTypeSelectBoxContainer.style.width = '100px';
this._register(this._searchTypeSelectBox.onDidSelect(e => { this._register(this._searchTypeSelectBox.onDidSelect(e => {
this._usePreviousSearchResult = false; this._usePreviousSearchResult = false;
switch (e.selected) { switch (e.selected) {
@@ -110,27 +114,21 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
})); }));
// search text input box // 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._searchTextInputBox.setAriaLabel(ENTER_SEARCH_VALUE_TITLE);
this._register(attachInputBoxStyler(this._searchTextInputBox, this.themeService));
this._searchTextInputBox.element.style.marginLeft = '5px'; this._searchTextInputBox.element.style.marginLeft = '5px';
this._register(attachInputBoxStyler(this._searchTextInputBox, this.themeService));
this._register(this._searchTextInputBox.onDidChange(e => { this._register(this._searchTextInputBox.onDidChange(e => {
this._usePreviousSearchResult = false; this._usePreviousSearchResult = false;
})); }));
// setting up key board shortcuts // setting up key board shortcuts
const goToPreviousMatchAction = new GoToPreviousMatchAction(); const goToPreviousMatchAction = this._register(new GoToPreviousMatchAction());
this._register(goToPreviousMatchAction); const goToNextMatchAction = this._register(new GoToNextMatchAction());
const cancelSearchAction = this._register(new CancelSearch());
const goToNextMatchAction = new GoToNextMatchAction();
this._register(goToNextMatchAction);
const cancelSearchAction = new CancelSearch();
this._register(cancelSearchAction);
const self = this; 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) { if (e.key === 'Enter' && e.shiftKey) {
await goToPreviousMatchAction.run(self); await goToPreviousMatchAction.run(self);
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
@@ -138,10 +136,10 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
await cancelSearchAction.run(self); await cancelSearchAction.run(self);
} }
}; }));
// Adding action bar // Adding action bar
this._actionBar = new ActionBar(this.container); this._actionBar = this._register(new ActionBar(this.container));
this._actionBar.context = this; this._actionBar.context = this;
this._actionBar.pushAction(goToPreviousMatchAction, { label: false, icon: true }); this._actionBar.pushAction(goToPreviousMatchAction, { label: false, icon: true });
this._actionBar.pushAction(goToNextMatchAction, { label: false, icon: true }); this._actionBar.pushAction(goToNextMatchAction, { label: false, icon: true });
@@ -155,6 +153,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
public searchNodes(): void { public searchNodes(): void {
this._currentSearchResultIndex = 0; this._currentSearchResultIndex = 0;
this._searchResults = this._executionPlanDiagram.searchNodes({ this._searchResults = this._executionPlanDiagram.searchNodes({
propertyName: this._propertyNameSelectBox.value, propertyName: this._propertyNameSelectBox.value,
value: this._searchTextInputBox.value, value: this._searchTextInputBox.value,
@@ -171,6 +170,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
this._executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]); this._executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]);
this._executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]); this._executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]);
this._currentSearchResultIndex = this._currentSearchResultIndex === this._searchResults.length - 1 ? this._currentSearchResultIndex = this._currentSearchResultIndex === this._searchResults.length - 1 ?
this._currentSearchResultIndex = 0 : this._currentSearchResultIndex = 0 :
this._currentSearchResultIndex = ++this._currentSearchResultIndex; this._currentSearchResultIndex = ++this._currentSearchResultIndex;
@@ -183,6 +183,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
this._executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]); this._executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]);
this._executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]); this._executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]);
this._currentSearchResultIndex = this._currentSearchResultIndex === 0 ? this._currentSearchResultIndex = this._currentSearchResultIndex === 0 ?
this._currentSearchResultIndex = this._searchResults.length - 1 : this._currentSearchResultIndex = this._searchResults.length - 1 :
this._currentSearchResultIndex = --this._currentSearchResultIndex; this._currentSearchResultIndex = --this._currentSearchResultIndex;

View File

@@ -726,7 +726,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
graphFileType: result.queryExecutionPlanFileExtension 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); await this.editorService.openEditor(executionPlanInput);
} }
else { else {
@@ -800,7 +800,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
this.currentHeight = size; this.currentHeight = size;
} }
// Table is always called with Orientation as VERTICAL // Table is always called with Orientation as VERTICAL
this.table.layout(size, Orientation.VERTICAL); this.table?.layout(size, Orientation.VERTICAL);
} }
public get minimumSize(): number { public get minimumSize(): number {