Query Execution Plan Expensive Operator Highlighting (#20579)

* Boilerplate for new context menu options

* Enables checkmarks for expensive operator context menu actions

* Updates azdataGraph version to 0.0.44

* Adds clearing logic to actions and retrieves additional info from nodes

* Removes unnecessary actions that aren't supported by other providers

* Finishes setting up the rest of the context menu actions

* Corrects context menu action label

* Defines new widget type for finding expensive operations

* Adds TODO for class icon name

* Creates action to open the expensive operation widget

* Adds escape and enter key shortcuts to findExpensiveOperation widget

* Styles find expensive operation widget

* Corrects class name for finding expensive operator action

* Corrects import statement.

* Code clean up

* Bumps azdataGraph version to 0.0.45

* Adds an info box for when a metric doesn't find any nodes

* Adds label to find expensive operator widget

* Invokes dispose when widget controller removes widget

* Implements disposable in all execution plan widgets.

* Adds off action to clear highlighting

* Adds a default setting and default prompt for highlighting metric

* Fixes not all code paths return error

* Removes shortcut key from text for widget actions

* Adds enum description

* Removes added dictionary type, and renames class name

* Removes unnecessary null checks

* Removes cost metric dictionary and adds corresponding properties

* Code review changes

* Removes incorrectly implemented key down event for widget.

* Renames widget and action class names for highlighting expensive ops

* File rename

* Cleans up labels to better reflect highlight action

* Removes hardcoded button width style and sets it to auto

* More clean up

* Clean up import statement

* Code review changes

* Drop down list only shows available metrics

* Updates highlight expensive operation icon

* Update STS version
This commit is contained in:
Lewis Sanchez
2022-10-03 12:32:39 -07:00
committed by GitHub
parent d0cab10cc9
commit f521d7cc63
22 changed files with 660 additions and 77 deletions

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "4.3.0.30",
"version": "4.3.0.32",
"downloadFileNames": {
"Windows_86": "win-x86-net6.0.zip",
"Windows_64": "win-x64-net6.0.zip",

View File

@@ -261,6 +261,29 @@
"default": false,
"description": "%mssql.intelliSense.lowerCaseSuggestions%"
},
"mssql.executionPlan.expensiveOperationMetric": {
"type": "string",
"description": "%mssql.executionPlan.expensiveOperationMetric%",
"default": "cost",
"enum": [
"off",
"actualElapsedTime",
"actualElapsedCpuTime",
"cost",
"subtreeCost",
"actualNumberOfRowsForAllExecutions",
"numberOfRowsRead"
],
"enumDescriptions": [
"%mssql.executionPlan.expensiveOperationMetric.off%",
"%mssql.executionPlan.expensiveOperationMetric.actualElapsedTime%",
"%mssql.executionPlan.expensiveOperationMetric.actualElapsedCpuTime%",
"%mssql.executionPlan.cost%",
"%mssql.executionPlan.subtreeCost%",
"%mssql.executionPlan.actualNumberOfRowsForAllExecutions%",
"%mssql.executionPlan.numberOfRowsRead%"
]
},
"mssql.query.rowCount": {
"type": "number",
"default": 0,

View File

@@ -39,6 +39,14 @@
"mssql.disabled": "Disabled",
"mssql.enabled": "Enabled",
"mssql.executionPlan.expensiveOperationMetric": "The default metric to use to highlight an expensive operation in query execution plans",
"mssql.executionPlan.expensiveOperationMetric.off": "Expensive operation highlighting will be turned off for execution plans.",
"mssql.executionPlan.expensiveOperationMetric.actualElapsedTime": "Highlights the execution plan operation that took the most time.",
"mssql.executionPlan.expensiveOperationMetric.actualElapsedCpuTime": "Highlights the execution plan operation that used the most CPU time.",
"mssql.executionPlan.cost": "Highlights the execution plan operation with the highest cost.",
"mssql.executionPlan.subtreeCost": "Highlights the execution plan operation with the highest subtree cost.",
"mssql.executionPlan.actualNumberOfRowsForAllExecutions": "Highlights the execution plan operation with the greatest actual number of rows for all executions.",
"mssql.executionPlan.numberOfRowsRead": "Highlights the execution plan operation with the greatest number of rows read.",
"mssql.exportNotebookToSql": "Export Notebook as SQL",
"mssql.exportSqlAsNotebook": "Export SQL as Notebook",
"mssql.configuration.title": "MSSQL configuration",

View File

@@ -76,7 +76,7 @@
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"applicationinsights": "1.0.8",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.42",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.45",
"chart.js": "^2.9.4",
"chokidar": "3.5.1",
"graceful-fs": "4.2.8",

View File

@@ -17,7 +17,7 @@
"applicationinsights": "1.0.8",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.42",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.45",
"chart.js": "^2.9.4",
"cookie": "^0.4.0",
"graceful-fs": "4.2.8",

View File

@@ -15,7 +15,7 @@
"@vscode/vscode-languagedetection": "1.0.21",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.42",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.45",
"chart.js": "^2.9.4",
"gridstack": "^3.1.3",
"kburtram-query-plan": "2.6.1",

View File

@@ -150,9 +150,9 @@ array-uniq@^1.0.2:
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
"azdataGraph@github:Microsoft/azdataGraph#0.0.42":
version "0.0.42"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/86f72ee9c9ea78a31c9ad2f402fb24d40e50c75b"
"azdataGraph@github:Microsoft/azdataGraph#0.0.45":
version "0.0.45"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/8675ac6dfc60aad331ccbf7e4145b9eaa25e7304"
chalk@^2.3.0, chalk@^2.4.1:
version "2.4.2"

View File

@@ -198,9 +198,9 @@ array-uniq@^1.0.2:
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
"azdataGraph@github:Microsoft/azdataGraph#0.0.42":
version "0.0.42"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/86f72ee9c9ea78a31c9ad2f402fb24d40e50c75b"
"azdataGraph@github:Microsoft/azdataGraph#0.0.45":
version "0.0.45"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/8675ac6dfc60aad331ccbf7e4145b9eaa25e7304"
bindings@^1.5.0:
version "1.5.0"

View File

@@ -1311,6 +1311,10 @@ declare module 'azdata' {
* Time take by the node operation in milliseconds
*/
elapsedTimeInMs: number;
/**
* CPU time taken by the node operation in milliseconds
*/
elapsedCpuTimeInMs: number;
/**
* Node properties to be shown in the tooltip
*/
@@ -1351,6 +1355,21 @@ declare module 'azdata' {
* Cost string for the node
*/
costDisplayString: string;
/**
* Cost metrics for the node
*/
costMetrics: CostMetric[];
}
export interface CostMetric {
/**
* Name of the cost metric.
*/
name: string;
/**
* The value of the cost metric
*/
value: number | undefined;
}
export interface ExecutionPlanBadge {

View File

@@ -81,6 +81,7 @@ export const enum TelemetryAction {
GeneratePreviewReport = 'GeneratePreviewReport',
GetDataGridItems = 'GetDataGridItems',
GetDataGridColumns = 'GetDataGridColumns',
HighlightExpensiveOperation = 'HighlightExpensiveOperation',
ModelViewDashboardOpened = 'ModelViewDashboardOpened',
ModalDialogClosed = 'ModalDialogClosed',
ModalDialogOpened = 'ModalDialogOpened',

View File

@@ -25,6 +25,7 @@ export class AzdataGraphView {
private _diagram: any;
private _diagramModel: AzDataGraphCell;
private _cellInFocus: AzDataGraphCell;
public expensiveMetricTypes: Set<ExpensiveMetricType> = new Set();
private _graphElementPropertiesSet: Set<string> = new Set();
@@ -230,6 +231,13 @@ export class AzdataGraphView {
return resultNodes;
}
public clearExpensiveOperatorHighlighting(): void {
this._diagram.clearExpensiveOperatorHighlighting();
}
public highlightExpensiveOperator(predicate: (cell: AzDataGraphCell) => number): boolean {
return this._diagram.highlightExpensiveOperator(predicate);
}
/**
* Brings a graph element to the center of the parent view.
@@ -295,40 +303,65 @@ export class AzdataGraphView {
if (!node.id.toString().startsWith(`element-`)) {
node.id = `element-${node.id}`;
}
this.expensiveMetricTypes.add(ExpensiveMetricType.Off);
diagramNode.id = node.id;
diagramNode.icon = node.type;
diagramNode.metrics = this.populateProperties(node.properties);
if (node.type) {
diagramNode.icon = node.type;
diagramNode.badges = [];
for (let i = 0; node.badges && i < node.badges.length; i++) {
diagramNode.badges.push(this.getBadgeTypeString(node.badges[i].type));
}
if (node.properties) {
diagramNode.metrics = this.populateProperties(node.properties);
diagramNode.edges = this.populateEdges(node.edges);
diagramNode.children = [];
for (let i = 0; node.children && i < node.children.length; ++i) {
diagramNode.children.push(this.populate(node.children[i]));
}
if (node.badges) {
diagramNode.badges = [];
for (let i = 0; i < node.badges.length; i++) {
diagramNode.badges.push(this.getBadgeTypeString(node.badges[i].type));
}
diagramNode.description = node.description;
diagramNode.cost = node.cost;
if (node.cost) {
this.expensiveMetricTypes.add(ExpensiveMetricType.Cost);
}
if (node.edges) {
diagramNode.edges = this.populateEdges(node.edges);
diagramNode.subTreeCost = node.subTreeCost;
if (node.subTreeCost) {
this.expensiveMetricTypes.add(ExpensiveMetricType.SubtreeCost);
}
if (node.children) {
diagramNode.children = [];
for (let i = 0; i < node.children.length; ++i) {
diagramNode.children.push(this.populate(node.children[i]));
}
diagramNode.relativeCost = node.relativeCost;
diagramNode.elapsedTimeInMs = node.elapsedTimeInMs;
if (node.elapsedTimeInMs) {
this.expensiveMetricTypes.add(ExpensiveMetricType.ActualElapsedTime);
}
if (node.description) {
diagramNode.description = node.description;
let costMetrics = [];
for (let i = 0; node.costMetrics && i < node.costMetrics.length; ++i) {
costMetrics.push(node.costMetrics[i]);
this.loadMetricTypesFromCostMetrics(node.costMetrics[i].name);
}
diagramNode.costMetrics = costMetrics;
return diagramNode;
}
private loadMetricTypesFromCostMetrics(costMetricName: string): void {
if (costMetricName === 'ElapsedCpuTime') {
this.expensiveMetricTypes.add(ExpensiveMetricType.ActualElapsedCpuTime);
}
else if (costMetricName === 'EstimateRowsAllExecs' || costMetricName === 'ActualRows') {
this.expensiveMetricTypes.add(ExpensiveMetricType.ActualNumberOfRowsForAllExecutions);
}
else if (costMetricName === 'EstimatedRowsRead' || costMetricName === 'ActualRowsRead') {
this.expensiveMetricTypes.add(ExpensiveMetricType.NumberOfRowsRead);
}
}
private getBadgeTypeString(badgeType: sqlExtHostType.executionPlan.BadgeType): {
type: string,
tooltip: string
@@ -357,7 +390,11 @@ export class AzdataGraphView {
}
}
private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): AzDataGraphCellMetric[] {
private populateProperties(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[] | undefined): AzDataGraphCellMetric[] {
if (!props) {
return [];
}
props.forEach(p => {
this._graphElementPropertiesSet.add(p.name);
});
@@ -372,7 +409,11 @@ export class AzdataGraphView {
});
}
private populateEdges(edges: InternalExecutionPlanEdge[]): AzDataGraphCellEdge[] {
private populateEdges(edges: InternalExecutionPlanEdge[] | undefined): AzDataGraphCellEdge[] {
if (!edges) {
return [];
}
return edges.map(e => {
e.id = this.createGraphElementId();
return {
@@ -473,6 +514,37 @@ export interface AzDataGraphCell {
*/
description: string;
badges: AzDataGraphNodeBadge[];
/**
* Cost associated with the node
*/
cost: number;
/**
* Cost of the node subtree
*/
subTreeCost: number;
/**
* Relative cost of the node compared to its siblings.
*/
relativeCost: number;
/**
* Time taken by the node operation in milliseconds
*/
elapsedTimeInMs: number;
/**
* cost metrics for the node
*/
costMetrics: CostMetric[];
}
export interface CostMetric {
/**
* Name of the cost metric.
*/
name: string;
/**
* The value of the cost metric
*/
value: number | undefined;
}
export interface AzDataGraphNodeBadge {
@@ -529,6 +601,17 @@ export enum SearchType {
LesserThanEqualTo,
LesserAndGreaterThan
}
export enum ExpensiveMetricType {
Off = 'off',
ActualElapsedTime = 'actualElapsedTime',
ActualElapsedCpuTime = 'actualElapsedCpuTime',
Cost = 'cost',
SubtreeCost = 'subtreeCost',
ActualNumberOfRowsForAllExecutions = 'actualNumberOfRowsForAllExecutions',
NumberOfRowsRead = 'numberOfRowsRead'
}
export interface SearchQuery {
/**
* property name to be searched

View File

@@ -265,6 +265,7 @@ export const collapseExpandNodeIconPaths = {
};
export const savePlanIconClassNames = 'ep-save-plan-icon';
export const highlightExpensiveOperationClassNames = 'ep-highlight-expensive-operation-icon';
export const openPropertiesIconClassNames = 'ep-open-properties-icon';
export const openQueryIconClassNames = 'ep-open-query-icon';
export const openPlanFileIconClassNames = 'ep-open-plan-file-icon';

View File

@@ -26,7 +26,7 @@ import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Action, Separator } from 'vs/base/common/actions';
import { localize } from 'vs/nls';
import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, executionPlanCompareIconClassName, executionPlanTopOperations, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import * as constants from 'sql/workbench/contrib/executionPlan/browser/constants';
import { URI } from 'vs/base/common/uri';
import { VSBuffer } from 'vs/base/common/buffer';
import { CustomZoomWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/customZoomWidget';
@@ -37,6 +37,7 @@ import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
import { ExecutionPlanFileView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanFileView';
import { QueryResultsView } from 'sql/workbench/contrib/query/browser/queryResultsView';
import { HighlightExpensiveOperationWidget } from 'sql/workbench/contrib/executionPlan/browser/widgets/highlightExpensiveNodeWidget';
export class ExecutionPlanView implements ISashLayoutProvider {
@@ -66,6 +67,9 @@ export class ExecutionPlanView implements ISashLayoutProvider {
// plan diagram
public executionPlanDiagram: AzdataGraphView;
// previous expensive operator action selected
public previousExpensiveOperatorAction: Action;
public actionBarToggleTopTip: Action;
public contextMenuToggleTooltipAction: Action;
constructor(
@@ -169,6 +173,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._instantiationService.createInstance(SearchNodeAction, 'ActionBar'),
this._instantiationService.createInstance(PropertiesAction, 'ActionBar'),
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ActionBar'),
this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ActionBar'),
this.actionBarToggleTopTip
];
// Setting up context menu
@@ -186,7 +191,9 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._instantiationService.createInstance(SearchNodeAction, 'ContextMenu'),
this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'),
this._instantiationService.createInstance(CompareExecutionPlanAction, 'ContextMenu'),
this.contextMenuToggleTooltipAction
this._instantiationService.createInstance(HighlightExpensiveOperationAction, 'ContextMenu'),
this.contextMenuToggleTooltipAction,
new Separator(),
];
if (this._queryResultsView) {
@@ -320,7 +327,7 @@ export class OpenQueryAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(OpenQueryAction.ID, OpenQueryAction.LABEL, openQueryIconClassNames);
super(OpenQueryAction.ID, OpenQueryAction.LABEL, constants.openQueryIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -340,7 +347,7 @@ export class PropertiesAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames);
super(PropertiesAction.ID, PropertiesAction.LABEL, constants.openPropertiesIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -360,7 +367,7 @@ export class ZoomInAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames);
super(ZoomInAction.ID, ZoomInAction.LABEL, constants.zoomInIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -380,7 +387,7 @@ export class ZoomOutAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames);
super(ZoomOutAction.ID, ZoomOutAction.LABEL, constants.zoomOutIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -400,7 +407,7 @@ export class ZoomToFitAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames);
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, constants.zoomToFitIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -418,7 +425,7 @@ export class SavePlanFile extends Action {
public static LABEL = localize('executionPlanSavePlanXML', "Save Plan File");
constructor() {
super(SavePlanFile.ID, SavePlanFile.LABEL, savePlanIconClassNames);
super(SavePlanFile.ID, SavePlanFile.LABEL, constants.savePlanIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -453,7 +460,7 @@ export class CustomZoomAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(CustomZoomAction.ID, CustomZoomAction.LABEL, customZoomIconClassNames);
super(CustomZoomAction.ID, CustomZoomAction.LABEL, constants.customZoomIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -473,7 +480,7 @@ export class SearchNodeAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(SearchNodeAction.ID, SearchNodeAction.LABEL, searchIconClassNames);
super(SearchNodeAction.ID, SearchNodeAction.LABEL, constants.searchIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -491,7 +498,7 @@ export class OpenPlanFile extends Action {
public static Label = localize('executionPlanOpenGraphFile', "Show Query Plan XML"); //TODO: add a contribution point for providers to set this text
constructor() {
super(OpenPlanFile.ID, OpenPlanFile.Label, openPlanFileIconClassNames);
super(OpenPlanFile.ID, OpenPlanFile.Label, constants.openPlanFileIconClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -505,17 +512,17 @@ export class ActionBarToggleTooltip extends Action {
public static WHEN_TOOLTIPS_DISABLED_LABEL = localize('executionPlanDisableTooltip', "Tooltips disabled");
constructor() {
super(ActionBarToggleTooltip.ID, ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL, enableTooltipIconClassName);
super(ActionBarToggleTooltip.ID, ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL, constants.enableTooltipIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const state = context.executionPlanDiagram.toggleTooltip();
if (!state) {
this.class = disableTooltipIconClassName;
this.class = constants.disableTooltipIconClassName;
this.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_DISABLED_LABEL;
context.contextMenuToggleTooltipAction.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_DISABLED_LABEL;
} else {
this.class = enableTooltipIconClassName;
this.class = constants.enableTooltipIconClassName;
this.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL;
context.contextMenuToggleTooltipAction.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL;
}
@@ -528,18 +535,18 @@ export class ContextMenuTooltipToggle extends Action {
public static WHEN_TOOLTIPS_DISABLED_LABEL = localize('executionPlanContextMenuEnableTooltip', "Enable Tooltips");
constructor() {
super(ContextMenuTooltipToggle.ID, ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL, enableTooltipIconClassName);
super(ContextMenuTooltipToggle.ID, ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL, constants.enableTooltipIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
const state = context.executionPlanDiagram.toggleTooltip();
if (!state) {
this.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_DISABLED_LABEL;
context.actionBarToggleTopTip.class = disableTooltipIconClassName;
context.actionBarToggleTopTip.class = constants.disableTooltipIconClassName;
context.actionBarToggleTopTip.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_DISABLED_LABEL;
} else {
this.label = ContextMenuTooltipToggle.WHEN_TOOLTIPS_ENABLED_LABEL;
context.actionBarToggleTopTip.class = enableTooltipIconClassName;
context.actionBarToggleTopTip.class = constants.enableTooltipIconClassName;
context.actionBarToggleTopTip.label = ActionBarToggleTooltip.WHEN_TOOLTIPS_ENABLED_LABEL;
}
}
@@ -552,7 +559,7 @@ export class CompareExecutionPlanAction extends Action {
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(CompareExecutionPlanAction.COMPARE_PLAN, CompareExecutionPlanAction.COMPARE_PLAN, executionPlanCompareIconClassName);
super(CompareExecutionPlanAction.COMPARE_PLAN, CompareExecutionPlanAction.COMPARE_PLAN, constants.executionPlanCompareIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
@@ -572,10 +579,30 @@ export class TopOperationsAction extends Action {
constructor
() {
super(TopOperationsAction.ID, TopOperationsAction.LABEL, executionPlanTopOperations);
super(TopOperationsAction.ID, TopOperationsAction.LABEL, constants.executionPlanTopOperations);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.openTopOperations();
}
}
export class HighlightExpensiveOperationAction extends Action {
public static ID = 'ep.highlightExpensiveOperation';
public static LABEL = localize('executionPlanHighlightExpensiveOperationAction', 'Highlight Expensive Operation');
constructor(private source: ExecutionPlanActionSource,
@IAdsTelemetryService private readonly telemetryService: IAdsTelemetryService
) {
super(HighlightExpensiveOperationAction.ID, HighlightExpensiveOperationAction.LABEL, constants.highlightExpensiveOperationClassNames);
}
public override async run(context: ExecutionPlanView): Promise<void> {
this.telemetryService
.createActionEvent(TelemetryKeys.TelemetryView.ExecutionPlan, TelemetryKeys.TelemetryAction.HighlightExpensiveOperation)
.withAdditionalProperties({ source: this.source })
.send();
context.widgetController.toggleWidget(context._instantiationService.createInstance(HighlightExpensiveOperationWidget, context.widgetController, context.executionPlanDiagram));
}
}

View File

@@ -3,13 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export abstract class ExecutionPlanWidgetBase {
import { Disposable } from 'vs/base/common/lifecycle';
export abstract class ExecutionPlanWidgetBase extends Disposable {
/**
*
* @param container HTML Element that contains the UI for the plan action view.
* @param identifier Uniquely identify the view to be added or removed. Note: Only 1 view with the same id can be added to the controller
*/
constructor(public container: HTMLElement, public identifier: string) {
super();
this.container = container;
this.identifier = identifier;
}

View File

@@ -26,6 +26,7 @@ export class ExecutionPlanWidgetController {
public removeWidget(widget: ExecutionPlanWidgetBase) {
if (widget.identifier) {
if (this._executionPlanWidgetMap.has(widget.identifier)) {
widget.dispose();
this._parentContainer.removeChild(this._executionPlanWidgetMap.get(widget.identifier).container);
this._executionPlanWidgetMap.delete(widget.identifier);
} else {

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g id="f4eb8f61-ee01-4fe9-a111-7a076ae7c7be" data-name="Icons">
<path d="M5,5H0V0H5Z"/>
<path d="M16,5H11V0h5ZM12,4h3V1H12Z"/>
<path d="M7.975.525l.75.75-.684.692H11V3.033H8.041l.684.692-.75.75L6,2.5Z"/>
<path d="M16,14H11V9h5Zm-4-1h3V10H12Z"/>
<rect x="13" y="5" width="1" height="5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="f4eb8f61-ee01-4fe9-a111-7a076ae7c7be">
<path class="st0" d="M5,5H0V0h5V5z"/>
<path class="st0" d="M16,5h-5V0h5V5z M12,4h3V1h-3V4z"/>
<path class="st0" d="M8,0.5l0.8,0.8L8,2h3V3H8l0.7,0.7L8,4.5l-2-2L8,0.5z"/>
<path class="st0" d="M16,14h-5V9h5V14z M12,13h3v-3h-3V13z"/>
<rect x="13" y="5" class="st0" width="1" height="4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 746 B

View File

@@ -92,11 +92,42 @@ However we always want it to be the width of the container it is resizing.
width: 240px;
}
/* Find expensive operation view */
.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget {
display: flex;
flex-direction: row;
padding: 5px;
height: auto;
width: auto;
}
.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .select-container {
margin-left: 5px;
display: flex;
align-items: center;
}
.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .select-container expensive-operation-name-select-box-label {
margin-right: 5px;
}
.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .select-container>select {
height: 100%;
}
.eps-container .execution-plan .plan .plan-action-container .find-expensive-operation-widget .monaco-button.monaco-text-button {
width: auto;
padding-left: 15px;
padding-right: 15px;
}
/* execution plan header that contains the relative query cost, query statement and recommendations */
.eps-container .execution-plan .plan .header,
.top-operations-tab .top-operations-container .query-row {
padding: 5px;
font-weight: bolder;
margin-left: 5px;
margin-right: 5px;
}
/* execution plan header that contains the relative query cost, query statement and recommendations */
@@ -478,6 +509,21 @@ However we always want it to be the width of the container it is resizing.
background-repeat: no-repeat;
}
.eps-container .ep-highlight-expensive-operation-icon {
background-image: url(../images/actionIcons/highlightExpensiveOperation.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.vs-dark .eps-container .ep-highlight-expensive-operation-icon,
.hc-black .eps-container .ep-highlight-expensive-operation-icon {
background-image: url(../images/actionIcons/highlightExpensiveOperationDark.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .ep-enable-tooltip-icon {
background-image: url(../images/actionIcons/enableTooltip.svg);
background-size: 16px 16px;

View File

@@ -39,7 +39,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
ariaLabel: zoomValueLabel,
flexibleWidth: false
});
attachInputBoxStyler(this.customZoomInputBox, this.themeService);
this._register(attachInputBoxStyler(this.customZoomInputBox, this.themeService));
const currentZoom = this.executionPlanDiagram.getZoomLevel();
@@ -57,14 +57,14 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
};
const applyButton = new Button(this.container, {
title: localize('customZoomApplyButtonTitle', "Apply Zoom (Enter)")
title: localize('customZoomApplyButtonTitle', "Apply Zoom")
});
applyButton.setWidth('60px');
applyButton.label = localize('customZoomApplyButton', "Apply");
applyButton.onDidClick(async e => {
this._register(applyButton.onDidClick(async e => {
await new CustomZoomAction().run(self);
});
}));
// Adding action bar
this._actionBar = new ActionBar(this.container);
@@ -80,7 +80,7 @@ export class CustomZoomWidget extends ExecutionPlanWidgetBase {
export class CustomZoomAction extends Action {
public static ID = 'qp.customZoomAction';
public static LABEL = localize('zoomAction', "Zoom (Enter)");
public static LABEL = localize('zoomAction', "Zoom");
constructor() {
super(CustomZoomAction.ID, CustomZoomAction.LABEL, zoomIconClassNames);
@@ -101,7 +101,7 @@ export class CustomZoomAction extends Action {
export class CancelZoom extends Action {
public static ID = 'qp.cancelCustomZoomAction';
public static LABEL = localize('cancelCustomZoomAction', "Close (Escape)");
public static LABEL = localize('cancelCustomZoomAction', "Close");
constructor() {
super(CancelZoom.ID, CancelZoom.LABEL, Codicon.chromeClose.classNames);

View File

@@ -0,0 +1,337 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExecutionPlanWidgetBase } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetBase';
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import * as errors from 'vs/base/common/errors';
import { Codicon } from 'vs/base/common/codicons';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachSelectBoxStyler } from 'sql/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Action } from 'vs/base/common/actions';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { AzDataGraphCell, AzdataGraphView, ExpensiveMetricType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { Button } from 'sql/base/browser/ui/button/button';
import { searchIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
const OFF_STRING = localize('executionPlanOff', 'Off');
const ACTUAL_ELAPSED_TIME_STRING = localize('executionPlanActualElapsedTime', 'Actual Elapsed Time');
const ACTUAL_ELAPSED_CPU_TIME_STRING = localize('executionPlanActualElapsedCpuTime', 'Actual Elapsed CPU Time');
const COST_STRING = localize('executionPlanCost', 'Cost');
const SUBTREE_COST_STRING = localize('executionPlanSubtreeCost', 'Subtree Cost');
const ACTUAL_NUMBER_OF_ROWS_FOR_ALL_EXECUTIONS_STRING = localize('actualNumberOfRowsForAllExecutionsAction', 'Actual Number of Rows For All Executions');
const NUMBER_OF_ROWS_READ_STRING = localize('executionPlanNumberOfRowsRead', 'Number of Rows Read');
export class HighlightExpensiveOperationWidget extends ExecutionPlanWidgetBase {
private _actionBar: ActionBar;
public expenseMetricSelectBox: SelectBox;
private _expenseMetricSelectBoxContainer: HTMLElement;
private _selectedExpensiveOperationType: ExpensiveMetricType = ExpensiveMetricType.Cost;
constructor(
public readonly widgetController: ExecutionPlanWidgetController,
public readonly executionPlanDiagram: AzdataGraphView,
@IContextViewService public readonly contextViewService: IContextViewService,
@IThemeService public readonly themeService: IThemeService,
@INotificationService public readonly notificationService: INotificationService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IStorageService private readonly _storageService: IStorageService
) {
super(DOM.$('.find-expensive-operation-widget'), 'findExpensiveOperation');
this.renderAndStyleWidget();
}
private getDefaultExpensiveOperationMetric(): ExpensiveMetricType {
const defaultMetricConfiguration = this._configurationService.getValue<string>('mssql.executionPlan.expensiveOperationMetric');
switch (defaultMetricConfiguration) {
case 'actualElapsedTime':
return ExpensiveMetricType.ActualElapsedTime;
case 'actualElapsedCpuTime':
return ExpensiveMetricType.ActualElapsedCpuTime;
case 'cost':
return ExpensiveMetricType.Cost;
case 'subtreeCost':
return ExpensiveMetricType.SubtreeCost;
case 'actualNumberOfRowsForAllExecutions':
return ExpensiveMetricType.ActualNumberOfRowsForAllExecutions;
case 'numberOfRowsRead':
return ExpensiveMetricType.NumberOfRowsRead;
default:
return ExpensiveMetricType.Off;
}
}
private renderAndStyleWidget(): void {
// Expensive Operation Dropdown
this._expenseMetricSelectBoxContainer = DOM.$('expensive-operation-name-select-box .dropdown-container');
const operationLabel = DOM.$('expensive-operation-name-select-box-label');
operationLabel.innerText = localize('expensiveOperationLabel', 'Metric:');
this._expenseMetricSelectBoxContainer.appendChild(operationLabel);
this.container.appendChild(this._expenseMetricSelectBoxContainer);
const selectBoxOptions = this.getSelectBoxOptionsFromExecutionPlanDiagram();
this.expenseMetricSelectBox = new SelectBox(selectBoxOptions, COST_STRING, this.contextViewService, this._expenseMetricSelectBoxContainer);
this.expenseMetricSelectBox.render(this._expenseMetricSelectBoxContainer);
this._register(attachSelectBoxStyler(this.expenseMetricSelectBox, this.themeService));
this._expenseMetricSelectBoxContainer.style.width = '200px';
this._expenseMetricSelectBoxContainer.style.marginRight = '5px';
this._register(this.expenseMetricSelectBox.onDidSelect(e => {
switch (e.selected) {
case ACTUAL_ELAPSED_TIME_STRING:
this._selectedExpensiveOperationType = ExpensiveMetricType.ActualElapsedTime;
break;
case ACTUAL_ELAPSED_CPU_TIME_STRING:
this._selectedExpensiveOperationType = ExpensiveMetricType.ActualElapsedCpuTime;
break;
case COST_STRING:
this._selectedExpensiveOperationType = ExpensiveMetricType.Cost;
break;
case SUBTREE_COST_STRING:
this._selectedExpensiveOperationType = ExpensiveMetricType.SubtreeCost;
break;
case ACTUAL_NUMBER_OF_ROWS_FOR_ALL_EXECUTIONS_STRING:
this._selectedExpensiveOperationType = ExpensiveMetricType.ActualNumberOfRowsForAllExecutions;
break;
case NUMBER_OF_ROWS_READ_STRING:
this._selectedExpensiveOperationType = ExpensiveMetricType.NumberOfRowsRead;
break;
default:
this._selectedExpensiveOperationType = ExpensiveMetricType.Off;
}
}));
// Apply Button
const highlightExpensiveOperationAction = new HighlightExpensiveOperationAction();
this._register(highlightExpensiveOperationAction);
const clearHighlightExpensiveOperationAction = new TurnOffExpensiveHighlightingOperationAction();
this._register(clearHighlightExpensiveOperationAction);
const cancelHighlightExpensiveOperationAction = new CancelHIghlightExpensiveOperationAction();
this._register(cancelHighlightExpensiveOperationAction);
const self = this;
const applyButton = new Button(this.container, {
title: localize('highlightExpensiveOperationButtonTitle', 'Highlight Expensive Operation')
});
applyButton.label = localize('highlightExpensiveOperationApplyButton', 'Apply');
this._register(applyButton.onDidClick(async e => {
if (this._selectedExpensiveOperationType === ExpensiveMetricType.Off) {
await clearHighlightExpensiveOperationAction.run(self);
}
else {
await highlightExpensiveOperationAction.run(self);
}
this.showStoreDefaultMetricPrompt();
}));
// Adds Action bar
this._actionBar = new ActionBar(this.container);
this._actionBar.context = this;
this._actionBar.pushAction(cancelHighlightExpensiveOperationAction, { label: false, icon: true });
}
private getSelectBoxOptionsFromExecutionPlanDiagram(): string[] {
const selectBoxOptions: string[] = [];
for (let expenseMetricType of this.executionPlanDiagram.expensiveMetricTypes) {
switch (expenseMetricType) {
case ExpensiveMetricType.Off:
selectBoxOptions.push(OFF_STRING);
break;
case ExpensiveMetricType.ActualElapsedTime:
selectBoxOptions.push(ACTUAL_ELAPSED_TIME_STRING);
break;
case ExpensiveMetricType.ActualElapsedCpuTime:
selectBoxOptions.push(ACTUAL_ELAPSED_CPU_TIME_STRING);
break;
case ExpensiveMetricType.Cost:
selectBoxOptions.push(COST_STRING);
break;
case ExpensiveMetricType.SubtreeCost:
selectBoxOptions.push(SUBTREE_COST_STRING);
break;
case ExpensiveMetricType.ActualNumberOfRowsForAllExecutions:
selectBoxOptions.push(ACTUAL_NUMBER_OF_ROWS_FOR_ALL_EXECUTIONS_STRING);
break;
case ExpensiveMetricType.NumberOfRowsRead:
selectBoxOptions.push(NUMBER_OF_ROWS_READ_STRING);
break;
}
}
return selectBoxOptions;
}
public showStoreDefaultMetricPrompt(): void {
const currentDefaultExpensiveOperationMetric = this.getDefaultExpensiveOperationMetric();
if (this._selectedExpensiveOperationType === currentDefaultExpensiveOperationMetric || !this._storageService.getBoolean('qp.expensiveOperationMetric.showChangeDefaultExpensiveMetricPrompt', StorageScope.GLOBAL, true)) {
return;
}
const infoMessage = localize('queryExecutionPlan.showUpdateDefaultMetricInfo', 'Set chosen metric as the default for query execution plans?');
const promptChoices = [
{
label: localize('qp.expensiveOperationMetric.yes', 'Yes'),
run: () => this._configurationService.updateValue('mssql.executionPlan.expensiveOperationMetric', this._selectedExpensiveOperationType.toString()).catch(e => errors.onUnexpectedError(e))
},
{
label: localize('qp.expensiveOperationMetric.no', 'No'),
run: () => { }
},
{
label: localize('qp.expensiveOperationMetric.dontShowAgain', "Don't Show Again"),
run: () => this._storageService.store('qp.expensiveOperationMetric.showChangeDefaultExpensiveMetricPrompt', false, StorageScope.GLOBAL, StorageTarget.USER)
}
];
this.notificationService.prompt(Severity.Info, infoMessage, promptChoices, { sticky: true });
}
public focus() {
this.expenseMetricSelectBox.focus();
}
public getExpensiveOperationDelegate(): (cell: AzDataGraphCell) => number | undefined {
const getElapsedTimeInMs = (cell: AzDataGraphCell): number | undefined => cell.elapsedTimeInMs;
const getElapsedCpuTimeInMs = (cell: AzDataGraphCell): number | undefined => {
const elapsedCpuMetric = cell.costMetrics.find(m => m.name === 'ElapsedCpuTime');
if (elapsedCpuMetric === undefined) {
return undefined;
}
else {
return Number(elapsedCpuMetric.value);
}
};
const getCost = (cell: AzDataGraphCell): number | undefined => cell.cost;
const getSubtreeCost = (cell: AzDataGraphCell): number | undefined => cell.subTreeCost;
const getRowsForAllExecutions = (cell: AzDataGraphCell): number | undefined => {
const actualRowsMetric = cell.costMetrics.find(m => m.name === 'ActualRows');
const estimateRowsForAllExecutionsMetric = cell.costMetrics.find(m => m.name === 'EstimateRowsAllExecs');
if (actualRowsMetric === undefined && estimateRowsForAllExecutionsMetric === undefined) {
return undefined;
}
let result = Number(actualRowsMetric?.value);
if (!result) {
result = Number(estimateRowsForAllExecutionsMetric?.value);
}
if (isNaN(result)) {
return undefined;
}
return result;
};
const getNumberOfRowsRead = (cell: AzDataGraphCell): number | undefined => {
const actualRowsReadMetric = cell.costMetrics.find(m => m.name === 'ActualRowsRead');
const estimatedRowsReadMetric = cell.costMetrics.find(m => m.name === 'EstimatedRowsRead');
if (actualRowsReadMetric === undefined && estimatedRowsReadMetric === undefined) {
return undefined;
}
let result = Number(actualRowsReadMetric?.value);
if (!result) {
result = Number(estimatedRowsReadMetric?.value);
}
if (isNaN(result)) {
return undefined;
}
return result;
};
let expensiveOperationDelegate = getCost;
switch (this._selectedExpensiveOperationType) {
case ExpensiveMetricType.ActualElapsedTime:
expensiveOperationDelegate = getElapsedTimeInMs;
break;
case ExpensiveMetricType.ActualElapsedCpuTime:
expensiveOperationDelegate = getElapsedCpuTimeInMs;
break;
case ExpensiveMetricType.SubtreeCost:
expensiveOperationDelegate = getSubtreeCost;
break;
case ExpensiveMetricType.ActualNumberOfRowsForAllExecutions:
expensiveOperationDelegate = getRowsForAllExecutions;
break;
case ExpensiveMetricType.NumberOfRowsRead:
expensiveOperationDelegate = getNumberOfRowsRead;
break;
}
return expensiveOperationDelegate;
}
}
export class HighlightExpensiveOperationAction extends Action {
public static ID = 'qp.highlightExpensiveOperationAction';
public static LABEL = localize('highlightExpensiveOperationAction', 'Apply');
constructor() {
super(HighlightExpensiveOperationAction.ID, HighlightExpensiveOperationAction.LABEL, searchIconClassNames);
}
public override async run(context: HighlightExpensiveOperationWidget): Promise<void> {
const expensiveOperationDelegate: (cell: AzDataGraphCell) => number | undefined = context.getExpensiveOperationDelegate();
context.executionPlanDiagram.clearExpensiveOperatorHighlighting();
let result = context.executionPlanDiagram.highlightExpensiveOperator(expensiveOperationDelegate);
if (!result) {
const metric = context.expenseMetricSelectBox.value;
context.notificationService.warn(localize('invalidPropertyExecutionPlanMetric', 'No nodes found with the {0} metric.', metric));
}
}
}
export class TurnOffExpensiveHighlightingOperationAction extends Action {
public static ID = 'qp.turnOffExpensiveHighlightingOperationAction';
public static LABEL = localize('turnOffExpensiveHighlightingOperationAction', 'Off');
constructor() {
super(TurnOffExpensiveHighlightingOperationAction.ID, TurnOffExpensiveHighlightingOperationAction.LABEL);
}
public override async run(context: HighlightExpensiveOperationWidget): Promise<void> {
context.executionPlanDiagram.clearExpensiveOperatorHighlighting();
}
}
export class CancelHIghlightExpensiveOperationAction extends Action {
public static ID = 'qp.cancelExpensiveOperationAction';
public static LABEL = localize('cancelExpensiveOperationAction', 'Close');
constructor() {
super(CancelHIghlightExpensiveOperationAction.ID, CancelHIghlightExpensiveOperationAction.LABEL, Codicon.chromeClose.classNames);
}
public override async run(context: HighlightExpensiveOperationWidget): Promise<void> {
context.widgetController.removeWidget(context);
}
}

View File

@@ -55,12 +55,12 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
this.container.appendChild(this._propertyNameSelectBoxContainer);
const propDropdownOptions = this._executionPlanDiagram.getUniqueElementProperties();
this._propertyNameSelectBox = new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer);
attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService);
this._register(attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService));
this._propertyNameSelectBoxContainer.style.width = '150px';
this._propertyNameSelectBox.render(this._propertyNameSelectBoxContainer);
this._propertyNameSelectBox.onDidSelect(e => {
this._register(this._propertyNameSelectBox.onDidSelect(e => {
this._usePreviousSearchResult = false;
});
}));
// search type dropdown
this._searchTypeSelectBoxContainer = DOM.$('.search-widget-search-type-select-box .dropdown-container');
@@ -75,9 +75,9 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
LESSER_AND_GREATER_DISPLAY_STRING
], EQUALS_DISPLAY_STRING, this.contextViewService, this._searchTypeSelectBoxContainer);
this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer);
attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService);
this._register(attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService));
this._searchTypeSelectBoxContainer.style.width = '100px';
this._searchTypeSelectBox.onDidSelect(e => {
this._register(this._searchTypeSelectBox.onDidSelect(e => {
this._usePreviousSearchResult = false;
switch (e.selected) {
case EQUALS_DISPLAY_STRING:
@@ -101,35 +101,44 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
case LESSER_AND_GREATER_DISPLAY_STRING:
this._selectedSearchType = SearchType.LesserAndGreaterThan;
}
});
}));
// search text input box
this._searchTextInputBox = new InputBox(this.container, this.contextViewService, {});
attachInputBoxStyler(this._searchTextInputBox, this.themeService);
this._register(attachInputBoxStyler(this._searchTextInputBox, this.themeService));
this._searchTextInputBox.element.style.marginLeft = '5px';
this._searchTextInputBox.onDidChange(e => {
this._register(this._searchTextInputBox.onDidChange(e => {
this._usePreviousSearchResult = false;
});
}));
// setting up key board shortcuts
const goToPreviousMatchAction = new GoToPreviousMatchAction();
this._register(goToPreviousMatchAction);
const goToNextMatchAction = new GoToNextMatchAction();
this._register(goToNextMatchAction);
const cancelSearchAction = new CancelSearch();
this._register(cancelSearchAction);
const self = this;
this._searchTextInputBox.element.onkeydown = async e => {
if (e.key === 'Enter' && e.shiftKey) {
await new GoToPreviousMatchAction().run(self);
await goToPreviousMatchAction.run(self);
} else if (e.key === 'Enter') {
await new GoToNextMatchAction().run(self);
await goToNextMatchAction.run(self);
} else if (e.key === 'Escape') {
await new CancelSearch().run(self);
await cancelSearchAction.run(self);
}
};
// Adding action bar
this._actionBar = new ActionBar(this.container);
this._actionBar.context = this;
this._actionBar.pushAction(new GoToPreviousMatchAction(), { label: false, icon: true });
this._actionBar.pushAction(new GoToNextMatchAction(), { label: false, icon: true });
this._actionBar.pushAction(new CancelSearch(), { label: false, icon: true });
this._actionBar.pushAction(goToPreviousMatchAction, { label: false, icon: true });
this._actionBar.pushAction(goToNextMatchAction, { label: false, icon: true });
this._actionBar.pushAction(cancelSearchAction, { label: false, icon: true });
}
// Initial focus is set to the search text input box
@@ -175,7 +184,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
export class GoToNextMatchAction extends Action {
public static ID = 'qp.NextSearchAction';
public static LABEL = localize('nextSearchItemAction', "Next Match (Enter)");
public static LABEL = localize('nextSearchItemAction', "Next Match");
constructor() {
super(GoToNextMatchAction.ID, GoToNextMatchAction.LABEL, Codicon.arrowDown.classNames);
@@ -188,7 +197,7 @@ export class GoToNextMatchAction extends Action {
export class GoToPreviousMatchAction extends Action {
public static ID = 'qp.PreviousSearchAction';
public static LABEL = localize('previousSearchItemAction', "Previous Match (Shift+Enter)");
public static LABEL = localize('previousSearchItemAction', "Previous Match");
constructor() {
super(GoToPreviousMatchAction.ID, GoToPreviousMatchAction.LABEL, Codicon.arrowUp.classNames);
@@ -201,7 +210,7 @@ export class GoToPreviousMatchAction extends Action {
export class CancelSearch extends Action {
public static ID = 'qp.cancelSearchAction';
public static LABEL = localize('cancelSearchAction', "Close (Escape)");
public static LABEL = localize('cancelSearchAction', "Close");
constructor() {
super(CancelSearch.ID, CancelSearch.LABEL, Codicon.chromeClose.classNames);

View File

@@ -1891,9 +1891,9 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
"azdataGraph@github:Microsoft/azdataGraph#0.0.42":
version "0.0.42"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/86f72ee9c9ea78a31c9ad2f402fb24d40e50c75b"
"azdataGraph@github:Microsoft/azdataGraph#0.0.45":
version "0.0.45"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/8675ac6dfc60aad331ccbf7e4145b9eaa25e7304"
bach@^1.0.0:
version "1.2.0"