Redoing Execution Plan Comparison Editor (#19375)

* Adding code for execution plan comparison editor

* Handling floating promises and fixing a loc string

* Fixing some polygon stuff

* Fixing azdatagraph null check bug

* Adding progress notification for similar areas

* Removing some floating promises

* Fixing button enabled state
This commit is contained in:
Aasim Khan
2022-05-23 14:33:18 -07:00
committed by GitHub
parent 000923207e
commit 8bb6b5fc1a
34 changed files with 1601 additions and 101 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>BranchCompare_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline"><path class="icon-vs-out" d="M14,9.619V5.5a3.507,3.507,0,0,0-2.717-3.412l.235-.234L9.664,0H9.129L6.656,2.473A3.25,3.25,0,1,0,2,6.131V10.5a3.506,3.506,0,0,0,2.717,3.412l-.235.234L6.336,16h.535L9.4,13.473A3.25,3.25,0,1,0,14,9.619Zm-3,0a3.224,3.224,0,0,0-1.6,1.907L6.6,8.732,5,10.336v-4.2A3.222,3.222,0,0,0,6.538,4.41L9.4,7.268l1.6-1.6Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,10.311V5.5A2.5,2.5,0,0,0,10.5,3H8.957L10.1,1.854,9.4,1.146,7.043,3.5,9.4,5.854l.708-.708L8.957,4H10.5A1.5,1.5,0,0,1,12,5.5v4.811a2.25,2.25,0,1,0,1,0Zm-.5,3.439a1.25,1.25,0,1,1,1.25-1.25A1.252,1.252,0,0,1,12.5,13.75Zm-6.6-2.9L7.043,12H5.5A1.5,1.5,0,0,1,4,10.5V5.44a2.25,2.25,0,1,0-1,0V10.5A2.5,2.5,0,0,0,5.5,13H7.043L5.9,14.146l.708.708L8.957,12.5,6.6,10.146ZM2.25,3.25A1.25,1.25,0,1,1,3.5,4.5,1.252,1.252,0,0,1,2.25,3.25Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M9 5v2H8V0H1v7h2v8h6v1h6V5H9zm0 7H6v-2h3v2z"/></g><path class="st2" d="M14 10V6h-4v2H5V6h2V1H2v5h2v8h6v1h4v-4h-4v2H5V9h5v1h4zm-3-3h2v2h-2V7zm0 5h2v2h-2v-2zM3 5V2h3v3H3z" id="icon_x5F_bg"/><g id="icon_x5F_fg"><path class="st3" d="M11 7h2v2h-2zM11 12h2v2h-2zM3 2h3v3H3z"/></g></svg>

After

Width:  |  Height:  |  Size: 503 B

View File

@@ -1529,6 +1529,12 @@
},
"table-graphedge": {
"iconPath": "./images/table-graphedge.svg"
},
"execution-plan": {
"iconPath": "./images/execution-plan.svg"
},
"execution-plan-compare": {
"iconPath": "./images/execution-plan-compare.svg"
}
},
"file": "_default",
@@ -1830,7 +1836,9 @@
"table-basic": "table-basic",
"table-temporal": "table-temporal",
"table-graphnode": "table-graphnode",
"table-graphedge": "table-graphedge"
"table-graphedge": "table-graphedge",
"execution-plan": "execution-plan",
"execution-plan-compare": "execution-plan-compare"
},
"languageIds": {
"bat": "_windows",

View File

@@ -75,7 +75,7 @@
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"applicationinsights": "1.0.8",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.26",
"chart.js": "^2.9.4",
"chokidar": "3.5.2",
"graceful-fs": "4.2.6",

View File

@@ -16,7 +16,7 @@
"applicationinsights": "1.0.8",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.26",
"chart.js": "^2.9.4",
"chokidar": "3.5.2",
"cookie": "^0.4.0",

View File

@@ -15,7 +15,7 @@
"@vscode/vscode-languagedetection": "1.0.18",
"angular2-grid": "2.0.6",
"ansi_up": "^5.1.0",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.21",
"azdataGraph": "github:Microsoft/azdataGraph#0.0.26",
"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.21":
version "0.0.21"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
"azdataGraph@github:Microsoft/azdataGraph#0.0.26":
version "0.0.26"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/a5f94e53cb655bc44f1a2727653bb403942e1cf9"
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.21":
version "0.0.21"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
"azdataGraph@github:Microsoft/azdataGraph#0.0.26":
version "0.0.26"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/a5f94e53cb655bc44f1a2727653bb403942e1cf9"
binary-extensions@^2.0.0:
version "2.0.0"

View File

@@ -1232,6 +1232,10 @@ declare module 'azdata' {
}
export interface ExecutionPlanNode {
/**
* Unique id given to node by the provider
*/
id: string;
/**
* Type of the node. This property determines the icon that is displayed for it
*/
@@ -1365,6 +1369,10 @@ declare module 'azdata' {
* File type for execution plan. This will be the file type of the editor when the user opens the graph file
*/
graphFileType: string;
/**
* Index of the execution plan in the file content
*/
planIndexInFile?: number;
}
export interface GetExecutionPlanResult extends ResultStatus {
@@ -1391,7 +1399,7 @@ declare module 'azdata' {
/**
* List of matching nodes for the ExecutionGraphComparisonResult.
*/
matchingNodes: ExecutionGraphComparisonResult[];
matchingNodesId: number[];
/**
* The parent of the ExecutionGraphComparisonResult.
*/

View File

@@ -553,7 +553,8 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData
public $registerExecutionPlanProvider(providerId: string, handle: number): void {
this._executionPlanService.registerProvider(providerId, <azdata.executionPlan.ExecutionPlanProvider>{
getExecutionPlan: (planFile: azdata.executionPlan.ExecutionPlanGraphInfo) => this._proxy.$getExecutionPlan(handle, planFile)
getExecutionPlan: (planFile: azdata.executionPlan.ExecutionPlanGraphInfo) => this._proxy.$getExecutionPlan(handle, planFile),
compareExecutionPlanGraph: (firstPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo, secondPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo) => this._proxy.$compareExecutionPlanGraph(handle, firstPlanFile, secondPlanFile)
});
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as azdataGraphModule from 'azdataGraph';
import type * as azdata from 'azdata';
import * as azdata from 'azdata';
import * as sqlExtHostType from 'sql/workbench/api/common/sqlExtHostTypes';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { isString } from 'vs/base/common/types';
@@ -12,7 +12,8 @@ import { badgeIconPaths, executionPlanNodeIconPaths } from 'sql/workbench/contri
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry';
import { foreground } from 'vs/platform/theme/common/colorRegistry';
import { generateUuid } from 'vs/base/common/uuid';
const azdataGraph = azdataGraphModule();
/**
@@ -23,7 +24,6 @@ export class AzdataGraphView {
private _diagram: any;
private _diagramModel: AzDataGraphCell;
private _uniqueElementId: number = -1;
private _cellInFocus: AzDataGraphCell;
private _graphElementPropertiesSet: Set<string> = new Set();
@@ -51,11 +51,6 @@ export class AzdataGraphView {
this._diagram.graph.tooltipHandler.delay = 700; // increasing delay for tooltips
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const iconBackground = theme.getColor(editorBackground);
if (iconBackground) {
this._diagram.setIconBackgroundColor(iconBackground);
}
const iconLabelColor = theme.getColor(foreground);
if (iconLabelColor) {
this._diagram.setTextFontColor(iconLabelColor);
@@ -99,7 +94,7 @@ export class AzdataGraphView {
if (element) {
cell = this._diagram.graph.model.getCell(element.id);
} else {
cell = this._diagram.graph.model.getCell((<InternalExecutionPlanNode>this._executionPlan.root).id);
cell = this._diagram.graph.model.getCell((<azdata.executionPlan.ExecutionPlanNode>this._executionPlan.root).id);
}
this._diagram.graph.getSelectionModel().setCell(cell);
if (bringToCenter) {
@@ -125,7 +120,6 @@ export class AzdataGraphView {
this._diagram.zoomIn();
}
/**
* Zooms out of the diagram
*/
@@ -163,7 +157,7 @@ export class AzdataGraphView {
* @param id id of the diagram element
*/
public getElementById(id: string): InternalExecutionPlanElement | undefined {
const nodeStack: InternalExecutionPlanNode[] = [];
const nodeStack: azdata.executionPlan.ExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) {
const currentNode = nodeStack.pop();
@@ -185,10 +179,10 @@ export class AzdataGraphView {
/**
* Searches the diagram nodes based on the search query provided.
*/
public searchNodes(searchQuery: SearchQuery): InternalExecutionPlanNode[] {
const resultNodes: InternalExecutionPlanNode[] = [];
public searchNodes(searchQuery: SearchQuery): azdata.executionPlan.ExecutionPlanNode[] {
const resultNodes: azdata.executionPlan.ExecutionPlanNode[] = [];
const nodeStack: InternalExecutionPlanNode[] = [];
const nodeStack: azdata.executionPlan.ExecutionPlanNode[] = [];
nodeStack.push(this._executionPlan.root);
while (nodeStack.length !== 0) {
@@ -287,13 +281,14 @@ export class AzdataGraphView {
});
}
private populate(node: InternalExecutionPlanNode): AzDataGraphCell {
private populate(node: azdata.executionPlan.ExecutionPlanNode): AzDataGraphCell {
let diagramNode: AzDataGraphCell = <AzDataGraphCell>{};
diagramNode.label = node.subtext.join(this.textResourcePropertiesService.getEOL(undefined));
diagramNode.tooltipTitle = node.name;
const nodeId = this.createGraphElementId();
diagramNode.id = nodeId;
node.id = nodeId;
if (!node.id.toString().startsWith(`element-`)) {
node.id = `element-${node.id}`;
}
diagramNode.id = node.id;
if (node.type) {
diagramNode.icon = node.type;
@@ -383,8 +378,7 @@ export class AzdataGraphView {
}
private createGraphElementId(): string {
this._uniqueElementId += 1;
return `element-${this._uniqueElementId}`;
return `element-${generateUuid()}`;
}
/**
@@ -406,13 +400,15 @@ export class AzdataGraphView {
}
return this._diagram.graph.tooltipHandler.enabled;
}
}
export interface InternalExecutionPlanNode extends azdata.executionPlan.ExecutionPlanNode {
/**
* Unique internal id given to graph node by ADS.
*/
id?: string;
public drawSubtreePolygon(subtreeRoot: string, fillColor: string, borderColor: string): void {
const drawPolygon = this._diagram.graph.model.getCell(`element-${subtreeRoot}`);
this._diagram.drawPolygon(drawPolygon, fillColor, borderColor);
}
public clearSubtreePolygon(): void {
this._diagram.removeDrawnPolygons();
}
}
export interface InternalExecutionPlanEdge extends azdata.executionPlan.ExecutionPlanEdge {
@@ -422,7 +418,7 @@ export interface InternalExecutionPlanEdge extends azdata.executionPlan.Executio
id?: string;
}
export type InternalExecutionPlanElement = InternalExecutionPlanEdge | InternalExecutionPlanNode;
export type InternalExecutionPlanElement = InternalExecutionPlanEdge | azdata.executionPlan.ExecutionPlanNode;
export interface AzDataGraphCell {
/**

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { localize } from 'vs/nls';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { URI } from 'vs/base/common/uri';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ExecutionPlanComparisonEditorView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView';
export class ExecutionPlanComparisonInput extends EditorInput {
public static ID: string = 'workbench.editorinputs.compareExecutionPlanInput';
public static SCHEME: string = 'compareExecutionPlanInput';
private readonly editorNamePrefix = localize('epCompare.editorName', "Compare Execution Plans");
private _editorName: string;
// Caching the views for faster tab switching
public _executionPlanComparisonView: ExecutionPlanComparisonEditorView;
constructor(
public preloadModel: ExecutionPlanComparisonEditorModel | undefined,
@IEditorService private readonly _editorService: IEditorService
) {
super();
// Getting name for the editor
const existingNames = this._editorService.editors.map(editor => editor.getName());
let i = 0;
this._editorName = `${this.editorNamePrefix} ${i}`;
while (existingNames.includes(this._editorName)) {
i++;
this._editorName = `${this.editorNamePrefix} ${i}`;
}
}
get typeId(): string {
return ExecutionPlanComparisonInput.ID;
}
get resource(): URI {
return URI.from({
scheme: ExecutionPlanComparisonInput.SCHEME,
path: 'execution-plan-compare'
});
}
public override getName(): string {
return this._editorName;
}
}
export interface ExecutionPlanComparisonEditorModel {
topExecutionPlan?: azdata.executionPlan.ExecutionPlanGraph[];
bottomExecutionPlan?: azdata.executionPlan.ExecutionPlanGraph[];
}

View File

@@ -273,3 +273,87 @@ export const zoomToFitIconClassNames = 'ep-zoom-to-fit-icon';
export const zoomIconClassNames = 'ep-zoom-icon';
export const enableTooltipIconClassName = 'ep-enable-tooltip-icon';
export const disableTooltipIconClassName = 'ep-disable-tooltip-icon';
export const addIconClassName = 'ep-add-icon';
export const settingsIconClassName = 'ep-settings-icon';
export const splitScreenHorizontallyIconClassName = 'ep-split-screen-horizontally-icon';
export const splitScreenVerticallyIconClassName = 'ep-split-screen-vertically-icon';
export const resetZoomIconClassName = 'ep-reset-zoom-icon';
export const executionPlanCompareIconClassName = 'ep-plan-compare-icon';
/**
* Plan comparison polygon border colors
*/
export const polygonBorderColor: string[] = [
`rgba(0, 188, 242)`, // "themeMain blue"
`rgba(236, 0, 140)`, // "themeError pink"
`rgba(0, 216, 204)`, // "h2 blue"
`rgba(236, 0, 140)`, // "b0 orange"
`rgba(255, 140, 0)`, // "themeWarning orange"
`rgba(127, 186, 0)`, // "themeSuccess green"
`rgba(252, 214, 241)`, // "paletteDiffDel light pink"
`rgba(252, 209, 22)`, // "a1 gold"
`rgba(68,35,89)`, // "e1 dark purple"
`rgba(0, 114, 198)`, // "g1 blue"
`rgba(160, 165, 168)`, // "i1 green"
`rgba(255, 140, 0)`, // "k1 grey"
`rgba(199, 241, 199)`, // "paletteDiffAdd light green"
`rgba(0, 24, 143)`, // "d0 pink",
`rgba(186, 216, 10)`, // "f0 royal blue"
`rgba(255, 252, 158)`, // "h0 seafoam green"
`rgba(221, 89, 0)`, // "j0 yellow green"
`rgba(155, 79, 150)`, // "a2 light yellow"
`rgba(109, 194, 233)`, // "c2 burnt orange"
`rgba(85, 212, 85)`, // "e2 purple"
`rgba(180, 0, 158)`, // "d1 purple"
`rgba(0, 32, 80)`, // "f1 navy blue"
`rgba(0, 130, 114)`, // "h1 blue green"
`rgba(127, 186, 0)`, // "j1 yellow green"
`rgba(255, 241, 0)`, // "a0 bright yellow"
`rgba(104, 33, 122)`, // "e0 purple"
`rgba(0, 188, 242)`, // "g0 sky blue"
`rgba(0, 158, 73)`, // "i0 green"
`rgba(187, 194, 202)`, // "k0 grey"
`rgba(255, 185, 0)`, // "b2 gold"
`rgba(244, 114, 208)`, // "d2 pink"
`rgba(70, 104, 197)`, // "f2 blue purple"
`rgba(226, 229, 132)`, // "j2 khaki"
];
/**
* Plan comparison polygon fill colors
*/
export const polygonFillColor: string[] = [
`rgba(0, 188, 242, 0.1)`, // "themeMain blue"
`rgba(236, 0, 140, 0.1)`, // "themeError pink"
`rgba(0, 216, 204, 0.1)`, // "h2 blue"
`rgba(236, 0, 140, 0.1)`, // "b0 orange"
`rgba(255, 140, 0, 0.1)`, // "themeWarning orange"
`rgba(127, 186, 0, 0.1)`, // "themeSuccess green"
`rgba(252, 214, 241, 0.1)`, // "paletteDiffDel light pink"
`rgba(252, 209, 22, 0.1)`, // "a1 gold"
`rgba(68,35,89, 0.1)`, // "e1 dark purple"
`rgba(0, 114, 198, 0.1)`, // "g1 blue"
`rgba(160, 165, 168, 0.1)`, // "i1 green"
`rgba(255, 140, 0, 0.1)`, // "k1 grey"
`rgba(199, 241, 199, 0.1)`, // "paletteDiffAdd light green"
`rgba(0, 24, 143, 0.1)`, // "d0 pink",
`rgba(186, 216, 10, 0.1)`, // "f0 royal blue"
`rgba(255, 252, 158, 0.1)`, // "h0 seafoam green"
`rgba(221, 89, 0, 0.1)`, // "j0 yellow green"
`rgba(155, 79, 150, 0.1)`, // "a2 light yellow"
`rgba(109, 194, 233, 0.1)`, // "c2 burnt orange"
`rgba(85, 212, 85, 0.1)`, // "e2 purple"
`rgba(180, 0, 158, 0.1)`, // "d1 purple"
`rgba(0, 32, 80, 0.1)`, // "f1 navy blue"
`rgba(0, 130, 114, 0.1)`, // "h1 blue green"
`rgba(127, 186, 0, 0.1)`, // "j1 yellow green"
`rgba(255, 241, 0, 0.1)`, // "a0 bright yellow"
`rgba(104, 33, 122, 0.1)`, // "e0 purple"
`rgba(0, 188, 242, 0.1)`, // "g0 sky blue"
`rgba(0, 158, 73, 0.1)`, // "i0 green"
`rgba(187, 194, 202, 0.1)`, // "k0 grey"
`rgba(255, 185, 0, 0.1)`, // "b2 gold"
`rgba(244, 114, 208, 0.1)`, // "d2 pink"
`rgba(70, 104, 197, 0.1)`, // "f2 blue purple"
`rgba(226, 229, 132, 0.1)`, // "j2 khaki"
];

View File

@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* 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 { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ExecutionPlanComparisonEditorView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditorView';
export class ExecutionPlanComparisonEditor extends EditorPane {
public static ID: string = 'workbench.editor.compareExecutionPlan';
public static LABEL: string = localize('compareExecutionPlanEditor', "Compare Execution Plan Editor");
private _editorContainer: HTMLElement;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IContextViewService readonly contextViewService: IContextViewService
) {
super(ExecutionPlanComparisonEditor.ID, telemetryService, themeService, storageService);
}
protected createEditor(parent: HTMLElement): void {
this._editorContainer = DOM.$('.eps-container');
parent.appendChild(this._editorContainer);
}
public override get input(): ExecutionPlanComparisonInput {
return <ExecutionPlanComparisonInput>super.input;
}
layout(dimension: DOM.Dimension): void {
this._editorContainer.style.width = dimension.width + 'px';
this._editorContainer.style.height = dimension.height + 'px';
}
public override async setInput(input: ExecutionPlanComparisonInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
const oldInput = this.input as ExecutionPlanComparisonInput;
// returning when new input is the same as current input
if (oldInput && input.matches(oldInput)) {
return Promise.resolve();
}
super.setInput(input, options, context, token);
// removing existing comparison containers
while (this._editorContainer.firstChild) {
this._editorContainer.removeChild(this._editorContainer.firstChild);
}
// creating a new comparison view if the new input does not already have a cached one.
if (!input._executionPlanComparisonView) {
input._executionPlanComparisonView = this._instantiationService.createInstance(ExecutionPlanComparisonEditorView, this._editorContainer);
if (this.input.preloadModel) {
if (this.input.preloadModel.topExecutionPlan) {
input._executionPlanComparisonView.addExecutionPlanGraph(this.input.preloadModel.topExecutionPlan);
}
if (this.input.preloadModel.bottomExecutionPlan) {
input._executionPlanComparisonView.addExecutionPlanGraph(this.input.preloadModel.bottomExecutionPlan);
}
}
} else { // Getting the cached comparison view from the input and adding it to the base editor node.
this._editorContainer.appendChild(input._executionPlanComparisonView.container);
}
}
}

View File

@@ -0,0 +1,675 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanComparisonPropertiesView } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonPropertiesView';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import { IHorizontalSashLayoutProvider, ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { Action } from 'vs/base/common/actions';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import * as DOM from 'vs/base/browser/dom';
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { localize } from 'vs/nls';
import { addIconClassName, openPropertiesIconClassNames, polygonBorderColor, polygonFillColor, resetZoomIconClassName, splitScreenHorizontallyIconClassName, splitScreenVerticallyIconClassName, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { extname } from 'vs/base/common/path';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { InfoBox } from 'sql/workbench/browser/ui/infoBox/infoBox';
import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner';
import { errorForeground, listHoverBackground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { ExecutionPlanViewHeader } from 'sql/workbench/contrib/executionPlan/browser/executionPlanViewHeader';
import { attachSelectBoxStyler } from 'sql/platform/theme/common/styler';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
export class ExecutionPlanComparisonEditorView {
public container: HTMLElement;
private _taskbarContainer: HTMLElement;
private _taskbar: Taskbar;
private _addExecutionPlanAction: Action;
private _zoomInAction: Action;
private _zoomOutAction: Action;
private _zoomToFitAction: Action;
private _resetZoomAction: Action;
private _propertiesAction: Action;
private _toggleOrientationAction: Action;
private _planComparisonContainer: HTMLElement;
private _propertiesContainer: HTMLElement;
private _propertiesView: ExecutionPlanComparisonPropertiesView;
public planSplitViewContainer: HTMLElement;
private _sashContainer: HTMLElement;
private _horizontalSash: Sash;
private _verticalSash: Sash;
private _orientation: 'horizontal' | 'vertical' = 'horizontal';
private _placeholderContainer: HTMLElement;
private _placeholderInfoboxContainer: HTMLElement;
private _placeholderInfobox: InfoBox;
private _placeholderLoading: LoadingSpinner;
private _topPlanContainer: HTMLElement;
private _topPlanDropdown: SelectBox;
private _topPlanDropdownContainer: HTMLElement;
private _topPlanDiagramContainers: HTMLElement[] = [];
public topPlanDiagrams: AzdataGraphView[] = [];
private _topPlanDiagramModels: azdata.executionPlan.ExecutionPlanGraph[];
private _activeTopPlanIndex: number = 0;
private _topPlanRecommendations: ExecutionPlanViewHeader;
private _topSimilarNode: Map<string, azdata.executionPlan.ExecutionGraphComparisonResult> = new Map();
private _polygonRootsMap: Map<number, {
topPolygon: azdata.executionPlan.ExecutionGraphComparisonResult,
bottomPolygon: azdata.executionPlan.ExecutionGraphComparisonResult
}> = new Map();
private get _activeTopPlanDiagram(): AzdataGraphView {
if (this.topPlanDiagrams.length > 0) {
return this.topPlanDiagrams[this._activeTopPlanIndex];
}
return undefined;
}
private _bottomPlanContainer: HTMLElement;
private _bottomPlanDropdown: SelectBox;
private _bottomPlanDropdownContainer: HTMLElement;
private _bottomPlanDiagramContainers: HTMLElement[] = [];
public bottomPlanDiagrams: AzdataGraphView[] = [];
private _bottomPlanDiagramModels: azdata.executionPlan.ExecutionPlanGraph[];
private _activeBottomPlanIndex: number = 0;
private _bottomPlanRecommendations: ExecutionPlanViewHeader;
private _bottomSimilarNode: Map<string, azdata.executionPlan.ExecutionGraphComparisonResult> = new Map();
private get _activeBottomPlanDiagram(): AzdataGraphView {
if (this.bottomPlanDiagrams.length > 0) {
return this.bottomPlanDiagrams[this._activeBottomPlanIndex];
}
return undefined;
}
constructor(
parentContainer: HTMLElement,
@IInstantiationService private _instantiationService: IInstantiationService,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService private themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IExecutionPlanService private _executionPlanService: IExecutionPlanService,
@IFileDialogService private _fileDialogService: IFileDialogService,
@IContextViewService readonly contextViewService: IContextViewService,
@ITextFileService private readonly _textFileService: ITextFileService,
@INotificationService private _notificationService: INotificationService,
@IProgressService private _progressService: IProgressService
) {
this.container = DOM.$('.comparison-editor');
parentContainer.appendChild(this.container);
this.initializeToolbar();
this.initializePlanComparison();
this.refreshSplitView();
}
// creating and adding editor toolbar actions
private initializeToolbar(): void {
this._taskbarContainer = DOM.$('.editor-toolbar');
this._taskbar = new Taskbar(this._taskbarContainer, {
orientation: ActionsOrientation.HORIZONTAL,
});
this._taskbar.context = this;
this._addExecutionPlanAction = new AddExecutionPlanAction();
this._zoomOutAction = new ZoomOutAction();
this._zoomInAction = new ZoomInAction();
this._zoomToFitAction = new ZoomToFitAction();
this._propertiesAction = new PropertiesAction();
this._toggleOrientationAction = new ToggleOrientation();
this._resetZoomAction = new ZoomReset();
const content: ITaskbarContent[] = [
{ action: this._addExecutionPlanAction },
{ action: this._zoomInAction },
{ action: this._zoomOutAction },
{ action: this._zoomToFitAction },
{ action: this._resetZoomAction },
{ action: this._toggleOrientationAction },
{ action: this._propertiesAction }
];
this._taskbar.setContent(content);
this.container.appendChild(this._taskbarContainer);
}
private initializePlanComparison(): void {
this._planComparisonContainer = DOM.$('.plan-comparison-container');
this.container.appendChild(this._planComparisonContainer);
this.initializeSplitView();
this.initializeProperties();
}
private initializeSplitView(): void {
this.planSplitViewContainer = DOM.$('.split-view-container');
this._planComparisonContainer.appendChild(this.planSplitViewContainer);
this._placeholderContainer = DOM.$('.placeholder');
this._placeholderInfoboxContainer = DOM.$('.placeholder-infobox');
this._placeholderLoading = new LoadingSpinner(this._placeholderContainer, {
fullSize: true,
showText: true
});
this._placeholderContainer.appendChild(this._placeholderInfoboxContainer);
this._placeholderLoading.loadingMessage = localize('epComapre.LoadingPlanMessage', "Loading execution plan");
this._placeholderLoading.loadingCompletedMessage = localize('epComapre.LoadingPlanCompleteMessage', "Execution plan successfully loaded");
this._placeholderInfobox = this._instantiationService.createInstance(InfoBox, this._placeholderInfoboxContainer, {
style: 'information',
text: ''
});
this._placeholderInfobox.text = localize('epComapre.placeholderInfoboxText', "Add execution plans to compare");
this._topPlanContainer = DOM.$('.plan-container');
this.planSplitViewContainer.appendChild(this._topPlanContainer);
this._topPlanDropdownContainer = DOM.$('.dropdown-container');
this._topPlanDropdown = new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._topPlanDropdownContainer);
this._topPlanDropdown.render(this._topPlanDropdownContainer);
this._topPlanDropdown.onDidSelect(async (e) => {
this._activeBottomPlanDiagram.clearSubtreePolygon();
this._activeTopPlanDiagram.clearSubtreePolygon();
this._topPlanDiagramContainers.forEach(c => {
c.style.display = 'none';
});
this._topPlanDiagramContainers[e.index].style.display = '';
this.topPlanDiagrams[e.index].selectElement(undefined);
this._propertiesView.setTopElement(this._topPlanDiagramModels[e.index].root);
this._topPlanRecommendations.recommendations = this._topPlanDiagramModels[e.index].recommendations;
this._activeTopPlanIndex = e.index;
await this.getSkeletonNodes();
});
attachSelectBoxStyler(this._topPlanDropdown, this.themeService);
this._topPlanContainer.appendChild(this._topPlanDropdownContainer);
this._topPlanRecommendations = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._topPlanContainer, undefined);
this.initializeSash();
this._bottomPlanContainer = DOM.$('.plan-container');
this.planSplitViewContainer.appendChild(this._bottomPlanContainer);
this._bottomPlanDropdownContainer = DOM.$('.dropdown-container');
this._bottomPlanDropdown = new SelectBox(['option 1', 'option2'], 'option1', this.contextViewService, this._bottomPlanDropdownContainer);
this._bottomPlanDropdown.render(this._bottomPlanDropdownContainer);
this._bottomPlanDropdown.onDidSelect(async (e) => {
this._activeBottomPlanDiagram.clearSubtreePolygon();
this._activeTopPlanDiagram.clearSubtreePolygon();
this._bottomPlanDiagramContainers.forEach(c => {
c.style.display = 'none';
});
this._bottomPlanDiagramContainers[e.index].style.display = '';
this.bottomPlanDiagrams[e.index].selectElement(undefined);
this._propertiesView.setTopElement(this._bottomPlanDiagramModels[e.index].root);
this._bottomPlanRecommendations.recommendations = this._bottomPlanDiagramModels[e.index].recommendations;
this._activeBottomPlanIndex = e.index;
await this.getSkeletonNodes();
});
attachSelectBoxStyler(this._bottomPlanDropdown, this.themeService);
this._bottomPlanContainer.appendChild(this._bottomPlanDropdownContainer);
this._bottomPlanRecommendations = this._instantiationService.createInstance(ExecutionPlanViewHeader, this._bottomPlanContainer, undefined);
}
private initializeSash(): void {
this._sashContainer = DOM.$('.sash-container');
this.planSplitViewContainer.appendChild(this._sashContainer);
this._verticalSash = new Sash(this._sashContainer, new VerticalSash(this), { orientation: Orientation.VERTICAL, size: 3 });
let originalWidth;
let change = 0;
this._verticalSash.onDidStart((e: ISashEvent) => {
originalWidth = this._topPlanContainer.offsetWidth;
});
this._verticalSash.onDidChange((evt: ISashEvent) => {
change = evt.startX - evt.currentX;
const newWidth = originalWidth - change;
if (newWidth < 200) {
return;
}
this._topPlanContainer.style.minWidth = '200px';
this._topPlanContainer.style.flex = `0 0 ${newWidth}px`;
});
this._horizontalSash = new Sash(this._sashContainer, new HorizontalSash(this), { orientation: Orientation.HORIZONTAL, size: 3 });
let startHeight;
this._horizontalSash.onDidStart((e: ISashEvent) => {
startHeight = this._topPlanContainer.offsetHeight;
});
this._horizontalSash.onDidChange((evt: ISashEvent) => {
change = evt.startY - evt.currentY;
const newHeight = startHeight - change;
if (newHeight < 200) {
return;
}
this._topPlanContainer.style.minHeight = '200px';
this._topPlanContainer.style.flex = `0 0 ${newHeight}px`;
});
}
private initializeProperties(): void {
this._propertiesContainer = DOM.$('.properties');
this._propertiesView = this._instantiationService.createInstance(ExecutionPlanComparisonPropertiesView, this._propertiesContainer);
this._planComparisonContainer.appendChild(this._propertiesContainer);
}
public async openAndAddExecutionPlanFile(): Promise<void> {
try {
const openedFileUris = await this._fileDialogService.showOpenDialog({
filters: [
{
extensions: await this._executionPlanService.getSupportedExecutionPlanExtensions(),
name: localize('epCompare.FileFilterDescription', "Execution Plan Files")
}
],
canSelectMany: false,
canSelectFiles: true
});
if (openedFileUris?.length === 1) {
this._placeholderInfoboxContainer.style.display = 'none';
this._placeholderLoading.loading = true;
const fileURI = openedFileUris[0];
const fileContent = (await this._textFileService.read(fileURI, { acceptTextOnly: true })).value;
let executionPlanGraphs = await this._executionPlanService.getExecutionPlan({
graphFileContent: fileContent,
graphFileType: extname(fileURI.fsPath).replace('.', '')
});
await this.addExecutionPlanGraph(executionPlanGraphs.graphs);
}
this._placeholderInfoboxContainer.style.display = '';
this._placeholderLoading.loading = false;
this._placeholderInfoboxContainer.style.display = '';
} catch (e) {
this._placeholderLoading.loading = false;
this._notificationService.error(e);
}
}
public async addExecutionPlanGraph(executionPlanGraphs: azdata.executionPlan.ExecutionPlanGraph[]): Promise<void> {
if (!this._topPlanDiagramModels) {
this._topPlanDiagramModels = executionPlanGraphs;
this._topPlanDropdown.setOptions(executionPlanGraphs.map(e => {
return {
text: e.query
};
}), 0);
executionPlanGraphs.forEach((e, i) => {
const graphContainer = DOM.$('.plan-diagram');
this._topPlanDiagramContainers.push(graphContainer);
this._topPlanContainer.appendChild(graphContainer);
const diagram = this._instantiationService.createInstance(AzdataGraphView, graphContainer, e);
diagram.onElementSelected(e => {
this._propertiesView.setTopElement(e);
const id = e.id.replace(`element-`, '');
if (this._topSimilarNode.has(id)) {
const similarNode = this._topSimilarNode.get(id);
const element = this._activeBottomPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]);
if (similarNode.matchingNodesId.find(m => this._activeBottomPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) {
return;
}
this._activeBottomPlanDiagram.selectElement(element);
}
});
this.topPlanDiagrams.push(diagram);
graphContainer.style.display = 'none';
});
this._topPlanDiagramContainers[0].style.display = '';
this._topPlanRecommendations.recommendations = executionPlanGraphs[0].recommendations;
this.topPlanDiagrams[0].selectElement(undefined);
this._propertiesView.setTopElement(executionPlanGraphs[0].root);
this._propertiesAction.enabled = true;
this._zoomInAction.enabled = true;
this._zoomOutAction.enabled = true;
this._resetZoomAction.enabled = true;
this._zoomToFitAction.enabled = true;
this._toggleOrientationAction.enabled = true;
} else {
this._bottomPlanDiagramModels = executionPlanGraphs;
this._bottomPlanDropdown.setOptions(executionPlanGraphs.map(e => {
return {
text: e.query
};
}), 0);
executionPlanGraphs.forEach((e, i) => {
const graphContainer = DOM.$('.plan-diagram');
this._bottomPlanDiagramContainers.push(graphContainer);
this._bottomPlanContainer.appendChild(graphContainer);
const diagram = this._instantiationService.createInstance(AzdataGraphView, graphContainer, e);
diagram.onElementSelected(e => {
this._propertiesView.setBottomElement(e);
const id = e.id.replace(`element-`, '');
if (this._bottomSimilarNode.has(id)) {
const similarNode = this._bottomSimilarNode.get(id);
const element = this._activeTopPlanDiagram.getElementById(`element-` + similarNode.matchingNodesId[0]);
if (similarNode.matchingNodesId.find(m => this._activeTopPlanDiagram.getSelectedElement().id === `element-` + m) !== undefined) {
return;
}
this._activeTopPlanDiagram.selectElement(element);
}
});
this.bottomPlanDiagrams.push(diagram);
graphContainer.style.display = 'none';
});
this._bottomPlanDiagramContainers[0].style.display = '';
this._bottomPlanRecommendations.recommendations = executionPlanGraphs[0].recommendations;
this.bottomPlanDiagrams[0].selectElement(undefined);
this._propertiesView.setBottomElement(executionPlanGraphs[0].root);
this._addExecutionPlanAction.enabled = false;
await this.getSkeletonNodes();
}
this.refreshSplitView();
}
private async getSkeletonNodes(): Promise<void> {
this._progressService.withProgress(
{
location: ProgressLocation.Notification,
title: localize('epCompare.comparisonProgess', "Loading similar areas in compared plans"),
cancellable: false
},
async (progress) => {
this._polygonRootsMap = new Map();
this._topSimilarNode = new Map();
this._bottomSimilarNode = new Map();
if (this._topPlanDiagramModels && this._bottomPlanDiagramModels) {
this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile.graphFileType = 'sqlplan';
this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile.graphFileType = 'sqlplan';
const result = await this._executionPlanService.compareExecutionPlanGraph(this._topPlanDiagramModels[this._activeTopPlanIndex].graphFile,
this._bottomPlanDiagramModels[this._activeBottomPlanIndex].graphFile);
this.getSimilarSubtrees(result.firstComparisonResult);
this.getSimilarSubtrees(result.secondComparisonResult, true);
let colorIndex = 0;
this._polygonRootsMap.forEach((v, k) => {
this._activeTopPlanDiagram.drawSubtreePolygon(v.topPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]);
this._activeBottomPlanDiagram.drawSubtreePolygon(v.bottomPolygon.baseNode.id, polygonFillColor[colorIndex], polygonBorderColor[colorIndex]);
colorIndex += 1;
});
}
return;
}
);
}
private getSimilarSubtrees(comparedNode: azdata.executionPlan.ExecutionGraphComparisonResult, isBottomPlan: boolean = false): void {
if (comparedNode.hasMatch) {
if (!isBottomPlan) {
this._topSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode);
if (!this._polygonRootsMap.has(comparedNode.groupIndex)) {
this._polygonRootsMap.set(comparedNode.groupIndex, {
topPolygon: comparedNode,
bottomPolygon: undefined
});
}
} else {
this._bottomSimilarNode.set(`${comparedNode.baseNode.id}`, comparedNode);
if (this._polygonRootsMap.get(comparedNode.groupIndex).bottomPolygon === undefined) {
const polygonMapEntry = this._polygonRootsMap.get(comparedNode.groupIndex);
polygonMapEntry.bottomPolygon = comparedNode;
this._polygonRootsMap.set(comparedNode.groupIndex, polygonMapEntry);
}
}
}
comparedNode.children.forEach(c => {
this.getSimilarSubtrees(c, isBottomPlan);
});
}
public togglePropertiesView(): void {
this._propertiesContainer.style.display = this._propertiesContainer.style.display === 'none' ? '' : 'none';
}
public toggleOrientation(): void {
if (this._orientation === 'vertical') {
this._sashContainer.style.width = '100%';
this._sashContainer.style.height = '3px';
this.planSplitViewContainer.style.flexDirection = 'column';
this._topPlanContainer.style.minHeight = '200px';
this._topPlanContainer.style.minWidth = '';
this._topPlanContainer.style.flex = '1';
this._orientation = 'horizontal';
this._toggleOrientationAction.class = splitScreenHorizontallyIconClassName;
} else {
this._sashContainer.style.width = '3px';
this._sashContainer.style.height = '100%';
this.planSplitViewContainer.style.flexDirection = 'row';
this._topPlanContainer.style.minHeight = '';
this._topPlanContainer.style.minWidth = '200px';
this._orientation = 'vertical';
this._toggleOrientationAction.class = splitScreenVerticallyIconClassName;
}
this._topPlanContainer.style.flex = '1';
this._bottomPlanContainer.style.flex = '1';
}
public refreshSplitView(): void {
if (this.planSplitViewContainer.contains(this._topPlanContainer)) {
this.planSplitViewContainer.removeChild(this._topPlanContainer);
}
if (this.planSplitViewContainer.contains(this._bottomPlanContainer)) {
this.planSplitViewContainer.removeChild(this._bottomPlanContainer);
}
if (this.planSplitViewContainer.contains(this._sashContainer)) {
this.planSplitViewContainer.removeChild(this._sashContainer);
}
if (this.planSplitViewContainer.contains(this._placeholderContainer)) {
this.planSplitViewContainer.removeChild(this._placeholderContainer);
}
if (!this._topPlanDiagramModels && !this._bottomPlanDiagramModels) {
this.planSplitViewContainer.appendChild(this._placeholderContainer);
} else if (this._topPlanDiagramModels && !this._bottomPlanDiagramModels) {
this.planSplitViewContainer.appendChild(this._topPlanContainer);
this.planSplitViewContainer.appendChild(this._sashContainer);
this.planSplitViewContainer.appendChild(this._placeholderContainer);
} else {
this.planSplitViewContainer.appendChild(this._topPlanContainer);
this.planSplitViewContainer.appendChild(this._sashContainer);
this.planSplitViewContainer.appendChild(this._bottomPlanContainer);
}
}
public zoomIn(): void {
this._activeTopPlanDiagram.zoomIn();
this._activeBottomPlanDiagram.zoomIn();
this.syncZoom();
}
public zoomOut(): void {
this._activeTopPlanDiagram.zoomOut();
this._activeBottomPlanDiagram.zoomOut();
this.syncZoom();
}
public zoomToFit(): void {
this._activeTopPlanDiagram.zoomToFit();
this._activeBottomPlanDiagram.zoomToFit();
this.syncZoom();
}
public resetZoom(): void {
if (this._activeTopPlanDiagram) {
this._activeTopPlanDiagram.setZoomLevel(100);
}
if (this._activeBottomPlanDiagram) {
this._activeBottomPlanDiagram.setZoomLevel(100);
}
}
private syncZoom(): void {
if (this._activeTopPlanDiagram.getZoomLevel() < this._activeBottomPlanDiagram.getZoomLevel()) {
this._activeBottomPlanDiagram.setZoomLevel(this._activeTopPlanDiagram.getZoomLevel());
} else {
this._activeTopPlanDiagram.setZoomLevel(this._activeBottomPlanDiagram.getZoomLevel());
}
}
}
class AddExecutionPlanAction extends Action {
public static ID = 'ep.AddExecutionPlan';
public static LABEL = localize('addExecutionPlanLabel', "Add execution plan");
constructor() {
super(AddExecutionPlanAction.ID, AddExecutionPlanAction.LABEL, addIconClassName);
}
public override async run(context: ExecutionPlanComparisonEditorView): Promise<void> {
await context.openAndAddExecutionPlanFile();
}
}
class ZoomInAction extends Action {
public static ID = 'ep.zoomIn';
public static LABEL = localize('epCompare.zoomInAction', "Zoom In");
constructor() {
super(ZoomInAction.ID, ZoomInAction.LABEL, zoomInIconClassNames);
this.enabled = false;
}
public override async run(context: ExecutionPlanComparisonEditorView): Promise<void> {
context.zoomIn();
}
}
class ZoomOutAction extends Action {
public static ID = 'ep.zoomOut';
public static LABEL = localize('epCompare.zoomOutAction', "Zoom Out");
constructor() {
super(ZoomOutAction.ID, ZoomOutAction.LABEL, zoomOutIconClassNames);
this.enabled = false;
}
public override async run(context: ExecutionPlanComparisonEditorView): Promise<void> {
context.zoomOut();
}
}
class ZoomToFitAction extends Action {
public static ID = 'ep.zoomToFit';
public static LABEL = localize('epCompare.zoomToFit', "Zoom to fit");
constructor() {
super(ZoomToFitAction.ID, ZoomToFitAction.LABEL, zoomToFitIconClassNames);
this.enabled = false;
}
public override async run(context: ExecutionPlanComparisonEditorView): Promise<void> {
context.zoomToFit();
}
}
class ZoomReset extends Action {
public static ID = 'ep.resetZoom';
public static LABEL = localize('epCompare.zoomReset', "Reset Zoom");
constructor() {
super(ZoomReset.ID, ZoomReset.LABEL, resetZoomIconClassName);
this.enabled = false;
}
public override async run(context: ExecutionPlanComparisonEditorView): Promise<void> {
context.resetZoom();
}
}
class ToggleOrientation extends Action {
public static ID = 'ep.toggleOrientation';
public static LABEL = localize('epCompare.toggleOrientation', "Toggle Orientation");
constructor() {
super(ToggleOrientation.ID, ToggleOrientation.LABEL, splitScreenHorizontallyIconClassName);
this.enabled = false;
}
public override async run(context: ExecutionPlanComparisonEditorView): Promise<void> {
context.toggleOrientation();
}
}
class PropertiesAction extends Action {
public static ID = 'epCompare.comparePropertiesAction';
public static LABEL = localize('epCompare.comparePropertiesAction', "Properties");
constructor() {
super(PropertiesAction.ID, PropertiesAction.LABEL, openPropertiesIconClassNames);
this.enabled = false;
}
public override async run(context: ExecutionPlanComparisonEditorView): Promise<void> {
context.togglePropertiesView();
}
}
class HorizontalSash implements IHorizontalSashLayoutProvider {
constructor(private _context: ExecutionPlanComparisonEditorView) {
}
getHorizontalSashTop(sash: Sash): number {
return 0;
}
getHorizontalSashLeft?(sash: Sash): number {
return 0;
}
getHorizontalSashWidth?(sash: Sash): number {
return this._context.planSplitViewContainer.clientWidth;
}
}
class VerticalSash implements IVerticalSashLayoutProvider {
constructor(private _context: ExecutionPlanComparisonEditorView) {
}
getVerticalSashLeft(sash: Sash): number {
return 0;
}
getVerticalSashTop?(sash: Sash): number {
return 0;
}
getVerticalSashHeight?(sash: Sash): number {
return this._context.planSplitViewContainer.clientHeight;
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const separatorColor = theme.getColor(errorForeground);
if (separatorColor) {
collector.addRule(`
.designer-component .issues-container .issue-item .issue-icon.codicon-error {
color: ${separatorColor};
}`);
}
const recommendationsColor = theme.getColor(textLinkForeground);
if (recommendationsColor) {
collector.addRule(`
.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .recommendations {
color: ${recommendationsColor};
}
`);
}
const menuBackgroundColor = theme.getColor(listHoverBackground);
if (menuBackgroundColor) {
collector.addRule(`
.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .recommendations {
background-color: ${menuBackgroundColor};
}
`);
}
});

View File

@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExecutionPlanPropertiesViewBase, PropertiesSortType } from 'sql/workbench/contrib/executionPlan/browser/executionPlanPropertiesViewBase';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import type * as azdata from 'azdata';
import { localize } from 'vs/nls';
import { textFormatter } from 'sql/base/browser/ui/table/formatters';
import { isString } from 'vs/base/common/types';
import { removeLineBreaks } from 'sql/base/common/strings';
import * as DOM from 'vs/base/browser/dom';
import { InternalExecutionPlanElement } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
export class ExecutionPlanComparisonPropertiesView extends ExecutionPlanPropertiesViewBase {
private _model: ExecutionPlanComparisonPropertiesViewModel;
private _topOperationNameContainer: HTMLElement;
private _bottomOperationNameContainer: HTMLElement;
public constructor(
parentContainer: HTMLElement,
@IThemeService themeService: IThemeService,
) {
super(parentContainer, themeService);
this._model = <ExecutionPlanComparisonPropertiesViewModel>{};
this._parentContainer.style.display = 'none';
const header = DOM.$('.compare-operation-name');
this._topOperationNameContainer = DOM.$('.compare-operation-name-text');
header.appendChild(this._topOperationNameContainer);
this._bottomOperationNameContainer = DOM.$('.compare-operation-name-text');
header.appendChild(this._bottomOperationNameContainer);
this.setHeader(header);
}
public setTopElement(e: InternalExecutionPlanElement): void {
this._model.topElement = e;
let target;
if ((<azdata.executionPlan.ExecutionPlanNode>e).name) {
target = removeLineBreaks((<azdata.executionPlan.ExecutionPlanNode>e).name);
} else {
target = localize('executionPlanPropertiesEdgeOperationName', "Edge");
}
const titleText = localize('executionPlanComparisonPropertiesTopOperation', "Top operation: {0}", target);
this._topOperationNameContainer.innerText = titleText;
this._bottomOperationNameContainer.title = titleText;
this.addDataToTable();
}
public setBottomElement(e: InternalExecutionPlanElement): void {
this._model.bottomElement = e;
let target;
if ((<azdata.executionPlan.ExecutionPlanNode>e)?.name) {
target = removeLineBreaks((<azdata.executionPlan.ExecutionPlanNode>e).name);
} else {
target = localize('executionPlanPropertiesEdgeOperationName', "Edge");
}
const titleText = localize('executionPlanComparisonPropertiesBottomOperation', "Bottom operation: {0}", target);
this._bottomOperationNameContainer.innerText = titleText;
this._bottomOperationNameContainer.title = titleText;
this.addDataToTable();
}
public addDataToTable() {
const columns: Slick.Column<Slick.SlickData>[] = [
];
if (this._model.topElement) {
columns.push({
id: 'name',
name: localize('nodePropertyViewNameNameColumnHeader', "Name"),
field: 'name',
width: 200,
editor: Slick.Editors.Text,
headerCssClass: 'prop-table-header',
formatter: textFormatter
});
columns.push({
id: 'value',
name: localize('nodePropertyViewNameValueColumnTopHeader', "Value (Top Plan)"),
field: 'value1',
width: 150,
editor: Slick.Editors.Text,
headerCssClass: 'prop-table-header',
formatter: textFormatter
});
}
if (this._model.bottomElement) {
columns.push({
id: 'value',
name: localize('nodePropertyViewNameValueColumnBottomHeader', "Value (Bottom Plan)"),
field: 'value2',
width: 150,
editor: Slick.Editors.Text,
headerCssClass: 'prop-table-header',
formatter: textFormatter
});
}
let topProps = [];
let bottomProps = [];
if (this._model.topElement?.properties) {
topProps = this._model.topElement.properties;
}
if (this._model.bottomElement?.properties) {
bottomProps = this._model.bottomElement.properties;
}
this.populateTable(columns, this.convertPropertiesToTableRows(topProps, bottomProps, -1, 0));
}
public sortPropertiesAlphabetically(props: Map<string, TablePropertiesMapEntry>): Map<string, TablePropertiesMapEntry> {
return new Map([...props.entries()].sort((a, b) => {
if (!a[1]?.name && !b[1]?.name) {
return 0;
} else if (!a[1]?.name) {
return -1;
} else if (!b[1]?.name) {
return 1;
} else {
return a[1].name.localeCompare(b[1].name);
}
}));
}
public sortPropertiesByImportance(props: Map<string, TablePropertiesMapEntry>): Map<string, TablePropertiesMapEntry> {
return new Map([...props.entries()].sort((a, b) => {
if (!a[1]?.displayOrder && !b[1]?.displayOrder) {
return 0;
} else if (!a[1]?.displayOrder) {
return -1;
} else if (!b[1]?.displayOrder) {
return 1;
} else {
return a[1].displayOrder - b[1].displayOrder;
}
}));
}
public sortPropertiesReverseAlphabetically(props: Map<string, TablePropertiesMapEntry>): Map<string, TablePropertiesMapEntry> {
return new Map([...props.entries()].sort((a, b) => {
if (!a[1]?.displayOrder && !b[1]?.displayOrder) {
return 0;
} else if (!a[1]?.displayOrder) {
return -1;
} else if (!b[1]?.displayOrder) {
return 1;
} else {
return b[1].displayOrder - a[1].displayOrder;
}
}));
}
private convertPropertiesToTableRows(topNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[], bottomNode: azdata.executionPlan.ExecutionPlanGraphElementProperty[], parentIndex: number, indent: number, rows: { [key: string]: string }[] = []): { [key: string]: string }[] {
let propertiesMap: Map<string, TablePropertiesMapEntry> = new Map();
if (topNode) {
topNode.forEach(p => {
propertiesMap.set(p.name, {
topProp: p,
bottomProp: undefined,
displayOrder: p.displayOrder,
name: p.name
});
});
}
if (bottomNode) {
bottomNode.forEach(p => {
if (propertiesMap.has(p.name)) {
propertiesMap.get(p.name).bottomProp = p;
} else {
propertiesMap.set(p.name, {
topProp: undefined,
bottomProp: p,
displayOrder: p.displayOrder,
name: p.name
});
}
});
}
switch (this.sortType) {
case PropertiesSortType.DisplayOrder:
propertiesMap = this.sortPropertiesByImportance(propertiesMap);
break;
case PropertiesSortType.Alphabetical:
propertiesMap = this.sortPropertiesAlphabetically(propertiesMap);
break;
case PropertiesSortType.ReverseAlphabetical:
propertiesMap = this.sortPropertiesReverseAlphabetically(propertiesMap);
break;
}
propertiesMap.forEach((v, k) => {
let row = {};
row['name'] = {
text: k
};
row['parent'] = parentIndex;
const topProp = v.topProp;
const bottomProp = v.bottomProp;
const parentRowCellStyling = 'font-weight: bold';
if (topProp && bottomProp) {
row['displayOrder'] = v.topProp.displayOrder;
row['value1'] = {
text: removeLineBreaks(v.topProp.displayValue, ' ')
};
row['value2'] = {
text: removeLineBreaks(v.bottomProp.displayValue, ' ')
};
if ((topProp && !isString(topProp.value)) || (bottomProp && !isString(bottomProp.value))) {
row['name'].style = parentRowCellStyling;
row['value1'].style = parentRowCellStyling;
row['value2'].style = parentRowCellStyling;
}
rows.push(row);
if (!isString(topProp.value) && !isString(bottomProp.value)) {
this.convertPropertiesToTableRows(topProp.value, bottomProp.value, rows.length - 1, indent + 2, rows);
} else if (isString(topProp?.value) && !isString(bottomProp.value)) {
this.convertPropertiesToTableRows(undefined, bottomProp.value, rows.length - 1, indent + 2, rows);
} else if (!isString(topProp.value) && !isString(bottomProp.value)) {
this.convertPropertiesToTableRows(topProp.value, undefined, rows.length - 1, indent + 2, rows);
}
} else if (topProp && !bottomProp) {
row['displayOrder'] = v.topProp.displayOrder;
row['value1'] = {
text: v.topProp.displayValue
};
rows.push(row);
if (!isString(topProp.value)) {
row['name'].style = parentRowCellStyling;
row['value1'].style = parentRowCellStyling;
this.convertPropertiesToTableRows(topProp.value, undefined, rows.length - 1, indent + 2, rows);
}
} else if (!topProp && bottomProp) {
row['displayOrder'] = v.bottomProp.displayOrder;
row['value2'] = {
text: v.bottomProp.displayValue
};
rows.push(row);
if (!isString(bottomProp.value)) {
row['name'].style = parentRowCellStyling;
row['value2'].style = parentRowCellStyling;
this.convertPropertiesToTableRows(undefined, bottomProp.value, rows.length - 1, indent + 2, rows);
}
}
});
return rows;
}
}
export interface ExecutionPlanComparisonPropertiesViewModel {
topElement: InternalExecutionPlanElement,
bottomElement: InternalExecutionPlanElement
}
interface TablePropertiesMapEntry {
topProp: azdata.executionPlan.ExecutionPlanGraphElementProperty,
bottomProp: azdata.executionPlan.ExecutionPlanGraphElementProperty,
displayOrder: number,
name: string
}

View File

@@ -11,11 +11,17 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces';
import { ExecutionPlanInput } from 'sql/workbench/contrib/executionPlan/common/executionPlanInput';
import { ExecutionPlanEditor } from 'sql/workbench/contrib/executionPlan/browser/executionPlanEditor';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ExecutionPlanComparisonEditor } from 'sql/workbench/contrib/executionPlan/browser/executionPlanComparisonEditor';
import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { localize } from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
// Execution Plan editor registration
@@ -39,7 +45,7 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen
this.registerEditorOverride();
this._capabilitiesService.onCapabilitiesRegistered(e => {
const newFileFormats = this._executionPlanService.getSupportedExecutionPlanExtensionsForProvider(e.id);
const newFileFormats = this._executionPlanService.getSupportedExecutionPlanExtensions(e.id);
if (newFileFormats?.length > 0) {
this._editorResolverService.updateUserAssociations(this.getGlobForFileExtensions(newFileFormats), ExecutionPlanEditor.ID); // Registering new file formats when new providers are registered.
}
@@ -76,3 +82,33 @@ export class ExecutionPlanEditorOverrideContribution extends Disposable implemen
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(ExecutionPlanEditorOverrideContribution, LifecyclePhase.Restored);
const comparisonExecutionPlanEditor = EditorPaneDescriptor.create(
ExecutionPlanComparisonEditor,
ExecutionPlanComparisonEditor.ID,
ExecutionPlanComparisonEditor.LABEL
);
const COMPARE_EXECUTION_PLAN_COMMAND_ID = 'compareExecutionPlan';
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane)
.registerEditorPane(comparisonExecutionPlanEditor, [new SyncDescriptor(ExecutionPlanComparisonInput)]);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: COMPARE_EXECUTION_PLAN_COMMAND_ID,
title: {
value: localize('executionPlanCompareCommandValue', "Compare execution plans"),
original: localize('executionPlanCompareCommandOriginalValue', "Compare execution plans")
},
category: 'Execution Plan'
}
});
CommandsRegistry.registerCommand(COMPARE_EXECUTION_PLAN_COMMAND_ID, (accessors: ServicesAccessor) => {
const editorService = accessors.get(IEditorService);
const instantiationService = accessors.get(IInstantiationService);
editorService.openEditor(instantiationService.createInstance(ExecutionPlanComparisonInput, undefined), {
pinned: true
});
});

View File

@@ -33,7 +33,7 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase
public set graphElement(element: azdata.executionPlan.ExecutionPlanNode | azdata.executionPlan.ExecutionPlanEdge) {
this._model.graphElement = element;
this.renderView();
this.addDataToTable();
}
public sortPropertiesAlphabetically(props: azdata.executionPlan.ExecutionPlanGraphElementProperty[]): azdata.executionPlan.ExecutionPlanGraphElementProperty[] {
@@ -79,7 +79,7 @@ export class ExecutionPlanPropertiesView extends ExecutionPlanPropertiesViewBase
});
}
public renderView(): void {
public addDataToTable(): void {
if (this._model.graphElement) {
const nodeName = (<azdata.executionPlan.ExecutionPlanNode>this._model.graphElement).name;
this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('executionPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'

View File

@@ -15,8 +15,9 @@ import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
import { contrastBorder, listHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { TreeGrid } from 'sql/base/browser/ui/table/treeGrid';
import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
export abstract class ExecutionPlanPropertiesViewBase {
export abstract class ExecutionPlanPropertiesViewBase implements IVerticalSashLayoutProvider {
// Title bar with close button action
private _titleBarContainer!: HTMLElement;
private _titleBarTextContainer!: HTMLElement;
@@ -39,6 +40,8 @@ export abstract class ExecutionPlanPropertiesViewBase {
public sortType: PropertiesSortType = PropertiesSortType.DisplayOrder;
public resizeSash: Sash;
constructor(
public _parentContainer: HTMLElement,
private _themeService: IThemeService
@@ -47,8 +50,28 @@ export abstract class ExecutionPlanPropertiesViewBase {
const sashContainer = DOM.$('.properties-sash');
this._parentContainer.appendChild(sashContainer);
this.resizeSash = new Sash(sashContainer, this, { orientation: Orientation.VERTICAL, size: 3 });
let originalWidth = 0;
this.resizeSash.onDidStart((e: ISashEvent) => {
originalWidth = this._parentContainer.clientWidth;
});
this.resizeSash.onDidChange((evt: ISashEvent) => {
const change = evt.startX - evt.currentX;
const newWidth = originalWidth + change;
if (newWidth < 200) {
return;
}
this._parentContainer.style.flex = `0 0 ${newWidth}px`;
propertiesContent.style.width = `${newWidth}px`;
});
this.resizeSash.onDidEnd(() => {
});
const propertiesContent = DOM.$('.properties-content');
this._parentContainer.appendChild(propertiesContent);
this._titleBarContainer = DOM.$('.title');
this._parentContainer.appendChild(this._titleBarContainer);
propertiesContent.appendChild(this._titleBarContainer);
this._titleBarTextContainer = DOM.$('h3');
this._titleBarTextContainer.classList.add('text');
@@ -64,10 +87,10 @@ export abstract class ExecutionPlanPropertiesViewBase {
this._titleActions.pushAction([new ClosePropertyViewAction()], { icon: true, label: false });
this._headerContainer = DOM.$('.header');
this._parentContainer.appendChild(this._headerContainer);
propertiesContent.appendChild(this._headerContainer);
this._headerActionsContainer = DOM.$('.table-action-bar');
this._parentContainer.appendChild(this._headerActionsContainer);
propertiesContent.appendChild(this._headerActionsContainer);
this._headerActions = new ActionBar(this._headerActionsContainer, {
orientation: ActionsOrientation.HORIZONTAL, context: this
});
@@ -75,7 +98,7 @@ export abstract class ExecutionPlanPropertiesViewBase {
this._tableContainer = DOM.$('.table-container');
this._parentContainer.appendChild(this._tableContainer);
propertiesContent.appendChild(this._tableContainer);
const table = DOM.$('.table');
this._tableContainer.appendChild(table);
@@ -90,9 +113,19 @@ export abstract class ExecutionPlanPropertiesViewBase {
attachTableStyler(this._tableComponent, this._themeService);
new ResizeObserver((e) => {
this.resizeSash.layout();
this.resizeTable();
}).observe(this._parentContainer);
}).observe(_parentContainer);
}
getVerticalSashLeft(sash: Sash): number {
return 0;
}
getVerticalSashTop?(sash: Sash): number {
return 0;
}
getVerticalSashHeight?(sash: Sash): number {
return this._parentContainer.clientHeight;
}
public setTitle(v: string): void {
@@ -106,14 +139,12 @@ export abstract class ExecutionPlanPropertiesViewBase {
public set tableHeight(value: number) {
if (this.tableHeight !== value) {
this._tableHeight = value;
this.renderView();
}
}
public set tableWidth(value: number) {
if (this._tableWidth !== value) {
this._tableWidth = value;
this.renderView();
}
}
@@ -125,11 +156,10 @@ export abstract class ExecutionPlanPropertiesViewBase {
return this._tableHeight;
}
public abstract renderView();
public abstract addDataToTable();
public toggleVisibility(): void {
this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'block' : 'none';
this.renderView();
this._parentContainer.style.display = this._parentContainer.style.display === 'none' ? 'flex' : 'none';
}
public populateTable(columns: Slick.Column<Slick.SlickData>[], data: { [key: string]: string }[]) {
@@ -177,7 +207,7 @@ export class SortPropertiesAlphabeticallyAction extends Action {
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.Alphabetical;
context.renderView();
context.addDataToTable();
}
}
@@ -191,7 +221,7 @@ export class SortPropertiesReverseAlphabeticallyAction extends Action {
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.ReverseAlphabetical;
context.renderView();
context.addDataToTable();
}
}
@@ -205,7 +235,7 @@ export class SortPropertiesByDisplayOrderAction extends Action {
public override async run(context: ExecutionPlanPropertiesViewBase): Promise<void> {
context.sortType = PropertiesSortType.DisplayOrder;
context.renderView();
context.addDataToTable();
}
}

View File

@@ -27,7 +27,7 @@ import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Action } from 'vs/base/common/actions';
import { localize } from 'vs/nls';
import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } from 'sql/workbench/contrib/executionPlan/browser/constants';
import { customZoomIconClassNames, disableTooltipIconClassName, enableTooltipIconClassName, executionPlanCompareIconClassName, openPlanFileIconClassNames, openPropertiesIconClassNames, openQueryIconClassNames, savePlanIconClassNames, searchIconClassNames, zoomInIconClassNames, zoomOutIconClassNames, zoomToFitIconClassNames } 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';
@@ -35,6 +35,7 @@ import { NodeSearchWidget } from 'sql/workbench/contrib/executionPlan/browser/wi
import { AzdataGraphView } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { ExecutionPlanComparisonInput } from 'sql/workbench/contrib/executionPlan/browser/compareExecutionPlanInput';
export class ExecutionPlanView implements ISashLayoutProvider {
@@ -78,6 +79,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
@IFileDialogService public fileDialogService: IFileDialogService,
@IFileService public fileService: IFileService,
@IWorkspaceContextService public workspaceContextService: IWorkspaceContextService,
@IEditorService private _editorService: IEditorService
) {
// parent container for query plan.
this._container = DOM.$('.execution-plan');
@@ -162,6 +164,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._instantiationService.createInstance(ZoomToFitAction, 'ActionBar'),
this._instantiationService.createInstance(CustomZoomAction, 'ActionBar'),
this._instantiationService.createInstance(PropertiesAction, 'ActionBar'),
new CompareExecutionPlanAction(),
this.actionBarToggleTopTip
];
this._actionBar.pushAction(actionBarActions, { icon: true, label: false });
@@ -178,6 +181,7 @@ export class ExecutionPlanView implements ISashLayoutProvider {
this._instantiationService.createInstance(ZoomToFitAction, 'ContextMenu'),
this._instantiationService.createInstance(CustomZoomAction, 'ContextMenu'),
this._instantiationService.createInstance(PropertiesAction, 'ContextMenu'),
new CompareExecutionPlanAction(),
this.contextMenuToggleTooltipAction
];
const self = this;
@@ -278,6 +282,14 @@ export class ExecutionPlanView implements ISashLayoutProvider {
public hideActionBar() {
this._actionBarContainer.style.display = 'none';
}
public compareCurrentExecutionPlan() {
this._editorService.openEditor(this._instantiationService.createInstance(ExecutionPlanComparisonInput, {
topExecutionPlan: [this._model]
}), {
pinned: true
});
}
}
type ExecutionPlanActionSource = 'ContextMenu' | 'ActionBar' | 'HotKey';
@@ -513,3 +525,16 @@ export class ContextMenuTooltipToggle extends Action {
}
}
}
export class CompareExecutionPlanAction extends Action {
public static ID = 'ep.tooltipToggleContextMenu';
public static COMPARE_PLAN = localize('executionPlanCompareExecutionPlanAction', "Compare execution plan");
constructor() {
super(CompareExecutionPlanAction.COMPARE_PLAN, CompareExecutionPlanAction.COMPARE_PLAN, executionPlanCompareIconClassName);
}
public override async run(context: ExecutionPlanView): Promise<void> {
context.compareCurrentExecutionPlan();
}
}

View File

@@ -27,13 +27,15 @@ export class ExecutionPlanViewHeader {
public constructor(
private _parentContainer: HTMLElement,
headerData: PlanHeaderData,
headerData: PlanHeaderData | undefined,
@IInstantiationService public readonly _instantiationService: IInstantiationService) {
this._graphIndex = headerData.planIndex;
this._relativeCost = headerData.relativeCost;
this._query = headerData.query;
this._recommendations = headerData.recommendations ?? [];
if (headerData) {
this._graphIndex = headerData.planIndex;
this._relativeCost = headerData.relativeCost;
this._query = headerData.query;
this._recommendations = headerData.recommendations ?? [];
}
this._graphIndexAndCostContainer = DOM.$('.index-row');
this._queryContainer = DOM.$('.query-row');
@@ -84,28 +86,31 @@ export class ExecutionPlanViewHeader {
}
private renderQueryText(): void {
this._queryContainer.innerText = this._query;
if (this._query) {
this._queryContainer.innerText = this._query;
}
}
private renderRecommendations(): void {
while (this._recommendationsContainer.firstChild) {
this._recommendationsContainer.removeChild(this._recommendationsContainer.firstChild);
if (this._recommendations) {
while (this._recommendationsContainer.firstChild) {
this._recommendationsContainer.removeChild(this._recommendationsContainer.firstChild);
}
this._recommendations.forEach(r => {
const link = new Button(this._recommendationsContainer, {
title: r.displayString,
secondary: true,
});
link.label = r.displayString;
//Enabling on click action for recommendations. It will open the recommendation File
link.onDidClick(e => {
this._instantiationService.invokeFunction(openNewQuery, undefined, r.queryWithDescription, RunQueryOnConnectionMode.none);
});
});
}
this._recommendations.forEach(r => {
const link = new Button(this._recommendationsContainer, {
title: r.displayString,
secondary: true,
});
link.label = r.displayString;
//Enabling on click action for recommendations. It will open the recommendation File
link.onDidClick(e => {
this._instantiationService.invokeFunction(openNewQuery, undefined, r.queryWithDescription, RunQueryOnConnectionMode.none);
});
});
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-action-green{fill:#388a34;}</style></defs><title>Add_16x</title><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/><path class="icon-vs-out" d="M6,16V10H0V6H6V0h4V6h6v4H10v6Z"/><path class="icon-vs-action-green" d="M15,9H9v6H7V9H1V7H7V1H9V7h6Z"/></svg>

After

Width:  |  Height:  |  Size: 429 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>BranchCompare_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline"><path class="icon-vs-out" d="M14,9.619V5.5a3.507,3.507,0,0,0-2.717-3.412l.235-.234L9.664,0H9.129L6.656,2.473A3.25,3.25,0,1,0,2,6.131V10.5a3.506,3.506,0,0,0,2.717,3.412l-.235.234L6.336,16h.535L9.4,13.473A3.25,3.25,0,1,0,14,9.619Zm-3,0a3.224,3.224,0,0,0-1.6,1.907L6.6,8.732,5,10.336v-4.2A3.222,3.222,0,0,0,6.538,4.41L9.4,7.268l1.6-1.6Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,10.311V5.5A2.5,2.5,0,0,0,10.5,3H8.957L10.1,1.854,9.4,1.146,7.043,3.5,9.4,5.854l.708-.708L8.957,4H10.5A1.5,1.5,0,0,1,12,5.5v4.811a2.25,2.25,0,1,0,1,0Zm-.5,3.439a1.25,1.25,0,1,1,1.25-1.25A1.252,1.252,0,0,1,12.5,13.75Zm-6.6-2.9L7.043,12H5.5A1.5,1.5,0,0,1,4,10.5V5.44a2.25,2.25,0,1,0-1,0V10.5A2.5,2.5,0,0,0,5.5,13H7.043L5.9,14.146l.708.708L8.957,12.5,6.6,10.146ZM2.25,3.25A1.25,1.25,0,1,1,3.5,4.5,1.252,1.252,0,0,1,2.25,3.25Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M9 5v2H8V0H1v7h2v8h6v1h6V5H9zm0 7H6v-2h3v2z"/></g><path class="st2" d="M14 10V6h-4v2H5V6h2V1H2v5h2v8h6v1h4v-4h-4v2H5V9h5v1h4zm-3-3h2v2h-2V7zm0 5h2v2h-2v-2zM3 5V2h3v3H3z" id="icon_x5F_bg"/><g id="icon_x5F_fg"><path class="st3" d="M11 7h2v2h-2zM11 12h2v2h-2zM3 2h3v3H3z"/></g></svg>

After

Width:  |  Height:  |  Size: 503 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.canvas,.icon-canvas-transparent{opacity:0;}.canvas{fill:none;}.icon-vs-bg{fill:#424242;}.icon-vs-action-purple{fill:#652d90;}</style></defs><title>ResetView_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline"><path class="icon-vs-out" d="M16,0V16H0V0Z"/></g><g id="iconBg"><path class="canvas" d="M16,16H0V0H16Z"/><path class="icon-vs-bg" d="M1,1H5V2H2V5H1ZM11,1V2h3V5h1V1Zm3,13H11v1h4V11H14ZM2,11H1v4H5V14H2Z"/></g><g id="colorImportance"><path class="icon-vs-action-purple" d="M8,3.567,4,5.876V10.5l4,2.31,4-2.31V5.876Zm2.536,2.619L8,7.65,5.464,6.186,8,4.722ZM5,7.072,7.5,8.516v2.846L5,9.917Zm3.5,4.29V8.516L11,7.072V9.917Z"/></g></svg>

After

Width:  |  Height:  |  Size: 816 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M5.986 16l-.373-2.237-1.846 1.317-2.848-2.847 1.319-1.847-2.238-.373v-4.027l2.238-.373-1.319-1.846 2.849-2.848 1.846 1.319.372-2.238h4.028l.372 2.238 1.847-1.319 2.847 2.848-1.318 1.846 2.238.373v4.028l-2.238.372 1.318 1.847-2.847 2.847-1.847-1.318-.373 2.238h-4.027z" id="outline"/><path class="icon-vs-bg" d="M9.964 3.257l-.443-.133-.354-2.124h-2.334l-.353 2.121c-.296.093-.58.21-.855.354l-1.75-1.25-1.65 1.65 1.252 1.752-.22.409-.133.443-2.124.354v2.333l2.121.354c.092.296.21.58.354.855l-1.25 1.75 1.65 1.65 1.752-1.252.408.219.444.134.354 2.124h2.333l.354-2.121c.296-.092.58-.21.854-.354l1.75 1.25 1.65-1.65-1.252-1.752.219-.408.134-.444 2.125-.354v-2.334l-2.121-.353c-.092-.296-.21-.58-.354-.854l1.25-1.75-1.65-1.65-1.752 1.252-.409-.221zm.248 4.743c0 1.222-.991 2.212-2.212 2.212-1.222 0-2.212-.991-2.212-2.212s.99-2.212 2.212-2.212c1.222 0 2.212.99 2.212 2.212z" id="iconBg"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>SplitScreenHorizontal_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline"><path class="icon-vs-out" d="M16,1V15H0V1Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M1,2V14H15V2ZM14,13H2V10H14Zm0-5H2V5H14Z"/></g></svg>

After

Width:  |  Height:  |  Size: 479 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>SplitScreenVertical_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline"><path class="icon-vs-out" d="M16,1V15H0V1Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M1,2V14H15V2ZM7,13H2V5H7Zm7,0H9V5h5Z"/></g></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -109,24 +109,34 @@ However we always want it to be the width of the container it is resizing.
position: relative;
}
/* Properties view in execution plan */
.eps-container .execution-plan .properties {
.eps-container .properties {
flex: 0 0 500px;
flex-direction: row;
}
.eps-container .properties-sash {
position: relative;
height: 100%;
width: 1px;
}
/* Properties view in execution plan */
.properties-content {
overflow: hidden;
width: 500px;
height: calc( 100% - 2px );
height: calc(100% - 2px);
display: flex;
flex-direction: column;
border-left: 1px solid;
border-left: 3px solid;
border-color: var(--separator-border)
}
/* Title container of the properties view */
.eps-container .execution-plan .properties .title {
.eps-container .properties .title {
line-height: 30px;
height: 22px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
overflow: hidden;
display: flex;
align-items: center;
@@ -137,7 +147,7 @@ However we always want it to be the width of the container it is resizing.
}
/* text in title container of properties view */
.eps-container .execution-plan .properties .title .text {
.eps-container .properties .title .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -148,7 +158,7 @@ However we always want it to be the width of the container it is resizing.
}
/* action bar in the title container for the properties view. This contains the close icon */
.eps-container .execution-plan .properties .title .action-bar {
.eps-container .properties .title .action-bar {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -160,7 +170,7 @@ However we always want it to be the width of the container it is resizing.
}
/* Operation name styling in the properties view. */
.eps-container .execution-plan .properties .operation-name {
.eps-container .properties .operation-name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -174,8 +184,8 @@ However we always want it to be the width of the container it is resizing.
}
/* Properties table container in properties view */
.eps-container .execution-plan .properties .table-container {
overflow-y: scroll;
.eps-container .properties .table-container {
overflow: hidden;
flex: 1;
flex-grow: 1;
}
@@ -183,6 +193,7 @@ However we always want it to be the width of the container it is resizing.
/* Action bar for the execution plan */
.eps-container .execution-plan .action-bar-container {
flex: 0 0 25px;
border-left: 1px solid var(--separator-border);
}
/* styling for the column headers in the properties table */
@@ -195,7 +206,6 @@ However we always want it to be the width of the container it is resizing.
-webkit-margin-before: 0;
-webkit-margin-after: 0;
font-weight: bold;
text-transform: uppercase;
}
.eps-container .properties-header {
@@ -331,3 +341,184 @@ However we always want it to be the width of the container it is resizing.
background-position: center;
background-repeat: no-repeat;
}
.ep-add-icon {
background-image: url(../images/actionIcons/add.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-settings-icon {
background-image: url(../images/actionIcons/settings.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-split-screen-horizontally-icon {
background-image: url(../images/actionIcons/splitScreenHorizontally.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-split-screen-vertically-icon {
background-image: url(../images/actionIcons/splitScreenVertically.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-reset-zoom-icon {
background-image: url(../images/actionIcons/resetZoom.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.ep-plan-compare-icon {
background-image: url(../images/actionIcons/execution-plan-compare.svg);
background-size: 16px 16px;
background-position: center;
background-repeat: no-repeat;
}
.eps-container .comparison-editor {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.eps-container .comparison-editor .editor-toolbar {
width: 100%;
flex: 0 0 auto;
border-bottom: 2px solid;
border-color: var(--separator-border);
}
.eps-container .comparison-editor .plan-comparison-container {
width: 100%;
height: 100%;
flex: 1;
display: flex;
flex-direction: row;
}
.eps-container .comparison-editor .plan-comparison-container {
width: 100%;
height: 100%;
flex: 1;
display: flex;
flex-direction: row;
}
.eps-container .comparison-editor .plan-comparison-container {
width: 100%;
height: calc(100% - 25px);
flex: 1;
display: flex;
flex-direction: row;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .dropdown-container {
flex: 0;
padding: 3px;
border-bottom: 1px solid var(--separator-border);
}
/* each link in execution plan recommendations */
.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .recommendations > a {
width: fit-content;
align-items: left;
text-align: left;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container .sash-container {
flex: 0 0 2px;
width: 100%;
position: relative;
background-color: var(--separator-border);
}
/*
The actual sash element constructed by code. Important is used here because the width of the sash is fixed.
However we always want it to be the width of the container it is resizing.
*/
.eps-container .comparison-editor .plan-comparison-container .split-view-container .sash-container .horizontal {
width: 100% !important;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container .sash-container .vertical {
height: 100% !important;
}
.eps-container .comparison-editor .plan-comparison-container .properties {
display: flex;
flex: 0 0 500px;
flex-direction: row;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container .placeholder {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
justify-content: center;
align-items: center;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container .placeholder .infobox-container {
width: fit-content;
height: fit-content;
}
.eps-container .comparison-editor .plan-comparison-container .split-view-container .plan-container .plan-diagram {
flex: 1;
overflow: scroll;
display: flex;
flex-direction: column;
position: relative;
}
.eps-container .comparison-editor .plan-comparison-container .properties .compare-operation-name {
display: flex;
flex-direction: row;
}
.eps-container .comparison-editor .plan-comparison-container .properties .compare-operation-name-text {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 13px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
margin-top: 3px;
margin-bottom: 5px;
margin-left: 5px;
}
.eps-container .comparison-editor .properties .table-container {
overflow: hidden;
}

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
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';
@@ -14,7 +15,7 @@ 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 { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { AzdataGraphView, InternalExecutionPlanNode, SearchType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { AzdataGraphView, SearchType } from 'sql/workbench/contrib/executionPlan/browser/azdataGraphView';
import { ExecutionPlanWidgetController } from 'sql/workbench/contrib/executionPlan/browser/executionPlanWidgetController';
const CONTAINS_DISPLAY_STRING = localize("executionPlanSearchTypeContains", 'Contains');
@@ -35,7 +36,7 @@ export class NodeSearchWidget extends ExecutionPlanWidgetBase {
private _selectedSearchType: SearchType = SearchType.Equals;
private _searchTextInputBox: InputBox;
private _searchResults: InternalExecutionPlanNode[] = [];
private _searchResults: azdata.executionPlan.ExecutionPlanNode[] = [];
private _currentSearchResultIndex = 0;
private _usePreviousSearchResult: boolean = false;

View File

@@ -67,6 +67,9 @@ export class ExecutionPlanInput extends EditorInput {
}
get resource(): URI | undefined {
return undefined;
return URI.from({
scheme: ExecutionPlanInput.SCHEMA,
path: 'execution-plan'
});
}
}

View File

@@ -133,5 +133,23 @@ export class ExecutionPlanService implements IExecutionPlanService {
return this._capabilitiesService.getCapabilities(providerId).connection.supportedExecutionPlanFileExtensions;
}
getSupportedExecutionPlanExtensions(providerId: string): string[] | undefined {
if (providerId) {
return this._capabilitiesService.getCapabilities(providerId).connection.supportedExecutionPlanFileExtensions;
} else {
const supportedFileExtensionsSet: Set<string> = new Set();
Object.keys(this._capabilitiesService.providers).forEach(v => {
const extensions = this._capabilitiesService.getCapabilities(v).connection.supportedExecutionPlanFileExtensions;
if (extensions) {
extensions.forEach(ext => {
supportedFileExtensionsSet.add(ext);
});
}
});
return [...supportedFileExtensionsSet];
}
}
_serviceBrand: undefined;
}

View File

@@ -30,7 +30,8 @@ export interface IExecutionPlanService {
compareExecutionPlanGraph(firstPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo, secondPlanFile: azdata.executionPlan.ExecutionPlanGraphInfo): Promise<azdata.executionPlan.ExecutionPlanComparisonResult>;
/**
* Get execution plan file extensions supported by the provider.
* Get execution plan file extensions supported by all registered providers.
* @param providerId optional parameter to get extensions only supported by a particular provider.
*/
getSupportedExecutionPlanExtensionsForProvider(providerId: string): string[];
getSupportedExecutionPlanExtensions(providerId?: string): string[];
}

View File

@@ -1831,9 +1831,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.21":
version "0.0.21"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/59f38fc96ab8beadb5ed2b6986ae3f39d662d040"
"azdataGraph@github:Microsoft/azdataGraph#0.0.26":
version "0.0.26"
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/a5f94e53cb655bc44f1a2727653bb403942e1cf9"
azure-storage@^2.10.2:
version "2.10.2"