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