Adding caching to execution plan and refactoring code and some other fixes (#18913)

* Making ep code modular for easy swithcing in and out

* Changing to innerText

* Fixing renames

* Fixing var name in one file
This commit is contained in:
Aasim Khan
2022-04-12 12:52:24 -07:00
committed by GitHub
parent 675969eebc
commit 387f4cd116
21 changed files with 1902 additions and 1122 deletions

View File

@@ -5,9 +5,20 @@
import type * as azdata from 'azdata';
/**
* This class holds the view and the graphs of the execution plans
* displayed in the results tab of a query editor
*/
export class ExecutionPlanState {
graphs: azdata.executionPlan.ExecutionPlanGraph[] = [];
clearExecutionPlanState() {
this.graphs = [];
private _graphs: azdata.executionPlan.ExecutionPlanGraph[] = [];
public executionPlanFileViewUUID: string;
public get graphs(): azdata.executionPlan.ExecutionPlanGraph[] {
return this._graphs;
}
public set graphs(v: azdata.executionPlan.ExecutionPlanGraph[]) {
this._graphs = v;
}
}

View File

@@ -29,7 +29,6 @@ export class ResultsViewState {
this.gridPanelState.dispose();
this.chartState.dispose();
this.queryPlanState.dispose();
this.executionPlanState.clearExecutionPlanState();
this.dynamicModelViewTabsState.forEach((state: QueryModelViewState, identifier: string) => {
state.dispose();
});

View File

@@ -0,0 +1,522 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataGraphModule from 'azdataGraph';
import type * as azdata from 'azdata';
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { isString } from 'vs/base/common/types';
import { badgeIconPaths, executionPlanNodeIconPaths } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry';
const azdataGraph = azdataGraphModule();
/**
* This view holds the azdataGraph diagram and provides different
* methods to manipulate the azdataGraph
*/
export class AzdataGraphView {
private _diagram: any;
private _diagramModel: AzDataGraphCell;
private _uniqueElementId: number = -1;
private _graphElementPropertiesSet: Set<string> = new Set();
private _onElementSelectedEmitter: Emitter<InternalExecutionPlanElement> = new Emitter<InternalExecutionPlanElement>();
public onElementSelected: Event<InternalExecutionPlanElement>;
constructor(
private _parentContainer: HTMLElement,
private _executionPlan: azdata.executionPlan.ExecutionPlanGraph,
@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,
) {
this._diagramModel = this.populate(this._executionPlan.root);
this._diagram = new azdataGraph.azdataQueryPlan(this._parentContainer, this._diagramModel, executionPlanNodeIconPaths, badgeIconPaths);
this.setGraphProperties();
this.initializeGraphEvents();
}
private setGraphProperties(): void {
this._diagram.graph.setCellsMovable(false); // preventing drag and drop of graph nodes.
this._diagram.graph.setCellsDisconnectable(false); // preventing graph edges to be disconnected from source and target nodes.
this._diagram.graph.tooltipHandler.delay = 700; // increasing delay for tooltips
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const iconBackground = theme.getColor(editorBackground);
if (iconBackground) {
this._diagram.setIconBackgroundColor(iconBackground);
}
const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) {
this._diagram.setTextFontColor(iconLabelColor);
this._diagram.setEdgeColor(iconLabelColor);
}
});
}
private initializeGraphEvents(): void {
this.onElementSelected = this._onElementSelectedEmitter.event;
this._diagram.graph.addListener('click', (sender, evt) => {
// Updating properties view table on node clicks
const cell = evt.properties['cell'];
let selectedGraphElement: InternalExecutionPlanElement;
if (cell) {
selectedGraphElement = this.getElementById(cell.id);
this.selectElement(cell.id);
} else if (!this.getSelectedElement()) {
selectedGraphElement = this._executionPlan.root;
this.selectElement(undefined);
}
this._onElementSelectedEmitter.fire(selectedGraphElement ?? this.getSelectedElement());
evt.consume();
});
}
/**
* Selects an execution plan node/edge in the graph diagram.
* @param element Element to be selected
* @param bringToCenter Check if the selected element has to be brought into the center of this view
*/
public selectElement(element: InternalExecutionPlanElement | undefined, bringToCenter: boolean = false): void {
let cell;
if (element) {
cell = this._diagram.graph.model.getCell(element.id);
} else {
cell = this._diagram.graph.model.getCell((<InternalExecutionPlanNode>this._executionPlan.root).id);
}
this._diagram.graph.getSelectionModel().setCell(cell);
if (bringToCenter) {
this.centerElement(element);
}
}
/**
* returns the currently selected graph element.
*/
public getSelectedElement(): InternalExecutionPlanElement | undefined {
const cell = this._diagram.graph.getSelectionCell();
if (cell?.id) {
return this.getElementById(cell.id);
}
return undefined;
}
/**
* Zooms in to the diagram.
*/
public zoomIn(): void {
this._diagram.graph.zoomIn();
}
/**
* Zooms out of the diagram
*/
public zoomOut(): void {
this._diagram.graph.zoomOut();
}
/**
* Fits the diagram into the parent container size.
*/
public zoomToFit(): void {
this._diagram.graph.fit();
this._diagram.graph.view.rendering = true;
this._diagram.graph.view.refresh();
}
/**
* Gets the current zoom level of the diagram.
*/
public getZoomLevel(): number {
return this._diagram.graph.view.getScale() * 100;
}
/**
* Sets the zoom level of the diagram
* @param level The scale factor to be be applied to the diagram.
*/
public setZoomLevel(level: number): void {
if (level < 1) {
throw new Error(localize('invalidExecutionPlanZoomError', "Zoom level cannot be 0 or negative"));
}
this._diagram.graph.view.setScale(level / 100);
}
/**
* Get the diagram element by its id
* @param id id of the diagram element
*/
public getElementById(id: string): InternalExecutionPlanElement | undefined {
const nodeStack: InternalExecutionPlanNode[] = [];
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) {
return currentNode.edges[i];
}
}
}
nodeStack.push(...currentNode.children);
}
return undefined;
}
/**
* Searches the diagram nodes based on the search query provided.
*/
public searchNodes(searchQuery: SearchQuery): InternalExecutionPlanNode[] {
const resultNodes: InternalExecutionPlanNode[] = [];
const nodeStack: InternalExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) {
const currentNode = nodeStack.pop();
const matchingProp = currentNode.properties.find(e => e.name === searchQuery.propertyName);
let matchFound = false;
// Searching only properties with string value.
if (isString(matchingProp?.value)) {
// If the search type is '=' we look for exact match and for 'contains' we look search string occurrences in prop value
switch (searchQuery.searchType) {
case SearchType.Equals:
matchFound = matchingProp.value === searchQuery.value;
break;
case SearchType.Contains:
matchFound = matchingProp.value.includes(searchQuery.value);
break;
case SearchType.GreaterThan:
matchFound = matchingProp.value > searchQuery.value;
break;
case SearchType.LesserThan:
matchFound = matchingProp.value < searchQuery.value;
break;
case SearchType.GreaterThanEqualTo:
matchFound = matchingProp.value >= searchQuery.value;
break;
case SearchType.LesserThanEqualTo:
matchFound = matchingProp.value <= searchQuery.value;
break;
case SearchType.LesserAndGreaterThan:
matchFound = matchingProp.value < searchQuery.value || matchingProp.value > searchQuery.value;
break;
}
if (matchFound) {
resultNodes.push(currentNode);
}
}
nodeStack.push(...currentNode.children);
}
return resultNodes;
}
/**
* Brings a graph element to the center of the parent view.
* @param node Node to be brought into the center
*/
public centerElement(node: InternalExecutionPlanElement): void {
/**
* The selected graph node might be hidden/partially visible if the graph is overflowing the parent container.
* Apart from the obvious problems in aesthetics, user do not get a proper feedback of the search result.
* To solve this problem, we will have to scroll the node into view. (preferably into the center of the view)
* Steps for that:
* 1. Get the bounding rect of the node on graph.
* 2. Get the midpoint of the node's bounding rect.
* 3. Find the dimensions of the parent container.
* 4. Since, we are trying to position the node into center, we set the left top corner position of parent to
* below x and y.
* x = node's x midpoint - half the width of parent container
* y = node's y midpoint - half the height of parent container
* 5. If the x and y are negative, we set them 0 as that is the minimum possible scroll position.
* 6. Smoothly scroll to the left top x and y calculated in step 4, 5.
*/
if (!node) {
return;
}
const cell = this._diagram.graph.model.getCell(node.id);
if (!cell) {
return;
}
this._diagram.graph.setSelectionCell(cell);
const cellRect = this._diagram.graph.getCellBounds(cell);
const cellMidPoint: Point = {
x: cellRect.x + cellRect.width / 2,
y: cellRect.y + cellRect.height / 2
};
const graphContainer = <HTMLElement>this._diagram.graph.container;
const diagramContainerRect = graphContainer.getBoundingClientRect();
const leftTopScrollPoint: Point = {
x: cellMidPoint.x - diagramContainerRect.width / 2,
y: cellMidPoint.y - diagramContainerRect.height / 2
};
leftTopScrollPoint.x = leftTopScrollPoint.x < 0 ? 0 : leftTopScrollPoint.x;
leftTopScrollPoint.y = leftTopScrollPoint.y < 0 ? 0 : leftTopScrollPoint.y;
graphContainer.scrollTo({
left: leftTopScrollPoint.x,
top: leftTopScrollPoint.y,
behavior: 'smooth'
});
}
private populate(node: InternalExecutionPlanNode): AzDataGraphCell {
let diagramNode: AzDataGraphCell = <AzDataGraphCell>{};
diagramNode.label = node.subtext.join(this.textResourcePropertiesService.getEOL(undefined));
diagramNode.tooltipTitle = node.name;
const nodeId = this.createGraphElementId();
diagramNode.id = nodeId;
node.id = nodeId;
if (node.type) {
diagramNode.icon = node.type;
}
if (node.properties) {
diagramNode.metrics = this.populateProperties(node.properties);
}
if (node.badges) {
diagramNode.badges = [];
for (let i = 0; i < node.badges.length; i++) {
diagramNode.badges.push(this.getBadgeTypeString(node.badges[i].type));
}
}
if (node.edges) {
diagramNode.edges = this.populateEdges(node.edges);
}
if (node.children) {
diagramNode.children = [];
for (let i = 0; i < node.children.length; ++i) {
diagramNode.children.push(this.populate(node.children[i]));
}
}
if (node.description) {
diagramNode.description = node.description;
}
return diagramNode;
}
private getBadgeTypeString(badgeType: sqlExtHostType.executionPlan.BadgeType): {
type: string,
tooltip: string
} | undefined {
/**
* TODO: Need to figure out if tooltip have to be removed. For now, they are empty
*/
switch (badgeType) {
case sqlExtHostType.executionPlan.BadgeType.Warning:
return {
type: 'warning',
tooltip: ''
};
case sqlExtHostType.executionPlan.BadgeType.CriticalWarning:
return {
type: 'criticalWarning',
tooltip: ''
};
case sqlExtHostType.executionPlan.BadgeType.Parallelism:
return {
type: 'parallelism',
tooltip: ''
};
default:
return undefined;
}
}
private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): AzDataGraphCellMetric[] {
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 => {
return {
name: e.name,
value: e.displayValue,
isLongString: e.positionAtBottom
};
});
}
private populateEdges(edges: InternalExecutionPlanEdge[]): AzDataGraphCellEdge[] {
return edges.map(e => {
e.id = this.createGraphElementId();
return {
id: e.id,
metrics: this.populateProperties(e.properties),
weight: Math.max(0.5, Math.min(0.5 + 0.75 * Math.log10(e.rowCount), 6)),
label: ''
};
});
}
private createGraphElementId(): string {
this._uniqueElementId += 1;
return `element-${this._uniqueElementId}`;
}
/**
* Gets a list of unique properties of the graph elements.
*/
public getUniqueElementProperties(): string[] {
return [...this._graphElementPropertiesSet].sort();
}
/**
* Enables/Disables the graph tooltips
* @returns state of the tooltip after toggling
*/
public toggleTooltip(): boolean {
if (this._diagram.graph.tooltipHandler.enabled) {
this._diagram.graph.tooltipHandler.setEnabled(false);
} else {
this._diagram.graph.tooltipHandler.setEnabled(true);
}
return this._diagram.graph.tooltipHandler.enabled;
}
}
export interface InternalExecutionPlanNode extends azdata.executionPlan.ExecutionPlanNode {
/**
* Unique internal id given to graph node by ADS.
*/
id?: string;
}
export interface InternalExecutionPlanEdge extends azdata.executionPlan.ExecutionPlanEdge {
/**
* Unique internal id given to graph edge by ADS.
*/
id?: string;
}
export type InternalExecutionPlanElement = InternalExecutionPlanEdge | InternalExecutionPlanNode;
export interface AzDataGraphCell {
/**
* Label for the azdata cell
*/
label: string;
/**
* unique identifier for the cell
*/
id: string;
/**
* icon for the cell
*/
icon: string;
/**
* title for the cell hover tooltip
*/
tooltipTitle: string;
/**
* metrics to be shown in the tooltip
*/
metrics: AzDataGraphCellMetric[];
/**
* cell edges
*/
edges: AzDataGraphCellEdge[];
/**
* child cells
*/
children: AzDataGraphCell[];
/**
* Description to be displayed in the cell tooltip
*/
description: string;
badges: AzDataGraphNodeBadge[];
}
export interface AzDataGraphNodeBadge {
type: string;
tooltip: string;
}
export interface AzDataGraphCellMetric {
/**
* name of the metric
*/
name: string;
/**
* display value of the metric
*/
value: string;
/**
* flag that indicates if the display property is a long string
* long strings will be displayed at the bottom
*/
isLongString: boolean;
}
export interface AzDataGraphCellEdge {
/**
* Label for the edge
*/
label: string;
/**
* Unique identifier for the edge
*/
id: string;
/**
* weight of the edge. This value determines the edge thickness
*/
weight: number;
/**
* metrics to be shown in the edge tooltip
*/
metrics: AzDataGraphCellMetric[];
}
interface Point {
x: number;
y: number;
}
export enum SearchType {
Equals,
Contains,
LesserThan,
GreaterThan,
GreaterThanEqualTo,
LesserThanEqualTo,
LesserAndGreaterThan
}
export interface SearchQuery {
/**
* property name to be searched
*/
propertyName: string,
/**
* expected value of the property
*/
value: string,
/**
* Type of search to be performed
*/
searchType: SearchType
}

View File

@@ -257,17 +257,19 @@ export const badgeIconPaths = {
criticalWarning: imageBasePath + 'badge_critical_warning.svg'
};
const parentContainer = 'qps-container';
export const savePlanIconClassNames = [parentContainer, 'save-plan-icon'].join(' ');
export const openPropertiesIconClassNames = [parentContainer, 'open-properties-icon'].join(' ');
export const openQueryIconClassNames = [parentContainer, 'open-query-icon'].join(' ');
export const openPlanFileIconClassNames = [parentContainer, 'open-plan-file-icon'].join(' ');
export const saveIconClassNames = [parentContainer, 'save-icon'].join(' ');
export const searchIconClassNames = [parentContainer, 'search-icon'].join(' ');
export const sortAlphabeticallyIconClassNames = [parentContainer, 'sort-alphabetically-icon'].join(' ');
export const sortByDisplayOrderIconClassNames = [parentContainer, 'sort-display-order-icon'].join(' ');
export const zoomInIconClassNames = [parentContainer, 'zoom-in-icon'].join(' ');
export const zoomOutIconClassNames = [parentContainer, 'zoom-out-icon'].join(' ');
export const customZoomIconClassNames = [parentContainer, 'custom-zoom-icon'].join(' ');
export const zoomToFitIconClassNames = [parentContainer, 'zoom-to-fit-icon'].join(' ');
export const zoomIconClassNames = [parentContainer, 'zoom-icon'].join(' ');
export const savePlanIconClassNames = 'ep-save-plan-icon';
export const openPropertiesIconClassNames = 'ep-open-properties-icon';
export const openQueryIconClassNames = 'ep-open-query-icon';
export const openPlanFileIconClassNames = 'ep-open-plan-file-icon';
export const saveIconClassNames = 'ep-save-icon';
export const searchIconClassNames = 'ep-search-icon';
export const sortAlphabeticallyIconClassNames = 'ep-sort-alphabetically-icon';
export const sortReverseAlphabeticallyIconClassNames = 'ep-sort-reverse-alphabetically-icon';
export const sortByDisplayOrderIconClassNames = 'ep-sort-display-order-icon';
export const zoomInIconClassNames = 'ep-zoom-in-icon';
export const zoomOutIconClassNames = 'ep-zoom-out-icon';
export const customZoomIconClassNames = 'ep-custom-zoom-icon';
export const zoomToFitIconClassNames = 'ep-zoom-to-fit-icon';
export const zoomIconClassNames = 'ep-zoom-icon';
export const enableTooltipIconClassName = 'ep-enable-tooltip-icon';
export const disableTooltipIconClassName = 'ep-disable-tooltip-icon';

View File

@@ -1,743 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/executionPlan';
import * as azdata from 'azdata';
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel';
import { localize } from 'vs/nls';
import { dispose } from 'vs/base/common/lifecycle';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import * as azdataGraphModule from 'azdataGraph';
import { customZoomIconClassNames, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, executionPlanNodeIconPaths, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames, badgeIconPaths } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { isString } from 'vs/base/common/types';
import { PlanHeader } from 'sql/workbench/contrib/executionPlan/browser/planHeader';
import { ExecutionPlanPropertiesView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView';
import { Action } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { contrastBorder, editorBackground, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { ISashEvent, ISashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IFileService } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
import { InfoBox } from 'sql/workbench/browser/ui/infoBox/infoBox';
let azdataGraph = azdataGraphModule();
export interface InternalExecutionPlanNode extends azdata.executionPlan.ExecutionPlanNode {
/**
* Unique internal id given to graph node by ADS.
*/
id?: string;
}
export interface InternalExecutionPlanEdge extends azdata.executionPlan.ExecutionPlanEdge {
/**
* Unique internal id given to graph edge by ADS.
*/
id?: string;
}
export class ExecutionPlanTab implements IPanelTab {
public readonly title = localize('executionPlanTitle', "Query Plan (Preview)");
public readonly identifier = 'ExecutionPlan2Tab';
public readonly view: ExecutionPlanView;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
) {
this.view = instantiationService.createInstance(ExecutionPlanView);
}
public dispose() {
dispose(this.view);
}
public clear() {
this.view.clear();
}
}
export class ExecutionPlanView implements IPanelView {
private _loadingSpinner: LoadingSpinner;
private _loadingErrorInfoBox: InfoBox;
private _eps?: ExecutionPlan[] = [];
private _graphs?: azdata.executionPlan.ExecutionPlanGraph[] = [];
private _container = DOM.$('.eps-container');
private _planCache: Map<string, azdata.executionPlan.ExecutionPlanGraph[]> = new Map();
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@IExecutionPlanService private executionPlanService: IExecutionPlanService
) {
}
public render(parent: HTMLElement): void {
parent.appendChild(this._container);
}
dispose() {
this._container.remove();
delete this._eps;
delete this._graphs;
}
public layout(dimension: DOM.Dimension): void {
}
public clear() {
this._eps = [];
this._graphs = [];
DOM.clearNode(this._container);
}
/**
* Adds executionPlanGraph to the graph controller.
* @param newGraphs ExecutionPlanGraphs to be added.
*/
public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) {
if (newGraphs) {
newGraphs.forEach(g => {
const ep = this.instantiationService.createInstance(ExecutionPlan, this._container, this._eps.length + 1);
ep.graphModel = g;
this._eps.push(ep);
this._graphs.push(g);
this.updateRelativeCosts();
});
}
}
/**
* Loads the graph file by converting the file to generic executionPlan graphs.
* This feature requires the right providers to be registered that can handle
* the graphFileType in the graphFile
* Please note: this method clears the existing graph in the graph control
* @param graphFile graph file to be loaded.
* @returns
*/
public async loadGraphFile(graphFile: azdata.executionPlan.ExecutionPlanGraphInfo) {
this.clear();
this._loadingSpinner = 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;
} else {
const graphs = (await this.executionPlanService.getExecutionPlan({
graphFileContent: graphFile.graphFileContent,
graphFileType: graphFile.graphFileType
})).graphs;
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, {
text: e.toString(),
style: 'error',
isClickable: false
});
this._loadingErrorInfoBox.isClickable = false;
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan");
} finally {
this._loadingSpinner.loading = false;
}
}
private updateRelativeCosts() {
const sum = this._graphs.reduce((prevCost: number, cg) => {
return prevCost += cg.root.subTreeCost + cg.root.cost;
}, 0);
if (sum > 0) {
this._eps.forEach(ep => {
ep.planHeader.relativeCost = ((ep.graphModel.root.subTreeCost + ep.graphModel.root.cost) / sum) * 100;
});
}
}
}
export class ExecutionPlan implements ISashLayoutProvider {
private _graphModel?: azdata.executionPlan.ExecutionPlanGraph;
private _container: HTMLElement;
private _actionBarContainer: HTMLElement;
private _actionBar: ActionBar;
public planHeader: PlanHeader;
private _planContainer: HTMLElement;
private _planHeaderContainer: HTMLElement;
public propertiesView: ExecutionPlanPropertiesView;
private _propContainer: HTMLElement;
private _planActionContainer: HTMLElement;
public planActionView: ExecutionPlanWidgetController;
public azdataGraphDiagram: any;
public graphElementPropertiesSet: Set<string> = new Set();
private uniqueElementId: number = -1;
constructor(
private _parent: HTMLElement,
private _graphIndex: number,
@IInstantiationService public readonly _instantiationService: IInstantiationService,
@IThemeService private readonly _themeService: IThemeService,
@IContextViewService public readonly contextViewService: IContextViewService,
@IUntitledTextEditorService private readonly _untitledEditorService: IUntitledTextEditorService,
@IEditorService private readonly editorService: IEditorService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IFileDialogService public fileDialogService: IFileDialogService,
@IFileService public fileService: IFileService,
@IWorkspaceContextService public workspaceContextService: IWorkspaceContextService,
@ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService,
) {
// parent container for query plan.
this._container = DOM.$('.execution-plan');
this._parent.appendChild(this._container);
const sashContainer = DOM.$('.execution-plan-sash');
this._parent.appendChild(sashContainer);
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL });
let originalHeight = this._container.offsetHeight;
let originalTableHeight = 0;
let change = 0;
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) => {
change = evt.startY - evt.currentY;
const newHeight = originalHeight - change;
if (newHeight < 200) {
return;
}
/**
* Since the parent container is flex, we will have
* to change the flex-basis property to change the height.
*/
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.propertiesView.tableHeight = originalTableHeight - change;
});
this._planContainer = DOM.$('.plan');
this._container.appendChild(this._planContainer);
// container that holds plan header info
this._planHeaderContainer = DOM.$('.header');
// Styling header text like the query editor
this._planHeaderContainer.style.fontFamily = EDITOR_FONT_DEFAULTS.fontFamily;
this._planHeaderContainer.style.fontSize = EDITOR_FONT_DEFAULTS.fontSize.toString();
this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight;
this._planContainer.appendChild(this._planHeaderContainer);
this.planHeader = this._instantiationService.createInstance(PlanHeader, this._planHeaderContainer, {
planIndex: this._graphIndex,
});
// container properties
this._propContainer = DOM.$('.properties');
this._container.appendChild(this._propContainer);
this.propertiesView = new ExecutionPlanPropertiesView(this._propContainer, this._themeService);
this._planActionContainer = DOM.$('.plan-action-container');
this._planContainer.appendChild(this._planActionContainer);
this.planActionView = new ExecutionPlanWidgetController(this._planActionContainer);
// container that holds actionbar icons
this._actionBarContainer = DOM.$('.action-bar-container');
this._container.appendChild(this._actionBarContainer);
this._actionBar = new ActionBar(this._actionBarContainer, {
orientation: ActionsOrientation.VERTICAL, context: this
});
const actions = [
new SavePlanFile(),
new OpenPlanFile(),
new OpenQueryAction(),
new SearchNodeAction(),
new ZoomInAction(),
new ZoomOutAction(),
new ZoomToFitAction(),
new CustomZoomAction(),
new PropertiesAction(),
];
this._actionBar.pushAction(actions, { icon: true, label: false });
// Setting up context menu
const self = this;
this._container.oncontextmenu = (e: MouseEvent) => {
if (actions) {
this._contextMenuService.showContextMenu({
getAnchor: () => {
return {
x: e.x,
y: e.y
};
},
getActions: () => actions,
getActionsContext: () => (self)
});
}
};
}
getHorizontalSashTop(sash: Sash): number {
return 0;
}
getHorizontalSashLeft?(sash: Sash): number {
return 0;
}
getHorizontalSashWidth?(sash: Sash): number {
return this._container.clientWidth;
}
private populate(node: InternalExecutionPlanNode, diagramNode: any): any {
diagramNode.label = node.subtext.join(this.textResourcePropertiesService.getEOL(undefined));
diagramNode.tooltipTitle = node.name;
const nodeId = this.createGraphElementId();
diagramNode.id = nodeId;
node.id = nodeId;
if (node.properties && node.properties.length > 0) {
diagramNode.metrics = this.populateProperties(node.properties);
}
if (node.type) {
diagramNode.icon = node.type;
}
if (node.edges) {
diagramNode.edges = [];
for (let i = 0; i < node.edges.length; i++) {
diagramNode.edges.push(this.populateEdges(node.edges[i], new Object()));
}
}
if (node.children) {
diagramNode.children = [];
for (let i = 0; i < node.children.length; ++i) {
diagramNode.children.push(this.populate(node.children[i], new Object()));
}
}
if (node.badges) {
diagramNode.badges = [];
for (let i = 0; i < node.badges.length; i++) {
diagramNode.badges.push(this.getBadgeTypeString(node.badges[i].type));
}
}
if (node.description) {
diagramNode.description = node.description;
}
return diagramNode;
}
private getBadgeTypeString(badgeType: sqlExtHostType.executionPlan.BadgeType): {
type: string,
tooltip: string
} | undefined {
/**
* TODO: Need to figure out if tooltip have to be removed. For now, they are empty
*/
switch (badgeType) {
case sqlExtHostType.executionPlan.BadgeType.Warning:
return {
type: 'warning',
tooltip: ''
};
case sqlExtHostType.executionPlan.BadgeType.CriticalWarning:
return {
type: 'criticalWarning',
tooltip: ''
};
case sqlExtHostType.executionPlan.BadgeType.Parallelism:
return {
type: 'parallelism',
tooltip: ''
};
default:
return undefined;
}
}
private populateEdges(edge: InternalExecutionPlanEdge, diagramEdge: any) {
diagramEdge.label = '';
const edgeId = this.createGraphElementId();
diagramEdge.id = edgeId;
edge.id = edgeId;
diagramEdge.metrics = this.populateProperties(edge.properties);
diagramEdge.weight = Math.max(0.5, Math.min(0.5 + 0.75 * Math.log10(edge.rowCount), 6));
return diagramEdge;
}
private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]) {
return props.filter(e => isString(e.displayValue) && e.showInTooltip)
.sort((a, b) => a.displayOrder - b.displayOrder)
.map(e => {
this.graphElementPropertiesSet.add(e.name);
return {
name: e.name,
value: e.displayValue,
isLongString: e.positionAtBottom
};
});
}
private createGraphElementId(): string {
this.uniqueElementId += 1;
return `element-${this.uniqueElementId}`;
}
private createPlanDiagram(container: HTMLElement) {
let diagramRoot: any = new Object();
let graphRoot: azdata.executionPlan.ExecutionPlanNode = this._graphModel.root;
this.populate(graphRoot, diagramRoot);
this.azdataGraphDiagram = new azdataGraph.azdataQueryPlan(container, diagramRoot, executionPlanNodeIconPaths, badgeIconPaths);
this.azdataGraphDiagram.graph.setCellsMovable(false); // preventing drag and drop of graph nodes.
this.azdataGraphDiagram.graph.setCellsDisconnectable(false); // preventing graph edges to be disconnected from source and target nodes.
this.azdataGraphDiagram.graph.addListener('click', (sender, evt) => {
// Updating properties view table on node clicks
const cell = evt.properties['cell'];
if (cell) {
this.propertiesView.graphElement = this.searchNodes(cell.id);
} else if (!this.azdataGraphDiagram.graph.getSelectionCell()) {
const root = this.azdataGraphDiagram.graph.model.getCell(diagramRoot.id);
this.azdataGraphDiagram.graph.getSelectionModel().setCell(root);
this.propertiesView.graphElement = this.searchNodes(diagramRoot.id);
evt.consume();
} else {
evt.consume();
}
});
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const iconBackground = theme.getColor(editorBackground);
if (iconBackground) {
this.azdataGraphDiagram.setIconBackgroundColor(iconBackground);
}
const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) {
this.azdataGraphDiagram.setTextFontColor(iconLabelColor);
this.azdataGraphDiagram.setEdgeColor(iconLabelColor);
}
});
}
public set graphModel(graph: azdata.executionPlan.ExecutionPlanGraph | undefined) {
this._graphModel = graph;
if (this._graphModel) {
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);
/**
* We do not want to scroll the diagram through mouse wheel.
* Instead, we pass this event to parent control. So, when user
* uses the scroll wheel, they scroll through graphs present in
* the graph control. To scroll the individual graphs, users should
* use the scroll bars.
*/
diagramContainer.addEventListener('wheel', e => {
this._parent.scrollTop += e.deltaY;
//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';
}
e.preventDefault();
e.stopPropagation();
});
this._planContainer.appendChild(diagramContainer);
this.propertiesView.graphElement = this._graphModel.root;
}
}
public get graphModel(): azdata.executionPlan.ExecutionPlanGraph | undefined {
return this._graphModel;
}
public openQuery() {
return this._instantiationService.invokeFunction(openNewQuery, undefined, this.graphModel.query, RunQueryOnConnectionMode.none).then();
}
public async openGraphFile() {
const input = this._untitledEditorService.create({ mode: this.graphModel.graphFile.graphFileType, initialValue: this.graphModel.graphFile.graphFileContent });
await input.resolve();
await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None);
input.setDirty(false);
this.editorService.openEditor(input);
}
public searchNodes(searchId: string): InternalExecutionPlanNode | InternalExecutionPlanEdge | undefined {
let stack: InternalExecutionPlanNode[] = [];
stack.push(this._graphModel.root);
while (stack.length !== 0) {
const currentNode = stack.pop();
if (currentNode.id === searchId) {
return currentNode;
}
stack.push(...currentNode.children);
const resultEdge = currentNode.edges.find(e => (<InternalExecutionPlanEdge>e).id === searchId);
if (resultEdge) {
return resultEdge;
}
}
return undefined;
}
}
class OpenQueryAction extends Action {
public static ID = 'ep.OpenQueryAction';
public static LABEL = localize('openQueryAction', "Open Query");
constructor() {
super(OpenQueryAction.ID, OpenQueryAction.LABEL, openQueryIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.openQuery();
}
}
class PropertiesAction extends Action {
public static ID = 'ep.propertiesAction';
public static LABEL = localize('executionPlanPropertiesActionLabel', "Properties");
constructor() {
super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.propertiesView.toggleVisibility();
}
}
class ZoomInAction extends Action {
public static ID = 'ep.ZoomInAction';
public static LABEL = localize('executionPlanZoomInActionLabel', "Zoom In");
constructor() {
super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.azdataGraphDiagram.graph.zoomIn();
}
}
class ZoomOutAction extends Action {
public static ID = 'ep.ZoomOutAction';
public static LABEL = localize('executionPlanZoomOutActionLabel', "Zoom Out");
constructor() {
super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.azdataGraphDiagram.graph.zoomOut();
}
}
class ZoomToFitAction extends Action {
public static ID = 'ep.FitGraph';
public static LABEL = localize('executionPlanFitGraphLabel', "Zoom to fit");
constructor() {
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.azdataGraphDiagram.graph.fit();
context.azdataGraphDiagram.graph.view.rendering = true;
context.azdataGraphDiagram.graph.refresh();
}
}
class SavePlanFile extends Action {
public static ID = 'ep.saveXML';
public static LABEL = localize('executionPlanSavePlanXML', "Save Plan File");
constructor() {
super(SavePlanFile.ID, SavePlanFile.LABEL, savePlanIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
const workspaceFolders = await context.workspaceContextService.getWorkspace().folders;
const defaultFileName = 'plan';
let currentWorkSpaceFolder: URI;
if (workspaceFolders.length !== 0) {
currentWorkSpaceFolder = workspaceFolders[0].uri;
currentWorkSpaceFolder = URI.joinPath(currentWorkSpaceFolder, defaultFileName); //appending default file name to workspace uri
} else {
currentWorkSpaceFolder = URI.parse(defaultFileName); // giving default name
}
const saveFileUri = await context.fileDialogService.showSaveDialog({
filters: [
{
extensions: ['sqlplan'], //TODO: Get this extension from provider
name: localize('executionPlan.SaveFileDescription', 'Execution Plan Files') //TODO: Get the names from providers.
}
],
defaultUri: currentWorkSpaceFolder // If no workspaces are opened this will be undefined
});
if (saveFileUri) {
await context.fileService.writeFile(saveFileUri, VSBuffer.fromString(context.graphModel.graphFile.graphFileContent));
}
}
}
class CustomZoomAction extends Action {
public static ID = 'ep.customZoom';
public static LABEL = localize('executionPlanCustomZoom', "Custom Zoom");
constructor() {
super(CustomZoomAction.ID, CustomZoomAction.LABEL, customZoomIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.planActionView.toggleWidget(context._instantiationService.createInstance(CustomZoomWidget, context));
}
}
class SearchNodeAction extends Action {
public static ID = 'ep.searchNode';
public static LABEL = localize('executionPlanSearchNodeAction', "Find Node");
constructor() {
super(SearchNodeAction.ID, SearchNodeAction.LABEL, searchIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
context.planActionView.toggleWidget(context._instantiationService.createInstance(NodeSearchWidget, context));
}
}
class OpenPlanFile extends Action {
public static ID = 'ep.openGraphFile';
public static Label = localize('executionPlanOpenGraphFile', "Show Query Plan XML"); //TODO: add a contribution point for providers to set this text
constructor() {
super(OpenPlanFile.ID, OpenPlanFile.Label, openPlanFileIconClassNames);
}
public override async run(context: ExecutionPlan): Promise<void> {
await context.openGraphFile();
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const recommendationsColor = theme.getColor(textLinkForeground);
if (recommendationsColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header .recommendations {
color: ${recommendationsColor};
}
`);
}
const shadow = theme.getColor(widgetShadow);
if (shadow) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child {
box-shadow: 0 0 8px 2px ${shadow};
}
`);
}
const menuBackgroundColor = theme.getColor(listHoverBackground);
if (menuBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar {
background-color: ${menuBackgroundColor};
}
`);
}
const widgetBackgroundColor = theme.getColor(editorWidgetBackground);
if (widgetBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.mxTooltip {
background-color: ${widgetBackgroundColor};
}
`);
}
const widgetBorderColor = theme.getColor(contrastBorder);
if (widgetBorderColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar,
.mxTooltip {
border: 1px solid ${widgetBorderColor};
}
`);
}
const textColor = theme.getColor(foreground);
if (textColor) {
collector.addRule(`
.mxTooltip {
color: ${textColor};
}
`);
}
});

View File

@@ -10,55 +10,74 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ExecutionPlanInput } from 'sql/workbench/contrib/executionPlan/common/executionPlanInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { ExecutionPlanView } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
import { generateUuid } from 'vs/base/common/uuid';
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
export class ExecutionPlanEditor extends EditorPane {
public static ID: string = 'workbench.editor.executionplan';
public static LABEL: string = localize('executionPlanEditor', "Query Execution Plan Editor");
private view: ExecutionPlanView;
private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
private _parentContainer: HTMLElement;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IInstantiationService private _instantiationService: IInstantiationService,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
) {
super(ExecutionPlanEditor.ID, telemetryService, themeService, storageService);
this.view = this._register(instantiationService.createInstance(ExecutionPlanView));
}
/**
* Called to create the editor in the parent element.
*/
public createEditor(parent: HTMLElement): void {
this._parentContainer = parent;
//Enable scrollbars when drawing area is larger than viewport
parent.style.overflow = 'auto';
this.view.render(parent);
}
/**
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
* To be called when the container of this editor changes size.
*/
public layout(dimension: DOM.Dimension): void {
this.view.layout(dimension);
}
public override async setInput(input: ExecutionPlanInput, options: IEditorOptions, context: IEditorOpenContext): Promise<void> {
if (this.input instanceof ExecutionPlanInput && this.input.matches(input)) {
return Promise.resolve(undefined);
public override async setInput(newInput: ExecutionPlanInput, options: IEditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
const oldInput = this.input as ExecutionPlanInput;
// returning if the new input is same as old input
if (oldInput && newInput.matches(oldInput)) {
return Promise.resolve();
}
super.setInput(newInput, options, context, token);
// clearing old input view if present in the editor
if (oldInput?._executionPlanFileViewUUID) {
const oldView = this._viewCache.executionPlanFileViewMap.get(oldInput._executionPlanFileViewUUID);
oldView.onHide(this._parentContainer);
}
// if new input already has a view we are just making it visible here.
let newView = this._viewCache.executionPlanFileViewMap.get(newInput.executionPlanFileViewUUID);
if (newView) {
newView.onShow(this._parentContainer);
} else {
// creating a new view for the new input
newInput._executionPlanFileViewUUID = generateUuid();
newView = this._register(this._instantiationService.createInstance(ExecutionPlanFileView));
newView.onShow(this._parentContainer);
newView.loadGraphFile({
graphFileContent: await newInput.content(),
graphFileType: newInput.getFileExtension().replace('.', '')
});
this._viewCache.executionPlanFileViewMap.set(newInput._executionPlanFileViewUUID, newView);
}
await input.resolve();
await super.setInput(input, options, context, CancellationToken.None);
this.view.loadGraphFile({
graphFileContent: input.content,
graphFileType: input.getFileExtension().replace('.', '')
});
}
}

View File

@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as azdata from 'azdata';
import { InfoBox } from 'sql/workbench/browser/ui/infoBox/infoBox';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
import { ExecutionPlanView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanView';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import * as DOM from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { contrastBorder, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
export class ExecutionPlanFileView {
private _parent: HTMLElement;
private _loadingSpinner: LoadingSpinner;
private _loadingErrorInfoBox: InfoBox;
private _executionPlanViews: ExecutionPlanView[] = [];
private _graphs?: azdata.executionPlan.ExecutionPlanGraph[] = [];
private _container = DOM.$('.eps-container');
private _planCache: Map<string, azdata.executionPlan.ExecutionPlanGraph[]> = new Map();
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@IExecutionPlanService private executionPlanService: IExecutionPlanService
) {
}
public render(parent: HTMLElement): void {
this._parent = parent;
this._parent.appendChild(this._container);
}
public onShow(parentContainer: HTMLElement): void {
this._parent = parentContainer;
this._parent.appendChild(this._container);
}
public onHide(parentContainer: HTMLElement): void {
if (parentContainer === this._parent) {
this._parent.removeChild(this._container);
}
}
dispose() {
}
/**
* Adds executionPlanGraph to the graph controller.
* @param newGraphs ExecutionPlanGraphs to be added.
*/
public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) {
if (newGraphs) {
newGraphs.forEach(g => {
const ep = this.instantiationService.createInstance(ExecutionPlanView, this._container, this._executionPlanViews.length + 1);
ep.model = g;
this._executionPlanViews.push(ep);
this._graphs.push(g);
this.updateRelativeCosts();
});
}
}
/**
* Loads the graph file by converting the file to generic executionPlan graphs.
* This feature requires the right providers to be registered that can handle
* the graphFileType in the graphFile
* Please note: this method clears the existing graph in the graph control
* @param graphFile graph file to be loaded.
* @returns
*/
public async loadGraphFile(graphFile: azdata.executionPlan.ExecutionPlanGraphInfo) {
this._loadingSpinner = 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;
} else {
const graphs = (await this.executionPlanService.getExecutionPlan({
graphFileContent: graphFile.graphFileContent,
graphFileType: graphFile.graphFileType
})).graphs;
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, {
text: e.toString(),
style: 'error',
isClickable: false
});
this._loadingErrorInfoBox.isClickable = false;
this._loadingSpinner.loadingCompletedMessage = localize('executionPlanFileLoadingFailed', "Failed to load execution plan");
} finally {
this._loadingSpinner.loading = false;
}
}
private updateRelativeCosts() {
const sum = this._graphs.reduce((prevCost: number, cg) => {
return prevCost += cg.root.subTreeCost + cg.root.cost;
}, 0);
if (sum > 0) {
this._executionPlanViews.forEach(ep => {
ep.planHeader.relativeCost = ((ep.model.root.subTreeCost + ep.model.root.cost) / sum) * 100;
});
}
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const recommendationsColor = theme.getColor(textLinkForeground);
if (recommendationsColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header .recommendations {
color: ${recommendationsColor};
}
`);
}
const shadow = theme.getColor(widgetShadow);
if (shadow) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child {
box-shadow: 0 0 8px 2px ${shadow};
}
`);
}
const menuBackgroundColor = theme.getColor(listHoverBackground);
if (menuBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar {
background-color: ${menuBackgroundColor};
}
`);
}
const widgetBackgroundColor = theme.getColor(editorWidgetBackground);
if (widgetBackgroundColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.mxTooltip {
background-color: ${widgetBackgroundColor};
}
`);
}
const widgetBorderColor = theme.getColor(contrastBorder);
if (widgetBorderColor) {
collector.addRule(`
.eps-container .execution-plan .plan .plan-action-container .child,
.eps-container .execution-plan .plan .header,
.eps-container .execution-plan .properties .title,
.eps-container .execution-plan .properties .table-action-bar,
.mxTooltip {
border: 1px solid ${widgetBorderColor};
}
`);
}
const textColor = theme.getColor(foreground);
if (textColor) {
collector.addRule(`
.mxTooltip {
color: ${textColor};
}
`);
}
});

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
export class ExecutionPlanFileViewCache {
private static instance: ExecutionPlanFileViewCache;
public executionPlanFileViewMap: Map<string, ExecutionPlanFileView> = new Map();
private constructor() { }
public static getInstance(): ExecutionPlanFileViewCache {
if (!ExecutionPlanFileViewCache.instance) {
ExecutionPlanFileViewCache.instance = new ExecutionPlanFileViewCache();
}
return ExecutionPlanFileViewCache.instance;
}
}

View File

@@ -6,89 +6,84 @@
import * as DOM from 'vs/base/browser/dom';
import type * as azdata from 'azdata';
import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { Table } from 'sql/base/browser/ui/table/table';
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { removeLineBreaks } from 'sql/base/common/strings';
import { isString } from 'vs/base/common/types';
import { sortAlphabeticallyIconClassNames, sortByDisplayOrderIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { textFormatter } from 'sql/base/browser/ui/table/formatters';
import { ExecutionPlanPropertiesViewBase, PropertiesSortType } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase';
export class ExecutionPlanPropertiesView {
// Title bar with close button action
private _propertiesTitle!: HTMLElement;
private _titleText!: HTMLElement;
private _titleActionBarContainer!: HTMLElement;
private _titleActionBar: ActionBar;
export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase {
// Div that holds the name of the element selected
private _operationName!: HTMLElement;
// Action bar that contains sorting option for the table
private _tableActionBarContainer!: HTMLElement;
private _tableActionBar!: ActionBar;
// Properties table
private _table: Table<Slick.SlickData>;
private _dataView: TableDataView<Slick.SlickData>;
private _data: { [key: string]: string }[];
private _tableContainer!: HTMLElement;
private _actualTable!: HTMLElement;
// Table dimensions.
private _tableWidth = 485;
private _tableHeight;
private _model: ExecutionPlanPropertiesViewModel;
public constructor(
private _parentContainer: HTMLElement,
private _themeService: IThemeService,
private _model: GraphElementPropertyViewData = <GraphElementPropertyViewData>{}
parentContainer: HTMLElement,
themeService: IThemeService
) {
this._parentContainer.style.display = 'none';
this._propertiesTitle = DOM.$('.title');
this._parentContainer.appendChild(this._propertiesTitle);
this._titleText = DOM.$('h3');
this._titleText.classList.add('text');
this._titleText.innerText = localize('nodePropertyViewTitle', "Properties");
this._propertiesTitle.appendChild(this._titleText);
this._titleActionBarContainer = DOM.$('.action-bar');
this._propertiesTitle.appendChild(this._titleActionBarContainer);
this._titleActionBar = new ActionBar(this._titleActionBarContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._titleActionBar.pushAction([new ClosePropertyViewAction()], { icon: true, label: false });
super(parentContainer, themeService);
this._model = <ExecutionPlanPropertiesView>{};
this._operationName = DOM.$('h3');
this._operationName.classList.add('operation-name');
this._parentContainer.appendChild(this._operationName);
this.setHeader(this._operationName);
this._tableActionBarContainer = DOM.$('.table-action-bar');
this._parentContainer.appendChild(this._tableActionBarContainer);
this._tableActionBar = new ActionBar(this._tableActionBarContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
this._parentContainer.style.display = 'none';
}
public set graphElement(element: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge) {
this._model.graphElement = element;
this.renderView();
}
public sortPropertiesAlphabetically(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
return props.sort((a, b) => {
if (!a?.name && !b?.name) {
return 0;
} else if (!a?.name) {
return -1;
} else if (!b?.name) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
this._tableActionBar.pushAction([new SortPropertiesByDisplayOrderAction(), new SortPropertiesAlphabeticallyAction()], { icon: true, label: false });
}
public sortPropertiesReverseAlphabetically(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
return props.sort((a, b) => {
if (!a?.name && !b?.name) {
return 0;
} else if (!a?.name) {
return -1;
} else if (!b?.name) {
return 1;
} else {
return b.name.localeCompare(a.name);
}
});
}
this._tableContainer = DOM.$('.table-container');
this._parentContainer.appendChild(this._tableContainer);
public sortPropertiesByImportance(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
return props.sort((a, b) => {
if (!a?.displayOrder && !b?.displayOrder) {
return 0;
} else if (!a?.displayOrder) {
return -1;
} else if (!b?.displayOrder) {
return 1;
} else {
return a.displayOrder - b.displayOrder;
}
});
}
this._actualTable = DOM.$('.table');
this._tableContainer.appendChild(this._actualTable);
this._dataView = new TableDataView();
this._data = [];
public renderView(): void {
if (this._model.graphElement) {
const nodeName = (<azdata.executionPlan.ExecutionPlanNode>this._model.graphElement).name;
this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('executionPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
}
const columns: Slick.Column<Slick.SlickData>[] = [
{
@@ -111,100 +106,26 @@ export class ExecutionPlanPropertiesView {
}
];
this._table = new Table(this._actualTable, {
dataProvider: this._dataView, columns: columns
}, {
rowHeight: RESULTS_GRID_DEFAULTS.rowHeight,
forceFitColumns: true,
defaultColumnWidth: 120
});
new ResizeObserver((e) => {
this.tableHeight = (this._parentContainer.getBoundingClientRect().height - 80);
}).observe(this._parentContainer);
attachTableStyler(this._table, this._themeService);
this.populateTable(columns, this.convertModelToTableRows(this._model.graphElement.properties, -1, 0));
}
public set graphElement(element: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge) {
this._model.graphElement = element;
this.sortPropertiesByImportance();
this.renderView();
}
public sortPropertiesAlphabetically(): void {
this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => {
if (!a?.name && !b?.name) {
return 0;
} else if (!a?.name) {
return -1;
} else if (!b?.name) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
this.renderView();
}
public sortPropertiesByImportance(): void {
this._model.graphElement.properties = this._model.graphElement.properties.sort((a, b) => {
if (!a?.displayOrder && !b?.displayOrder) {
return 0;
} else if (!a?.displayOrder) {
return -1;
} else if (!b?.displayOrder) {
return 1;
} else {
return a.displayOrder - b.displayOrder;
}
});
this.renderView();
}
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
this.renderView();
}
}
public get tableHeight(): number {
return this._tableHeight;
}
public set tableWidth(value: number) {
if (this._tableWidth !== value) {
this._tableWidth = value;
this.renderView();
}
}
public get tableWidth(): number {
return this._tableWidth;
}
private renderView(): void {
if (this._model.graphElement) {
const nodeName = (<azdata.executionPlan.ExecutionPlanNode>this._model.graphElement).name;
this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('executionPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
}
this._tableContainer.scrollTo(0, 0);
this._dataView.clear();
this._data = this.convertPropertiesToTableRows(this._model.graphElement.properties, -1, 0);
this._dataView.push(this._data);
this._table.setData(this._dataView);
this._table.autosizeColumns();
this._table.updateRowCount();
this.tableHeight = (this._parentContainer.getBoundingClientRect().height - 80); //80px is the space taken by the title and toolbar
this._table.layout(new DOM.Dimension(this._tableWidth, this._tableHeight));
this._table.resizeCanvas();
}
private convertPropertiesToTableRows(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] {
private convertModelToTableRows(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] {
if (!props) {
return rows;
}
switch (this.sortType) {
case PropertiesSortType.DisplayOrder:
props = this.sortPropertiesByImportance(props);
break;
case PropertiesSortType.Alphabetical:
props = this.sortPropertiesAlphabetically(props);
break;
case PropertiesSortType.ReverseAlphabetical:
props = this.sortPropertiesReverseAlphabetically(props);
break;
}
props.forEach((p, i) => {
let row = {};
rows.push(row);
@@ -212,7 +133,7 @@ export class ExecutionPlanPropertiesView {
row['parent'] = parentIndex;
if (!isString(p.value)) {
row['value'] = removeLineBreaks(p.displayValue, ' ');
this.convertPropertiesToTableRows(p.value, rows.length - 1, indent + 2, rows);
this.convertModelToTableRows(p.value, rows.length - 1, indent + 2, rows);
} else {
row['value'] = removeLineBreaks(p.value, ' ');
row['tooltip'] = p.value;
@@ -220,52 +141,8 @@ export class ExecutionPlanPropertiesView {
});
return rows;
}
public toggleVisibility(): void {
this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none';
this.renderView();
}
}
export interface GraphElementPropertyViewData {
export interface ExecutionPlanPropertiesViewModel {
graphElement: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge;
}
export class ClosePropertyViewAction extends Action {
public static ID = 'ep.propertiesView.close';
public static LABEL = localize('executionPlanPropertyViewClose', "Close");
constructor() {
super(ClosePropertyViewAction.ID, ClosePropertyViewAction.LABEL, Codicon.close.classNames);
}
public override async run(context: ExecutionPlanPropertiesView): Promise<void> {
context.toggleVisibility();
}
}
export class SortPropertiesAlphabeticallyAction extends Action {
public static ID = 'ep.propertiesView.sortByAlphabet';
public static LABEL = localize('executionPlanPropertyViewSortAlphabetically', "Alphabetical");
constructor() {
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, sortAlphabeticallyIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesView): Promise<void> {
context.sortPropertiesAlphabetically();
}
}
export class SortPropertiesByDisplayOrderAction extends Action {
public static ID = 'ep.propertiesView.sortByDisplayOrder';
public static LABEL = localize('executionPlanPropertyViewSortByDisplayOrder', "Categorized");
constructor() {
super(SortPropertiesByDisplayOrderAction.ID, SortPropertiesByDisplayOrderAction.LABEL, sortByDisplayOrderIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesView): Promise<void> {
context.sortPropertiesByImportance();
}
}

View File

@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { Table } from 'sql/base/browser/ui/table/table';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { localize } from 'vs/nls';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { sortAlphabeticallyIconClassNames, sortByDisplayOrderIconClassNames, sortReverseAlphabeticallyIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
import { contrastBorder, listHoverBackground } from 'vs/platform/theme/common/colorRegistry';
export abstract class ExecutionPlanPropertiesViewBase {
// Title bar with close button action
private _titleBarContainer!: HTMLElement;
private _titleBarTextContainer!: HTMLElement;
private _titleBarActionsContainer!: HTMLElement;
private _titleActions: ActionBar;
// Header container
private _headerContainer: HTMLElement;
// Properties actions
private _headerActionsContainer!: HTMLElement;
private _headerActions: ActionBar;
// Properties table
private _tableComponent: Table<Slick.SlickData>;
private _tableComponentDataView: TableDataView<Slick.SlickData>;
private _tableComponentDataModel: { [key: string]: string }[];
private _tableContainer!: HTMLElement;
private _tableWidth;
private _tableHeight;
public sortType: PropertiesSortType = PropertiesSortType.DisplayOrder;
constructor(
public _parentContainer: HTMLElement,
private _themeService: IThemeService
) {
const sashContainer = DOM.$('.properties-sash');
this._parentContainer.appendChild(sashContainer);
this._titleBarContainer = DOM.$('.title');
this._parentContainer.appendChild(this._titleBarContainer);
this._titleBarTextContainer = DOM.$('h3');
this._titleBarTextContainer.classList.add('text');
this._titleBarTextContainer.innerText = localize('nodePropertyViewTitle', "Properties");
this._titleBarContainer.appendChild(this._titleBarTextContainer);
this._titleBarActionsContainer = DOM.$('.action-bar');
this._titleBarContainer.appendChild(this._titleBarActionsContainer);
this._titleActions = new ActionBar(this._titleBarActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._titleActions.pushAction([new ClosePropertyViewAction()], { icon: true, label: false });
this._headerContainer = DOM.$('.header');
this._parentContainer.appendChild(this._headerContainer);
this._headerActionsContainer = DOM.$('.table-action-bar');
this._parentContainer.appendChild(this._headerActionsContainer);
this._headerActions = new ActionBar(this._headerActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
this._headerActions.pushAction([new SortPropertiesByDisplayOrderAction(), new SortPropertiesAlphabeticallyAction(), new SortPropertiesReverseAlphabeticallyAction()], { icon: true, label: false });
this._tableContainer = DOM.$('.table-container');
this._parentContainer.appendChild(this._tableContainer);
const table = DOM.$('.table');
this._tableContainer.appendChild(table);
this._tableComponentDataView = new TableDataView();
this._tableComponentDataModel = [];
this._tableComponent = new Table(table, {
dataProvider: this._tableComponentDataView, columns: []
}, {
rowHeight: RESULTS_GRID_DEFAULTS.rowHeight,
forceFitColumns: true,
defaultColumnWidth: 120
});
attachTableStyler(this._tableComponent, this._themeService);
new ResizeObserver((e) => {
this.resizeTable();
}).observe(this._parentContainer);
}
public setTitle(v: string): void {
this._titleBarTextContainer.innerText = v;
}
public setHeader(c: HTMLElement): void {
this._headerContainer.appendChild(c);
}
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
this.renderView();
}
}
public set tableWidth(value: number) {
if (this._tableWidth !== value) {
this._tableWidth = value;
this.renderView();
}
}
public get tableWidth(): number {
return this._tableWidth;
}
public get tableHeight(): number {
return this._tableHeight;
}
public abstract renderView();
public toggleVisibility(): void {
this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none';
this.renderView();
}
public populateTable(columns: Slick.Column<Slick.SlickData>[], data: { [key: string]: string }[]) {
this._tableComponent.columns = columns;
this._tableContainer.scrollTo(0, 0);
this._tableComponentDataView.clear();
this._tableComponentDataModel = data;
this._tableComponentDataView.push(this._tableComponentDataModel);
this._tableComponent.setData(this._tableComponentDataView);
this._tableComponent.autosizeColumns();
this._tableComponent.updateRowCount();
this.resizeTable();
}
private resizeTable(): void {
const spaceOccupied = (this._titleBarContainer.getBoundingClientRect().height
+ this._headerContainer.getBoundingClientRect().height
+ this._headerActionsContainer.getBoundingClientRect().height);
this.tableHeight = (this._parentContainer.getBoundingClientRect().height - spaceOccupied - 15);
this.tableWidth = (this._parentContainer.getBoundingClientRect().width - 15);
this._tableComponent.layout(new DOM.Dimension(this._tableWidth, this._tableHeight));
this._tableComponent.resizeCanvas();
}
}
export class ClosePropertyViewAction extends Action {
public static ID = 'ep.propertiesView.close';
public static LABEL = localize('executionPlanPropertyViewClose', "Close");
constructor() {
super(ClosePropertyViewAction.ID, ClosePropertyViewAction.LABEL, Codicon.close.classNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.toggleVisibility();
}
}
export class SortPropertiesAlphabeticallyAction extends Action {
public static ID = 'ep.propertiesView.sortByAlphabet';
public static LABEL = localize('executionPlanPropertyViewSortAlphabetically', "Alphabetical");
constructor() {
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, sortAlphabeticallyIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.Alphabetical;
context.renderView();
}
}
export class SortPropertiesReverseAlphabeticallyAction extends Action {
public static ID = 'ep.propertiesView.sortByAlphabet';
public static LABEL = localize('executionPlanPropertyViewSortReverseAlphabetically', "Reverse Alphabetical");
constructor() {
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, sortReverseAlphabeticallyIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.ReverseAlphabetical;
context.renderView();
}
}
export class SortPropertiesByDisplayOrderAction extends Action {
public static ID = 'ep.propertiesView.sortByDisplayOrder';
public static LABEL = localize('executionPlanPropertyViewSortByDisplayOrder', "Categorized");
constructor() {
super(SortPropertiesByDisplayOrderAction.ID, SortPropertiesByDisplayOrderAction.LABEL, sortByDisplayOrderIconClassNames);
}
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.DisplayOrder;
context.renderView();
}
}
export enum PropertiesSortType {
DisplayOrder,
Alphabetical,
ReverseAlphabetical
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const menuBackgroundColor = theme.getColor(listHoverBackground);
if (menuBackgroundColor) {
collector.addRule(`
.properties .title,
.properties .table-action-bar {
background-color: ${menuBackgroundColor};
}
`);
}
const widgetBorderColor = theme.getColor(contrastBorder);
if (widgetBorderColor) {
collector.addRule(`
.properties .title,
.properties .table-action-bar,
.mxTooltip {
border: 1px solid ${widgetBorderColor};
}
`);
}
});

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/executionPlan';
import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel';
import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ExecutionPlanState } from 'sql/workbench/common/editor/query/executionPlanState';
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
import { generateUuid } from 'vs/base/common/uuid';
export class ExecutionPlanTab implements IPanelTab {
public readonly title = localize('executionPlanTitle', "Query Plan (Preview)");
public readonly identifier = 'ExecutionPlan2Tab';
public readonly view: ExecutionPlanTabView;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
) {
this.view = instantiationService.createInstance(ExecutionPlanTabView);
}
public dispose() {
}
public clear() {
this.view.clear();
}
}
export class ExecutionPlanTabView implements IPanelView {
private _container: HTMLElement = DOM.$('.execution-plan-tab');
private _input: ExecutionPlanState;
private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
) {
}
public set state(newInput: ExecutionPlanState) {
const oldInput = this._input;
// clearing old input view
if (oldInput?.executionPlanFileViewUUID) {
const oldView = this._viewCache.executionPlanFileViewMap.get(oldInput.executionPlanFileViewUUID);
oldView.onHide(this._container);
}
// if new input already has a view we are just making it visible here.
let newView = this._viewCache.executionPlanFileViewMap.get(newInput.executionPlanFileViewUUID);
if (newView) {
newView.onShow(this._container);
} else {
// creating a new view for the new input
newInput.executionPlanFileViewUUID = generateUuid();
newView = this._instantiationService.createInstance(ExecutionPlanFileView);
newView.onShow(this._container);
newView.addGraphs(
newInput.graphs
);
this._viewCache.executionPlanFileViewMap.set(newInput.executionPlanFileViewUUID, newView);
}
this._input = newInput;
}
public render(parent: HTMLElement): void {
parent.appendChild(this._container);
}
public layout(dimension: DOM.Dimension): void {
this._container.style.width = dimension.width + 'px';
this._container.style.height = dimension.height + 'px';
}
public clearPlans(): void {
let currentView = this._viewCache.executionPlanFileViewMap.get(this._input.executionPlanFileViewUUID);
if (currentView) {
currentView.onHide(this._container);
this._input.graphs = [];
currentView = this._instantiationService.createInstance(ExecutionPlanFileView);
currentView.render(this._container);
}
}
public clear() {
}
}

View File

@@ -0,0 +1,453 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as DOM from 'vs/base/browser/dom';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { ExecutionPlanPropertiesView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
import { ExecutionPlanViewHeader } from 'sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader';
import { ISashEvent, ISashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Action } from 'vs/base/common/actions';
import { localize } from 'vs/nls';
import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { URI } from 'vs/base/common/uri';
import { VSBuffer } from 'vs/base/common/buffer';
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget';
import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
export class ExecutionPlanView implements ISashLayoutProvider {
// Underlying execution plan displayed in the view
private _model?: azdata.executionPlan.ExecutionPlanGraph;
// container for the view
private _container: HTMLElement;
// action bar for the view
private _actionBarContainer: HTMLElement;
private _actionBar: ActionBar;
// plan header section
public planHeader: ExecutionPlanViewHeader;
private _planContainer: HTMLElement;
private _planHeaderContainer: HTMLElement;
// properties view
public propertiesView: ExecutionPlanPropertiesView;
private _propContainer: HTMLElement;
// plan widgets
private _widgetContainer: HTMLElement;
public widgetController: ExecutionPlanWidgetController;
// plan diagram
public executionPlanDiagram: AzdataGraphView;
public actionBarToggleTopTip: Action;
public contextMenuToggleTooltipAction: Action;
constructor(
private _parent: HTMLElement,
private _graphIndex: number,
@IInstantiationService public readonly _instantiationService: IInstantiationService,
@IThemeService private readonly _themeService: IThemeService,
@IContextViewService public readonly contextViewService: IContextViewService,
@IUntitledTextEditorService private readonly _untitledEditorService: IUntitledTextEditorService,
@IEditorService private readonly editorService: IEditorService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IFileDialogService public fileDialogService: IFileDialogService,
@IFileService public fileService: IFileService,
@IWorkspaceContextService public workspaceContextService: IWorkspaceContextService,
) {
// parent container for query plan.
this._container = DOM.$('.execution-plan');
this._parent.appendChild(this._container);
const sashContainer = DOM.$('.execution-plan-sash');
this._parent.appendChild(sashContainer);
// resizing sash for the query plan.
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL, size: 3 });
let originalHeight = this._container.offsetHeight;
let originalTableHeight = 0;
let change = 0;
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) => {
change = evt.startY - evt.currentY;
const newHeight = originalHeight - change;
if (newHeight < 200) {
return;
}
/**
* Since the parent container is flex, we will have
* to change the flex-basis property to change the height.
*/
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.propertiesView.tableHeight = originalTableHeight - change;
});
this._planContainer = DOM.$('.plan');
this._container.appendChild(this._planContainer);
// container that holds plan header info
this._planHeaderContainer = DOM.$('.header');
// Styling header text like the query editor
this._planHeaderContainer.style.fontFamily = EDITOR_FONT_DEFAULTS.fontFamily;
this._planHeaderContainer.style.fontSize = EDITOR_FONT_DEFAULTS.fontSize.toString();
this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight;
this._planContainer.appendChild(this._planHeaderContainer);
this.planHeader = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._planHeaderContainer, {
planIndex: this._graphIndex,
});
// container properties
this._propContainer = DOM.$('.properties');
this._container.appendChild(this._propContainer);
this.propertiesView = new ExecutionPlanPropertiesView(this._propContainer, this._themeService);
this._widgetContainer = DOM.$('.plan-action-container');
this._planContainer.appendChild(this._widgetContainer);
this.widgetController = new ExecutionPlanWidgetController(this._widgetContainer);
// container that holds action bar icons
this._actionBarContainer = DOM.$('.action-bar-container');
this._container.appendChild(this._actionBarContainer);
this._actionBar = new ActionBar(this._actionBarContainer, {
orientation: ActionsOrientation.VERTICAL, context: this
});
this.actionBarToggleTopTip = new ActionBarToggleTooltip();
const actionBarActions = [
new SavePlanFile(),
new OpenPlanFile(),
new OpenQueryAction(),
new SearchNodeAction(),
new ZoomInAction(),
new ZoomOutAction(),
new ZoomToFitAction(),
new CustomZoomAction(),
new PropertiesAction(),
this.actionBarToggleTopTip
];
this._actionBar.pushAction(actionBarActions, { icon: true, label: false });
// Setting up context menu
this.contextMenuToggleTooltipAction = new ContextMenuTooltipToggle();
const contextMenuAction = [
new SavePlanFile(),
new OpenPlanFile(),
new OpenQueryAction(),
new SearchNodeAction(),
new ZoomInAction(),
new ZoomOutAction(),
new ZoomToFitAction(),
new CustomZoomAction(),
new PropertiesAction(),
this.contextMenuToggleTooltipAction
];
const self = this;
this._container.oncontextmenu = (e: MouseEvent) => {
if (contextMenuAction) {
this._contextMenuService.showContextMenu({
getAnchor: () => {
return {
x: e.x,
y: e.y
};
},
getActions: () => contextMenuAction,
getActionsContext: () => (self)
});
}
};
}
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.propertiesView.graphElement = e;
});
}
public set model(graph: azdata.executionPlan.ExecutionPlanGraph | undefined) {
this._model = graph;
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);
/**
* We do not want to scroll the diagram through mouse wheel.
* Instead, we pass this event to parent control. So, when user
* uses the scroll wheel, they scroll through graphs present in
* the graph control. To scroll the individual graphs, users should
* use the scroll bars.
*/
diagramContainer.addEventListener('wheel', e => {
this._parent.scrollTop += e.deltaY;
//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';
}
e.preventDefault();
e.stopPropagation();
});
this._planContainer.appendChild(diagramContainer);
this.propertiesView.graphElement = this._model.root;
}
}
public get model(): azdata.executionPlan.ExecutionPlanGraph | undefined {
return this._model;
}
public openQuery() {
return this._instantiationService.invokeFunction(openNewQuery, undefined, this.model.query, RunQueryOnConnectionMode.none).then();
}
public async openGraphFile() {
const input = this._untitledEditorService.create({ mode: this.model.graphFile.graphFileType, initialValue: this.model.graphFile.graphFileContent });
await input.resolve();
await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None);
input.setDirty(false);
this.editorService.openEditor(input);
}
public hideActionBar() {
this._actionBarContainer.style.display = 'none';
}
}
export class OpenQueryAction extends Action {
public static ID = 'ep.OpenQueryAction';
public static LABEL = localize('openQueryAction', "Open Query");
constructor() {
super(OpenQueryAction.ID, OpenQueryAction.LABEL, openQueryIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.openQuery();
}
}
export class PropertiesAction extends Action {
public static ID = 'ep.propertiesAction';
public static LABEL = localize('executionPlanPropertiesActionLabel', "Properties");
constructor() {
super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.propertiesView.toggleVisibility();
}
}
export class ZoomInAction extends Action {
public static ID = 'ep.ZoomInAction';
public static LABEL = localize('executionPlanZoomInActionLabel', "Zoom In");
constructor() {
super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.executionPlanDiagram.zoomIn();
}
}
export class ZoomOutAction extends Action {
public static ID = 'ep.ZoomOutAction';
public static LABEL = localize('executionPlanZoomOutActionLabel', "Zoom Out");
constructor() {
super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.executionPlanDiagram.zoomOut();
}
}
export class ZoomToFitAction extends Action {
public static ID = 'ep.FitGraph';
public static LABEL = localize('executionPlanFitGraphLabel', "Zoom to fit");
constructor() {
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.executionPlanDiagram.zoomToFit();
}
}
export class SavePlanFile extends Action {
public static ID = 'ep.saveXML';
public static LABEL = localize('executionPlanSavePlanXML', "Save Plan File");
constructor() {
super(SavePlanFile.ID, SavePlanFile.LABEL, savePlanIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const workspaceFolders = await context.workspaceContextService.getWorkspace().folders;
const defaultFileName = 'plan';
let currentWorkSpaceFolder: URI;
if (workspaceFolders.length !== 0) {
currentWorkSpaceFolder = workspaceFolders[0].uri;
currentWorkSpaceFolder = URI.joinPath(currentWorkSpaceFolder, defaultFileName); //appending default file name to workspace uri
} else {
currentWorkSpaceFolder = URI.parse(defaultFileName); // giving default name
}
const saveFileUri = await context.fileDialogService.showSaveDialog({
filters: [
{
extensions: ['sqlplan'], //TODO: Get this extension from provider
name: localize('executionPlan.SaveFileDescription', 'Execution Plan Files') //TODO: Get the names from providers.
}
],
defaultUri: currentWorkSpaceFolder // If no workspaces are opened this will be undefined
});
if (saveFileUri) {
await context.fileService.writeFile(saveFileUri, VSBuffer.fromString(context.model.graphFile.graphFileContent));
}
}
}
export class CustomZoomAction extends Action {
public static ID = 'ep.customZoom';
public static LABEL = localize('executionPlanCustomZoom', "Custom Zoom");
constructor() {
super(CustomZoomAction.ID, CustomZoomAction.LABEL, customZoomIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.widgetController.toggleWidget(context._instantiationService.createInstance(CustomZoomWidget, context.widgetController, context.executionPlanDiagram));
}
}
export class SearchNodeAction extends Action {
public static ID = 'ep.searchNode';
public static LABEL = localize('executionPlanSearchNodeAction', "Find Node");
constructor() {
super(SearchNodeAction.ID, SearchNodeAction.LABEL, searchIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.widgetController.toggleWidget(context._instantiationService.createInstance(NodeSearchWidget, context.widgetController, context.executionPlanDiagram));
}
}
export class OpenPlanFile extends Action {
public static ID = 'ep.openGraphFile';
public static Label = localize('executionPlanOpenGraphFile', "Show Query Plan XML"); //TODO: add a contribution point for providers to set this text
constructor() {
super(OpenPlanFile.ID, OpenPlanFile.Label, openPlanFileIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
await context.openGraphFile();
}
}
export class ActionBarToggleTooltip extends Action {
public static ID = 'ep.tooltipToggleActionBar';
public static ENABLE_LABEL = localize('executionPlanEnableTooltip', "Tooltips enabled");
public static DISABLE_LABEL = localize('executionPlanDisableTooltip', "Tooltips disabled");
constructor() {
super(ActionBarToggleTooltip.ID, ActionBarToggleTooltip.ENABLE_LABEL, enableTooltipIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const state = context.executionPlanDiagram.toggleTooltip();
if (!state) {
this.class = disableTooltipIconClassName;
this.label = ActionBarToggleTooltip.DISABLE_LABEL;
context.actionBarToggleTopTip.label = ContextMenuTooltipToggle.DISABLE_LABEL;
} else {
this.class = enableTooltipIconClassName;
this.label = ActionBarToggleTooltip.ENABLE_LABEL;
context.actionBarToggleTopTip.label = ContextMenuTooltipToggle.ENABLE_LABEL;
}
}
}
export class ContextMenuTooltipToggle extends Action {
public static ID = 'ep.tooltipToggleContextMenu';
public static ENABLE_LABEL = localize('executionPlanContextMenuEnableTooltip', "Enable Tooltips");
public static DISABLE_LABEL = localize('executionPlanContextMenuDisableTooltip', "Disable Tooltips");
constructor() {
super(ContextMenuTooltipToggle.ID, ContextMenuTooltipToggle.ENABLE_LABEL, enableTooltipIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const state = context.executionPlanDiagram.toggleTooltip();
if (!state) {
this.label = ContextMenuTooltipToggle.ENABLE_LABEL;
context.actionBarToggleTopTip.class = disableTooltipIconClassName;
context.actionBarToggleTopTip.label = ActionBarToggleTooltip.DISABLE_LABEL;
} else {
this.label = ContextMenuTooltipToggle.DISABLE_LABEL;
context.actionBarToggleTopTip.class = enableTooltipIconClassName;
context.actionBarToggleTopTip.label = ActionBarToggleTooltip.ENABLE_LABEL;
}
}
}

View File

@@ -12,7 +12,7 @@ import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connect
import { Button } from 'sql/base/browser/ui/button/button';
import { removeLineBreaks } from 'sql/base/common/strings';
export class PlanHeader {
export class ExecutionPlanViewHeader {
private _graphIndex: number; // Index of the graph in the view
private _relativeCost: number; // Relative cost of the graph to the script

View File

@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style>
.st0 {
opacity: 0
}
.st0,
.st1 {
fill: #f6f6f6
}
.st2 {
fill: #424242
}
.st3 {
fill: #f0eff1
}
.icon-vs-action-red {
fill: #a1260d
}
</style>
<g id="outline">
<path class="st0" d="M0 0h16v16H0z" />
<path class="st1" d="M6.771 5L1.382 0H0v10.825l2.337-2.566L3 9.755V16h13V5z" />
</g>
<g id="icon_x5F_bg">
<path class="st2"
d="M8.4 12h2.2l.4 1h1l-2-5H9l-2 5h1l.4-1zm1.1-2.75l.7 1.75H8.8l.7-1.75zM4.955 8.463L3.869 5.998h2.518L1 1v7.231l1.629-1.789 1.137 2.562z" />
<path class="st2" d="M7.849 6l1.077 1H14v7H5V9.551l-1 .454V15h11V6z" />
</g>
<g id="icon_x5F_fg">
<path class="st3" d="M9.5 9.25L8.8 11h1.4z" />
<path class="st3"
d="M8.926 7l.008.008H5.402l.866 1.966L5 9.551V14h9V7H8.926zM11 13l-.4-1H8.4L8 13H7l2-5h1l2 5h-1z" />
</g>
<path class="icon-vs-action-red"
d="M13.03 12.03L15 14l-1.061 1.061-1.97-1.97L10 15.061 8.939 14l1.97-1.97-1.97-1.97L10 9l1.97 1.97L13.939 9 15 10.061l-1.97 1.969z"
id="colorAction" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style>
.st0 {
opacity: 0
}
.st0,
.st1 {
fill: #f6f6f6
}
.st2 {
fill: #424242
}
.st3 {
fill: #f0eff1
}
.icon-vs-green {
fill: #393
}
</style>
<g id="outline">
<path class="st0" d="M0 0h16v16H0z" />
<path class="st1" d="M6.771 5L1.382 0H0v10.825l2.337-2.566L3 9.755V16h13V5z" />
</g>
<g id="icon_x5F_bg">
<path class="st2"
d="M8.4 12h2.2l.4 1h1l-2-5H9l-2 5h1l.4-1zm1.1-2.75l.7 1.75H8.8l.7-1.75zM4.955 8.463L3.869 5.998h2.518L1 1v7.231l1.629-1.789 1.137 2.562z" />
<path class="st2" d="M7.849 6l1.077 1H14v7H5V9.551l-1 .454V15h11V6z" />
</g>
<g id="icon_x5F_fg">
<path class="st3" d="M9.5 9.25L8.8 11h1.4z" />
<path class="st3"
d="M8.926 7l.008.008H5.402l.866 1.966L5 9.551V14h9V7H8.926zM11 13l-.4-1H8.4L8 13H7l2-5h1l2 5h-1z" />
</g>
<g id="colorImportance">
<path class="icon-vs-green" d="M8.558 15l-3.442-3.441.884-.885 2.558 2.558 5.557-5.558.885.884z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1001 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}.icon-vs-fg{fill:#f0eff1;}.icon-vs-action-blue{fill:#00539c;}</style></defs><title>SortDescending_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline"><path class="icon-vs-out" d="M16,9.4v.536l-4.484,4.5L9,11.923V16H0V7H1V4.586L2.586,3H1V0H8V3H7.462L6.449,4H8V7H9v.682l1,1V3h3V8.7l1.148-1.144Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M7,6H2V5L5,2H2V1H7V2H6.952L7,2.051,4.013,5H7ZM3.8,12H5.2l-.7-1.75ZM8,8v7H1V8ZM7,14,5,9H4L2,14H3l.4-1H5.6L6,14Z"/></g><g id="iconFg"><path class="icon-vs-fg" d="M5,9H4L2,14H3l.4-1H5.6L6,14H7ZM3.8,12l.7-1.75L5.2,12Z"/></g><g id="colorAction"><path class="icon-vs-action-blue" d="M14.855,9.671l-3.34,3.352L8.163,9.671l.707-.707L11,11.086V4h1v7.1l2.148-2.14Z"/></g></svg>

After

Width:  |  Height:  |  Size: 948 B

View File

@@ -227,87 +227,107 @@ However we always want it to be the width of the container it is resizing.
z-index: 3;
}
.eps-container .save-plan-icon {
.ep-save-plan-icon {
background-image: url(../images/actionIcons/save.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .open-properties-icon {
.ep-open-properties-icon {
background-image: url(../images/actionIcons/openProperties.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .open-query-icon {
.ep-open-query-icon {
background-image: url(../images/actionIcons/openQuery.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .open-plan-file-icon {
.ep-open-plan-file-icon {
background-image: url(../images/actionIcons/openPlanFile.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .search-icon {
.ep-search-icon {
background-image: url(../images/actionIcons/search.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .sort-alphabetically-icon {
.ep-sort-alphabetically-icon {
background-image: url(../images/actionIcons/sortAlphabetically.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .sort-display-order-icon {
.ep-sort-reverse-alphabetically-icon {
background-image: url(../images/actionIcons/sortReverseAlphabetically.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-sort-display-order-icon {
background-image: url(../images/actionIcons/sortByDisplayOrder.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-in-icon {
.ep-zoom-in-icon {
background-image: url(../images/actionIcons/zoomIn.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-out-icon {
.ep-zoom-out-icon {
background-image: url(../images/actionIcons/zoomOut.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .custom-zoom-icon {
.ep-custom-zoom-icon {
background-image: url(../images/actionIcons/customZoom.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-to-fit-icon {
.ep-zoom-to-fit-icon {
background-image: url(../images/actionIcons/zoomToFit.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .zoom-icon {
.ep-zoom-icon {
background-image: url(../images/actionIcons/zoom.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-enable-tooltip-icon {
background-image: url(../images/actionIcons/enableTooltip.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-disable-tooltip-icon {
background-image: url(../images/actionIcons/disableTooltip.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}

View File

@@ -7,7 +7,6 @@ import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
import { ExecutionPlanWidgetBase } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase';
import { ExecutionPlan } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import * as DOM from 'vs/base/browser/dom';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
@@ -17,13 +16,16 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { zoomIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { Button } from 'sql/base/browser/ui/button/button';
import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
export class CustomZoomWidget extends ExecutionPlanWidgetBase {
private _actionBar: ActionBar;
public customZoomInputBox: InputBox;
constructor(
public readonly executionPlanView: ExecutionPlan,
public readonly widgetController: ExecutionPlanWidgetController,
public readonly executionPlanDiagram: AzdataGraphView,
@IContextViewService public readonly contextViewService: IContextViewService,
@IThemeService public readonly themeService: IThemeService,
@INotificationService public readonly notificationService: INotificationService
@@ -39,7 +41,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
});
attachInputBoxStyler(this.customZoomInputBox, this.themeService);
const currentZoom = executionPlanView.azdataGraphDiagram.graph.view.getScale() * 100;
const currentZoom = this.executionPlanDiagram.getZoomLevel();
// Setting initial value to graph's current zoom
this.customZoomInputBox.value = Math.round(currentZoom).toString();
@@ -50,7 +52,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
if (ev.key === 'Enter') {
await new CustomZoomAction().run(self);
} else if (ev.key === 'Escape') {
executionPlanView.planActionView.removeWidget(self);
this.widgetController.removeWidget(self);
}
};
@@ -87,8 +89,8 @@ export class CustomZoomAction extends Action {
public override async run(context: CustomZoomWidget): Promise<void> {
const newValue = parseInt(context.customZoomInputBox.value);
if (newValue <= 200 && newValue >= 1) { // Getting max and min zoom values from SSMS
context.executionPlanView.azdataGraphDiagram.graph.view.setScale(newValue / 100);
context.executionPlanView.planActionView.removeWidget(context);
context.executionPlanDiagram.setZoomLevel(newValue);
context.widgetController.removeWidget(context);
} else {
context.notificationService.error(
localize('invalidCustomZoomError', "Select a zoom value between 1 to 200")
@@ -106,7 +108,7 @@ export class CancelZoom extends Action {
}
public override async run(context: CustomZoomWidget): Promise<void> {
context.executionPlanView.planActionView.removeWidget(context);
context.widgetController.removeWidget(context);
}
}

View File

@@ -8,17 +8,22 @@ import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { Codicon } from 'vs/base/common/codicons';
import { InternalExecutionPlanNode, ExecutionPlan } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachInputBoxStyler, attachSelectBoxStyler } from 'sql/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Action } from 'vs/base/common/actions';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { isString } from 'vs/base/common/types';
import { AzdataGraphView, InternalExecutionPlanNode, SearchType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
const CONTAINS_DISPLAY_STRING = localize("executionPlanSearchTypeContains", 'Contains');
const EQUALS_DISPLAY_STRING = localize("executionPlanSearchTypeEquals", 'Equals');
const GREATER_DISPLAY_STRING = '>';
const LESSER_DISPLAY_STRING = '<';
const GREATER_EQUAL_DISPLAY_STRING = '>=';
const LESSER_EQUAL_DISPLAY_STRING = '<=';
const LESSER_AND_GREATER_DISPLAY_STRING = '<>';
export class NodeSearchWidget extends ExecutionPlanWidgetBase {
@@ -27,16 +32,18 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
private _searchTypeSelectBoxContainer: HTMLElement;
private _searchTypeSelectBox: SelectBox;
private _selectedSearchType: SearchType = SearchType.Equals;
private _searchTextInputBox: InputBox;
private _searchResults: string[] = [];
private _searchResults: InternalExecutionPlanNode[] = [];
private _currentSearchResultIndex = 0;
private _usePreviousSearchResult: boolean = false;
private _actionBar: ActionBar;
constructor(
public readonly executionPlanView: ExecutionPlan,
public readonly planActionView: ExecutionPlanWidgetController,
public readonly executionPlanDiagram: AzdataGraphView,
@IContextViewService public readonly contextViewService: IContextViewService,
@IThemeService public readonly themeService: IThemeService
@@ -46,7 +53,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
// property name dropdown
this._propertyNameSelectBoxContainer = DOM.$('.search-widget-property-name-select-box .dropdown-container');
this.container.appendChild(this._propertyNameSelectBoxContainer);
const propDropdownOptions = [...executionPlanView.graphElementPropertiesSet].sort();
const propDropdownOptions = executionPlanDiagram.getUniqueElementProperties();
this._propertyNameSelectBox = new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer);
attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService);
this._propertyNameSelectBoxContainer.style.width = '150px';
@@ -60,13 +67,40 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
this.container.appendChild(this._searchTypeSelectBoxContainer);
this._searchTypeSelectBox = new SelectBox([
EQUALS_DISPLAY_STRING,
CONTAINS_DISPLAY_STRING
CONTAINS_DISPLAY_STRING,
GREATER_DISPLAY_STRING,
LESSER_DISPLAY_STRING,
GREATER_EQUAL_DISPLAY_STRING,
LESSER_EQUAL_DISPLAY_STRING,
LESSER_AND_GREATER_DISPLAY_STRING
], EQUALS_DISPLAY_STRING, this.contextViewService, this._searchTypeSelectBoxContainer);
this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer);
attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService);
this._searchTypeSelectBoxContainer.style.width = '100px';
this._searchTypeSelectBox.onDidSelect(e => {
this._usePreviousSearchResult = false;
switch (e.selected) {
case EQUALS_DISPLAY_STRING:
this._selectedSearchType = SearchType.Equals;
break;
case CONTAINS_DISPLAY_STRING:
this._selectedSearchType = SearchType.Contains;
break;
case GREATER_DISPLAY_STRING:
this._selectedSearchType = SearchType.GreaterThan;
break;
case LESSER_DISPLAY_STRING:
this._selectedSearchType = SearchType.LesserThan;
break;
case GREATER_EQUAL_DISPLAY_STRING:
this._selectedSearchType = SearchType.GreaterThanEqualTo;
break;
case LESSER_EQUAL_DISPLAY_STRING:
this._selectedSearchType = SearchType.LesserThanEqualTo;
break;
case LESSER_AND_GREATER_DISPLAY_STRING:
this._selectedSearchType = SearchType.LesserAndGreaterThan;
}
});
// search text input box
@@ -103,108 +137,39 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
this._searchTextInputBox.focus();
}
public searchNode(returnPreviousResult: boolean): void {
// Searching again as the input params have changed
if (!this._usePreviousSearchResult) {
this._searchResults = [];
this._currentSearchResultIndex = 0; //Resetting search Index to 0;
this._usePreviousSearchResult = true;
// Doing depth first search in the graphModel to find nodes with matching prop values.
const graphModel = this.executionPlanView.graphModel;
const stack: InternalExecutionPlanNode[] = [];
stack.push(graphModel.root);
while (stack.length !== 0) {
const currentNode = stack.pop();
const matchingProp = currentNode.properties.find(e => e.name === this._propertyNameSelectBox.value);
// Searching only properties with string value.
if (isString(matchingProp?.value)) {
// If the search type is '=' we look for exact match and for 'contains' we look search string occurance in prop value
if (
this._searchTypeSelectBox.value === EQUALS_DISPLAY_STRING && matchingProp.value === this._searchTextInputBox.value ||
this._searchTypeSelectBox.value === CONTAINS_DISPLAY_STRING && matchingProp.value.includes(this._searchTextInputBox.value)
) {
this._searchResults.push(currentNode.id);
}
}
stack.push(...currentNode.children);
}
}
// Returning if no results found.
if (this._searchResults.length === 0) {
return;
}
// Getting the node at search index
const resultCell = this.executionPlanView.azdataGraphDiagram.graph.model.getCell(this._searchResults[this._currentSearchResultIndex]);
// Selecting the node on graph diagram
this.executionPlanView.azdataGraphDiagram.graph.setSelectionCell(resultCell);
this.executionPlanView.propertiesView.graphElement = this.executionPlanView.searchNodes(resultCell.id);
/**
* The selected graph node might be hidden/partially visible if the graph is overflowing the parent container.
* Apart from the obvious problems in aesthetics, user do not get a proper feedback of the search result.
* To solve this problem, we will have to scroll the node into view. (preferably into the center of the view)
* Steps for that:
* 1. Get the bounding rect of the node on graph.
* 2. Get the midpoint of the node's bounding rect.
* 3. Find the dimensions of the parent container.
* 4. Since, we are trying to position the node into center, we set the left top corner position of parent to
* below x and y.
* x = node's x midpoint - half the width of parent container
* y = node's y midpoint - half the height of parent container
* 5. If the x and y are negative, we set them 0 as that is the minimum possible scroll position.
* 6. Smoothly scroll to the left top x and y calculated in step 4, 5.
*/
const cellRect = this.executionPlanView.azdataGraphDiagram.graph.getCellBounds(resultCell);
const cellMidPoint: Point = {
x: cellRect.x + cellRect.width / 2,
y: cellRect.y + cellRect.height / 2,
};
const graphContainer = <HTMLElement>this.executionPlanView.azdataGraphDiagram.container;
const containerBoundingRect = graphContainer.getBoundingClientRect();
const leftTopScrollPoint: Point = {
x: cellMidPoint.x - containerBoundingRect.width / 2,
y: cellMidPoint.y - containerBoundingRect.height / 2
};
leftTopScrollPoint.x = leftTopScrollPoint.x < 0 ? 0 : leftTopScrollPoint.x;
leftTopScrollPoint.y = leftTopScrollPoint.y < 0 ? 0 : leftTopScrollPoint.y;
graphContainer.scrollTo({
left: leftTopScrollPoint.x,
top: leftTopScrollPoint.y,
behavior: 'smooth'
public searchNodes(): void {
this._currentSearchResultIndex = 0;
this._searchResults = this.executionPlanDiagram.searchNodes({
propertyName: this._propertyNameSelectBox.value,
value: this._searchTextInputBox.value,
searchType: this._selectedSearchType
});
// Updating search result index based on prev flag
if (returnPreviousResult) {
// going to the end of list if the index is 0 on prev
this._currentSearchResultIndex = this._currentSearchResultIndex === 0 ?
this._currentSearchResultIndex = this._searchResults.length - 1 :
this._currentSearchResultIndex = --this._currentSearchResultIndex;
} else {
// going to the front of list if we are at the last element
this._currentSearchResultIndex = this._currentSearchResultIndex === this._searchResults.length - 1 ?
this._currentSearchResultIndex = 0 :
this._currentSearchResultIndex = ++this._currentSearchResultIndex;
}
this._usePreviousSearchResult = true;
}
}
interface Point {
x: number;
y: number;
public next(): void {
if (!this._usePreviousSearchResult) {
this.searchNodes();
}
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;
}
public previous(): void {
if (!this._usePreviousSearchResult) {
this.searchNodes();
}
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;
}
}
export class GoToNextMatchAction extends Action {
@@ -216,7 +181,7 @@ export class GoToNextMatchAction extends Action {
}
public override async run(context: NodeSearchWidget): Promise<void> {
context.searchNode(false);
context.next();
}
}
@@ -229,7 +194,7 @@ export class GoToPreviousMatchAction extends Action {
}
public override async run(context: NodeSearchWidget): Promise<void> {
context.searchNode(true);
context.previous();
}
}
@@ -242,6 +207,6 @@ export class CancelSearch extends Action {
}
public override async run(context: NodeSearchWidget): Promise<void> {
context.executionPlanView.planActionView.removeWidget(context);
context.planActionView.removeWidget(context);
}
}

View File

@@ -15,6 +15,7 @@ export class ExecutionPlanInput extends EditorInput {
public static SCHEMA: string = 'executionplan';
private _content?: string;
public _executionPlanFileViewUUID: string;
constructor(
private _uri: URI,
@@ -23,6 +24,14 @@ export class ExecutionPlanInput extends EditorInput {
super();
}
public get executionPlanFileViewUUID(): string {
return this._executionPlanFileViewUUID;
}
public set executionPlanFileViewUUID(v: string) {
this._executionPlanFileViewUUID = v;
}
override get typeId(): string {
return ExecutionPlanInput.ID;
}
@@ -31,7 +40,10 @@ export class ExecutionPlanInput extends EditorInput {
return path.basename(this._uri.fsPath);
}
public get content(): string | undefined {
public async content(): Promise<string> {
if (!this._content) {
this._content = (await this._fileService.read(this._uri, { acceptTextOnly: true })).value;
}
return this._content;
}

View File

@@ -24,7 +24,8 @@ import { URI } from 'vs/base/common/uri';
import { attachTabbedPanelStyler } from 'sql/workbench/common/styler';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ILogService } from 'vs/platform/log/common/log';
import { ExecutionPlanTab } from 'sql/workbench/contrib/executionPlan/browser/executionPlan';
import { ExecutionPlanTab } from 'sql/workbench/contrib/executionPlan/browser/executionPlanTab';
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
class MessagesView extends Disposable implements IPanelView {
private messagePanel: MessagePanel;
@@ -223,7 +224,9 @@ export class QueryResultsView extends Disposable {
this.hideResults();
this.hideChart();
this.hideTopOperations();
this.hidePlan2();
this.hidePlan();
// clearing execution plans whenever a new query starts executing
this.executionPlanTab.view.clearPlans();
this.hideDynamicViewModelTabs();
this.input?.state.visibleTabs.clear();
if (this.input) {
@@ -248,12 +251,19 @@ export class QueryResultsView extends Disposable {
this.runnerDisposables.add(runner.onExecutionPlanAvailable(e => {
if (this.executionPlanTab) {
if (!this.input.state.visibleTabs.has(this.executionPlanTab.identifier)) {
this.showPlan2();
/**
* Adding execution plan graphs to execution plan file view
* when they become available
*/
const executionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
if (executionPlanFileViewCache) {
const view = executionPlanFileViewCache.executionPlanFileViewMap.get(
this.input.state.executionPlanState.executionPlanFileViewUUID
);
if (view) {
view.addGraphs(e.planGraphs);
}
}
// Adding graph to state and tab as they become available
this.input.state.executionPlanState.graphs.push(...e.planGraphs);
this.executionPlanTab.view.addGraphs(e.planGraphs);
}
}));
@@ -301,6 +311,7 @@ export class QueryResultsView extends Disposable {
this.runnerDisposables.add(runner.onQueryEnd(() => {
if (runner.isQueryPlan) {
runner.planXml.then(e => {
this.showPlan();
this.showTopOperations(e);
});
}
@@ -326,9 +337,9 @@ export class QueryResultsView extends Disposable {
if (input) {
this.resultsTab.view.state = input.state.gridPanelState;
this.executionPlanTab.view.addGraphs(input.state.executionPlanState.graphs);
this.topOperationsTab.view.setState(input.state.topOperationsState);
this.chartTab.view.state = input.state.chartState;
this.executionPlanTab.view.state = input.state.executionPlanState;
this.dynamicModelViewTabs.forEach((dynamicTab: QueryModelViewTab) => {
dynamicTab.captureState(input.state.dynamicModelViewTabsState);
});
@@ -368,6 +379,7 @@ export class QueryResultsView extends Disposable {
this.messagesTab.clear();
this.topOperationsTab.clear();
this.chartTab.clear();
this.executionPlanTab.clear();
this.dynamicModelViewTabs.forEach(t => t.clear());
}
@@ -416,7 +428,7 @@ export class QueryResultsView extends Disposable {
this.topOperationsTab.view.showPlan(xml);
}
public showPlan2() {
public showPlan() {
if (!this._panelView.contains(this.executionPlanTab.identifier)) {
this.input?.state.visibleTabs.add(this.executionPlanTab.identifier);
if (!this._panelView.contains(this.executionPlanTab.identifier)) {
@@ -432,11 +444,10 @@ export class QueryResultsView extends Disposable {
}
}
public hidePlan2() {
public hidePlan() {
if (this._panelView.contains(this.executionPlanTab.identifier)) {
this.executionPlanTab.clear();
this.input.state.executionPlanState.clearExecutionPlanState();
this._panelView.removeTab(this.executionPlanTab.identifier);
this.executionPlanTab.clear();
}
}