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

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

View File

@@ -14,13 +14,14 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { 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;
}

View File

@@ -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}`;

View File

@@ -66,7 +66,8 @@ export class ExecutionPlanComparisonEditor extends EditorPane {
// creating a new comparison view if the new input does not already have a cached one.
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);

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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 {

View File

@@ -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();
}

View File

@@ -14,8 +14,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { contrastBorder, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { 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 {

View File

@@ -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'] = {

View File

@@ -75,15 +75,19 @@ export abstract class ExecutionPlanPropertiesViewBase extends Disposable impleme
this.resizeSash = this._register(new Sash(sashContainer, this, { orientation: Orientation.VERTICAL, size: 3 }));
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;
}
}

View File

@@ -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);
}

View File

@@ -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, '');
}

View File

@@ -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)));
}
}

View File

@@ -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);
});
}));
});
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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.
);
}
}

View File

@@ -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 });
}

View File

@@ -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;

View File

@@ -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 {