mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-27 17:23:21 -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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user