diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts index 3057db139b..c99d4c01be 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts @@ -19,18 +19,20 @@ import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticip import * as DOM from 'vs/base/browser/dom'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { localize } from 'vs/nls'; -import { addIconClassName, openPropertiesIconClassNames, polygonBorderColor, polygonFillColor, resetZoomIconClassName, splitScreenHorizontallyIconClassName, splitScreenVerticallyIconClassName, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants'; +import { addIconClassName, openPropertiesIconClassNames, polygonBorderColor, polygonFillColor, resetZoomIconClassName, searchIconClassNames, splitScreenHorizontallyIconClassName, splitScreenVerticallyIconClassName, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { extname } from 'vs/base/common/path'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { InfoBox } from 'sql/workbench/browser/ui/infoBox/infoBox'; import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner'; -import { errorForeground, listHoverBackground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorWidgetBackground, errorForeground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { ExecutionPlanViewHeader } from 'sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader'; import { attachSelectBoxStyler } from 'sql/platform/theme/common/styler'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { generateUuid } from 'vs/base/common/uuid'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController'; +import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget'; export class ExecutionPlanComparisonEditorView { @@ -45,6 +47,8 @@ export class ExecutionPlanComparisonEditorView { private _resetZoomAction: Action; private _propertiesAction: Action; private _toggleOrientationAction: Action; + private _searchNodeAction: Action; + private _searchNodeActionForAddedPlan: Action; private _planComparisonContainer: HTMLElement; @@ -58,6 +62,11 @@ export class ExecutionPlanComparisonEditorView { private _verticalSash: Sash; private _orientation: ExecutionPlanCompareOrientation = ExecutionPlanCompareOrientation.Horizontal; + private _topWidgetContainer: HTMLElement; + public topWidgetController: ExecutionPlanWidgetController; + private _bottomWidgetContainer: HTMLElement; + public bottomWidgetController: ExecutionPlanWidgetController; + private _placeholderContainer: HTMLElement; private _placeholderInfoboxContainer: HTMLElement; private _placeholderInfobox: InfoBox; @@ -77,10 +86,11 @@ export class ExecutionPlanComparisonEditorView { bottomPolygon: azdata.executionPlan.ExecutionGraphComparisonResult }> = new Map(); - private get _activeTopPlanDiagram(): AzdataGraphView { + public get activeTopPlanDiagram(): AzdataGraphView | undefined { if (this.topPlanDiagrams.length > 0) { return this.topPlanDiagrams[this._activeTopPlanIndex]; } + return undefined; } @@ -95,10 +105,11 @@ export class ExecutionPlanComparisonEditorView { private _bottomSimilarNode: Map = new Map(); private _latestRequestUuid: string; - private get _activeBottomPlanDiagram(): AzdataGraphView { + public get activeBottomPlanDiagram(): AzdataGraphView | undefined { if (this.bottomPlanDiagrams.length > 0) { return this.bottomPlanDiagrams[this._activeBottomPlanIndex]; } + return undefined; } @@ -108,7 +119,7 @@ export class ExecutionPlanComparisonEditorView { constructor( parentContainer: HTMLElement, - @IInstantiationService private _instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private themeService: IThemeService, @IExecutionPlanService private _executionPlanService: IExecutionPlanService, @IFileDialogService private _fileDialogService: IFileDialogService, @@ -139,6 +150,8 @@ export class ExecutionPlanComparisonEditorView { this._zoomToFitAction = new ZoomToFitAction(); this._propertiesAction = this._instantiationService.createInstance(PropertiesAction); this._toggleOrientationAction = new ToggleOrientation(); + this._searchNodeAction = this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Primary); + this._searchNodeActionForAddedPlan = this._instantiationService.createInstance(SearchNodeAction, PlanIdentifier.Added); this._resetZoomAction = new ZoomReset(); const content: ITaskbarContent[] = [ { action: this._addExecutionPlanAction }, @@ -147,7 +160,9 @@ export class ExecutionPlanComparisonEditorView { { action: this._zoomToFitAction }, { action: this._resetZoomAction }, { action: this._toggleOrientationAction }, - { action: this._propertiesAction } + { action: this._propertiesAction }, + { action: this._searchNodeAction }, + { action: this._searchNodeActionForAddedPlan } ]; this._taskbar.setContent(content); this.container.appendChild(this._taskbarContainer); @@ -158,6 +173,7 @@ export class ExecutionPlanComparisonEditorView { this.container.appendChild(this._planComparisonContainer); this.initializeSplitView(); this.initializeProperties(); + this.initializeWidgetControllers(); } private initializeSplitView(): void { @@ -185,10 +201,9 @@ export class ExecutionPlanComparisonEditorView { this._topPlanDropdown = new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._topPlanDropdownContainer); this._topPlanDropdown.render(this._topPlanDropdownContainer); this._topPlanDropdown.onDidSelect(async (e) => { - if (this._activeBottomPlanDiagram) { - this._activeBottomPlanDiagram.clearSubtreePolygon(); - } - this._activeTopPlanDiagram.clearSubtreePolygon(); + this.activeBottomPlanDiagram?.clearSubtreePolygon(); + this.activeTopPlanDiagram?.clearSubtreePolygon(); + this._topPlanDiagramContainers.forEach(c => { c.style.display = 'none'; }); @@ -211,10 +226,9 @@ export class ExecutionPlanComparisonEditorView { this._bottomPlanDropdown = new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._bottomPlanDropdownContainer); this._bottomPlanDropdown.render(this._bottomPlanDropdownContainer); this._bottomPlanDropdown.onDidSelect(async (e) => { - this._activeBottomPlanDiagram.clearSubtreePolygon(); - if (this._activeTopPlanDiagram) { - this._activeTopPlanDiagram.clearSubtreePolygon(); - } + this.activeBottomPlanDiagram?.clearSubtreePolygon(); + this.activeTopPlanDiagram?.clearSubtreePolygon(); + this._bottomPlanDiagramContainers.forEach(c => { c.style.display = 'none'; }); @@ -273,6 +287,16 @@ export class ExecutionPlanComparisonEditorView { this._planComparisonContainer.appendChild(this._propertiesContainer); } + private initializeWidgetControllers(): void { + this._topWidgetContainer = DOM.$('.plan-action-container'); + this._topPlanContainer.appendChild(this._topWidgetContainer); + this.topWidgetController = new ExecutionPlanWidgetController(this._topWidgetContainer); + + this._bottomWidgetContainer = DOM.$('.plan-action-container'); + this._bottomPlanContainer.appendChild(this._bottomWidgetContainer); + this.bottomWidgetController = new ExecutionPlanWidgetController(this._bottomWidgetContainer); + } + public async openAndAddExecutionPlanFile(): Promise { try { const openedFileUris = await this._fileDialogService.showOpenDialog({ @@ -325,11 +349,15 @@ export class ExecutionPlanComparisonEditorView { const id = e.id.replace(`element-`, ''); if (this._topSimilarNode.has(id)) { const similarNode = this._topSimilarNode.get(id); - const element = this._activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); - if (similarNode.matchingNodesId.find(m => this._activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { - return; + + if (this.activeBottomPlanDiagram) { + const element = this.activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); + if (similarNode.matchingNodesId.find(m => this.activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { + return; + } + + this.activeBottomPlanDiagram.selectElement(element); } - this._activeBottomPlanDiagram.selectElement(element); } }); this.topPlanDiagrams.push(diagram); @@ -343,6 +371,7 @@ export class ExecutionPlanComparisonEditorView { this._resetZoomAction.enabled = true; this._zoomToFitAction.enabled = true; this._toggleOrientationAction.enabled = true; + this._searchNodeAction.enabled = true; } else { this._bottomPlanDiagramModels = executionPlanGraphs; this._bottomPlanDropdown.setOptions(executionPlanGraphs.map((e, index) => { @@ -360,11 +389,15 @@ export class ExecutionPlanComparisonEditorView { const id = e.id.replace(`element-`, ''); if (this._bottomSimilarNode.has(id)) { const similarNode = this._bottomSimilarNode.get(id); - const element = this._activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); - if (similarNode.matchingNodesId.find(m => this._activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { - return; + + if (this.activeTopPlanDiagram) { + const element = this.activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); + if (similarNode.matchingNodesId.find(m => this.activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { + return; + } + + this.activeTopPlanDiagram.selectElement(element); } - this._activeTopPlanDiagram.selectElement(element); } }); this.bottomPlanDiagrams.push(diagram); @@ -373,15 +406,17 @@ export class ExecutionPlanComparisonEditorView { this._bottomPlanDropdown.select(preSelectIndex); this._propertiesView.setBottomElement(executionPlanGraphs[0].root); this._addExecutionPlanAction.enabled = false; + this._searchNodeActionForAddedPlan.enabled = true; } this.refreshSplitView(); } private async getSkeletonNodes(): Promise { - if (!this._activeBottomPlanDiagram) { + if (!this.activeBottomPlanDiagram) { return; } - this._progressService.withProgress( + + await this._progressService.withProgress( { location: ProgressLocation.Notification, title: localize('epCompare.comparisonProgess', "Loading similar areas in compared plans"), @@ -406,9 +441,11 @@ export class ExecutionPlanComparisonEditorView { this.getSimilarSubtrees(result.secondComparisonResult, true); let colorIndex = 0; this._polygonRootsMap.forEach((v, k) => { - this._activeTopPlanDiagram.drawSubtreePolygon(v.topPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]); - this._activeBottomPlanDiagram.drawSubtreePolygon(v.bottomPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]); - colorIndex += 1; + if (this.activeTopPlanDiagram && this.activeBottomPlanDiagram) { + this.activeTopPlanDiagram.drawSubtreePolygon(v.topPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]); + this.activeBottomPlanDiagram.drawSubtreePolygon(v.bottomPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]); + colorIndex += 1; + } }); } return; @@ -500,37 +537,40 @@ export class ExecutionPlanComparisonEditorView { } public zoomIn(): void { - this._activeTopPlanDiagram.zoomIn(); - this._activeBottomPlanDiagram.zoomIn(); + this.activeTopPlanDiagram?.zoomIn(); + this.activeBottomPlanDiagram?.zoomIn(); + this.syncZoom(); } public zoomOut(): void { - this._activeTopPlanDiagram.zoomOut(); - this._activeBottomPlanDiagram.zoomOut(); + this.activeTopPlanDiagram?.zoomOut(); + this.activeBottomPlanDiagram?.zoomOut(); + this.syncZoom(); } public zoomToFit(): void { - this._activeTopPlanDiagram.zoomToFit(); - this._activeBottomPlanDiagram.zoomToFit(); + this.activeTopPlanDiagram?.zoomToFit(); + this.activeBottomPlanDiagram.zoomToFit(); + this.syncZoom(); } public resetZoom(): void { - if (this._activeTopPlanDiagram) { - this._activeTopPlanDiagram.setZoomLevel(100); - } - if (this._activeBottomPlanDiagram) { - this._activeBottomPlanDiagram.setZoomLevel(100); - } + this.activeTopPlanDiagram?.setZoomLevel(100); + this.activeBottomPlanDiagram?.setZoomLevel(100); } private syncZoom(): void { - if (this._activeTopPlanDiagram.getZoomLevel() < this._activeBottomPlanDiagram.getZoomLevel()) { - this._activeBottomPlanDiagram.setZoomLevel(this._activeTopPlanDiagram.getZoomLevel()); + if (this.activeTopPlanDiagram === undefined && this.activeBottomPlanDiagram === undefined) { + return; + } + + if (this.activeTopPlanDiagram.getZoomLevel() < this.activeBottomPlanDiagram.getZoomLevel()) { + this.activeBottomPlanDiagram.setZoomLevel(this.activeTopPlanDiagram.getZoomLevel()); } else { - this._activeTopPlanDiagram.setZoomLevel(this._activeBottomPlanDiagram.getZoomLevel()); + this.activeTopPlanDiagram.setZoomLevel(this.activeBottomPlanDiagram.getZoomLevel()); } } } @@ -640,6 +680,41 @@ class PropertiesAction extends Action { } } +enum PlanIdentifier { + Primary = 0, + Added = 1 +} + +class SearchNodeAction extends Action { + public static ID = 'epCompare.searchNodeAction'; + public static LABEL = localize('epCompare.searchNodeAction', 'Find Node'); + public static LABEL_FOR_ADDED_PLAN = localize('epCompare.searchNodeActionAddedPlan', 'Find Node - Added Plan'); + + constructor(private readonly _planIdentifier: PlanIdentifier, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAdsTelemetryService private readonly _telemetryService: IAdsTelemetryService) { + const getLabelForAction = () => { + return _planIdentifier === PlanIdentifier.Added ? SearchNodeAction.LABEL_FOR_ADDED_PLAN : SearchNodeAction.LABEL; + }; + + super(SearchNodeAction.ID, getLabelForAction(), searchIconClassNames); + this.enabled = false; + } + + public override async run(context: ExecutionPlanComparisonEditorView): Promise { + let executionPlan = this._planIdentifier === PlanIdentifier.Added ? context.activeBottomPlanDiagram : context.activeTopPlanDiagram; + let widgetController = this._planIdentifier === PlanIdentifier.Added ? context.bottomWidgetController : context.topWidgetController; + + if (executionPlan) { + this._telemetryService + .createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.FindNode) + .withAdditionalProperties({ source: 'ComparisonView' }) + .send(); + + let nodeSearchWidget = this._instantiationService.createInstance(NodeSearchWidget, widgetController, executionPlan); + widgetController.toggleWidget(nodeSearchWidget); + } + } +} + class HorizontalSash implements IHorizontalSashLayoutProvider { constructor(private _context: ExecutionPlanComparisonEditorView) { } @@ -695,4 +770,28 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = } `); } + const shadow = theme.getColor(widgetShadow); + if (shadow) { + collector.addRule(` + .eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-action-container .child { + box-shadow: 0 0 8px 2px ${shadow}; + } + `); + } + const widgetBackgroundColor = theme.getColor(editorWidgetBackground); + if (widgetBackgroundColor) { + collector.addRule(` + .eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-action-container .child { + background-color: ${widgetBackgroundColor}; + } + `); + } + const widgetBorderColor = theme.getColor(contrastBorder); + if (widgetBorderColor) { + collector.addRule(` + .eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-action-container .child { + border: 1px solid ${widgetBorderColor}; + } + `); + } }); diff --git a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css index 8d51622b94..dd94a62939 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css +++ b/src/sql/workbench/contrib/executionPlan/browser/media/executionPlan.css @@ -55,13 +55,15 @@ However we always want it to be the width of the container it is resizing. } /* views created by the action-bar actions */ -.eps-container .execution-plan .plan .plan-action-container .child { +.eps-container .execution-plan .plan .plan-action-container .child, +.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-action-container .child { flex: 0 0 25px; margin-left: auto; } /* Search node action view */ -.eps-container .execution-plan .plan .plan-action-container .search-node-widget { +.eps-container .execution-plan .plan .plan-action-container .search-node-widget, +.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-action-container .search-node-widget { display: flex; flex-direction: row; padding: 5px; @@ -70,12 +72,14 @@ However we always want it to be the width of the container it is resizing. } /* input bar styling in search node action view */ -.eps-container .execution-plan .plan .plan-action-container .search-node-widget .select-container { +.eps-container .execution-plan .plan .plan-action-container .search-node-widget .select-container, +.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-action-container .search-node-widget .select-container { margin-left: 5px; } /* styling for select element in search node action view */ -.eps-container .execution-plan .plan .plan-action-container .search-node-widget .select-container>select { +.eps-container .execution-plan .plan .plan-action-container .search-node-widget .select-container>select, +.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-action-container .search-node-widget .select-container>select { height: 100%; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts index d81577e703..db85f5a0cc 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/widgets/nodeSearchWidget.ts @@ -44,17 +44,16 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { constructor( public readonly planActionView: ExecutionPlanWidgetController, - public readonly executionPlanDiagram: AzdataGraphView, + private readonly _executionPlanDiagram: AzdataGraphView, @IContextViewService public readonly contextViewService: IContextViewService, @IThemeService public readonly themeService: IThemeService - ) { super(DOM.$('.search-node-widget'), 'searchWidget'); // property name dropdown this._propertyNameSelectBoxContainer = DOM.$('.search-widget-property-name-select-box .dropdown-container'); this.container.appendChild(this._propertyNameSelectBoxContainer); - const propDropdownOptions = executionPlanDiagram.getUniqueElementProperties(); + const propDropdownOptions = this._executionPlanDiagram.getUniqueElementProperties(); this._propertyNameSelectBox = new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer); attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService); this._propertyNameSelectBoxContainer.style.width = '150px'; @@ -140,11 +139,12 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { public searchNodes(): void { this._currentSearchResultIndex = 0; - this._searchResults = this.executionPlanDiagram.searchNodes({ + this._searchResults = this._executionPlanDiagram.searchNodes({ propertyName: this._propertyNameSelectBox.value, value: this._searchTextInputBox.value, searchType: this._selectedSearchType }); + this._usePreviousSearchResult = true; } @@ -153,8 +153,8 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { this.searchNodes(); } - this.executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]); - this.executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]); + 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; @@ -165,8 +165,8 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase { this.searchNodes(); } - this.executionPlanDiagram.centerElement(this._searchResults[this._currentSearchResultIndex]); - this.executionPlanDiagram.selectElement(this._searchResults[this._currentSearchResultIndex]); + 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;