/*--------------------------------------------------------------------------------------------- * 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++) { (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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; } } }