From a7a337f063e089a4df1ed35c4ce5515fa5a2bc74 Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Thu, 11 Aug 2022 11:22:12 -0700 Subject: [PATCH] Adding properties grid enhancements in execution plan (#20208) * init push * Fixing properties in plan comparison * Add long Text Cell viewer * Disabling auto edit by default * Removing text editor --- .../ui/table/plugins/textWithIconColumn.ts | 4 +- .../executionPlanComparisonEditorView.ts | 4 +- .../executionPlanComparisonPropertiesView.ts | 14 ++-- .../browser/executionPlanEditor.ts | 10 +++ .../browser/executionPlanPropertiesView.ts | 10 ++- .../executionPlanPropertiesViewBase.ts | 81 ++++++++++++++++++- .../browser/executionPlanView.ts | 6 +- 7 files changed, 110 insertions(+), 19 deletions(-) diff --git a/src/sql/base/browser/ui/table/plugins/textWithIconColumn.ts b/src/sql/base/browser/ui/table/plugins/textWithIconColumn.ts index 26f9e49ad3..29d3c937ee 100644 --- a/src/sql/base/browser/ui/table/plugins/textWithIconColumn.ts +++ b/src/sql/base/browser/ui/table/plugins/textWithIconColumn.ts @@ -7,6 +7,7 @@ import { escape } from 'sql/base/common/strings'; import { getIconCellValue, IconColumnOptions, TableColumn } from 'sql/base/browser/ui/table/plugins/tableColumn'; export interface TextWithIconColumnOptions extends IconColumnOptions { + editor?: any; } export class TextWithIconColumn implements TableColumn { @@ -26,7 +27,8 @@ export class TextWithIconColumn implements TableColum width: this.options.width, name: this.options.name, cssClass: 'slick-icon-cell', - headerCssClass: this.options.headerCssClass + headerCssClass: this.options.headerCssClass, + editor: this.options.editor }; } } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts index c99d4c01be..1b588b0d62 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView.ts @@ -352,7 +352,7 @@ export class ExecutionPlanComparisonEditorView { if (this.activeBottomPlanDiagram) { const element = this.activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); - if (similarNode.matchingNodesId.find(m => this.activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { + if (this.activeBottomPlanDiagram.getSelectedElement() && similarNode.matchingNodesId.find(m => this.activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { return; } @@ -392,7 +392,7 @@ export class ExecutionPlanComparisonEditorView { if (this.activeTopPlanDiagram) { const element = this.activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]); - if (similarNode.matchingNodesId.find(m => this.activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { + if (this.activeTopPlanDiagram.getSelectedElement() && similarNode.matchingNodesId.find(m => this.activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) { return; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts index c1e315333a..8707637890 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView.ts @@ -15,6 +15,8 @@ import { InternalExecutionPlanElement } from 'sql/workbench/contrib/executionPla import { executionPlanComparisonPropertiesDifferent, executionPlanComparisonPropertiesUpArrow, executionPlanComparisonPropertiesDownArrow } from 'sql/workbench/contrib/executionPlan/browser/constants'; import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes'; import { TextWithIconColumn } from 'sql/base/browser/ui/table/plugins/textWithIconColumn'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export enum ExecutionPlanCompareOrientation { Horizontal = 'horizontal', @@ -37,8 +39,10 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti public constructor( parentContainer: HTMLElement, @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IContextMenuService contextMenuService: IContextMenuService ) { - super(parentContainer, themeService); + super(parentContainer, themeService, instantiationService, contextMenuService); this._model = {}; this._parentContainer.style.display = 'none'; const header = DOM.$('.compare-operation-name'); @@ -128,30 +132,27 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti name: localize('nodePropertyViewNameNameColumnHeader', "Name"), field: 'name', width: 200, - editor: Slick.Editors.Text, headerCssClass: 'prop-table-header', formatter: textFormatter }); columns.push({ - id: 'value', + id: 'value1', name: getPropertyViewNameValueColumnTopHeaderForOrientation(this._orientation), field: 'primary', width: 150, - editor: Slick.Editors.Text, headerCssClass: 'prop-table-header', formatter: textFormatter }); } if (this._model.bottomElement) { columns.push(new TextWithIconColumn({ - id: 'value', + id: 'value2', name: getPropertyViewNameValueColumnBottomHeaderForOrientation(this._orientation), field: 'secondary', width: 150, headerCssClass: 'prop-table-header', }).definition); } - return columns; } @@ -313,6 +314,7 @@ export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanProperti } }); + return rows; } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts index 7133b59581..882a6f175d 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanEditor.ts @@ -48,6 +48,16 @@ export class ExecutionPlanEditor extends EditorPane { public layout(dimension: DOM.Dimension): void { } + override clearInput(): void { + const currentInput = this.input as ExecutionPlanInput; + // clearing old input view if present in the editor + if (currentInput?._executionPlanFileViewUUID) { + const oldView = this._viewCache.executionPlanFileViewMap.get(currentInput._executionPlanFileViewUUID); + oldView.onHide(this._parentContainer); + } + super.clearInput(); + } + public override async setInput(newInput: ExecutionPlanInput, options: IEditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { const oldInput = this.input as ExecutionPlanInput; diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts index 7082f647d3..437dfd42a3 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesView.ts @@ -11,6 +11,8 @@ import { removeLineBreaks } from 'sql/base/common/strings'; import { isString } from 'vs/base/common/types'; import { textFormatter } from 'sql/base/browser/ui/table/formatters'; import { ExecutionPlanPropertiesViewBase, PropertiesSortType } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase { // Div that holds the name of the element selected @@ -19,9 +21,11 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase public constructor( parentContainer: HTMLElement, - themeService: IThemeService + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IContextMenuService contextMenuService: IContextMenuService, ) { - super(parentContainer, themeService); + super(parentContainer, themeService, instantiationService, contextMenuService); this._model = {}; this._operationName = DOM.$('h3'); this._operationName.classList.add('operation-name'); @@ -91,7 +95,6 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase name: localize('nodePropertyViewNameNameColumnHeader', "Name"), field: 'name', width: 250, - editor: Slick.Editors.Text, headerCssClass: 'prop-table-header', formatter: textFormatter }, @@ -100,7 +103,6 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase name: localize('nodePropertyViewNameValueColumnHeader', "Value"), field: 'value', width: 250, - editor: Slick.Editors.Text, headerCssClass: 'prop-table-header', formatter: textFormatter } diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts index 3c4f2404a9..810c4a9be5 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase.ts @@ -16,6 +16,12 @@ import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants'; import { contrastBorder, listHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { TreeGrid } from 'sql/base/browser/ui/table/treeGrid'; import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { isString } from 'vs/base/common/types'; +import { CopyKeybind } from 'sql/base/browser/ui/table/plugins/copyKeybind.plugin'; export abstract class ExecutionPlanPropertiesViewBase implements IVerticalSashLayoutProvider { // Title bar with close button action @@ -42,9 +48,13 @@ export abstract class ExecutionPlanPropertiesViewBase implements IVerticalSashLa public resizeSash: Sash; + private _selectionModel: CellSelectionModel; + constructor( public _parentContainer: HTMLElement, - private _themeService: IThemeService + private _themeService: IThemeService, + @IInstantiationService private _instantiationService: IInstantiationService, + @IContextMenuService private _contextMenuService: IContextMenuService ) { const sashContainer = DOM.$('.properties-sash'); @@ -103,14 +113,38 @@ export abstract class ExecutionPlanPropertiesViewBase implements IVerticalSashLa const table = DOM.$('.table'); this._tableContainer.appendChild(table); + this._selectionModel = new CellSelectionModel(); + this._tableComponent = new TreeGrid(table, { columns: [] }, { rowHeight: RESULTS_GRID_DEFAULTS.rowHeight, forceFitColumns: true, - defaultColumnWidth: 120 + defaultColumnWidth: 120, + editable: true, + autoEdit: false }); attachTableStyler(this._tableComponent, this._themeService); + this._tableComponent.setSelectionModel(this._selectionModel); + + const contextMenuAction = [ + this._instantiationService.createInstance(CopyTableData), + ]; + + this._tableComponent.onContextMenu(e => { + this._contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => contextMenuAction, + getActionsContext: () => this.getCopyString() + }); + }); + + let copyHandler = new CopyKeybind(); + this._tableComponent.registerPlugin(copyHandler); + + copyHandler.onCopy(e => { + this._instantiationService.createInstance(CopyTableData).run(this.getCopyString()); + }); new ResizeObserver((e) => { this.resizeSash.layout(); @@ -118,6 +152,33 @@ export abstract class ExecutionPlanPropertiesViewBase implements IVerticalSashLa }).observe(_parentContainer); } + + public getCopyString(): string { + const selectedDataRange = this._selectionModel.getSelectedRanges()[0]; + let csvString = ''; + if (selectedDataRange) { + const data = []; + + for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) { + const dataRow = this._tableComponent.getData().getItem(rowIndex); + const row = []; + for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) { + const dataItem = dataRow[this._tableComponent.grid.getColumns()[colIndex].field]; + if (dataItem) { + row.push(isString(dataItem) ? dataItem : dataItem.displayText ?? dataItem.text ?? dataItem.title); + } else { + row.push(''); + } + } + data.push(row); + } + csvString = data.map(row => + row.map(x => `${x}`).join('\t') + ).join('\n'); + } + return csvString; + } + getVerticalSashLeft(sash: Sash): number { return 0; } @@ -273,3 +334,19 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = } }); + +export class CopyTableData extends Action { + public static ID = 'ep.CopyTableData'; + public static LABEL = localize('ep.topOperationsCopyTableData', "Copy"); + + constructor( + @IClipboardService private _clipboardService: IClipboardService + ) { + super(CopyTableData.ID, CopyTableData.LABEL, ''); + } + + public override async run(context: string): Promise { + + this._clipboardService.writeText(context); + } +} diff --git a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts index 2ef0c5adeb..d8d9528c0b 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/executionPlanView.ts @@ -14,7 +14,6 @@ import { IContextMenuService, IContextViewService } from 'vs/platform/contextvie 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'; @@ -75,7 +74,6 @@ export class ExecutionPlanView implements ISashLayoutProvider { private _executionPlanFileView: ExecutionPlanFileView, private _queryResultsView: QueryResultsView, @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, @@ -144,7 +142,7 @@ export class ExecutionPlanView implements ISashLayoutProvider { // container properties this._propContainer = DOM.$('.properties'); this.container.appendChild(this._propContainer); - this.propertiesView = new ExecutionPlanPropertiesView(this._propContainer, this._themeService); + this.propertiesView = this._instantiationService.createInstance(ExecutionPlanPropertiesView, this._propContainer); this._widgetContainer = DOM.$('.plan-action-container'); this._planContainer.appendChild(this._widgetContainer); @@ -197,7 +195,7 @@ export class ExecutionPlanView implements ISashLayoutProvider { this._actionBar.pushAction(actionBarActions, { icon: true, label: false }); const self = this; - this.container.oncontextmenu = (e: MouseEvent) => { + this._planContainer.oncontextmenu = (e: MouseEvent) => { if (contextMenuAction) { this._contextMenuService.showContextMenu({ getAnchor: () => {