mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Adding top operations to execution plans (#19902)
* Adding top operations to execution plans * Adding title to links * Fixing pr comments * Hiding top operations icon in execution plan editor * Reducing outline width, adding separator and removing placeholder text * Registering TopOperationsTabView
This commit is contained in:
19
src/sql/azdata.proposed.d.ts
vendored
19
src/sql/azdata.proposed.d.ts
vendored
@@ -1310,6 +1310,10 @@ declare module 'azdata' {
|
|||||||
* Warning/parallelism badges applicable to the current node
|
* Warning/parallelism badges applicable to the current node
|
||||||
*/
|
*/
|
||||||
badges: ExecutionPlanBadge[];
|
badges: ExecutionPlanBadge[];
|
||||||
|
/**
|
||||||
|
* Data to show in top operations table for the node.
|
||||||
|
*/
|
||||||
|
topOperationsData: TopOperationsDataItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutionPlanBadge {
|
export interface ExecutionPlanBadge {
|
||||||
@@ -1475,6 +1479,21 @@ declare module 'azdata' {
|
|||||||
*/
|
*/
|
||||||
compareExecutionPlanGraph(firstPlanFile: ExecutionPlanGraphInfo, secondPlanFile: ExecutionPlanGraphInfo): Thenable<ExecutionPlanComparisonResult>;
|
compareExecutionPlanGraph(firstPlanFile: ExecutionPlanGraphInfo, secondPlanFile: ExecutionPlanGraphInfo): Thenable<ExecutionPlanComparisonResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TopOperationsDataItem {
|
||||||
|
/**
|
||||||
|
* Column name for the top operation data item
|
||||||
|
*/
|
||||||
|
columnName: string;
|
||||||
|
/**
|
||||||
|
* Cell data type for the top operation data item
|
||||||
|
*/
|
||||||
|
dataType: ExecutionPlanGraphElementPropertyDataType;
|
||||||
|
/**
|
||||||
|
* Cell value for the top operation data item
|
||||||
|
*/
|
||||||
|
displayValue: string | number | boolean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export function hyperLinkFormatter(row: number | undefined, cell: any | undefine
|
|||||||
cellClasses += ' missing-value';
|
cellClasses += ' missing-value';
|
||||||
}
|
}
|
||||||
} else if (isHyperlinkCellValue(value)) {
|
} else if (isHyperlinkCellValue(value)) {
|
||||||
return `<a class="${cellClasses}" href="#" >${escape(value.displayText)}</a>`;
|
return `<a class="${cellClasses}" href="#" title="${escape(value.displayText)}">${escape(value.displayText)}</a>`;
|
||||||
}
|
}
|
||||||
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;
|
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,3 +31,8 @@ export interface FilterableColumn<T> extends Slick.Column<T> {
|
|||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
filterValues?: Array<string>;
|
filterValues?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ITableKeyboardEvent {
|
||||||
|
cell?: { row: number, cell: number };
|
||||||
|
event: KeyboardEvent;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'vs/css!./media/slick.grid';
|
|||||||
import 'vs/css!./media/slickColorTheme';
|
import 'vs/css!./media/slickColorTheme';
|
||||||
|
|
||||||
import { TableDataView } from './tableDataView';
|
import { TableDataView } from './tableDataView';
|
||||||
import { ITableSorter, ITableMouseEvent, ITableConfiguration, ITableStyles } from 'sql/base/browser/ui/table/interfaces';
|
import { ITableSorter, ITableMouseEvent, ITableConfiguration, ITableStyles, ITableKeyboardEvent } from 'sql/base/browser/ui/table/interfaces';
|
||||||
|
|
||||||
import * as DOM from 'vs/base/browser/dom';
|
import * as DOM from 'vs/base/browser/dom';
|
||||||
import { mixin } from 'vs/base/common/objects';
|
import { mixin } from 'vs/base/common/objects';
|
||||||
@@ -59,6 +59,9 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
|
|||||||
private _onColumnResize = new Emitter<void>();
|
private _onColumnResize = new Emitter<void>();
|
||||||
public readonly onColumnResize = this._onColumnResize.event;
|
public readonly onColumnResize = this._onColumnResize.event;
|
||||||
|
|
||||||
|
private _onKeyDown = new Emitter<ITableKeyboardEvent>();
|
||||||
|
public readonly onKeyDown = this._onKeyDown.event;
|
||||||
|
|
||||||
private _onBlur = new Emitter<void>();
|
private _onBlur = new Emitter<void>();
|
||||||
public readonly onBlur = this._onBlur.event;
|
public readonly onBlur = this._onBlur.event;
|
||||||
|
|
||||||
@@ -126,6 +129,17 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
|
|||||||
this.mapMouseEvent(this._grid.onHeaderClick, this._onHeaderClick);
|
this.mapMouseEvent(this._grid.onHeaderClick, this._onHeaderClick);
|
||||||
this.mapMouseEvent(this._grid.onDblClick, this._onDoubleClick);
|
this.mapMouseEvent(this._grid.onDblClick, this._onDoubleClick);
|
||||||
this._grid.onColumnsResized.subscribe(() => this._onColumnResize.fire());
|
this._grid.onColumnsResized.subscribe(() => this._onColumnResize.fire());
|
||||||
|
|
||||||
|
this._grid.onKeyDown.subscribe((e, args: Slick.OnKeyDownEventArgs<T>) => {
|
||||||
|
const evt = (e as JQuery.Event).originalEvent as KeyboardEvent;
|
||||||
|
this._onKeyDown.fire({
|
||||||
|
event: evt,
|
||||||
|
cell: {
|
||||||
|
row: args.row,
|
||||||
|
cell: args.cell
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public rerenderGrid() {
|
public rerenderGrid() {
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ export const executionPlanCompareIconClassName = 'ep-plan-compare-icon';
|
|||||||
export const executionPlanComparisonPropertiesDifferent = 'ep-properties-different';
|
export const executionPlanComparisonPropertiesDifferent = 'ep-properties-different';
|
||||||
export const executionPlanComparisonPropertiesDownArrow = 'ep-properties-down-arrow';
|
export const executionPlanComparisonPropertiesDownArrow = 'ep-properties-down-arrow';
|
||||||
export const executionPlanComparisonPropertiesUpArrow = 'ep-properties-up-arrow';
|
export const executionPlanComparisonPropertiesUpArrow = 'ep-properties-up-arrow';
|
||||||
|
export const executionPlanTopOperations = 'ep-top-operations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plan comparison polygon border colors
|
* Plan comparison polygon border colors
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class ExecutionPlanEditor extends EditorPane {
|
|||||||
} else {
|
} else {
|
||||||
// creating a new view for the new input
|
// creating a new view for the new input
|
||||||
newInput._executionPlanFileViewUUID = generateUuid();
|
newInput._executionPlanFileViewUUID = generateUuid();
|
||||||
newView = this._register(this._instantiationService.createInstance(ExecutionPlanFileView));
|
newView = this._register(this._instantiationService.createInstance(ExecutionPlanFileView, undefined));
|
||||||
newView.onShow(this._parentContainer);
|
newView.onShow(this._parentContainer);
|
||||||
newView.loadGraphFile({
|
newView.loadGraphFile({
|
||||||
graphFileContent: await newInput.content(),
|
graphFileContent: await newInput.content(),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { localize } from 'vs/nls';
|
|||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { contrastBorder, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
import { contrastBorder, editorWidgetBackground, foreground, listHoverBackground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||||
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||||
|
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView';
|
||||||
|
|
||||||
export class ExecutionPlanFileView {
|
export class ExecutionPlanFileView {
|
||||||
private _parent: HTMLElement;
|
private _parent: HTMLElement;
|
||||||
@@ -25,6 +26,7 @@ export class ExecutionPlanFileView {
|
|||||||
private _planCache: Map<string, azdata.executionPlan.ExecutionPlanGraph[]> = new Map();
|
private _planCache: Map<string, azdata.executionPlan.ExecutionPlanGraph[]> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private _queryResultsView: QueryResultsView,
|
||||||
@IInstantiationService private instantiationService: IInstantiationService,
|
@IInstantiationService private instantiationService: IInstantiationService,
|
||||||
@IExecutionPlanService private executionPlanService: IExecutionPlanService
|
@IExecutionPlanService private executionPlanService: IExecutionPlanService
|
||||||
) {
|
) {
|
||||||
@@ -56,7 +58,7 @@ export class ExecutionPlanFileView {
|
|||||||
public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) {
|
public addGraphs(newGraphs: azdata.executionPlan.ExecutionPlanGraph[] | undefined) {
|
||||||
if (newGraphs) {
|
if (newGraphs) {
|
||||||
newGraphs.forEach(g => {
|
newGraphs.forEach(g => {
|
||||||
const ep = this.instantiationService.createInstance(ExecutionPlanView, this._container, this._executionPlanViews.length + 1, this);
|
const ep = this.instantiationService.createInstance(ExecutionPlanView, this._container, this._executionPlanViews.length + 1, this, this._queryResultsView);
|
||||||
ep.model = g;
|
ep.model = g;
|
||||||
this._executionPlanViews.push(ep);
|
this._executionPlanViews.push(ep);
|
||||||
this.graphs.push(g);
|
this.graphs.push(g);
|
||||||
@@ -114,6 +116,13 @@ export class ExecutionPlanFileView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public scrollToNode(planId: number, nodeId: string): void {
|
||||||
|
this._executionPlanViews[planId].container.scrollIntoView(true);
|
||||||
|
const element = this._executionPlanViews[planId].executionPlanDiagram.getElementById(nodeId);
|
||||||
|
this._executionPlanViews[planId].executionPlanDiagram.centerElement(element);
|
||||||
|
this._executionPlanViews[planId].executionPlanDiagram.selectElement(element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ExecutionPlanState } from 'sql/workbench/common/editor/query/executionP
|
|||||||
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
|
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
|
||||||
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
|
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
|
||||||
import { generateUuid } from 'vs/base/common/uuid';
|
import { generateUuid } from 'vs/base/common/uuid';
|
||||||
|
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView';
|
||||||
|
|
||||||
export class ExecutionPlanTab implements IPanelTab {
|
export class ExecutionPlanTab implements IPanelTab {
|
||||||
public readonly title = localize('executionPlanTitle', "Query Plan (Preview)");
|
public readonly title = localize('executionPlanTitle', "Query Plan (Preview)");
|
||||||
@@ -19,9 +20,10 @@ export class ExecutionPlanTab implements IPanelTab {
|
|||||||
public readonly view: ExecutionPlanTabView;
|
public readonly view: ExecutionPlanTabView;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private _queryResultsView: QueryResultsView,
|
||||||
@IInstantiationService instantiationService: IInstantiationService,
|
@IInstantiationService instantiationService: IInstantiationService,
|
||||||
) {
|
) {
|
||||||
this.view = instantiationService.createInstance(ExecutionPlanTabView);
|
this.view = instantiationService.createInstance(ExecutionPlanTabView, this._queryResultsView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
@@ -37,8 +39,10 @@ export class ExecutionPlanTabView implements IPanelView {
|
|||||||
private _container: HTMLElement = DOM.$('.execution-plan-tab');
|
private _container: HTMLElement = DOM.$('.execution-plan-tab');
|
||||||
private _input: ExecutionPlanState;
|
private _input: ExecutionPlanState;
|
||||||
private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
|
private _viewCache: ExecutionPlanFileViewCache = ExecutionPlanFileViewCache.getInstance();
|
||||||
|
public currentFileView: ExecutionPlanFileView;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private _queryResultsView: QueryResultsView,
|
||||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -59,13 +63,14 @@ export class ExecutionPlanTabView implements IPanelView {
|
|||||||
} else {
|
} else {
|
||||||
// creating a new view for the new input
|
// creating a new view for the new input
|
||||||
newInput.executionPlanFileViewUUID = generateUuid();
|
newInput.executionPlanFileViewUUID = generateUuid();
|
||||||
newView = this._instantiationService.createInstance(ExecutionPlanFileView);
|
newView = this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView);
|
||||||
newView.onShow(this._container);
|
newView.onShow(this._container);
|
||||||
newView.addGraphs(
|
newView.addGraphs(
|
||||||
newInput.graphs
|
newInput.graphs
|
||||||
);
|
);
|
||||||
this._viewCache.executionPlanFileViewMap.set(newInput.executionPlanFileViewUUID, newView);
|
this._viewCache.executionPlanFileViewMap.set(newInput.executionPlanFileViewUUID, newView);
|
||||||
}
|
}
|
||||||
|
this.currentFileView = newView;
|
||||||
this._input = newInput;
|
this._input = newInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +88,11 @@ export class ExecutionPlanTabView implements IPanelView {
|
|||||||
if (currentView) {
|
if (currentView) {
|
||||||
currentView.onHide(this._container);
|
currentView.onHide(this._container);
|
||||||
this._input.graphs = [];
|
this._input.graphs = [];
|
||||||
currentView = this._instantiationService.createInstance(ExecutionPlanFileView);
|
currentView = this._instantiationService.createInstance(ExecutionPlanFileView, this._queryResultsView);
|
||||||
this._viewCache.executionPlanFileViewMap.set(this._input.executionPlanFileViewUUID, currentView);
|
this._viewCache.executionPlanFileViewMap.set(this._input.executionPlanFileViewUUID, currentView);
|
||||||
currentView.render(this._container);
|
currentView.render(this._container);
|
||||||
}
|
}
|
||||||
|
this.currentFileView = currentView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear() {
|
public clear() {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connect
|
|||||||
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
|
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
|
||||||
import { Progress } from 'vs/platform/progress/common/progress';
|
import { Progress } from 'vs/platform/progress/common/progress';
|
||||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||||
import { Action } from 'vs/base/common/actions';
|
import { Action, Separator } from 'vs/base/common/actions';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, executionPlanCompareIconClassName, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
|
import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, executionPlanCompareIconClassName, executionPlanTopOperations, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { VSBuffer } from 'vs/base/common/buffer';
|
import { VSBuffer } from 'vs/base/common/buffer';
|
||||||
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
|
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
|
||||||
@@ -37,6 +37,7 @@ import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
|||||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||||
import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
|
import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
|
||||||
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
|
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
|
||||||
|
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView';
|
||||||
|
|
||||||
export class ExecutionPlanView implements ISashLayoutProvider {
|
export class ExecutionPlanView implements ISashLayoutProvider {
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
private _model?: azdata.executionPlan.ExecutionPlanGraph;
|
private _model?: azdata.executionPlan.ExecutionPlanGraph;
|
||||||
|
|
||||||
// container for the view
|
// container for the view
|
||||||
private _container: HTMLElement;
|
public container: HTMLElement;
|
||||||
|
|
||||||
// action bar for the view
|
// action bar for the view
|
||||||
private _actionBarContainer: HTMLElement;
|
private _actionBarContainer: HTMLElement;
|
||||||
@@ -72,6 +73,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
private _parent: HTMLElement,
|
private _parent: HTMLElement,
|
||||||
private _graphIndex: number,
|
private _graphIndex: number,
|
||||||
private _executionPlanFileView: ExecutionPlanFileView,
|
private _executionPlanFileView: ExecutionPlanFileView,
|
||||||
|
private _queryResultsView: QueryResultsView,
|
||||||
@IInstantiationService public readonly _instantiationService: IInstantiationService,
|
@IInstantiationService public readonly _instantiationService: IInstantiationService,
|
||||||
@IThemeService private readonly _themeService: IThemeService,
|
@IThemeService private readonly _themeService: IThemeService,
|
||||||
@IContextViewService public readonly contextViewService: IContextViewService,
|
@IContextViewService public readonly contextViewService: IContextViewService,
|
||||||
@@ -84,18 +86,18 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
@IEditorService private _editorService: IEditorService
|
@IEditorService private _editorService: IEditorService
|
||||||
) {
|
) {
|
||||||
// parent container for query plan.
|
// parent container for query plan.
|
||||||
this._container = DOM.$('.execution-plan');
|
this.container = DOM.$('.execution-plan');
|
||||||
this._parent.appendChild(this._container);
|
this._parent.appendChild(this.container);
|
||||||
const sashContainer = DOM.$('.execution-plan-sash');
|
const sashContainer = DOM.$('.execution-plan-sash');
|
||||||
this._parent.appendChild(sashContainer);
|
this._parent.appendChild(sashContainer);
|
||||||
|
|
||||||
// resizing sash for the query plan.
|
// resizing sash for the query plan.
|
||||||
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL, size: 3 });
|
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL, size: 3 });
|
||||||
let originalHeight = this._container.offsetHeight;
|
let originalHeight = this.container.offsetHeight;
|
||||||
let originalTableHeight = 0;
|
let originalTableHeight = 0;
|
||||||
let change = 0;
|
let change = 0;
|
||||||
sash.onDidStart((e: ISashEvent) => {
|
sash.onDidStart((e: ISashEvent) => {
|
||||||
originalHeight = this._container.offsetHeight;
|
originalHeight = this.container.offsetHeight;
|
||||||
originalTableHeight = this.propertiesView.tableHeight;
|
originalTableHeight = this.propertiesView.tableHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,8 +114,8 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
* Since the parent container is flex, we will have
|
* Since the parent container is flex, we will have
|
||||||
* to change the flex-basis property to change the height.
|
* to change the flex-basis property to change the height.
|
||||||
*/
|
*/
|
||||||
this._container.style.minHeight = '200px';
|
this.container.style.minHeight = '200px';
|
||||||
this._container.style.flex = `0 0 ${newHeight}px`;
|
this.container.style.flex = `0 0 ${newHeight}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,7 +126,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._planContainer = DOM.$('.plan');
|
this._planContainer = DOM.$('.plan');
|
||||||
this._container.appendChild(this._planContainer);
|
this.container.appendChild(this._planContainer);
|
||||||
|
|
||||||
// container that holds plan header info
|
// container that holds plan header info
|
||||||
this._planHeaderContainer = DOM.$('.header');
|
this._planHeaderContainer = DOM.$('.header');
|
||||||
@@ -141,7 +143,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
|
|
||||||
// container properties
|
// container properties
|
||||||
this._propContainer = DOM.$('.properties');
|
this._propContainer = DOM.$('.properties');
|
||||||
this._container.appendChild(this._propContainer);
|
this.container.appendChild(this._propContainer);
|
||||||
this.propertiesView = new ExecutionPlanPropertiesView(this._propContainer, this._themeService);
|
this.propertiesView = new ExecutionPlanPropertiesView(this._propContainer, this._themeService);
|
||||||
|
|
||||||
this._widgetContainer = DOM.$('.plan-action-container');
|
this._widgetContainer = DOM.$('.plan-action-container');
|
||||||
@@ -150,7 +152,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
|
|
||||||
// container that holds action bar icons
|
// container that holds action bar icons
|
||||||
this._actionBarContainer = DOM.$('.action-bar-container');
|
this._actionBarContainer = DOM.$('.action-bar-container');
|
||||||
this._container.appendChild(this._actionBarContainer);
|
this.container.appendChild(this._actionBarContainer);
|
||||||
this._actionBar = new ActionBar(this._actionBarContainer, {
|
this._actionBar = new ActionBar(this._actionBarContainer, {
|
||||||
orientation: ActionsOrientation.VERTICAL, context: this
|
orientation: ActionsOrientation.VERTICAL, context: this
|
||||||
});
|
});
|
||||||
@@ -160,34 +162,42 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
new SavePlanFile(),
|
new SavePlanFile(),
|
||||||
new OpenPlanFile(),
|
new OpenPlanFile(),
|
||||||
this._instantiationService.createInstance(OpenQueryAction, 'ActionBar'),
|
this._instantiationService.createInstance(OpenQueryAction, 'ActionBar'),
|
||||||
this._instantiationService.createInstance(SearchNodeAction, 'ActionBar'),
|
|
||||||
this._instantiationService.createInstance(ZoomInAction, 'ActionBar'),
|
this._instantiationService.createInstance(ZoomInAction, 'ActionBar'),
|
||||||
this._instantiationService.createInstance(ZoomOutAction, 'ActionBar'),
|
this._instantiationService.createInstance(ZoomOutAction, 'ActionBar'),
|
||||||
this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar'),
|
this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar'),
|
||||||
this._instantiationService.createInstance(CustomZoomAction, 'ActionBar'),
|
this._instantiationService.createInstance(CustomZoomAction, 'ActionBar'),
|
||||||
|
this._instantiationService.createInstance(SearchNodeAction, 'ActionBar'),
|
||||||
this._instantiationService.createInstance(PropertiesAction, 'ActionBar'),
|
this._instantiationService.createInstance(PropertiesAction, 'ActionBar'),
|
||||||
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar'),
|
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar'),
|
||||||
this.actionBarToggleTopTip
|
this.actionBarToggleTopTip
|
||||||
];
|
];
|
||||||
this._actionBar.pushAction(actionBarActions, { icon: true, label: false });
|
|
||||||
|
|
||||||
// Setting up context menu
|
// Setting up context menu
|
||||||
this.contextMenuToggleTooltipAction = new ContextMenuTooltipToggle();
|
this.contextMenuToggleTooltipAction = new ContextMenuTooltipToggle();
|
||||||
const contextMenuAction = [
|
const contextMenuAction = [
|
||||||
new SavePlanFile(),
|
new SavePlanFile(),
|
||||||
new OpenPlanFile(),
|
new OpenPlanFile(),
|
||||||
this._instantiationService.createInstance(OpenQueryAction, 'ContextMenu'),
|
this._instantiationService.createInstance(OpenQueryAction, 'ContextMenu'),
|
||||||
this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu'),
|
new Separator(),
|
||||||
this._instantiationService.createInstance(ZoomInAction, 'ContextMenu'),
|
this._instantiationService.createInstance(ZoomInAction, 'ContextMenu'),
|
||||||
this._instantiationService.createInstance(ZoomOutAction, 'ContextMenu'),
|
this._instantiationService.createInstance(ZoomOutAction, 'ContextMenu'),
|
||||||
this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu'),
|
this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu'),
|
||||||
this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu'),
|
this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu'),
|
||||||
|
new Separator(),
|
||||||
|
this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu'),
|
||||||
this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'),
|
this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'),
|
||||||
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu'),
|
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu'),
|
||||||
this.contextMenuToggleTooltipAction
|
this.contextMenuToggleTooltipAction
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (this._queryResultsView) {
|
||||||
|
actionBarActions.push(this._instantiationService.createInstance(TopOperationsAction));
|
||||||
|
contextMenuAction.push(this._instantiationService.createInstance(TopOperationsAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._actionBar.pushAction(actionBarActions, { icon: true, label: false });
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
this._container.oncontextmenu = (e: MouseEvent) => {
|
this.container.oncontextmenu = (e: MouseEvent) => {
|
||||||
if (contextMenuAction) {
|
if (contextMenuAction) {
|
||||||
this._contextMenuService.showContextMenu({
|
this._contextMenuService.showContextMenu({
|
||||||
getAnchor: () => {
|
getAnchor: () => {
|
||||||
@@ -202,7 +212,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._container.onkeydown = (e: KeyboardEvent) => {
|
this.container.onkeydown = (e: KeyboardEvent) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||||||
let searchNodeAction = self._instantiationService.createInstance(SearchNodeAction, 'HotKey');
|
let searchNodeAction = self._instantiationService.createInstance(SearchNodeAction, 'HotKey');
|
||||||
searchNodeAction.run(self);
|
searchNodeAction.run(self);
|
||||||
@@ -219,12 +229,13 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
getHorizontalSashWidth?(sash: Sash): number {
|
getHorizontalSashWidth?(sash: Sash): number {
|
||||||
return this._container.clientWidth;
|
return this.container.clientWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPlanDiagram(container: HTMLElement) {
|
private createPlanDiagram(container: HTMLElement) {
|
||||||
this.executionPlanDiagram = this._instantiationService.createInstance(AzdataGraphView, container, this._model);
|
this.executionPlanDiagram = this._instantiationService.createInstance(AzdataGraphView, container, this._model);
|
||||||
this.executionPlanDiagram.onElementSelected(e => {
|
this.executionPlanDiagram.onElementSelected(e => {
|
||||||
|
container.focus();
|
||||||
this.propertiesView.graphElement = e;
|
this.propertiesView.graphElement = e;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -293,6 +304,11 @@ export class ExecutionPlanView implements ISashLayoutProvider {
|
|||||||
pinned: true
|
pinned: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openTopOperations() {
|
||||||
|
this._queryResultsView.switchToTopOperationsTab();
|
||||||
|
this._queryResultsView.scrollToTable(this._graphIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecutionPlanActionSource = 'ContextMenu' | 'ActionBar' | 'HotKey';
|
type ExecutionPlanActionSource = 'ContextMenu' | 'ActionBar' | 'HotKey';
|
||||||
@@ -548,3 +564,18 @@ export class CompareExecutionPlanAction extends Action {
|
|||||||
context.compareCurrentExecutionPlan();
|
context.compareCurrentExecutionPlan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TopOperationsAction extends Action {
|
||||||
|
|
||||||
|
public static ID = 'ep.topOperationsAction';
|
||||||
|
public static LABEL = localize('executionPlanTopOperationsAction', "Top Operations");
|
||||||
|
|
||||||
|
constructor
|
||||||
|
() {
|
||||||
|
super(TopOperationsAction.ID, TopOperationsAction.LABEL, executionPlanTopOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async run(context: ExecutionPlanView): Promise<void> {
|
||||||
|
context.openTopOperations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" ><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;} .icon-vs-fg{fill:#F0EFF1;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M14 14h-12v-12h12v12z" id="outline"/><path class="icon-vs-bg" d="M3 3v10h10v-10h-10zm3 9h-2v-2h2v2zm0-3h-2v-2h2v2zm0-3h-2v-2h2v2zm3 6h-2v-2h2v2zm0-3h-2v-2h2v2zm0-3h-2v-2h2v2zm3 6h-2v-2h2v2zm0-3h-2v-2h2v2zm0-3h-2v-2h2v2z" id="iconBg"/><path class="icon-vs-fg" d="M9 9h-2v-2h2v2zm0 1h-2v2h2v-2zm0-6h-2v2h2v-2zm3 3h-2v2h2v-2zm0 3h-2v2h2v-2zm0-6h-2v2h2v-2zm-6 0h-2v2h2v-2zm0 3h-2v2h2v-2zm0 3h-2v2h2v-2z" id="iconFg"/></svg>
|
||||||
|
After Width: | Height: | Size: 815 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16" width="16" height="16" ><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#0000;} .icon-vs-bg{fill:#fff;} .icon-vs-fg{fill:#0000;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M14 14h-12v-12h12v12z" id="outline"/><path class="icon-vs-bg" d="M3 3v10h10v-10h-10zm3 9h-2v-2h2v2zm0-3h-2v-2h2v2zm0-3h-2v-2h2v2zm3 6h-2v-2h2v2zm0-3h-2v-2h2v2zm0-3h-2v-2h2v2zm3 6h-2v-2h2v2zm0-3h-2v-2h2v2zm0-3h-2v-2h2v2z" id="iconBg"/><path class="icon-vs-fg" d="M9 9h-2v-2h2v2zm0 1h-2v2h2v-2zm0-6h-2v2h2v-2zm3 3h-2v2h2v-2zm0 3h-2v2h2v-2zm0-6h-2v2h2v-2zm-6 0h-2v2h2v-2zm0 3h-2v2h2v-2zm0 3h-2v2h2v-2z" id="iconFg"/></svg>
|
||||||
|
After Width: | Height: | Size: 807 B |
@@ -89,13 +89,15 @@ However we always want it to be the width of the container it is resizing.
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* execution plan header that contains the relative query cost, query statement and recommendations */
|
/* execution plan header that contains the relative query cost, query statement and recommendations */
|
||||||
.eps-container .execution-plan .plan .header {
|
.eps-container .execution-plan .plan .header,
|
||||||
|
.top-operations-tab .top-operations-container .query-row {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* execution plan header that contains the relative query cost, query statement and recommendations */
|
/* execution plan header that contains the relative query cost, query statement and recommendations */
|
||||||
.eps-container .execution-plan .plan .header .query-row {
|
.eps-container .execution-plan .plan .header .query-row,
|
||||||
|
.top-operations-tab .top-operations-container .query-row{
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
@@ -117,6 +119,8 @@ However we always want it to be the width of the container it is resizing.
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
outline-offset: -3px;
|
||||||
|
outline-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eps-container .properties {
|
.eps-container .properties {
|
||||||
@@ -613,6 +617,23 @@ However we always want it to be the width of the container it is resizing.
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eps-container .ep-top-operations {
|
||||||
|
background-image: url(../images/actionIcons/topOperations.svg);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.vs-dark .eps-container .ep-top-operations,
|
||||||
|
.hc-black .eps-container .ep-top-operations {
|
||||||
|
background-image: url(../images/actionIcons/topOperationsDark.svg);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.eps-container .comparison-editor {
|
.eps-container .comparison-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -759,3 +780,22 @@ However we always want it to be the width of the container it is resizing.
|
|||||||
.eps-container .comparison-editor .parent-row-styling {
|
.eps-container .comparison-editor .parent-row-styling {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-operations-tab {
|
||||||
|
overflow: scroll;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.top-operations-tab .top-operations-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-operations-tab .top-operations-container .table-container {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 { IPanelTab, IPanelView } from 'sql/base/browser/ui/panel/panel';
|
||||||
|
import { localize } from 'vs/nls';
|
||||||
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
|
import * as DOM from 'vs/base/browser/dom';
|
||||||
|
import { ExecutionPlanState } from 'sql/workbench/common/editor/query/executionPlanState';
|
||||||
|
import { Table } from 'sql/base/browser/ui/table/table';
|
||||||
|
import { hyperLinkFormatter, textFormatter } from 'sql/base/browser/ui/table/formatters';
|
||||||
|
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
|
||||||
|
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||||
|
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||||
|
import { ExecutionPlanViewHeader } from 'sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader';
|
||||||
|
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView';
|
||||||
|
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
|
||||||
|
import { CopyKeybind } from 'sql/base/browser/ui/table/plugins/copyKeybind.plugin';
|
||||||
|
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||||
|
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
|
import { listHoverBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||||
|
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||||
|
import { Action } from 'vs/base/common/actions';
|
||||||
|
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||||
|
import { ITableKeyboardEvent } from 'sql/base/browser/ui/table/interfaces';
|
||||||
|
import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
|
export class TopOperationsTab extends Disposable implements IPanelTab {
|
||||||
|
public readonly title = localize('topOperationsTabTitle', "Top Operations (Preview)");
|
||||||
|
public readonly identifier: string = 'TopOperationsTab';
|
||||||
|
public readonly view: TopOperationsTabView;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _queryResultsView: QueryResultsView,
|
||||||
|
@IInstantiationService instantiationService: IInstantiationService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.view = this._register(instantiationService.createInstance(TopOperationsTabView, this._queryResultsView));
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TopOperationsTabView extends Disposable implements IPanelView {
|
||||||
|
private _container: HTMLElement = DOM.$('.top-operations-tab');
|
||||||
|
private _input: ExecutionPlanState;
|
||||||
|
private _topOperationsContainers: HTMLElement[] = [];
|
||||||
|
private _tables: Table<Slick.SlickData>[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _queryResultsView: QueryResultsView,
|
||||||
|
@IThemeService private _themeService: IThemeService,
|
||||||
|
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||||
|
@IContextMenuService private _contextMenuService: IContextMenuService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public scrollToIndex(index: number) {
|
||||||
|
index = index - 1;
|
||||||
|
this._topOperationsContainers[index].scrollIntoView(true);
|
||||||
|
this._tables.forEach(t => {
|
||||||
|
t.getSelectionModel().setSelectedRanges([]);
|
||||||
|
});
|
||||||
|
this._tables[index].getSelectionModel().setSelectedRanges([new Slick.Range(0, 1, 0, 1)]);
|
||||||
|
this._tables[index].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public set state(newInput: ExecutionPlanState) {
|
||||||
|
const oldInput = this._input;
|
||||||
|
|
||||||
|
if (oldInput === newInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._input = newInput;
|
||||||
|
this.renderInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(parent: HTMLElement): void {
|
||||||
|
parent.appendChild(this._container);
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderInput(): void {
|
||||||
|
while (this._container.firstChild) {
|
||||||
|
this._container.removeChild(this._container.firstChild);
|
||||||
|
}
|
||||||
|
this._input.graphs.forEach((g, i) => {
|
||||||
|
this.convertExecutionPlanGraphToTable(g, i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public convertExecutionPlanGraphToTable(graph: azdata.executionPlan.ExecutionPlanGraph, index: number): Table<Slick.SlickData> {
|
||||||
|
|
||||||
|
const dataMap: { [key: string]: any }[] = [];
|
||||||
|
const columnValues: string[] = [];
|
||||||
|
|
||||||
|
const stack: azdata.executionPlan.ExecutionPlanNode[] = [];
|
||||||
|
stack.push(...graph.root.children);
|
||||||
|
while (stack.length !== 0) {
|
||||||
|
const node = stack.pop();
|
||||||
|
const row: { [key: string]: any } = {};
|
||||||
|
node.topOperationsData.forEach((d, i) => {
|
||||||
|
let displayText = d.displayValue.toString();
|
||||||
|
if (i === 0) {
|
||||||
|
row[d.columnName] = {
|
||||||
|
displayText: displayText,
|
||||||
|
linkOrCommand: ' ',
|
||||||
|
dataType: d.dataType
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
row[d.columnName] = {
|
||||||
|
text: displayText,
|
||||||
|
ariaLabel: d.displayValue,
|
||||||
|
dataType: d.dataType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (columnValues.indexOf(d.columnName) === -1) {
|
||||||
|
columnValues.splice(i, 0, d.columnName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
row['nodeId'] = node.id;
|
||||||
|
if (node.children) {
|
||||||
|
node.children.forEach(c => stack.push(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMap.push(row);
|
||||||
|
}
|
||||||
|
const columns = columnValues.map((c, i) => {
|
||||||
|
return <Slick.Column<Slick.SlickData>>{
|
||||||
|
id: c.toString(),
|
||||||
|
name: c,
|
||||||
|
field: c.toString(),
|
||||||
|
formatter: i === 0 ? hyperLinkFormatter : textFormatter,
|
||||||
|
sortable: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const topOperationContainer = DOM.$('.top-operations-container');
|
||||||
|
this._container.appendChild(topOperationContainer);
|
||||||
|
const header = this._instantiationService.createInstance(ExecutionPlanViewHeader, topOperationContainer, {
|
||||||
|
planIndex: index,
|
||||||
|
});
|
||||||
|
header.query = graph.query;
|
||||||
|
header.relativeCost = graph.root.relativeCost;
|
||||||
|
const tableContainer = DOM.$('.table-container');
|
||||||
|
topOperationContainer.appendChild(tableContainer);
|
||||||
|
this._topOperationsContainers.push(topOperationContainer);
|
||||||
|
|
||||||
|
const rowNumberColumn = new RowNumberColumn({ numberOfRows: dataMap.length });
|
||||||
|
columns.unshift(rowNumberColumn.getColumnDefinition());
|
||||||
|
|
||||||
|
let copyHandler = new CopyKeybind<any>();
|
||||||
|
this._register(copyHandler.onCopy(e => {
|
||||||
|
|
||||||
|
const selectedDataRange = selectionModel.getSelectedRanges()[0];
|
||||||
|
let csvString = '';
|
||||||
|
if (selectedDataRange) {
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) {
|
||||||
|
const dataRow = table.getData().getItem(rowIndex);
|
||||||
|
const row = [];
|
||||||
|
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
|
||||||
|
const dataItem = dataRow[table.columns[colIndex].field];
|
||||||
|
if (dataItem) {
|
||||||
|
row.push(dataItem.displayText ?? dataItem.text);
|
||||||
|
} else {
|
||||||
|
row.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.push(row);
|
||||||
|
}
|
||||||
|
csvString = data.map(row =>
|
||||||
|
row.map(x => `${x}`).join('\t')
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
const columns = [];
|
||||||
|
|
||||||
|
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
|
||||||
|
columns.push(table.columns[colIndex].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this._instantiationService.createInstance(CopyTableData).run({
|
||||||
|
selectedText: csvString
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectionModel = new CellSelectionModel<Slick.SlickData>();
|
||||||
|
|
||||||
|
const table = new Table<Slick.SlickData>(tableContainer, {
|
||||||
|
columns: columns,
|
||||||
|
sorter: (args) => {
|
||||||
|
const column = args.sortCol.field;
|
||||||
|
dataMap.sort((a, b) => {
|
||||||
|
let result = -1;
|
||||||
|
|
||||||
|
if (!a[column]) {
|
||||||
|
result = 1;
|
||||||
|
} else {
|
||||||
|
if (!b[column]) {
|
||||||
|
result = -1;
|
||||||
|
} else {
|
||||||
|
const dataType = a[column].dataType;
|
||||||
|
|
||||||
|
const aText = a[column].displayText ?? a[column].text;
|
||||||
|
const bText = b[column].displayText ?? b[column].text;
|
||||||
|
if (aText === bText) {
|
||||||
|
result = 0;
|
||||||
|
} else {
|
||||||
|
switch (dataType) {
|
||||||
|
case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.String:
|
||||||
|
case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.Boolean:
|
||||||
|
result = aText.localeCompare(bText);
|
||||||
|
break;
|
||||||
|
case sqlExtHostType.executionPlan.ExecutionPlanGraphElementPropertyDataType.Number:
|
||||||
|
result = parseFloat(aText) - parseFloat(bText);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args.sortAsc ? result : -result;
|
||||||
|
});
|
||||||
|
table.setData(dataMap);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
rowHeight: RESULTS_GRID_DEFAULTS.rowHeight,
|
||||||
|
forceFitColumns: false,
|
||||||
|
defaultColumnWidth: 120,
|
||||||
|
showRowNumber: true
|
||||||
|
});
|
||||||
|
table.setSelectionModel(selectionModel);
|
||||||
|
table.setData(dataMap);
|
||||||
|
|
||||||
|
table.registerPlugin(copyHandler);
|
||||||
|
|
||||||
|
table.setTableTitle(localize('topOperationsTableTitle', "Top Operations"));
|
||||||
|
this._register(table.onClick(e => {
|
||||||
|
if (e.cell.cell === 1) {
|
||||||
|
const row = table.getData().getItem(e.cell.row);
|
||||||
|
const nodeId = row['nodeId'];
|
||||||
|
const planId = index;
|
||||||
|
this._queryResultsView.switchToExecutionPlanTab();
|
||||||
|
this._queryResultsView.focusOnNode(planId, nodeId);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this._tables.push(table);
|
||||||
|
const contextMenuAction = [
|
||||||
|
this._instantiationService.createInstance(CopyTableData),
|
||||||
|
this._instantiationService.createInstance(CopyTableDataWithHeader),
|
||||||
|
this._instantiationService.createInstance(SelectAll)
|
||||||
|
];
|
||||||
|
|
||||||
|
this._register(table.onKeyDown((evt: ITableKeyboardEvent) => {
|
||||||
|
if (evt.event.ctrlKey && (evt.event.key === 'a' || evt.event.key === 'A')) {
|
||||||
|
selectionModel.setSelectedRanges([new Slick.Range(0, 1, table.getData().getLength() - 1, table.columns.length - 1)]);
|
||||||
|
table.focus();
|
||||||
|
evt.event.preventDefault();
|
||||||
|
evt.event.stopPropagation();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this._register(table.onContextMenu(e => {
|
||||||
|
const selectedDataRange = selectionModel.getSelectedRanges()[0];
|
||||||
|
let csvString = '';
|
||||||
|
let csvStringWithHeader = '';
|
||||||
|
if (selectedDataRange) {
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
for (let rowIndex = selectedDataRange.fromRow; rowIndex <= selectedDataRange.toRow; rowIndex++) {
|
||||||
|
const dataRow = table.getData().getItem(rowIndex);
|
||||||
|
const row = [];
|
||||||
|
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
|
||||||
|
const dataItem = dataRow[table.columns[colIndex].field];
|
||||||
|
if (dataItem) {
|
||||||
|
row.push(dataItem.displayText ?? dataItem.text);
|
||||||
|
} else {
|
||||||
|
row.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.push(row);
|
||||||
|
}
|
||||||
|
csvString = data.map(row =>
|
||||||
|
row.map(x => `${x}`).join('\t')
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
const columns = [];
|
||||||
|
|
||||||
|
for (let colIndex = selectedDataRange.fromCell; colIndex <= selectedDataRange.toCell; colIndex++) {
|
||||||
|
columns.push(table.columns[colIndex].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
csvStringWithHeader = columns.join('\t') + '\n' + csvString;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._contextMenuService.showContextMenu({
|
||||||
|
getAnchor: () => e.anchor,
|
||||||
|
getActions: () => contextMenuAction,
|
||||||
|
getActionsContext: () => ({
|
||||||
|
selectedText: csvString,
|
||||||
|
selectionModel: selectionModel,
|
||||||
|
table: table,
|
||||||
|
selectionTextWithHeader: csvStringWithHeader
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
attachTableStyler(table, this._themeService);
|
||||||
|
|
||||||
|
new ResizeObserver((e) => {
|
||||||
|
table.layout(new DOM.Dimension(tableContainer.clientWidth, tableContainer.clientHeight));
|
||||||
|
}).observe(tableContainer);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(dimension: DOM.Dimension): void {
|
||||||
|
this._container.style.width = dimension.width + 'px';
|
||||||
|
this._container.style.height = dimension.height + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
remove?(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow?(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide?(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
|
||||||
|
const menuBackgroundColor = theme.getColor(listHoverBackground);
|
||||||
|
if (menuBackgroundColor) {
|
||||||
|
collector.addRule(`
|
||||||
|
.top-operations-tab .top-operations-container .query-row {
|
||||||
|
background-color: ${menuBackgroundColor};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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: ContextMenuModel): Promise<void> {
|
||||||
|
|
||||||
|
this._clipboardService.writeText(context.selectedText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyTableDataWithHeader extends Action {
|
||||||
|
public static ID = 'ep.CopyTableData';
|
||||||
|
public static LABEL = localize('ep.topOperationsCopyWithHeader', "Copy with Header");
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@IClipboardService private _clipboardService: IClipboardService
|
||||||
|
) {
|
||||||
|
super(CopyTableDataWithHeader.ID, CopyTableDataWithHeader.LABEL, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async run(context: ContextMenuModel): Promise<void> {
|
||||||
|
|
||||||
|
this._clipboardService.writeText(context.selectionTextWithHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SelectAll extends Action {
|
||||||
|
public static ID = 'ep.SelectAllTableData';
|
||||||
|
public static LABEL = localize('ep.topOperationsSelectAll', "Select All");
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
) {
|
||||||
|
super(SelectAll.ID, SelectAll.LABEL, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async run(context: ContextMenuModel): Promise<void> {
|
||||||
|
context.selectionModel.setSelectedRanges([new Slick.Range(0, 1, context.table.getData().getLength() - 1, context.table.columns.length - 1)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextMenuModel {
|
||||||
|
selectedText?: string;
|
||||||
|
selectionModel?: CellSelectionModel<Slick.SlickData>;
|
||||||
|
table?: Table<Slick.SlickData>;
|
||||||
|
selectionTextWithHeader?: string;
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import QueryRunner from 'sql/workbench/services/query/common/queryRunner';
|
|||||||
import { MessagePanel } from 'sql/workbench/contrib/query/browser/messagePanel';
|
import { MessagePanel } from 'sql/workbench/contrib/query/browser/messagePanel';
|
||||||
import { GridPanel } from 'sql/workbench/contrib/query/browser/gridPanel';
|
import { GridPanel } from 'sql/workbench/contrib/query/browser/gridPanel';
|
||||||
import { ChartTab } from 'sql/workbench/contrib/charts/browser/chartTab';
|
import { ChartTab } from 'sql/workbench/contrib/charts/browser/chartTab';
|
||||||
import { TopOperationsTab } from 'sql/workbench/contrib/queryPlan/browser/topOperations';
|
|
||||||
import { QueryModelViewTab } from 'sql/workbench/contrib/query/browser/modelViewTab/queryModelViewTab';
|
import { QueryModelViewTab } from 'sql/workbench/contrib/query/browser/modelViewTab/queryModelViewTab';
|
||||||
import { GridPanelState } from 'sql/workbench/common/editor/query/gridTableState';
|
import { GridPanelState } from 'sql/workbench/common/editor/query/gridTableState';
|
||||||
|
|
||||||
@@ -26,6 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
|
|||||||
import { ILogService } from 'vs/platform/log/common/log';
|
import { ILogService } from 'vs/platform/log/common/log';
|
||||||
import { ExecutionPlanTab } from 'sql/workbench/contrib/executionPlan/browser/executionPlanTab';
|
import { ExecutionPlanTab } from 'sql/workbench/contrib/executionPlan/browser/executionPlanTab';
|
||||||
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
|
import { ExecutionPlanFileViewCache } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileViewCache';
|
||||||
|
import { TopOperationsTab } from 'sql/workbench/contrib/executionPlan/browser/topOperationsTab';
|
||||||
|
|
||||||
class MessagesView extends Disposable implements IPanelView {
|
class MessagesView extends Disposable implements IPanelView {
|
||||||
private messagePanel: MessagePanel;
|
private messagePanel: MessagePanel;
|
||||||
@@ -183,9 +183,8 @@ export class QueryResultsView extends Disposable {
|
|||||||
this.chartTab = this._register(new ChartTab(instantiationService));
|
this.chartTab = this._register(new ChartTab(instantiationService));
|
||||||
this._panelView = this._register(new TabbedPanel(container, { showHeaderWhenSingleView: true }));
|
this._panelView = this._register(new TabbedPanel(container, { showHeaderWhenSingleView: true }));
|
||||||
this._register(attachTabbedPanelStyler(this._panelView, themeService));
|
this._register(attachTabbedPanelStyler(this._panelView, themeService));
|
||||||
this.executionPlanTab = this._register(this.instantiationService.createInstance(ExecutionPlanTab));
|
this.executionPlanTab = this._register(this.instantiationService.createInstance(ExecutionPlanTab, this));
|
||||||
this.topOperationsTab = this._register(new TopOperationsTab(instantiationService));
|
this.topOperationsTab = this._register(this.instantiationService.createInstance(TopOperationsTab, this));
|
||||||
|
|
||||||
this._panelView.pushTab(this.resultsTab);
|
this._panelView.pushTab(this.resultsTab);
|
||||||
this._panelView.pushTab(this.messagesTab);
|
this._panelView.pushTab(this.messagesTab);
|
||||||
this._register(this._panelView.onTabChange(e => {
|
this._register(this._panelView.onTabChange(e => {
|
||||||
@@ -260,9 +259,11 @@ export class QueryResultsView extends Disposable {
|
|||||||
const view = executionPlanFileViewCache.executionPlanFileViewMap.get(
|
const view = executionPlanFileViewCache.executionPlanFileViewMap.get(
|
||||||
this.input.state.executionPlanState.executionPlanFileViewUUID
|
this.input.state.executionPlanState.executionPlanFileViewUUID
|
||||||
);
|
);
|
||||||
|
this.input.state.executionPlanState.graphs.push(...e.planGraphs);
|
||||||
if (view) {
|
if (view) {
|
||||||
view.addGraphs(e.planGraphs);
|
view.addGraphs(e.planGraphs);
|
||||||
}
|
}
|
||||||
|
this.topOperationsTab.view.renderInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -337,7 +338,7 @@ export class QueryResultsView extends Disposable {
|
|||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
this.resultsTab.view.state = input.state.gridPanelState;
|
this.resultsTab.view.state = input.state.gridPanelState;
|
||||||
this.topOperationsTab.view.setState(input.state.topOperationsState);
|
this.topOperationsTab.view.state = input.state.executionPlanState;
|
||||||
this.chartTab.view.state = input.state.chartState;
|
this.chartTab.view.state = input.state.chartState;
|
||||||
this.executionPlanTab.view.state = input.state.executionPlanState;
|
this.executionPlanTab.view.state = input.state.executionPlanState;
|
||||||
this.dynamicModelViewTabs.forEach((dynamicTab: QueryModelViewTab) => {
|
this.dynamicModelViewTabs.forEach((dynamicTab: QueryModelViewTab) => {
|
||||||
@@ -425,7 +426,14 @@ export class QueryResultsView extends Disposable {
|
|||||||
if (!this._panelView.contains(this.topOperationsTab.identifier)) {
|
if (!this._panelView.contains(this.topOperationsTab.identifier)) {
|
||||||
this._panelView.pushTab(this.topOperationsTab);
|
this._panelView.pushTab(this.topOperationsTab);
|
||||||
}
|
}
|
||||||
this.topOperationsTab.view.showPlan(xml);
|
}
|
||||||
|
|
||||||
|
public switchToTopOperationsTab() {
|
||||||
|
this._panelView.showTab(this.topOperationsTab.identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public scrollToTable(planId: number) {
|
||||||
|
this.topOperationsTab.view.scrollToIndex(planId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public showPlan() {
|
public showPlan() {
|
||||||
@@ -438,6 +446,14 @@ export class QueryResultsView extends Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public switchToExecutionPlanTab() {
|
||||||
|
this._panelView.showTab(this.executionPlanTab.identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public focusOnNode(planId: number, nodeId: string) {
|
||||||
|
this.executionPlanTab.view.currentFileView.scrollToNode(planId, nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
public hideTopOperations() {
|
public hideTopOperations() {
|
||||||
if (this._panelView.contains(this.topOperationsTab.identifier)) {
|
if (this._panelView.contains(this.topOperationsTab.identifier)) {
|
||||||
this._panelView.removeTab(this.topOperationsTab.identifier);
|
this._panelView.removeTab(this.topOperationsTab.identifier);
|
||||||
|
|||||||
Reference in New Issue
Block a user